Lines 67
##### Keywords
convert (3) integer (1) roman numeral (1) unittest (1)
##### Permissions
Viewable by Everyone
Editable by All Siafoo Users
Meet people who work on similar things as you – get help if you need it

# Convert Integers To and From Roman Numerals 0

 In Brief Call toRoman() to convert an integer to a roman numeral, or fromRoman() to convert a roman numeral to an integer.... more
 Language Python
# '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"1314import re1516#Define exceptions17class RomanError(Exception): pass18class OutOfRangeError(RomanError): pass19class NotIntegerError(RomanError): pass20class InvalidRomanNumeralError(RomanError): pass2122#Define digit mapping23romanNumeralMap = (('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))3637def 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"4344    result = ""45    for numeral, integer in romanNumeralMap:46        while n >= integer:47            result += numeral48            n -= integer49    return result5051#Define pattern to detect valid Roman numerals52romanNumeralPattern = re.compile('''53    ^                   # beginning of string54    M{0,4}              # thousands - 0 to 4 M's55    (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 string62    ''' ,re.VERBOSE)6364def 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' % s7071    result = 072    index = 073    for numeral, integer in romanNumeralMap:74        while s[index:index+len(numeral)] == numeral:75            result += integer76            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) 99100    def testNegative(self):101        """toRoman should fail with negative input"""102        self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)103104    def testDecimal(self):105        """toRoman should fail with non-integer input"""106        self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)107108class 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)113114    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)118119    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)124125    def testBlank(self):126        """fromRoman should fail with blank string"""127        self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "")128129class 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)136137class 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())143144    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())151152if __name__ == "__main__":153    unittest.main()`