Converting base-16 roman numbers to arabic numbers (and vice-versa)
Posted: July 2, 2012 Filed under: Programming Languages | Tags: Mac OS X, Python, Unittest Leave a comment »Here is neat python programming challenge.
A hex roman numeral is very much like the standard roman numeral, except with different values. In normal roman numerals, I = 1, V = 5, X = 10 and so on. In hex roman numerals, I = 1, V = 8, X = 16, L = 128, C = 256, D = 2048 and M = 4096. So for example:
VIIII = 8 + 1 + 1 + 1 + 1 = 12
IX = 16 – 1 = 15
XV = 16 + 8 = 24
XL = 128 – 16 = 112
The goal is to write a program in python that converts it in either direction. If given a decimal number, it should return the hex roman numeral version of the number and if given a hex roman numeral, it should return the decimal version of the number.
I started this by creating a program that performs a normal roman to arabic conversion. This wasn’t too hard, especially since python has a ton of neat features such as as dictionaries and solid string parsing methods. Since I am using unittest to test my code, I’ve named this file roman_numerals.py.
|
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 |
import sys, re def roman_to_arabic(number): """return the roman numeral string representation of integer number""" roman_dict={"I":1,"V":5,"X":10,"L":50,"C":100,"D":500,"M":1000} lst = [ roman_dict[i] for i in list(number) ] for n in xrange(len(lst)-1): if (lst[n]<lst[n+1]): lst[n]=-lst[n] return(sum(lst)) def arabic_to_roman(number): """return the arabic numeral integer representation of roman string number""" units = ("I","II","III","IV","V","VI","VII","VIII","IX","") tens = ("X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC", "") hundreds = ("C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM", "") thousands = ("M", "MM", "MMM", "MMMM","MMMMM","MMMMM","MMMMMM","MMMMMMM","MMMMMMMM","") #not quite sure how the romans dealt with very small or very large #numbers... also not quite sure how they worked with floating points assert(number<=7000) assert(number>0) a=list(str(number)) #string-ify numbers b=a[-1::-1] #reverse order of list conversion="" if (len(b)>0): conversion=units[eval(b[0])-1]+conversion if (len(b)>1): conversion=tens[eval(b[1])-1]+conversion if (len(b)>2): conversion=hundreds[eval(b[2])-1]+conversion if (len(b)>3): conversion=thousands[eval(b[3])-1]+conversion return(conversion) if __name__== '__main__': try: if (re.match("I|V|X|L|D|C|M", sys.argv[1])): print roman_to_arabic(sys.argv[1]) else: print arabic_to_roman(eval(sys.argv[1])) except: print "Error: You either specified an: \n\t-invalid number \n\t-out of [1 to 7000] range \n\t-inexistent number" |
The package unittest provides a great way to test your programs. I love it. You can pretty much run a another script and it will perform all the necessary assertions as it tests the proper package. Here is my unittest code, which I named test_roman_numerals.py.
|
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 |
import unittest, roman_numerals class ProductTestCase(unittest.TestCase): def test_arabic_to_roman(self): self.failUnless("I"==roman_numerals.arabic_to_roman(1)) self.failUnless("III"==roman_numerals.arabic_to_roman(3)) self.failUnless("V"==roman_numerals.arabic_to_roman(5)) self.failUnless("X"==roman_numerals.arabic_to_roman(10)) self.failUnless("XI"==roman_numerals.arabic_to_roman(11)) self.failUnless("VIII"==roman_numerals.arabic_to_roman(8)) self.failUnless("IX"==roman_numerals.arabic_to_roman(9)) self.failUnless("XV"==roman_numerals.arabic_to_roman(15)) self.failUnless("XL"==roman_numerals.arabic_to_roman(40)) self.failUnless("CXV"==roman_numerals.arabic_to_roman(115)) self.failUnless("XLVI"==roman_numerals.arabic_to_roman(46)) self.failUnless("MMXII"==roman_numerals.arabic_to_roman(2012)) def test_roman_to_arabic(self): self.failUnless(1==roman_numerals.roman_to_arabic("I")) self.failUnless(3==roman_numerals.roman_to_arabic("III")) self.failUnless(5==roman_numerals.roman_to_arabic("V")) self.failUnless(10==roman_numerals.roman_to_arabic("X")) self.failUnless(11==roman_numerals.roman_to_arabic("XI")) self.failUnless(8==roman_numerals.roman_to_arabic("VIII")) self.failUnless(9==roman_numerals.roman_to_arabic("IX")) self.failUnless(15==roman_numerals.roman_to_arabic("XV")) self.failUnless(40==roman_numerals.roman_to_arabic("XL")) self.failUnless(115==roman_numerals.roman_to_arabic("CXV")) self.failUnless(46==roman_numerals.roman_to_arabic("XLVI")) self.failUnless(2012==roman_numerals.roman_to_arabic("MMXII")) if __name__== '__main__': unittest.main() |
Here are some screenshots of the program in action: first testing through the command line, and then testing it with unittest.

