CODE-0004 · claude-sonnet-5 (high)
import re

_ROMAN_PATTERN = re.compile(
    r'^(M{0,3})(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
)

_VALUES = {
    'M': 1000, 'CM': 900, 'CD': 400, 'D': 500, 'C': 100,
    'XC': 90, 'XL': 40, 'L': 50, 'X': 10,
    'IX': 9, 'IV': 4, 'V': 5, 'I': 1,
}


def int_to_roman(n):
    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 between 1 and 3999 inclusive")

    table = [
        (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'),
    ]

    result = []
    remaining = n
    for value, symbol in table:
        while remaining >= value:
            result.append(symbol)
            remaining -= value

    return ''.join(result)


def roman_to_int(s):
    if not isinstance(s, str) or not s:
        raise ValueError("Input must be a non-empty string")

    match = _ROMAN_PATTERN.match(s)
    if not match:
        raise ValueError(f"Invalid Roman numeral: {s!r}")

    thousands, hundreds, tens, units = match.groups()

    total = 0
    for group in (thousands, hundreds, tens, units):
        if not group:
            continue
        if group in _VALUES:
            total += _VALUES[group]
        else:
            for ch in group:
                total += _VALUES[ch]

    if total == 0:
        raise ValueError(f"Invalid Roman numeral: {s!r}")

    return total