13.1. Introduction to Roman numerals
In previous chapters, you “dived in” by immediately looking at code and trying to understand it as quickly as possible. Now that you have some Python under your belt, you're going to step back and look at the steps that happen before the code gets written.
In the next few chapters, you're going to write, debug, and optimize a set of utility functions to convert to and from Roman
numerals. You saw the mechanics of constructing and validating Roman numerals in Section 7.3, “Case Study: Roman Numerals”, but now let's step back and consider what it would take to expand that into a two-way utility.
The rules for Roman numerals lead to a number of interesting observations:
- There is only one correct way to represent a particular number as Roman numerals.
- The converse is also true: if a string of characters is a valid Roman numeral, it represents only one number (i.e. it can only be read one way).
- There is a limited range of numbers that can be expressed as Roman numerals, specifically 1 through 3999. (The Romans did have several ways of expressing larger numbers, for instance by having a bar over a numeral to represent
that its normal value should be multiplied by 1000, but you're not going to deal with that. For the purposes of this chapter, let's stipulate that Roman numerals go from 1 to 3999.)
- There is no way to represent 0 in Roman numerals. (Amazingly, the ancient Romans had no concept of 0 as a number. Numbers were for counting things you had; how can you count what you don't have?)
- There is no way to represent negative numbers in Roman numerals.
- There is no way to represent fractions or non-integer numbers in Roman numerals.
Given all of this, what would you expect out of a set of functions to convert to and from Roman numerals?
- toRoman should return the Roman numeral representation for all integers 1 to 3999.
- toRoman should fail when given an integer outside the range 1 to 3999.
- toRoman should fail when given a non-integer number.
- fromRoman should take a valid Roman numeral and return the number that it represents.
- fromRoman should fail when given an invalid Roman numeral.
- If you take a number, convert it to Roman numerals, then convert that back to a number, you should end up with the number
you started with. So fromRoman(toRoman(n)) == n for all n in 1..3999.
- toRoman should always return a Roman numeral using uppercase letters.
- fromRoman should only accept uppercase Roman numerals (i.e. it should fail when given lowercase input).
- This site has more on Roman numerals, including a fascinating history of how Romans and other civilizations really used them (short answer: haphazardly and inconsistently).