I have produced an answer that works for any int >= 0:
Save the following as romanize.py
def get_roman(input_number: int, overline_code: str = '\u0305') -> str:
"""
Recursive function which returns roman numeral (string), given input number (int)
>>> get_roman(0)
'N'
>>> get_roman(3999)
'MMMCMXCIX'
>>> get_roman(4000)
'MV\u0305'
>>> get_roman(4000, overline_code='^')
'MV^'
"""
if input_number < 0 or not isinstance(input_number, int):
raise ValueError(f'Only integers, n, within range, n >= 0 are supported.')
if input_number <= 1000:
numeral, remainder = core_lookup(input_number=input_number)
else:
numeral, remainder = thousand_lookup(input_number=input_number, overline_code=overline_code)
if remainder != 0:
numeral += get_roman(input_number=remainder, overline_code=overline_code)
return numeral
def core_lookup(input_number: int) -> (str, int):
"""
Returns highest roman numeral (string) which can (or a multiple thereof) be looked up from number map and the
remainder (int).
>>> core_lookup(3)
('III', 0)
>>> core_lookup(999)
('CM', 99)
>>> core_lookup(1000)
('M', 0)
"""
if input_number < 0 or input_number > 1000 or not isinstance(input_number, int):
raise ValueError(f'Only integers, n, within range, 0 <= n <= 1000 are supported.')
basic_lookup = NUMBER_MAP.get(input_number)
if basic_lookup:
numeral = basic_lookup
remainder = 0
else:
multiple = get_multiple(input_number=input_number, multiples=NUMBER_MAP.keys())
count = input_number // multiple
remainder = input_number % multiple
numeral = NUMBER_MAP[multiple] * count
return numeral, remainder
def thousand_lookup(input_number: int, overline_code: str = '\u0305') -> (str, int):
"""
Returns highest roman numeral possible, that is a multiple of or a thousand that of which can be looked up from
number map and the remainder (int).
>>> thousand_lookup(3000)
('MMM', 0)
>>> thousand_lookup(300001, overline_code='^')
('C^C^C^', 1)
>>> thousand_lookup(30000002, overline_code='^')
('X^^X^^X^^', 2)
"""
if input_number <= 1000 or not isinstance(input_number, int):
raise ValueError(f'Only integers, n, within range, n > 1000 are supported.')
num, k, remainder = get_thousand_count(input_number=input_number)
numeral = get_roman(input_number=num, overline_code=overline_code)
numeral = add_overlines(base_numeral=numeral, num_overlines=k, overline_code=overline_code)
# Assume:
# 4000 -> MV^, https://en.wikipedia.org/wiki/4000_(number)
# 6000 -> V^M, see https://en.wikipedia.org/wiki/6000_(number)
# 9000 -> MX^, see https://en.wikipedia.org/wiki/9000_(number)
numeral = numeral.replace(NUMBER_MAP[1] + overline_code, NUMBER_MAP[1000])
return numeral, remainder
def get_thousand_count(input_number: int) -> (int, int, int):
"""
Returns three integers defining the number, number of thousands and remainder
>>> get_thousand_count(999)
(999, 0, 0)
>>> get_thousand_count(1001)
(1, 1, 1)
>>> get_thousand_count(2000002)
(2, 2, 2)
"""
num = input_number
k = 0
while num >= 1000:
k += 1
num //= 1000
remainder = input_number - (num * 1000 ** k)
return num, k, remainder
def get_multiple(input_number: int, multiples: iter) -> int:
"""
Given an input number(int) and a list of numbers, finds the number in list closest (rounded down) to input number
>>> get_multiple(45, [1, 2, 3])
3
>>> get_multiple(45, [1, 2, 3, 44, 45, 46])
45
>>> get_multiple(45, [1, 4, 5, 9, 10, 40, 50, 90])
40
"""
options = sorted(list(multiples) + [input_number])
return options[options.index(input_number) - int(input_number not in multiples)]
def add_overlines(base_numeral: str, num_overlines: int = 1, overline_code: str = '\u0305') -> str:
"""
Adds overlines to input base numeral (string) and returns the result.
>>> add_overlines(base_numeral='II', num_overlines=1, overline_code='^')
'I^I^'
>>> add_overlines(base_numeral='I^I^', num_overlines=1, overline_code='^')
'I^^I^^'
>>> add_overlines(base_numeral='II', num_overlines=2, overline_code='^')
'I^^I^^'
"""
return ''.join([char + overline_code*num_overlines if char.isalnum() else char for char in base_numeral])
def gen_number_map() -> dict:
"""
Returns base number mapping including combinations like 4 -> IV and 9 -> IX, etc.
"""
mapping = {
1000: 'M',
500: 'D',
100: 'C',
50: 'L',
10: 'X',
5: 'V',
1: 'I',
0: 'N'
}
for exponent in range(3):
for num in (4, 9,):
power = 10 ** exponent
mapping[num * power] = mapping[1 * power] + mapping[(num + 1) * power]
return mapping
NUMBER_MAP = gen_number_map()
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True, raise_on_error=True)
# Optional extra tests
# doctest.testfile('test_romanize.txt', verbose=True)
Here are some extra tests in case useful.
Save the following as test_romanize.txt in the same directory as the romanize.py:
The ``romanize`` module
=======================
The ``get_roman`` function
--------------------------
Import statement:
>>> from romanize import get_roman
Tests:
>>> get_roman(0)
'N'
>>> get_roman(6)
'VI'
>>> get_roman(11)
'XI'
>>> get_roman(345)
'CCCXLV'
>>> get_roman(989)
'CMLXXXIX'
>>> get_roman(989000000, overline_code='^')
'C^^M^^L^^X^^X^^X^^M^X^^'
>>> get_roman(1000)
'M'
>>> get_roman(1001)
'MI'
>>> get_roman(2000)
'MM'
>>> get_roman(2001)
'MMI'
>>> get_roman(900)
'CM'
>>> get_roman(4000, overline_code='^')
'MV^'
>>> get_roman(6000, overline_code='^')
'V^M'
>>> get_roman(9000, overline_code='^')
'MX^'
>>> get_roman(6001, overline_code='^')
'V^MI'
>>> get_roman(9013, overline_code='^')
'MX^XIII'
>>> get_roman(70000000000, overline_code='^')
'L^^^X^^^X^^^'
>>> get_roman(9000013, overline_code='^')
'M^X^^XIII'
>>> get_roman(989888003, overline_code='^')
'C^^M^^L^^X^^X^^X^^M^X^^D^C^C^C^L^X^X^X^V^MMMIII'
The ``get_thousand_count`` function
--------------------------
Import statement:
>>> from romanize import get_thousand_count
Tests:
>>> get_thousand_count(13)
(13, 0, 0)
>>> get_thousand_count(6013)
(6, 1, 13)
>>> get_thousand_count(60013)
(60, 1, 13)
>>> get_thousand_count(600013)
(600, 1, 13)
>>> get_thousand_count(6000013)
(6, 2, 13)
>>> get_thousand_count(999000000000000000000000000999)
(999, 9, 999)
>>> get_thousand_count(2005)
(2, 1, 5)
>>> get_thousand_count(2147483647)
(2, 3, 147483647)
The ``core_lookup`` function
--------------------------
Import statement:
>>> from romanize import core_lookup
Tests:
>>> core_lookup(2)
('II', 0)
>>> core_lookup(6)
('V', 1)
>>> core_lookup(7)
('V', 2)
>>> core_lookup(19)
('X', 9)
>>> core_lookup(900)
('CM', 0)
>>> core_lookup(999)
('CM', 99)
>>> core_lookup(1000)
('M', 0)
>>> core_lookup(1000.2)
Traceback (most recent call last):
ValueError: Only integers, n, within range, 0 <= n <= 1000 are supported.
>>> core_lookup(10001)
Traceback (most recent call last):
ValueError: Only integers, n, within range, 0 <= n <= 1000 are supported.
>>> core_lookup(-1)
Traceback (most recent call last):
ValueError: Only integers, n, within range, 0 <= n <= 1000 are supported.
The ``gen_number_map`` function
--------------------------
Import statement:
>>> from romanize import gen_number_map
Tests:
>>> gen_number_map()
{1000: 'M', 500: 'D', 100: 'C', 50: 'L', 10: 'X', 5: 'V', 1: 'I', 0: 'N', 4: 'IV', 9: 'IX', 40: 'XL', 90: 'XC', 400: 'CD', 900: 'CM'}
The ``get_multiple`` function
--------------------------
Import statement:
>>> from romanize import get_multiple
>>> multiples = [0, 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000]
Tests:
>>> get_multiple(0, multiples)
0
>>> get_multiple(1, multiples)
1
>>> get_multiple(2, multiples)
1
>>> get_multiple(3, multiples)
1
>>> get_multiple(4, multiples)
4
>>> get_multiple(5, multiples)
5
>>> get_multiple(6, multiples)
5
>>> get_multiple(9, multiples)
9
>>> get_multiple(13, multiples)
10
>>> get_multiple(401, multiples)
400
>>> get_multiple(399, multiples)
100
>>> get_multiple(100, multiples)
100
>>> get_multiple(99, multiples)
90
The ``add_overlines`` function
--------------------------
Import statement:
>>> from romanize import add_overlines
Tests:
>>> add_overlines('AB')
'A\u0305B\u0305'
>>> add_overlines('A\u0305B\u0305')
'A\u0305\u0305B\u0305\u0305'
>>> add_overlines('AB', num_overlines=3, overline_code='^')
'A^^^B^^^'
>>> add_overlines('A^B^', num_overlines=1, overline_code='^')
'A^^B^^'
>>> add_overlines('AB', num_overlines=3, overline_code='\u0305')
'A\u0305\u0305\u0305B\u0305\u0305\u0305'
>>> add_overlines('A\u0305B\u0305', num_overlines=1, overline_code='\u0305')
'A\u0305\u0305B\u0305\u0305'
>>> add_overlines('A^B', num_overlines=3, overline_code='^')
'A^^^^B^^^'
>>> add_overlines('A^B', num_overlines=0, overline_code='^')
'A^B'