- C-1 (№30): сумма с более чем двумя знаками после запятой не принимается (amount=null, string_value, is_recognized=false); confidence ячеек и шапки приводится к [0, 100], NaN и бесконечность дают 0 - F-A (№28): ошибочный файл набора при успешном понижает статус до needs_check - F-B: «ИНН не распознан» только когда ИНН не извлечён, иначе «ИНН распознан неуверенно» - №29: предупреждения «строк с нечисловым кодом пропущено: N» и «строк без кода с суммами пропущено: N» - I-3 (№31): тексты дат длиннее 200 символов не разбираются и не проходят fix_lookalikes - T1.3 (№33): загрузчик yaml отклоняет повторяющиеся ключи; тавтологичный тест дублей заменён настоящим - гигиена: докстринг amounts.py описывает фактическое поведение (все точки удаляются), мёртвые noqa в ocr/__init__.py Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
104 lines
4.8 KiB
Python
104 lines
4.8 KiB
Python
"""Разбор дат из 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 # одним символом больше — не разбирается, хотя дата в начале есть
|