整数・ローマ数字変換(Python版)

http://d.hatena.ne.jp/fumokmm/20110822/1314013182

アラビア数字 <=> ローマ数字変換を行う関数、arabicToRoman および romanToArabic を実装せよ。

条件)
・ローマ数字の表記法についてはローマ数字 - Wikipediaを参考にすること。
・ローマ数字は半角英「I,V,X,L,C,D,M,i,v,x,l,c,d,m」のみ使用した文字列とし、それ以外はエラーとする。
・アラビア数字は整数 1 から 3999 のみ使用するものとし、それ以外はエラーとする。

変換例)
11 <=> XI
12 <=> XII
14 <=> XIV
18 <=> XVIII
24 <=> XXIV
43 <=> XLIII
99 <=> XCIX
495 <=> CDXCV
1888 <=> MDCCCLXXXVIII
1945 <=> MCMXLV
3999 <=> MMMCMXCIX

コードサンプル)
arabicToRoman(11) => "XI"
romanToArabic("MDCCCLXXXVIII") => 1888
romanToArabic("mdccclxxxviii") => 1888
romanToArabic("McmXLv") => 1945
arabicToRoman(0) => エラー
romanToArabic("A") => エラー

Pythonで解いてみました。関数名は変えてあります。

#!python3
#encoding:shift-jis
import re

DIGIT_STRINGS = {
    1: dict(enumerate("I II III IV V VI VII VIII IX".split(), 1)),
    2: dict(enumerate("X XX XXX XL L LX LXX LXXX XC".split(), 1)),
    3: dict(enumerate("C CC CCC CD D DC DCC DCCC CM".split(), 1)),
    4: dict(enumerate("M MM MMM".split(), 1)),
}

ROMAN_REGEX = re.compile(r"""
(?:(?P<_3000>MMM)|(?P<_2000>MM)|(?P<_1000>M))?
(?:(?P<_900>CM)|(?P<_800>DCCC)|(?P<_700>DCC)|(?P<_600>DC)
    |(?P<_500>D)|(?P<_400>CD)|(?P<_300>CCC)|(?P<_200>CC)|(?P<_100>C))?
(?:(?P<_90>XC)|(?P<_80>LXXX)|(?P<_70>LXX)|(?P<_60>LX)
    |(?P<_50>L)|(?P<_40>XL)|(?P<_30>XXX)|(?P<_20>XX)|(?P<_10>X))?
(?:(?P<_9>IX)|(?P<_8>VIII)|(?P<_7>VII)|(?P<_6>VI)
    |(?P<_5>V)|(?P<_4>IV)|(?P<_3>III)|(?P<_2>II)|(?P<_1>I))?
    $
""", re.VERBOSE | re.IGNORECASE)


def roman_to_int(roman):
    """
    >>> roman_to_int("XI")
    11
    >>> roman_to_int("MDCCCLXXXVIII")
    1888
    >>> roman_to_int("MMMCMXCIX")
    3999
    >>> roman_to_int("McmXLv")
    1945
    >>> roman_to_int("CCCC")
    Traceback (most recent call last):
    ValueError: 'CCCC' is not valid roman
    >>> roman_to_int("A")
    Traceback (most recent call last):
    ValueError: 'A' is not valid roman
    >>> roman_to_int("")
    Traceback (most recent call last):
    ValueError: '' is not valid roman
    """
    if not roman:
        raise ValueError("{} is not valid roman".format(repr(roman)))
    m = ROMAN_REGEX.match(roman)
    if m is None:
        raise ValueError("{} is not valid roman".format(repr(roman)))
    ret = 0
    for key, value in m.groupdict().items():
        if value is not None:
            ret += int(key[1:])
    assert ret >= 0
    return ret


def int_to_roman(n):
    """
    >>> int_to_roman(11)
    'XI'
    >>> int_to_roman(3999)
    'MMMCMXCIX'
    >>> int_to_roman(1888)
    'MDCCCLXXXVIII'
    >>> int_to_roman(4000)
    Traceback (most recent call last):
    ValueError: 4000 is not in range(1, 4000)
    >>> int_to_roman(0)
    Traceback (most recent call last):
    ValueError: 0 is not in range(1, 4000)
    >>> int_to_roman(0.1)
    Traceback (most recent call last):
    TypeError: 0.1 is not integer
    >>> int_to_roman("10")
    Traceback (most recent call last):
    TypeError: '10' is not integer
    >>>
    """
    if not isinstance(n, int):
        raise TypeError("{} is not integer".format(repr(n)))
    if n not in range(1, 4000):
        raise ValueError("{} is not in range(1, 4000)".format(repr(n)))
    strs = 
    for digit, x in enumerate(str(n)[::-1], 1):
        x = int(x)
        if x > 0:
            strs.append(DIGIT_STRINGS[digit][x])
    return "".join(reversed(strs))
    
    
def int_to_digits(n, base=10):
    """
    >>> int_to_digits(0)
    
    >>> int_to_digits(1)
    [1]
    >>> int_to_digits(12345)
    [5, 4, 3, 2, 1]
    >>> int_to_digits(0, base=2)
    
    >>> int_to_digits(1, base=2)
    [1]
    >>> int_to_digits(2, base=2)
    [0, 1]
    >>> int_to_digits(0b1011, base=2)
    [1, 1, 0, 1]
    >>> int_to_digits(3**4, base=3)
    [0, 0, 0, 0, 1]
    >>> int_to_digits(-1)
    Traceback (most recent call last):
    ValueError: -1 is not >= 0
    >>> int_to_digits(1.0)
    Traceback (most recent call last):
    TypeError: 1.0 is not integer
    """
    if not isinstance(n, int):  
        raise TypeError("{} is not integer".format(repr(n)))
    if not isinstance(base, int):  
        raise TypeError("base {} is not integer".format(repr(base)))
    if n < 0:
        raise ValueError("{} is not >= 0".format(repr(n)))
    if base < 2:
        raise ValueError("base {} is not >= 2".format(repr(base)))
    
    ret = 
    ret_append = ret.append
    while n:
        ret_append(n % base)
        n //= base
    return ret
    

if __name__ == "__main__":
    import doctest
    doctest.testmod()

docstringに書いてあるように、エラー処理もしてあります。

追記:PyPIromanというモジュールがあります。