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