CODE-0004 · claude-opus-4-8 (high)"""Convert between integers and canonical (subtractive) Roman numerals."""
# Ordered from largest to smallest, including the six subtractive forms.
_VALUES = (
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
)
def int_to_roman(n):
"""Convert an integer in 1..3999 to its canonical Roman numeral string.
Raises ValueError for any integer outside 1..3999.
"""
if not isinstance(n, int) or isinstance(n, bool):
raise ValueError("n must be an integer")
if n < 1 or n > 3999:
raise ValueError("n must be in the range 1..3999")
result = []
for value, symbol in _VALUES:
count, n = divmod(n, value)
result.append(symbol * count)
return "".join(result)
def roman_to_int(s):
"""Convert a canonical Roman numeral string to its integer value.
Raises ValueError for an empty string, or a malformed or non-canonical
numeral (for example "IIII", "VX", or "ABC").
"""
if not isinstance(s, str):
raise ValueError("s must be a string")
if s == "":
raise ValueError("empty string is not a valid Roman numeral")
# Greedily consume the canonical symbols from largest to smallest. A
# canonical numeral matches this greedy parse exactly; anything that does
# not consume the whole string (or uses a forbidden run length) is invalid.
value = 0
pos = 0
for amount, symbol in _VALUES:
# The additive symbols (M, C, X, I) may repeat at most three times;
# the others (D, L, V) and the subtractive pairs at most once. Cap the
# match count so a run like "IIII" or "VV" fails canonicality.
max_repeat = 3 if symbol in ("M", "C", "X", "I") else 1
count = 0
while s.startswith(symbol, pos) and count < max_repeat:
pos += len(symbol)
value += amount
count += 1
if pos != len(s):
raise ValueError("malformed or non-canonical Roman numeral: %r" % s)
return value