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

This Roman class is a subclass of int and supports the same methods int does, but any special methods that would normally return ints are return a new instance of Roman. You can use instances of this class in math expressions and a Roman instance will be returned, for example.

The class decorator used to achieve this was suggested by Alex Martelli 'here on stackoverlow.com.

Python, 135 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
'''
The int methods of this Roman class are overloaded to return new
Roman instances, so you can use it to perform wrap ints and
perform basic math, like this:

>>> x = Roman(3)
>>> y = Roman(30)

>>> print x, y
Roman(3, III) Roman(30, XXX)

>>> print x + y
Roman(33, XXXIII)

>>> print x * y
Roman(90, LXXXX)

>>> print y / x
Roman(10, X)

>>> print Roman(2) ** 3
Roman(8, VIII)

>>> print Roman(25) % 11 
Roman(3, III)

>>> print Roman(-45)
Roman(-45, -DCCCCLV)

>>> sum(map(Roman, range(10)))
Roman(45)

>>> reduce(operator.mul, map(Roman, range(1, 5)))
Roman(24)

>>> print sum(map(Roman, range(10)))
Roman(45, XXXXV)

>>> print reduce(operator.mul, map(Roman, range(1, 5)))
Roman(24, XXIV)
'''

__author__ = "Thom"
__copyright__ = None
__license__ = "Python"

import re

def returnthisclassfrom(methods):
    '''
    Class decorator by Alex Martelli.

    see ('http://stackoverflow.com/questions/1242589/
          subclassing-int-to-attain-a-hex-representation/1243045#1243045)
    '''
    def wrapit(cls, method):
        return lambda *a: cls(method(*a))
    def dowrap(cls):
        for n in methods:
          method = getattr(cls, n)
          setattr(cls, n, wrapit(cls, method))
        return cls

    return dowrap

methods = "mul add sub div pow mod divmod".split()
methods += [c + s for s in methods for c in list('ir')]
methods = ['__%s__' % s for s in methods]
methods = set(methods) & set(dir(int))

@returnthisclassfrom(methods)
class Roman(int):

    i_ones = range(10)
    r_ones = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX']
    ones_i2r = dict(zip(i_ones, r_ones))
    
    i_mul = [10, 50, 100, 500, 1000]
    r_mul = ["X", "L", "C", "D", "M"]
    mul_i2r = dict(zip(i_mul, r_mul))

    i = i_ones + i_mul
    r = r_ones + r_mul
    rom2int = dict(zip(r, i))
    re_rom = re.compile('|'.join(sorted(r, key=len, reverse=True)))

    def as_roman(self):
        
        sign = "-" if self < 0 else ""
        res = [sign]
        n = int(self)
        mul_i2r = self.mul_i2r
        
        for x in reversed(self.i_mul):
            
            div, mod = divmod(n, x)
            n = mod
            res.append(mul_i2r[x] * div)

        res.append(self.ones_i2r[n])

        return "".join(res)

    def __str__(self):
        return "Roman(%d, %s)" % (self, self.as_roman())

    def __repr__(self):
        return "Roman(%d)" % self

    @classmethod
    def fromstring(cls, s):
        return cls(sum(map(cls.rom2int.get, cls.re_rom.findall(s))))
        
            
if __name__ == "__main__":
    
    for x in range(1, 1001):
        r = Roman(x)
        assert r == Roman.fromstring(r.as_roman())
        print x, r

    print '\n'
    
    import operator
    x, y = Roman(20), Roman(5)
    print 'x =', x
    print 'y = ', y

    for s in "add sub div mul".split():
        print '%s(x, y) = %s' % (s, getattr(operator, s)(x, y))

    print '\n'

    x -= 1
    print 'x % y = ', x % y
            

4 comments

Barry Walker 10 years, 7 months ago  # | flag

">>> print Roman(-45)"

"Roman(-45, -DCCCCLV)"

There is no such thing as a Roman numeral style negative number. Their numbering system had no concept of zero either.

thom neale (author) 9 years, 9 months ago  # | flag

I just invented a negative roman numeral then. So now it exists.

Pat O'Brien 7 years, 8 months ago  # | flag

Some interesting pieces of code brought together here, thanks!

However, a basic rule of Roman Numeral representation has been broken, viz. - a sequence of 4 or more identical adjacent numerals is not allowed, as in your example:

Roman(90, LXXXX)

90 is represented as XC, which is why it is not exactly trivial to create Roman Numeral sequences using only mathematical operations like +, *, /, or %; some form of lookup is also required so as to conform with accepted representations.

thom neale (author) 7 years, 8 months ago  # | flag

I'm afraid you're quite right Pat--and now when I need to handle Roman numerals I just use a pair of mappings.