Files
Aislo/resources/master_data/scripts/master_basis.py
T
eomsangdonandClaude Opus 5.5 881c204ca8 feat(712-2): 단위 한 벌 master_unit — 별칭 · 개수 묶음 · 결과단위 배 읽기 · 같은 성격 환산 · 근거 글
- 별칭(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>
2026-11-01 16:03:54 +09:00

152 lines
6.6 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.
# -*- 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 "")