License Python Software Foundation License (Python 2.x)
Lines 67
Keywords
convert (3) integer (1) roman numeral (1) unittest (1)
Included in this Library
Permissions
Viewable by Everyone
Editable by All Siafoo Users

Convert Integers To and From Roman Numerals Atom Feed 0

In Brief Call toRoman() to convert an integer to a roman numeral, or fromRoman() to convert a roman numeral to an integer.... more
# 's
 1"""Convert to and from Roman numerals
2
3This program is part of "Dive Into Python", a free Python book for
4experienced programmers. Visit http://diveintopython.org/ for the
5latest 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
14import re
15
16#Define exceptions
17class RomanError(Exception): pass
18class OutOfRangeError(RomanError): pass
19class NotIntegerError(RomanError): pass
20class InvalidRomanNumeralError(RomanError): pass
21
22#Define digit mapping
23romanNumeralMap = (('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
37def 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
52romanNumeralPattern = 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
64def 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

Call toRoman() to convert an integer to a roman numeral, or fromRoman() to convert a roman numeral to an integer.

Here's a unittest to make sure everything works:

  1"""Unit test for roman.py
2
3This program is part of "Dive Into Python", a free Python book for
4experienced programmers. Visit http://diveintopython.org/ for the
5latest version.
6"""
7
8__author__ = "Mark Pilgrim (mark@diveintopython.org)"
9__version__ = "$Revision: 1.2 $"
10__date__ = "$Date: 2004/05/05 21:57:19 $"
11__copyright__ = "Copyright (c) 2001 Mark Pilgrim"
12__license__ = "Python"
13
14import roman
15import unittest
16
17class KnownValues(unittest.TestCase):
18 knownValues = ( (1, 'I'),
19 (2, 'II'),
20 (3, 'III'),
21 (4, 'IV'),
22 (5, 'V'),
23 (6, 'VI'),
24 (7, 'VII'),
25 (8, 'VIII'),
26 (9, 'IX'),
27 (10, 'X'),
28 (50, 'L'),
29 (100, 'C'),
30 (500, 'D'),
31 (1000, 'M'),
32 (31, 'XXXI'),
33 (148, 'CXLVIII'),
34 (294, 'CCXCIV'),
35 (312, 'CCCXII'),
36 (421, 'CDXXI'),
37 (528, 'DXXVIII'),
38 (621, 'DCXXI'),
39 (782, 'DCCLXXXII'),
40 (870, 'DCCCLXX'),
41 (941, 'CMXLI'),
42 (1043, 'MXLIII'),
43 (1110, 'MCX'),
44 (1226, 'MCCXXVI'),
45 (1301, 'MCCCI'),
46 (1485, 'MCDLXXXV'),
47 (1509, 'MDIX'),
48 (1607, 'MDCVII'),
49 (1754, 'MDCCLIV'),
50 (1832, 'MDCCCXXXII'),
51 (1993, 'MCMXCIII'),
52 (2074, 'MMLXXIV'),
53 (2152, 'MMCLII'),
54 (2212, 'MMCCXII'),
55 (2343, 'MMCCCXLIII'),
56 (2499, 'MMCDXCIX'),
57 (2574, 'MMDLXXIV'),
58 (2646, 'MMDCXLVI'),
59 (2723, 'MMDCCXXIII'),
60 (2892, 'MMDCCCXCII'),
61 (2975, 'MMCMLXXV'),
62 (3051, 'MMMLI'),
63 (3185, 'MMMCLXXXV'),
64 (3250, 'MMMCCL'),
65 (3313, 'MMMCCCXIII'),
66 (3408, 'MMMCDVIII'),
67 (3501, 'MMMDI'),
68 (3610, 'MMMDCX'),
69 (3743, 'MMMDCCXLIII'),
70 (3844, 'MMMDCCCXLIV'),
71 (3888, 'MMMDCCCLXXXVIII'),
72 (3940, 'MMMCMXL'),
73 (3999, 'MMMCMXCIX'),
74 (4000, 'MMMM'),
75 (4500, 'MMMMD'),
76 (4888, 'MMMMDCCCLXXXVIII'),
77 (4999, 'MMMMCMXCIX'))
78
79 def testToRomanKnownValues(self):
80 """toRoman should give known result with known input"""
81 for integer, numeral in self.knownValues:
82 result = roman.toRoman(integer)
83 self.assertEqual(numeral, result)
84
85 def testFromRomanKnownValues(self):
86 """fromRoman should give known result with known input"""
87 for integer, numeral in self.knownValues:
88 result = roman.fromRoman(numeral)
89 self.assertEqual(integer, result)
90
91class ToRomanBadInput(unittest.TestCase):
92 def testTooLarge(self):
93 """toRoman should fail with large input"""
94 self.assertRaises(roman.OutOfRangeError, roman.toRoman, 5000)
95
96 def testZero(self):
97 """toRoman should fail with 0 input"""
98 self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)
99
100 def testNegative(self):
101 """toRoman should fail with negative input"""
102 self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)
103
104 def testDecimal(self):
105 """toRoman should fail with non-integer input"""
106 self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)
107
108class FromRomanBadInput(unittest.TestCase):
109 def testTooManyRepeatedNumerals(self):
110 """fromRoman should fail with too many repeated numerals"""
111 for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
112 self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
113
114 def testRepeatedPairs(self):
115 """fromRoman should fail with repeated pairs of numerals"""
116 for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
117 self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
118
119 def testMalformedAntecedent(self):
120 """fromRoman should fail with malformed antecedents"""
121 for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
122 'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
123 self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
124
125 def testBlank(self):
126 """fromRoman should fail with blank string"""
127 self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "")
128
129class SanityCheck(unittest.TestCase):
130 def testSanity(self):
131 """fromRoman(toRoman(n))==n for all n"""
132 for integer in range(1, 5000):
133 numeral = roman.toRoman(integer)
134 result = roman.fromRoman(numeral)
135 self.assertEqual(integer, result)
136
137class CaseCheck(unittest.TestCase):
138 def testToRomanCase(self):
139 """toRoman should always return uppercase"""
140 for integer in range(1, 5000):
141 numeral = roman.toRoman(integer)
142 self.assertEqual(numeral, numeral.upper())
143
144 def testFromRomanCase(self):
145 """fromRoman should only accept uppercase input"""
146 for integer in range(1, 5000):
147 numeral = roman.toRoman(integer)
148 roman.fromRoman(numeral.upper())
149 self.assertRaises(roman.InvalidRomanNumeralError,
150 roman.fromRoman, numeral.lower())
151
152if __name__ == "__main__":
153 unittest.main()