AURORA-1734, AURORA-1735: разрез таблицы по смене формы и редкий столбец, 0.1.5 #6

Merged
aep merged 5 commits from feat/5-form-split into main 2026-11-07 09:20:45 +03:00
5 changed files with 36 additions and 6 deletions
Showing only changes of commit 01968eb121 - Show all commits

View File

@ -6,7 +6,7 @@
`min_codes` кодов которого — другой формы, начинает новую часть со своими колонками и периодами (`locator` с хвостом
`;form:<k>`, `type_hint = None`). Хост вызывает разрез перед `normalize_candidate`; без вызова поведение прежнее.
- Редкий столбец с неразобранной подписью между колонками периодов (число не более чем в одной строке с кодом при трёх и
более у каждой колонки периода) — не колонка сумм; его числа — в `NUMBERS_OUTSIDE_TABLE` с текстом «числа вне колонок
более у каждой колонки периода и числах всех колонок периода в строке его числа) — не колонка сумм; его числа — в `NUMBERS_OUTSIDE_TABLE` с текстом «числа вне колонок
формы не учитываются» (AURORA-1735).
## 0.1.4 — 2026-11-06

View File

@ -177,12 +177,16 @@ def _is_sparse( # noqa: PLR0913, PLR0917 - внутренняя проверк
source: Source,
rules: Rules,
) -> bool:
"""Редкий столбец: колонки периода левее и правее, у него ≤ 1 числа, у каждой колонки периода ≥ 3."""
"""Редкий столбец: колонки периода левее и правее, у него ≤ 1 числа, у каждой колонки периода ≥ 3, и в строке его
числа у всех колонок периода тоже есть числа — число лишнее, а не сдвинутое OCR из пустой ячейки периода."""
if not any(c < col for c in dated) or not any(c > col for c in dated):
return False
if any(count < _PERIOD_MIN_NUMBERS for count in dated.values()):
return False
return _number_count(rows, data_rows, col, code_col, source, rules) <= _SPARSE_MAX_NUMBERS
hits = [r for r in _code_rows(rows, data_rows, code_col, rules) if _is_number(cell_at(rows, r, col), source, rules)]
if len(hits) > _SPARSE_MAX_NUMBERS:
return False
return all(_is_number(cell_at(rows, r, period), source, rules) for r in hits for period in dated)
def _last_dated_column( # noqa: PLR0913, PLR0917 - внутренняя сборка границы таблицы

View File

@ -8,10 +8,14 @@
from dataclasses import replace
from arm_statements.codes import document_type_of, majority_section, section_of
from arm_statements.issues import WarningCode
from arm_statements.loader import Rules, default_rules
from arm_statements.model import DocumentType, Evidence, FormCandidate
from arm_statements.segments import split_segments
# предупреждения источника, нужные каждой части: без числовых форматов xlsb шаг 4 иначе проверяет масштаб (ревью I-2)
_SOURCE_WARNINGS = frozenset({WarningCode.XLSB_NO_FORMATS})
def _opening_type(codes: tuple[str, ...], rules: Rules) -> DocumentType | None:
"""Тип формы, которой сегмент открывается: первые `min_codes` кодов одной формы; иначе `None`."""
@ -42,7 +46,8 @@ def split_by_form(candidate: FormCandidate, rules: Rules | None = None) -> tuple
"""Части кандидата по смене формы; без смены — `(candidate,)` тем же объектом.
Первая часть сохраняет поля исходного кандидата (кроме строк и границ листов); часть `k` (с 1) — `locator`
с хвостом `;form:<k>`, `type_hint = None`, пустые `evidence` и `warnings`: тип — только по кодам на шаге 6.
с хвостом `;form:<k>`, `type_hint = None`, пустые `evidence`, из `warnings` — только предупреждения источника
(`XLSB_NO_FORMATS`): тип — только по кодам на шаге 6.
"""
rules = rules if rules is not None else default_rules()
cuts = _cuts(candidate, rules)
@ -64,7 +69,7 @@ def split_by_form(candidate: FormCandidate, rules: Rules | None = None) -> tuple
evidence=Evidence(okud=None, title=None),
rows=rows,
segment_breaks=breaks,
warnings=(),
warnings=tuple(w for w in candidate.warnings if w.code in _SOURCE_WARNINGS),
)
)
return tuple(parts)

View File

@ -257,3 +257,15 @@ def test_sparse_column_without_period_to_the_right_stays(rules: Rules) -> None:
assert doc is not None
assert [c.column_index for c in doc.columns] == [2, 3, 4]
assert SPARSE_COL in [w.column_index for w in doc.warnings if w.code == "COLUMN_WITHOUT_DATE"]
def test_shifted_value_in_sparse_column_keeps_column(rules: Rules) -> None:
"""Единственное число столбца стоит в строке, где колонка периода пуста, — это сдвиг OCR, не мусор (ревью I-1)."""
rows = kaltan_rows()
rows[5][3] = None # без постороннего «$301»: единственное число столбца — сдвинутое значение 2460
rows.insert(5, ["Прочее", "2460", "12", "55", None])
doc = normalize_candidate(ocr(rows), rules).document
assert doc is not None
assert [c.column_index for c in doc.columns] == [2, 3, 4]
assert SPARSE_COL in [w.column_index for w in doc.warnings if w.code == "COLUMN_WITHOUT_DATE"]
assert doc.status is DocStatus.NEEDS_CHECK

View File

@ -2,7 +2,7 @@
from datetime import date
from arm_statements import normalize_candidate, split_by_form
from arm_statements import WarningCode, make_warning, normalize_candidate, split_by_form
from arm_statements.loader import Rules
from arm_statements.model import DocumentType, Evidence
from tests.builders import excel, hdr, ocr
@ -102,3 +102,12 @@ def test_excel_breaks_shifted(rules: Rules) -> None:
assert inc.segment_breaks == (0, len(income_rows()))
assert inc.locator == f"{cand.locator};form:1"
assert len(inc.rows) == len(income_rows()) + len(continuation)
def test_xlsb_source_warning_kept_in_every_part(rules: Rules) -> None:
"""`XLSB_NO_FORMATS` — свойство источника, нужно каждой части для проверки масштаба (ревью I-2)."""
no_formats = make_warning(WarningCode.XLSB_NO_FORMATS, "xlsb без числовых форматов", rules)
cand = excel([("Б", balance_rows()), ("О", income_rows())], header=HEADER, warnings=[no_formats])
bal, inc = split_by_form(cand, rules)
assert bal.warnings == (no_formats,)
assert inc.warnings == (no_formats,)