"""Совпадение наименований строки формы со справочными (05-domain-rules §11.9, спека «Совпадение наименований»).""" import difflib import re from collections.abc import Iterable __all__ = ["names_match", "normalize", "section_number"] _NON_ALNUM = re.compile(r"[\W_]+") _TOTAL_PREFIX = "итого по разделу" _MIN_CONTAINED = 5 _MIN_RATIO = 0.8 _ROMAN_LATIN = str.maketrans({"і": "i", "х": "x"}) # кириллические «і», «х» -> латиница _ROMAN_HEAD = re.compile(r"[ivx]+") def normalize(text: str) -> str: """Нижний регистр, `ё` -> `е`, всё кроме букв и цифр — пробел, схлопывание, `strip`.""" return _NON_ALNUM.sub(" ", text.lower().replace("ё", "е")).strip() def section_number(normalized: str) -> str | None: """Номер раздела «Итого по разделу …» (строчными буквами i, v, x) из нормализованного имени; остаток слова — хвост OCR («v1» -> «v») — отбрасывается. Имя не «итого по разделу» или номер не выделен — `None`.""" if not normalized.startswith(_TOTAL_PREFIX): return None words = normalized[len(_TOTAL_PREFIX) :].split() if not words: return None head = _ROMAN_HEAD.match(words[0].translate(_ROMAN_LATIN)) return head.group() if head else None def _match_pair(a: str, b: str) -> bool: if a.startswith(_TOTAL_PREFIX) or b.startswith(_TOTAL_PREFIX): na, nb = section_number(a), section_number(b) return a == b if na is None or nb is None else na == nb if a == b: return True short, long_ = sorted((a, b), key=len) if len(short) >= _MIN_CONTAINED and short in long_: return True return difflib.SequenceMatcher(None, a, b).ratio() >= _MIN_RATIO def names_match(recognized: str | None, references: Iterable[str]) -> bool: """Наименование совпало хотя бы с одним справочным; пустое имя или нет справочных — `False`.""" name = normalize(recognized or "") if not name: return False return any(ref and _match_pair(name, ref) for ref in map(normalize, references))