| 1 | """Convert to and from Roman numerals |
|---|
| 2 | |
|---|
| 3 | This program is part of "Dive Into Python", a free Python book for |
|---|
| 4 | experienced programmers. Visit http://diveintopython.org/ for the |
|---|
| 5 | latest version. |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | __author__ = "Mark Pilgrim (mark@diveintopython.org)" |
|---|
| 9 | __version__ = "$Revision: 1.3 $" |
|---|
| 10 | __date__ = "$Date: 2004/05/05 21:57:19 $" |
|---|
| 11 | __copyright__ = "Copyright (c) 2001 Mark Pilgrim" |
|---|
| 12 | __license__ = "Python" |
|---|
| 13 | |
|---|
| 14 | import re |
|---|
| 15 | |
|---|
| 16 | #Define exceptions |
|---|
| 17 | class RomanError(Exception): pass |
|---|
| 18 | class OutOfRangeError(RomanError): pass |
|---|
| 19 | class NotIntegerError(RomanError): pass |
|---|
| 20 | class InvalidRomanNumeralError(RomanError): pass |
|---|
| 21 | |
|---|
| 22 | #Define digit mapping |
|---|
| 23 | romanNumeralMap = (('M', 1000), |
|---|
| 24 | ('CM', 900), |
|---|
| 25 | ('D', 500), |
|---|
| 26 | ('CD', 400), |
|---|
| 27 | ('C', 100), |
|---|
| 28 | ('XC', 90), |
|---|
| 29 | ('L', 50), |
|---|
| 30 | ('XL', 40), |
|---|
| 31 | ('X', 10), |
|---|
| 32 | ('IX', 9), |
|---|
| 33 | ('V', 5), |
|---|
| 34 | ('IV', 4), |
|---|
| 35 | ('I', 1)) |
|---|
| 36 | |
|---|
| 37 | def toRoman(n): |
|---|
| 38 | """convert integer to Roman numeral""" |
|---|
| 39 | if not (0 < n < 5000): |
|---|
| 40 | raise OutOfRangeError, "number out of range (must be 1..4999)" |
|---|
| 41 | if int(n) <> n: |
|---|
| 42 | raise NotIntegerError, "non-integers can not be converted" |
|---|
| 43 | |
|---|
| 44 | result = "" |
|---|
| 45 | for numeral, integer in romanNumeralMap: |
|---|
| 46 | while n >= integer: |
|---|
| 47 | result += numeral |
|---|
| 48 | n -= integer |
|---|
| 49 | return result |
|---|
| 50 | |
|---|
| 51 | #Define pattern to detect valid Roman numerals |
|---|
| 52 | romanNumeralPattern = re.compile(''' |
|---|
| 53 | ^ # beginning of string |
|---|
| 54 | M{0,4} # thousands - 0 to 4 M's |
|---|
| 55 | (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), |
|---|
| 56 | # or 500-800 (D, followed by 0 to 3 C's) |
|---|
| 57 | (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), |
|---|
| 58 | # or 50-80 (L, followed by 0 to 3 X's) |
|---|
| 59 | (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), |
|---|
| 60 | # or 5-8 (V, followed by 0 to 3 I's) |
|---|
| 61 | $ # end of string |
|---|
| 62 | ''' ,re.VERBOSE) |
|---|
| 63 | |
|---|
| 64 | def fromRoman(s): |
|---|
| 65 | """convert Roman numeral to integer""" |
|---|
| 66 | if not s: |
|---|
| 67 | raise InvalidRomanNumeralError, 'Input can not be blank' |
|---|
| 68 | if not romanNumeralPattern.search(s): |
|---|
| 69 | raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s |
|---|
| 70 | |
|---|
| 71 | result = 0 |
|---|
| 72 | index = 0 |
|---|
| 73 | for numeral, integer in romanNumeralMap: |
|---|
| 74 | while s[index:index+len(numeral)] == numeral: |
|---|
| 75 | result += integer |
|---|
| 76 | index += len(numeral) |
|---|
| 77 | return result |
|---|