Files
Aislo/resources/tester/test_796_3_var_names.py
T
eomsangdonandClaude Sonnet 5.5 d7e6fe5cf8 feat(801-5): M01 로직 단위 글 건설품셈 원문 꼴 — 시간당 ㎥ · ㎡ → hr · 초 → sec · mm · cm · kg → ㎜ · ㎝ · ㎏ · TON · t → ton · km/hr → ㎞/hr
- 대응표 master_unit_text.RULES(규칙마다 원문 근거 절) · 빌더 build_단위표기_801_5(--check · --list · 셈 지문 전후 같을 때만 씀)
- M01 로직 53 개 · 칸 79 개 글만 바꿈(변수 입력 · 중간 단위 · 결과단위) · 계수 · 소요량 표 값칸 · 호표 줄 단위 · 원문이 두 꼴을 다 쓰는 단위(㎞ · km · 분 · min)는 안 건드림
- M02 사본 새로 씀 도구 M02_Copy_Units(M02_Copy_Symbols 의 fill 만 바꿔 끼움) — 사본 16 문서 28 칸 · 금액 전후 같음
- 검토 목록 docs/raw/요구_1104/_목록_801-5_단위표기.xlsx
- 옛 시험 기대 고침: test_796_3 굴착기 cm 단위 초 → sec(원문 표 머리 「싸이클시간(Sec)」)
- 새 test_801_5_units 6

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:00:30 +09:00

