- 별칭(master_material) · 같은 성격 환산(master_pick) · 결과단위 배 읽기 셋(Store_Combo · Hopyo_Ours · master_basis) · 보이는 꼴 별칭(master_basis) 갈아 끼움 · 엑셀 맞춤(Hopyo_Match)은 master_pick 이름 그대로 이음 - 개수 묶음(개 · EA · 본 · 매) 1:1 — 본 ↔ 개 단위경고 4 줄 사라짐 · 값 같음 - 부피 ↔ 무게 = 단위중량 표 CC000337 고른 줄 받을 때만(기본 줄 없음) · 밀도 · ㎾h 경고 글만 - B08 /1000 · /40 손 식 = 실무 엑셀 G열 식이라 그대로(시험이 같은 값 대조) - 지킴 시험 test_712_2_unit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
6.6 KiB
Python
152 lines
6.6 KiB
Python
# -*- 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"(?<![\d:,.A-Za-z])(\d[\d,]*(?:\.\d+)?)?\s*({'|'.join(map(re.escape, _UNITS))})$"
|
||
)
|
||
_HANGUL = re.compile(r"[가-힣]")
|
||
_HEAD = re.compile(r"^\(\s*(?:단위\s*:\s*)?([^()]*당[^()]*)\)$") # 원문 머리 줄 「(단위: ㎥당)」
|
||
|
||
|
||
def unit(text: str) -> 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 "")
|