"""Разбор дат из OCR-текста (§11.1 п. 2): дословный перенос `date_utils` старого ядра, вход — после `fix_lookalikes`.""" import time from datetime import date import pytest from src.normalizer.ocr.dates import MAX_DATE_TEXT_LENGTH, is_date_sized, 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 class TestLengthCap: """Решение №31: текст длиннее `MAX_DATE_TEXT_LENGTH` не разбирается; патологические строки не вешают процесс.""" PATHOLOGICAL = { "spaces-then-letter": "1" + " " * 4000 + "b", "newlines": "1" + "\n" * 4000, # после strip — «1»: короткий текст, отсекается не длиной "newlines-inside": "1" + "\n" * 4000 + "b", "lookalikes-after-digit": "1" + "О" * 5000, } @pytest.mark.parametrize("raw", PATHOLOGICAL.values(), ids=PATHOLOGICAL.keys()) def test_pathological_text_is_fast_and_none(self, raw: str) -> None: start = time.perf_counter() assert parse_date(raw) is None assert parse_date(fix_lookalikes(raw, LOOKALIKES)) is None if is_date_sized(raw) else True assert time.perf_counter() - start < 1.0 def test_long_text_is_not_date_sized(self) -> None: assert not is_date_sized("1" + " " * 4000 + "b") and not is_date_sized("1" + "О" * 5000) def test_boundary(self) -> None: date_text = "на 30 сентября 2025 г." padded = date_text + " " * 300 # пробелы по краям не считаются assert is_date_sized(padded) and parse_date(padded) == date(2025, 9, 30) at_limit = date_text + "x" * (MAX_DATE_TEXT_LENGTH - len(date_text)) assert len(at_limit) == MAX_DATE_TEXT_LENGTH and is_date_sized(at_limit) assert parse_date(at_limit) == date(2025, 9, 30) # на пределе дата ещё разбирается over = date_text + "x" * (MAX_DATE_TEXT_LENGTH + 1 - len(date_text)) assert len(over) == MAX_DATE_TEXT_LENGTH + 1 and not is_date_sized(over) assert parse_date(over) is None # одним символом больше — не разбирается, хотя дата в начале есть