Andrey Epifancev 897dfba360 AURORA-1734: split_by_form — разрез кандидата по смене формы (#5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:00:02 +04:00

71 lines
3.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.

"""Разрез кандидата по смене формы до шагов 2–8 (спека «Шаг 2 — разрез кандидата по смене формы», AURORA-1734).
ИС Сканирование может вернуть ББ и ОФР одной таблицей: шапка ОФР внутри таблицы — повторная шапка, и без разреза строки
ОФР попадают в ББ под его колонками. Часть начинается с сегмента со своей шапкой, первые `min_codes` кодов которого —
одной формы, другой, чем у текущей части (тип части — по большинству кодов её первого сегмента с кодами).
"""
from dataclasses import replace
from arm_statements.codes import document_type_of, majority_section, section_of
from arm_statements.loader import Rules, default_rules
from arm_statements.model import DocumentType, Evidence, FormCandidate
from arm_statements.segments import split_segments
def _opening_type(codes: tuple[str, ...], rules: Rules) -> DocumentType | None:
"""Тип формы, которой сегмент открывается: первые `min_codes` кодов одной формы; иначе `None`."""
head = codes[: rules.code_ranges.min_codes]
if len(head) < rules.code_ranges.min_codes:
return None
kinds = {None if (s := section_of(code, rules)) is None else document_type_of(s) for code in head}
return kinds.pop() if len(kinds) == 1 else None
def _cuts(candidate: FormCandidate, rules: Rules) -> list[int]:
"""Строки сетки, с которых начинаются части после первой."""
cuts: list[int] = []
current: DocumentType | None = None
for segment in split_segments(candidate, rules):
if current is None:
section = majority_section(list(segment.codes), rules)
current = None if section is None else document_type_of(section)
continue
opening = _opening_type(segment.codes, rules)
if segment.has_header and opening is not None and opening is not current and segment.first > 0:
cuts.append(segment.first)
current = opening
return cuts
def split_by_form(candidate: FormCandidate, rules: Rules | None = None) -> tuple[FormCandidate, ...]:
"""Части кандидата по смене формы; без смены — `(candidate,)` тем же объектом.
Первая часть сохраняет поля исходного кандидата (кроме строк и границ листов); часть `k` (с 1) — `locator`
с хвостом `;form:<k>`, `type_hint = None`, пустые `evidence` и `warnings`: тип — только по кодам на шаге 6.
"""
rules = rules if rules is not None else default_rules()
cuts = _cuts(candidate, rules)
if not cuts:
return (candidate,)
bounds = [0, *cuts, len(candidate.rows)]
parts: list[FormCandidate] = []
for k, (start, end) in enumerate(zip(bounds, bounds[1:], strict=False)):
breaks = tuple(b - start for b in candidate.segment_breaks if start <= b < end)
rows = candidate.rows[start:end]
if k == 0:
parts.append(replace(candidate, rows=rows, segment_breaks=breaks))
else:
parts.append(
replace(
candidate,
locator=f"{candidate.locator};form:{k}",
type_hint=None,
evidence=Evidence(okud=None, title=None),
rows=rows,
segment_breaks=breaks,
warnings=(),
)
)
return tuple(parts)