201 lines
7.7 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.
"""796-3 변수 국문 이름 · 기호 · 첨자 · 단위 기본값 지킴 (요구 659-2 · 656-4).
① 빌더(`build_변수표_796_3.py`) — 정본에서 변수표를 걷은 상태에 돌리면 결정 자료 그대로 채움 · 채운 뒤 또 돌리면 쓸 것 없음.
② 변수표 열쇠 = 엔진 변수(입력 · 중간 이름) · 한 로직 안 기호(첨자 포함) · 이름 겹침 0.
③ 보이기만 — 변수표 · 채운 단위를 걷어도 엔진 셈 · 끝수 지문 같음(결정 줄 전부).
④ 표본 — GC001147(굴착기 8-2-3) · GC001140(인력운반 1-2-7) · GC001144(불도저 8-2-1) 기호 · 단위 = 품셈 원문 「여기서」.
"""
from __future__ import annotations
import copy
import hashlib
import importlib
import json
import re
from functools import lru_cache
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as store
meq = importlib.import_module("master_equation")
mf = importlib.import_module("master_formula")
mcut = importlib.import_module("master_cut")
ma = importlib.import_module("master_auto")
build = importlib.import_module("build_변수표_796_3")
ROOT = Path(__file__).resolve().parents[2]
BOOK = ROOT / "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문"
SIGN = re.compile(r"^[^\W\d_]\w{0,9}$") # master_equation_edit.SIGN_FORM 과 같게
SUB = re.compile(r"^[\w′'*]{1,6}$")
@lru_cache(maxsize=1)
def loaded():
return store.loaded()
def logic_rows() -> list[dict]:
return [r for d in loaded()[0].values() if d.get("그룹") == "로직" for r in d.get("줄", [])]
def raw_state() -> dict[str, dict]:
"""변수표를 걷고 결정 줄의 채운 단위를 걷은 사본(= 채우기 전) — 파일 이름 → 자료."""
want = build.decisions()
out = {}
for name, data in loaded()[0].items():
if data.get("그룹") != "로직":
out[name] = data
continue
data = copy.deepcopy(data)
for row in data["줄"]:
row.pop(meq.FIELD, None)
for key, one in (want.get(row["키"]) or {}).items():
if not one.get("단위"):
continue
for kind in ("입력", "중간"):
for spec in row.get(kind) or []:
if spec["이름"] == key and spec.get("단위") == one["단위"]:
spec.pop("단위")
out[name] = data
return out
def test_결정_자료_꼴():
want = build.decisions()
assert len(want) >= 150
names = {r["키"]: {s["이름"] for k in ("입력", "중간") for s in r[k]} for r in logic_rows()}
n = 0
for code, one in want.items():
assert code in names, code
for key, e in one.items():
n += 1
assert key in names[code], (code, key)
assert e["확신"] in ("원문", "추정"), (code, key)
assert e.get("근거"), (code, key)
assert len(e.get("이름", "")) <= 40
if e.get("기호"):
assert SIGN.match(e["기호"]), (code, key)
for f in ("위", "아래"):
assert not e.get(f) or SUB.match(e[f]), (code, key, f)
assert "원" not in e.get("단위", ""), (
code,
key,
) # 엔진이 「원」 단위를 금액 입력으로 봄
assert any(e.get(f) for f in ("이름", "기호", "아래", "위", "단위")), (code, key)
assert n >= 500
def test_빌더_채움_그리고_또_돌리면_쓸것_없음():
want = build.decisions()
batch, stat = build.plan(raw_state(), want) # 채우기 전 → 결정 그대로
assert stat["줄"] == len(want) == len(batch and [c for b in batch for c in b["changes"]])
assert stat["변수표"] == sum(
1 for one in want.values() for e in one.values() if any(e.get(f) for f in build.LOOK)
)
assert stat["이미있음"] == 0
batch2, stat2 = build.plan(loaded()[0], want) # 정본(이미 채움) → 쓸 것 없음
assert batch2 == [] and stat2["변수표"] == 0 and stat2["단위"] == 0
def test_변수표_열쇠는_엔진_변수_기호_이름_겹침_0():
for r in logic_rows():
names = {s["이름"] for k in ("입력", "중간") for s in r[k]}
assert set(r.get(meq.FIELD) or {}) <= names, r["키"]
seen: dict[str, str] = {}
for v in meq.table(r):
for word in {meq.token(v), v["이름"]}:
assert seen.get(word, v["열쇠"]) == v["열쇠"], (r["키"], word)
seen.setdefault(word, v["열쇠"])
def _print(whole, row) -> str:
try:
raw = mf.run(
whole,
str(row["키"]),
{
**ma.values(whole, row),
**{k: v for k, v in ma.defaults(whole, row).items() if v is not None},
},
)
text = json.dumps(raw, default=str, sort_keys=True, ensure_ascii=False)
text += json.dumps(
mcut.engine(whole, str(row["키"]), raw), default=str, sort_keys=True, ensure_ascii=False
)
except Exception as e: # 멈춤도 지문
text = f"ERR {type(e).__name__} {e}"
return hashlib.sha256(text.encode()).hexdigest()[:16]
def test_셈_전후_같음_결정_줄_전부():
want = build.decisions()
now = loaded()[1]
before_files = raw_state()
before = mf.Master(before_files)
for code in want:
row_now = now.logic(code)
row_before = before.logic(code)
assert meq.FIELD not in row_before
assert _print(now, row_now) == _print(before, row_before), code
def _table(code: str) -> dict[str, dict]:
return {v["열쇠"]: v for v in meq.table(loaded()[1].logic(code))}
def test_표본_기호_단위_원문_같음():
excav = (BOOK / "제08장_건설기계/8-02-03_굴착기.md").read_text(encoding="utf-8")
for line in (
"q : 버킷용량(㎥)",
"E : 작업효율",
"K : 버킷계수",
"f : 체적환산계수",
"cm : 1회 싸이클 시간(초)",
):
assert line in excav, line
t = _table("GC001147")
assert (t["버킷용량"]["기호"], t["버킷용량"]["단위"]) == ("q", "㎥")
assert (t["K"]["이름"], t["E"]["이름"], t["f"]["이름"]) == (
"버킷계수",
"작업효율",
"체적환산계수",
)
assert (t["cm"]["이름"], t["cm"]["단위"]) == ("1회 싸이클시간", "sec") # 801-5 원문 표 머리 「싸이클시간(Sec)」 꼴
carry = (BOOK / "제01장_적용기준/1-02_설계_및_수량.md").read_text(encoding="utf-8")
for line in (
"T : 1일 실작업시간",
"L : 운반거리(m)",
"t : 적재적하 시간(분)",
"V : 평균왕복속도(m/hr)",
"N : 1일 운반횟수",
):
assert line in carry, line
t = _table("GC001140")
got = {
k: meq.token(t[k])
for k in ("운반거리", "실작업시간", "적재적하시간", "평균왕복속도", "일운반회수")
}
assert got == {
"운반거리": "L",
"실작업시간": "T",
"적재적하시간": "t",
"평균왕복속도": "V",
"일운반회수": "N",
}
dozer = (BOOK / "제08장_건설기계/8-02-01_불도저.md").read_text(encoding="utf-8")
for line in ("V_1 : 전진속도(m/분)", "V_2 : 후진속도(m/분)", "e : 운반거리계수"):
assert line in dozer, line
t = _table("GC001144")
assert (t["V1"]["기호"], t["V1"]["아래"], t["V1"]["이름"]) == ("V", "1", "전진속도")
assert (t["V2"]["기호"], t["V2"]["아래"], t["V2"]["이름"]) == ("V", "2", "후진속도")
assert meq.token(t["V1"]) == "V_1" and t["e"]["이름"] == "운반거리계수"
if __name__ == "__main__":
pytest.main([__file__, "-q"])