7.3. Case study: Roman numerals
You've most likely seen Roman numerals, even if you didn't recognize them. You may have seen them in copyrights of old movies
and television shows (“Copyright MCMXLVI” instead of “Copyright 1946”), or on the dedication walls of libraries or universities (“established MDCCCLXXXVIII” instead of “established 1888”). You may also have seen them in outlines and bibliographical references. It's a system of representing numbers that really
does date back to the ancient Roman empire (hence the name).
In Roman numerals, there are seven characters which are repeated and combined in various ways to represent numbers.
- I = 1
- V = 5
- X = 10
- L = 50
- C = 100
- D = 500
- M = 1000
There are some general rules for constructing Roman numerals:
- Characters are additive. I is 1, II is 2, and III is 3. VI is 6 (literally, “5 and 1”), VII is 7, and VIII is 8.
- The tens characters (I, X, C, and M) can be repeated up to three times. At 4, you have to subtract from the next highest fives character. You can't represent 4 as IIII; instead, it is represented as IV (“1 less than 5”). 40 is written as XL (“10 less than 50”), 41 as XLI, 42 as XLII, 43 as XLIII, and then 44 as XLIV (“10 less than 50, then 1 less than 5”).
- Similarly, at 9, you have to subtract from the next highest tens character: 8 is VIII, but 9 is IX (“1 less than 10”), not VIIII (since the I character can not be repeated four times). 90 is XC, 900 is CM.
- The fives characters can not be repeated. 10 is always represented as X, never as VV. 100 is always C, never LL.
- Roman numerals are always written highest to lowest, and read left to right, so order of characters matters very much. DC is 600; CD is a completely different number (400, “100 less than 500”). CI is 101; IC is not even a valid Roman numeral (because you can't subtract 1 directly from 100; you would have to write it as XCIX, “10 less than 100, then 1 less than 10”).
Since Roman numerals are always written highest to lowest, let's start with the highest: the thousands place. For numbers
1000 and higher, the thousands are represented by a series of M characters.
Example 7.3. Checking for thousands
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')
>>> re.search(pattern, '')
<SRE_Match object at 0106F4A8>
|
This pattern has three parts:
- ^ - match what follows only at the beginning of the string. If this were not specified, the pattern would match no matter
where the M characters were, which is not what we want. We want to make sure that the M characters, if they're there, are at the beginning of the string.
- M? - optionally match a single M character. Since this is repeated three times, we're matching anywhere from 0 to 3 M characters in a row.
- $ - match what precedes only at the end of the string. When combined with the ^ character at the beginning, this means that the pattern must match the entire string, with no other characters before or
after the M characters.
|
|
The essense of the re module is the search function, which takes a regular expression (pattern) and a string ('M') to try to match against the regular expression. If a match is found, search returns an object which has various methods to describe the match; if no match is found, search returns None, the Python null value. We won't go into detail about the object that search returns (although it's very interesting), because all we care about at the moment is whether the pattern matches, which we
can tell by just looking at the return value of search. 'M' matches this regular expression, because the first optional M matches and the second and third optional M characters are ignored.
|
|
'MM' matches because the first and second optional M characters match and the third M is ignored.
|
|
'MMM' matches because all three M characters match.
|
|
'MMMM' does not match. All three M characters match, but then the regular expression insists on the string ending (because of the $ character), and the string doesn't end yet (because of the fourth M). So search returns None.
|
|
Interestingly, an empty string also matches this regular expression, since all the M characters are optional.
|
The hundreds place is more difficult than the thousands, because there are several mutually exclusive ways it could be expressed,
depending on its value.
- 100 = C
- 200 = CC
- 300 = CCC
- 400 = CD
- 500 = D
- 600 = DC
- 700 = DCC
- 800 = DCCC
- 900 = CM
So there are four possible patterns:
- CM
- CD
- 0 to 3 C characters (0 if the hundreds place is 0)
- D, followed by 0 to 3 C characters
The last two patterns can be combined:
- an optional D, followed by 0 to 3 C characters
Example 7.4. Checking for hundreds
>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'
>>> re.search(pattern, 'MCM')
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')
>>> re.search(pattern, '')
<SRE_Match object at 01071D98>
|
This pattern starts out the same as our previous one, checking for the beginning of the string (^), then the thousands place (M?M?M?). Then we have the new part, in parentheses, which defines a set of three mutually exclusive patterns, separated by vertical
bars: CM, CD, and D?C?C?C? (which is an optional D followed by 0 to 3 optional C characters). The regular expression parser checks for each of these patterns in order (from left to right), takes the first
one that matches, and ignores the rest.
|
|
'MCM' matches because the first M matches, the second and third M characters are ignored, and the CM matches (so the CD and D?C?C?C? patterns are never even considered). MCM is the Roman numeral representation of 1900.
|
|
'MD' matches because the first M matches, the second and third M characters are ignored, and the D?C?C?C? pattern matches D (each of the 3 C characters are optional and are ignored). MD is the Roman numeral representation of 1500.
|
|
'MMMCCC' matches because all 3 M characters match, and the D?C?C?C? pattern matches CCC (the D is optional and is ignored). MMMCCC is the Roman numeral representation of 3300.
|
|
'MCMC' does not match. The first M matches, the second and third M characters are ignored, and the CM matches, but then the $ does not match because we're not at the end of the string yet (we still have an unmatched C character). The C does not match as part of the D?C?C?C? pattern, because the mutually exclusive CM pattern has already matched.
|
|
Interestingly, an empty string still matches this pattern, because all the M characters are optional and ignored, and the empty string matches the D?C?C?C? pattern where all the characters are optional and ignored.
|
Whew! See how quickly regular expressions can get nasty? And we've only covered the thousands and hundreds places. Luckily,
if you followed all that, the tens and ones places are easy, because they're exactly the same pattern.