44 lines
1.1 KiB
Python
44 lines
1.1 KiB
Python
"""Roman numeral parsing helpers."""
|
||
|
||
import re
|
||
|
||
_ROMAN_VALID_RE = re.compile(
|
||
r"^M{0,3}(CM|CD|D?C{0,3})"
|
||
r"(XC|XL|L?X{0,3})"
|
||
r"(IX|IV|V?I{0,3})$"
|
||
)
|
||
_ROMAN_TOKEN_RE = re.compile(r"(?<![A-Za-zА-Яа-яЁё0-9])[IVXLCDMivxlcdm]+(?![A-Za-zА-Яа-яЁё0-9])")
|
||
_ROMAN_VALUES = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
|
||
|
||
|
||
def roman_to_int(token: str) -> int | None:
|
||
if not token:
|
||
return None
|
||
|
||
roman = token.strip().upper()
|
||
if not roman or not _ROMAN_VALID_RE.fullmatch(roman):
|
||
return None
|
||
|
||
total = 0
|
||
prev = 0
|
||
for char in reversed(roman):
|
||
value = _ROMAN_VALUES[char]
|
||
if value < prev:
|
||
total -= value
|
||
else:
|
||
total += value
|
||
prev = value
|
||
return total
|
||
|
||
|
||
def replace_roman_numerals(text: str) -> str:
|
||
if not text:
|
||
return text
|
||
|
||
def _repl(match: re.Match) -> str:
|
||
token = match.group(0)
|
||
value = roman_to_int(token)
|
||
return str(value) if value is not None else token
|
||
|
||
return _ROMAN_TOKEN_RE.sub(_repl, text)
|