CODE-0004 · gpt-5.4-mini (high)
_ROMAN_PAIRS = (
    (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_DIGIT_VALUES = {
    "I": 1,
    "V": 5,
    "X": 10,
    "L": 50,
    "C": 100,
    "D": 500,
    "M": 1000,
}


def int_to_roman(n):
    if not isinstance(n, int) or isinstance(n, bool) or not (1 <= n <= 3999):
        raise ValueError

    parts = []
    for value, symbol in _ROMAN_PAIRS:
        count, n = divmod(n, value)
        if count:
            parts.append(symbol * count)
    return "".join(parts)


def roman_to_int(s):
    if not isinstance(s, str) or not s:
        raise ValueError

    total = 0
    i = 0
    try:
        while i < len(s):
            current = _ROMAN_DIGIT_VALUES[s[i]]
            if i + 1 < len(s):
                nxt = _ROMAN_DIGIT_VALUES[s[i + 1]]
                if current < nxt:
                    total += nxt - current
                    i += 2
                    continue
            total += current
            i += 1
    except KeyError:
        raise ValueError from None

    if not (1 <= total <= 3999) or int_to_roman(total) != s:
        raise ValueError
    return total