ASPN ActiveState Programmer Network  
ActiveState, a division of Sophos
/ Home / Perl / PHP / Python / Tcl / XSLT /
/ Safari / My ASPN /
Cookbooks | Documentation | Mailing Lists | Modules | News Feeds | Products | User Groups
Submit Recipe
My Recipes

All Recipes
All Cookbooks


View by Category

Title: Roman Numerals
Submitter: Paul Winkler (other recipes)Paul Winkler (other recipes)
Last Updated: 2001/10/14
Version no: 1.1
Category:

 

Not Rated yet


Description:

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

Source: Text Source

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

Discussion:

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.



Add comment

Number of comments: 3

int_to_roman: another approach, Paul Winkler,Paul Winkler, 2001/10/14
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 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 

Add comment

Alternative - roman.py with unit testing, Graham Ashton, 2001/10/15
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.
Add comment

A better roman_to_int... Thanks Graham (and Mark!)., Paul Winkler,Paul Winkler, 2001/10/15
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 > maxcount:
            raise ValueError, 'input is not a valid roman numeral: %s' % input
         result += value
         index += len(numeral)
   if index 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 > maxcount:
            raise ValueError, 'input is not a valid roman numeral: %s' % input
         result += value
         index += len(numeral)
   if index 

Add comment



Highest rated recipes:

1. A simple XML-RPC server

2. Web service accessible ...

3. IPy Notify

4. Changing return value ...

5. Quantum Superposition

6. Pickle objects under ...

7. Generalized delegates ...

8. Reorder a sequence (uses ...

9. Setting Win32 System ...

10. ObjectMerger




Privacy Policy | Email Opt-out | Feedback | Syndication
© 2006 ActiveState Software Inc. All rights reserved.