Taking this base code and making it compatible with base-16 numerals was trivial. All that I needed to do was make a minor modification to the dictionary roman_dict and adding extra elements to the lists units, tens, hundreds and thousands. Of course I had to perform a base conversion with the hex2dec function each time I wanted to access a position in the list.
Here is the code that converts base-16 roman numbers to arabic numbers. I saved this file as roman_numerals_base16.py.
|
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 |
import sys, re def roman_to_hex_arabic(number): """return the roman numeral string representation of integer number""" roman_dict={"I":1,"V":8,"X":16,"L":128,"C":256,"D":2048,"M":4096} lst = [ roman_dict[i] for i in list(number) ] for n in xrange(len(lst)-1): if (lst[n]<lst[n+1]): lst[n]=-lst[n] return(sum(lst)) def arabic_to_hex_roman(number): """return the arabic numeral integer representation of roman string number""" units = ("I", "II", "III", "IIII", "IIIII", "IIIIII", "IV", "V", "VI", "VII", "VIII", "VIIII", "VIIIII", "VIIIIII", "IX", "") tens = ("X", "XX", "XXX", "XXXX", "XXXXX", "XXXXXX", "XL", "L", "LX", "LXX", "LXXX", "LXXXX", "LXXXXX", "LXXXXXX", "XC", "") hundreds = ("C", "CC", "CCC", "CCCC", "CCCCC", "CCCCCC", "CD", "D", "DC", "DCC", "DCCC", "DCCCC", "DCCCCC", "DCCCCCC", "CM", "") thousands = ("M", "MM", "MMM", "MMMM","MMMMM","MMMMM","MMMMMM","MMMMMMM","MMMMMMMM","") #not quite sure how the romans dealt with very small or very large #numbers... also not quite sure how they worked with floating points assert(number<=7000) assert(number>0) a=list(str(dec2hex(number))) #string-ify numbers b=a[-1::-1] #reverse order of list conversion="" if (len(b)>0): conversion=units[hex2dec(b[0])-1]+conversion if (len(b)>1): conversion=tens[hex2dec(b[1])-1]+conversion if (len(b)>2): conversion=hundreds[hex2dec(b[2])-1]+conversion if (len(b)>3): conversion=thousands[hex2dec(b[3])-1]+conversion return(conversion) def dec2hex(n): """return the hexadecimal string representation of integer n""" return "%X" % n def hex2dec(s): """return the integer value of a hexadecimal string s""" return int(s, 16) if __name__== '__main__': try: if (re.match("I|V|X|L|D|C|M", sys.argv[1])): print roman_to_hex_arabic(sys.argv[1]) else: print arabic_to_hex_roman(eval(sys.argv[1])) except: print "Error: You either specified an: \n\t-invalid number \n\t-out of [1 to 7000] range \n\t-inexistent number" |
And here are my test cases, taken directly from the problem statement and saved as test_roman_numerals_base16.py.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import unittest, roman_numerals_base16 class ProductTestCase(unittest.TestCase): def test_arabic_to_hex_roman(self): self.failUnless("VIIII"==roman_numerals_base16.arabic_to_hex_roman(12)) self.failUnless("IX"==roman_numerals_base16.arabic_to_hex_roman(15)) self.failUnless("XV"==roman_numerals_base16.arabic_to_hex_roman(24)) self.failUnless("XL"==roman_numerals_base16.arabic_to_hex_roman(112)) self.failUnless("XI"==roman_numerals_base16.arabic_to_hex_roman(17)) def test_roman_to_hex_arabic(self): self.failUnless(12==roman_numerals_base16.roman_to_hex_arabic("VIIII")) self.failUnless(15==roman_numerals_base16.roman_to_hex_arabic("IX")) self.failUnless(24==roman_numerals_base16.roman_to_hex_arabic("XV")) self.failUnless(112==roman_numerals_base16.roman_to_hex_arabic("XL")) self.failUnless(17==roman_numerals_base16.roman_to_hex_arabic("XI")) if __name__== '__main__': unittest.main() |
Finally, here is a screenshot of the program in action.

