- 대응표 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>
201 lines
7.7 KiB
Python
201 lines
7.7 KiB
Python
"""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"])
|