Functions to convert between integers and Roman numerals. Doctest examples included.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | def int_to_roman(input):
"""
Convert an integer to Roman numerals.
Examples:
>>> int_to_roman(0)
Traceback (most recent call last):
ValueError: Argument must be between 1 and 3999
>>> int_to_roman(-1)
Traceback (most recent call last):
ValueError: Argument must be between 1 and 3999
>>> int_to_roman(1.5)
Traceback (most recent call last):
TypeError: expected integer, got <type 'float'>
>>> for i in range(1, 21): print int_to_roman(i)
...
I
II
III
IV
V
VI
VII
VIII
IX
X
XI
XII
XIII
XIV
XV
XVI
XVII
XVIII
XIX
XX
>>> print int_to_roman(2000)
MM
>>> print int_to_roman(1999)
MCMXCIX
"""
if type(input) != type(1):
raise TypeError, "expected integer, got %s" % type(input)
if not 0 < input < 4000:
raise ValueError, "Argument must be between 1 and 3999"
ints = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
nums = ('M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
result = ""
for i in range(len(ints)):
count = int(input / ints[i])
result += nums[i] * count
input -= ints[i] * count
return result
def roman_to_int(input):
"""
Convert a roman numeral to an integer.
>>> r = range(1, 4000)
>>> nums = [int_to_roman(i) for i in r]
>>> ints = [roman_to_int(n) for n in nums]
>>> print r == ints
1
>>> roman_to_int('VVVIV')
Traceback (most recent call last):
...
ValueError: input is not a valid roman numeral: VVVIV
>>> roman_to_int(1)
Traceback (most recent call last):
...
TypeError: expected string, got <type 'int'>
>>> roman_to_int('a')
Traceback (most recent call last):
...
ValueError: input is not a valid roman numeral: A
>>> roman_to_int('IL')
Traceback (most recent call last):
...
ValueError: input is not a valid roman numeral: IL
"""
if type(input) != type(""):
raise TypeError, "expected string, got %s" % type(input)
input = input.upper()
nums = ['M', 'D', 'C', 'L', 'X', 'V', 'I']
ints = [1000, 500, 100, 50, 10, 5, 1]
places = []
for c in input:
if not c in nums:
raise ValueError, "input is not a valid roman numeral: %s" % input
for i in range(len(input)):
c = input[i]
value = ints[nums.index(c)]
# If the next place holds a larger number, this value is negative.
try:
nextvalue = ints[nums.index(input[i +1])]
if nextvalue > value:
value *= -1
except IndexError:
# there is no next place.
pass
places.append(value)
sum = 0
for n in places: sum += n
# Easiest test for validity...
if int_to_roman(sum) == input:
return sum
else:
raise ValueError, 'input is not a valid roman numeral: %s' % input
|
There are probably many algorithms to create roman numerals. This is the easiest to read that I've been able to come up with. I just establish a mapping between integer values and roman numerals, then count how many of each value can fit into the input integer. I use two tuples for the mapping, instead of a dictionary, because I need to go through them sequentially and don't care about random access; therefore a dictionary would be more hindrance than help.
Rules for roman numerals: A) I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000. Zero is not represented. Numbers greater than 3999 are not represented.
B) Roman numerals are repeated to add value: III is equivalent to 1 +1 +1 = 3.
C) Only powers of 10 may be repeated in this way. Thus, VV is invalid; 5 + 5 would be better expressed as X.
D) No more than three repetitions of a numeral can be used. Five can be represented with a single larger numeral; to represent four, use the next larger numeral, but precede it with a numeral to subtract from it. Thus, IIII is invalid and would be written as IV (one less than five); XC represents 90 (ten less than 100), and XL represents 40 (ten less than 50). A numeral used for subtraction in this way must be the largest power of 10 that is less than the numeral it precedes. Thus, XC is valid but IC is invalid.
int_to_roman: another approach. The following is my first attempt at int_to_roman. In this version, my approach was simply to follow, as closely as I could, the plain english description of the rules given above. I rejected this version because it actually ends up being longer and more complex than the version given above. It turns out to be easier to just forcibly assign values to "IV" and its friends than it is to implement the rule that determines the values.
Alternative - roman.py with unit testing. See Mark Pilgrim's chapter on developing a roman numeral conversion module, with unit testing in his excellent Dive into Python book: http://diveintopython.org/roman_divein.html for another alternative.
A better roman_to_int... Thanks Graham (and Mark!). I checked out Mark's "Dive Into Python". Good stuff, and all free under the Python license.
His roman-to-int converter is much simpler than the one I posted above. However, Mark relies on a regexp to validate the input. That's fine, and makes the function nicely short; but it puts a lot of the logic in the regexp, which I feel is more likely to be misread than is a slightly longer function.
So here's my latest version. It's based on Mark's, except that I use an additional field in each tuple to enforce the maximum allowed repetitions of each numeral, and I rely on the ordering of the tuple to enforce the correct ordering of numerals. With that done, we don't need a regexp, nor do we need to test by calling int_to_roman(result) as my first version did.
These can be simplified a bit more, especially using the Dive Into Python book as inspiration.... Let Python use its fast operations, such as joining and zipping. Looped string concatenation is kind of janky these days:
I know there's no one way to do it, but I was looking for just a simple way to hammer it out for a quick page numbering function, and there was no way I was going to include a huge couple of functions like the originally given ones.
And yes, I know there's no documentation on my code. But 22 lines beats 114 for "just give it to me" code.
Updated to work with Python 3.