"""Разрез кандидата по смене формы до шагов 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:`, `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)