# -*- coding: utf-8 -*- """표 기준 → 「기준배 · 기준단위」 — **읽을 때 파생**(정본 표 파일에 쓰지 않음 · PLAN 712-3). - 표 `기준` 글(「10㎡당」 · 「인/100본당」 · 「ton 당」)을 배 · 단위로 읽음. - `기준` 이 비었으면 원문 절 머리(「(단위: ㎥당)」 · 「(일당)」)에서 — 절과 아래 절 머리가 한 꼴일 때만. - 단위가 아닌 기준(「인력품」 · 「할증」 · 「정의」)은 `갈래` 「비단위」 · 못 읽음은 「모름」. - 배 대조(`check_times`) = 로직 결과단위 배(원/10㎡ → 10) ↔ 그 로직이 부르는 표 기준배 (`check_master` 「배」). """ from __future__ import annotations import re from decimal import Decimal from functools import lru_cache import master_unit as mu from check_master_body import section_of _UNITS = sorted( ( "㎡ ㎥ ㎢ ㎞ km ㎜ mm ㎝ cm m ha ton 톤 t kg ㎏ 개소 개 본 대 회 조 기 틀 " "일 시간 hr 분 min 초 매 주 도엽 도곽 필지 점 발파 인 노선 공 경간 련 식 " "개월 월 년 가닥 면 장 측점 세트 set 호 층 단계 ea" ).split(), key=len, reverse=True, ) _UNIT_AT_END = re.compile( rf"(? str: return mu.shown(text) # 보이는 꼴 별칭 한 벌(712-2 `master_unit`) def _times(text: str | None) -> Decimal: return Decimal(text.replace(",", "")) if text else Decimal(1) def parse(text) -> dict | None: """기준 글 → `{배, 단위}` · 단위 아닌 글이면 None.""" core = re.split(r"\s+[—-]\s+", str(text or "").strip())[0] core = re.sub(r"\([^()]*\)", "", core).strip() if not core or core.startswith("단위"): # 「단위: 인」 = 값 칸 단위(기준 아님) return None if re.search( r",\s|[∼~]", core ): # 「구하는점 5점, 주어진점 3점」 · 「400필지∼450필지」 = 조건 글 return None core = core.rsplit("/", 1)[-1].strip() # 「인/100본당」 → 「100본당」(자원/기준) if "당" in core: # 「1필지당 1,500㎡」 · 「1대당 높이 2m 기준」 → 「당」 앞까지 core = core[: core.rfind("당")].strip() m = _UNIT_AT_END.search(core) if not m: return None one, at = m.group(2), m.start(2) if len(one) == 1 and _HANGUL.match(one) and _HANGUL.match(core[at - 1 : at] or " "): return None # 「참조」 의 「조」 — 한 글자 한글 단위가 낱말 끝이면 단위 아님 return {"배": _times(m.group(1)), "단위": unit(m.group(2))} @lru_cache(maxsize=None) def _heads(source: str) -> tuple[str, ...]: """원문 절(과 아래 절) 머리 줄 글 — 차례대로 · 겹침 뺌.""" _, _, text = section_of(source) found = (_HEAD.match(line.strip()) for line in text.split("\n")) return tuple(dict.fromkeys(m.group(1).strip() for m in found if m)) def basis(table: dict) -> dict: """표 하나의 파생 칸 — `{기준배, 기준단위, 기준근거, 갈래}`(갈래 = 단위 · 비단위 · 모름).""" said = str(table.get("기준") or "").strip() if said: got = parse(said) if got: return {"기준배": got["배"], "기준단위": got["단위"], "기준근거": "표", "갈래": "단위"} return {"기준배": None, "기준단위": None, "기준근거": "표", "갈래": "비단위"} heads = [h for h in _heads(str(table.get("출처") or "")) if parse(h)] picks = {(parse(h)["배"], parse(h)["단위"]) for h in heads} if len(picks) == 1: # 절 머리가 한 꼴일 때만 — 둘 넘으면 손 확인 times, one = picks.pop() return { "기준배": times, "기준단위": one, "기준근거": f"원문 「{heads[0]}」", "갈래": "단위", } return {"기준배": None, "기준단위": None, "기준근거": None, "갈래": "모름"} def result_per(result_unit) -> list[tuple[Decimal, str]]: """로직 결과단위 분모 — 「원/10㎡」 → [(10, ㎡)] · 여럿이면 여럿.""" return [(n, unit(u)) for n, u in mu.per(result_unit)] def _formula_text(row: dict) -> str: return str([row.get(k) for k in ("중간", "호표", "덧줄")]) def _divides(text: str, times: Decimal) -> bool: """식이 그 배로 나누거나 곱하는가 — 「/ 10」 · 「÷10」 · 「* 0.1」.""" n = format(times.normalize(), "f") inverse = format((1 / times).normalize(), "f") pattern = rf"(?:[/÷]\s*{re.escape(n)}|\*\s*{re.escape(inverse)})(?![\d.])" return re.search(pattern, text) is not None def check_times(files: dict[str, dict]) -> list[str]: """배 어긋남 — 로직이 부르는 표(`용도.로직키`)의 기준배 ≠ 1 인데 결과단위가 같은 배가 아니고 식에서도 그 배로 나누지 않는 짝(시스템 관리자 원문 확인 거리).""" logic_rows = { str(r.get("키")): r for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄") or [] } out = [] for name, data in files.items(): for table in data.get("표") or []: got = basis(table) if got["갈래"] != "단위" or got["기준배"] == 1: continue for key in (table.get("용도") or {}).get("로직키") or []: row = logic_rows.get(str(key)) if row is None: continue per = result_per(row.get("결과단위")) shown = format(got["기준배"].normalize(), "f") said = str(row.get("결과단위") or "") # 「원/보양길이 100m」 if ( (got["기준배"], got["기준단위"]) in per or f"{shown}{got['기준단위']}" in said or _divides(_formula_text(row), got["기준배"]) ): continue out.append( f"{key} · 결과단위 「{row.get('결과단위')}」 ↔ 표 {table.get('키')}" f" 기준 「{shown}{got['기준단위']}」 — 배 어긋남" ) return out def edition(text) -> str: """파일 머리 판 → 해(「2026-01-01」 · 「2026」 → 「2026」) · 해 없으면 그대로(파일 안 씀).""" m = re.match(r"\s*(\d{4})(?:\D|$)", str(text or "")) return m.group(1) if m else str(text or "")