CODE-0004 · claude-sonnet-4-6 (high)"""
Roman numeral conversion: int_to_roman and roman_to_int.
Both use canonical (subtractive) notation for the range 1..3999.
"""
import re
# Mapping from integer value to Roman numeral symbol, in descending order.
# Subtractive pairs are included (e.g. 900=CM, 400=CD, 90=XC, 40=XL, 9=IX, 4=IV).
_INT_TO_ROMAN = [
(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"),
]
_ROMAN_TO_INT = {symbol: value for value, symbol in _INT_TO_ROMAN}
# A regex that matches exactly the canonical form of a Roman numeral in 1..3999.
# Structure: M{0,3} then hundreds then tens then ones, each using subtractive forms.
_CANONICAL_RE = re.compile(
r"^"
r"M{0,3}" # thousands: 0-3 M's
r"(CM|CD|D?C{0,3})" # hundreds
r"(XC|XL|L?X{0,3})" # tens
r"(IX|IV|V?I{0,3})" # ones
r"$"
)
def int_to_roman(n: int) -> str:
"""Convert an integer in 1..3999 to its canonical Roman numeral string.
Raises ValueError for integers outside that range.
"""
if not isinstance(n, int) or isinstance(n, bool):
raise ValueError(f"Expected an integer, got {type(n).__name__}")
if n < 1 or n > 3999:
raise ValueError(f"Integer must be in range 1..3999, got {n}")
result = []
remaining = n
for value, symbol in _INT_TO_ROMAN:
while remaining >= value:
result.append(symbol)
remaining -= value
return "".join(result)
def roman_to_int(s: str) -> int:
"""Convert a canonical Roman numeral string to its integer value.
Raises ValueError for an empty string or a malformed/non-canonical numeral.
"""
if not s:
raise ValueError("Roman numeral string must not be empty")
# Reject any string that is not a valid canonical Roman numeral.
# The regex enforces canonical form including subtractive rules and
# maximum repetition counts, and rejects non-Roman characters.
if not _CANONICAL_RE.match(s):
raise ValueError(f"Invalid or non-canonical Roman numeral: {s!r}")
# Parse left-to-right: if the current symbol has a smaller value than the
# next one, subtract it; otherwise add it. Because we have already
# validated the string, this always yields the correct result.
total = 0
prev = 0
for ch in reversed(s):
value = _ROMAN_TO_INT.get(ch)
if value is None:
raise ValueError(f"Unknown Roman numeral character: {ch!r}")
if value < prev:
total -= value
else:
total += value
prev = value
return total