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 <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.

5 comments

Paul Winkler (author) 22 years, 6 months ago  # | flag

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, 6 months ago  # | flag

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, 6 months ago  # | flag

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 14 years ago  # | flag

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, 10 months ago  # | flag

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)
Python recipes (4591)
Paul Winkler's recipes (2)
Python Cookbook Edition 1 (103)

Required Modules

  • (none specified)

Other Information and Tasks