Welcome, guest | Sign In | My Account | Store | Cart

Functions to convert between integers and Roman numerals. Doctest examples included.

Python, 114 lines
 ``` 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 >>> 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 >>> 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. Paul Winkler (author) 22 years, 1 month ago

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.

``````def int_to_roman(input):
if type(input) != type(1):
raise TypeError, "expected integer, got %s" % type(input)
if not 0 &lt; input &lt; 4000:
raise ValueError, "Argument must be between 1 and 3999"
result = ""
nums = ('M', 'D', 'C', 'L', 'X', 'V', 'I')
ints = (1000, 500, 100, 50,  10,  5,   1)
places = [0,] * len(nums)
# count how many times each int we have
for i in range(len(ints)):
value = ints[i]
count = input / value
places[i] = count
if count: input -= count * value
# Format the output string
for i in range(len(places)):
if places[i] &lt; 4:
result += nums[i] * places[i]
else:
# 4 repetitions means we're trying to represent 4 or 9
# of something.
if places[i -1] == 0:
# it's a 4.
result += nums[i] + nums[i -1]
else:
# it's a 9.
# 'the next' character is 2 away from here, not 1;
# otherwise we'd get 'IV' when we want 'IX'.
# We'll also need to delete the previous character,
# or we'll get stuff like 'VIX' when we want 'IX'.
result = result[:-1] + nums[i] + nums[i -2]
return result
`````` Graham Ashton 22 years, 1 month ago

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. Paul Winkler (author) 22 years, 1 month ago

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.

``````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
>>> 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
"""
try:
input = input.upper()
except AttributeError:
raise TypeError, 'expected string, got %s' % type(input)
# map of (numeral, value, maxcount) tuples
roman_numeral_map = (('M',  1000, 3), ('CM', 900, 1),
('D',  500, 1), ('CD', 400, 1),
('C',  100, 3), ('XC', 90, 1),
('L',  50, 1), ('XL', 40, 1),
('X',  10, 3), ('IX', 9, 1),
('V',  5, 1),  ('IV', 4, 1), ('I',  1, 3))
result, index = 0, 0
for numeral, value, maxcount in roman_numeral_map:
count = 0
while input[index: index +len(numeral)] == numeral:
count += 1 # how many of this numeral we have
if count &gt; maxcount:
raise ValueError, 'input is not a valid roman numeral: %s' % input
result += value
index += len(numeral)
if index &lt; len(input): # There are characters unaccounted for.
raise ValueError, 'input is not a valid roman numeral: %s'%input
else:
return result
`````` Tim Valenta 13 years, 8 months ago

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:

``````numeral_map = zip(
(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
)

def int_to_roman(i):
result = []
for integer, numeral in numeral_map:
count = int(i / integer)
result.append(numeral * count)
i -= integer * count
return ''.join(result)

def roman_to_int(n):
n = unicode(n).upper()

i = result = 0
for integer, numeral in numeral_map:
while n[i:i + len(numeral)] == numeral:
result += integer
i += len(numeral)
return result
``````

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. dln385 10 years, 6 months ago

Updated to work with Python 3.

``````numeral_map = tuple(zip(
(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I')
))

def int_to_roman(i):
result = []
for integer, numeral in numeral_map:
count = i // integer
result.append(numeral * count)
i -= integer * count
return ''.join(result)

def roman_to_int(n):
i = result = 0
for integer, numeral in numeral_map:
while n[i:i + len(numeral)] == numeral:
result += integer
i += len(numeral)
return result
`````` Created by Paul Winkler on Sun, 14 Oct 2001 (PSF)

### Required Modules

• (none specified)