Andrey Epifancev 5f56f62dbb feat(normalizer): разбор дат из OCR — дословный перенос date_utils старого ядра (§11.1 п. 2)
Портирован модуль date_utils.py из DKB_ARMKA с перечисленными в §11.1 правками:
- удалены _LOOKALIKES, _LOOKALIKE_CHARS, _NEAR_DIGIT, _normalize (перенесены в text.fix_lookalikes)
- переименована _parse_date в parse_date с новой сигнатурой (value: object -> date | None)
- обновлены аннотации типов: Optional[...] -> ... | None
- добавлена аннотация для _PATTERNS: list[tuple[re.Pattern[str], Callable[[re.Match[str]], date | None]]]
- добавлены noqa/type: ignore комментарии для прохода линтеров

Тесты: 35 тестов пройдены (парсинг дат в текстовых и числовых формах, отклонение названий организаций).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 00:06:14 +04:00

71 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Разбор дат из OCR-текста (§11.1 п. 2): дословный перенос `date_utils` старого ядра, вход — после `fix_lookalikes`."""
from datetime import date
import pytest
from src.normalizer.ocr.dates import parse_date
from src.normalizer.ocr.text import fix_lookalikes
LOOKALIKES = {"О": "0", "о": "0", "З": "3", "l": "1", "I": "1", "|": "1", "б": "6", "S": "5", "Ч": "4"}
@pytest.mark.parametrize(
("raw", "expected"),
[
("на 30 сентября 2025 г.", date(2025, 9, 30)),
("На 30 июня\n2025 г", date(2025, 6, 30)),
("31 декабря 2024 года", date(2024, 12, 31)),
("30 сентября 2025 r.", date(2025, 9, 30)), # латинская r вместо г
("с 01.01.2025 по 30.09.2025", date(2025, 9, 30)), # диапазон — конец
("I квартал 2026", date(2026, 3, 31)),
("1-й кв. 2026", date(2026, 3, 31)),
("первый квартал 2025 г.", date(2025, 3, 31)),
("1 полугодие 2026", date(2026, 6, 30)),
("1 п/г 2026", date(2026, 6, 30)),
("9 месяцев 2025", date(2025, 9, 30)),
("9 мес. 2025", date(2025, 9, 30)),
("За январь - июнь\n2025 г.", date(2025, 6, 30)),
("январь – сентябрь 2025", date(2025, 9, 30)),
("за 2025 г.", date(2025, 12, 31)),
("2024 год", date(2024, 12, 31)),
("30.09.2025", date(2025, 9, 30)),
("2025 03 31", date(2025, 3, 31)),
("30/09/25", date(2025, 9, 30)),
("2 0 2 4", date(2024, 12, 31)),
("30 09 2 0 2 5", date(2025, 9, 30)),
("30092025", date(2025, 9, 30)),
("20250930", date(2025, 9, 30)),
("30 09 25", date(2025, 9, 30)),
("30.09.2025 0:00:00", date(2025, 9, 30)),
("2|0|2|4|", date(2024, 12, 31)), # через fix_lookalikes
("3l.12.2О24", date(2024, 12, 31)),
],
)
def test_parses_real_forms(raw: str, expected: date) -> None:
assert parse_date(fix_lookalikes(raw, LOOKALIKES)) == expected
@pytest.mark.parametrize(
"raw",
[
"Общество с ограниченной ответственностью", # Review Focus 3: название организации — не дата
"ЗАКРЫТОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО",
"",
"+",
"тыс. руб.",
"1999999999999", # 13 цифр — ни одна форма
],
)
def test_organization_name_is_not_a_date(raw: str) -> None:
assert parse_date(fix_lookalikes(raw, LOOKALIKES)) is None
def test_non_string_is_none() -> None:
assert parse_date(None) is None
assert parse_date(20250930) is None
def test_year_out_of_range_is_none() -> None:
assert parse_date("1 2 3 4") is None # 1234 вне 1990–2100