- M02_San_Expr — eval 없는 식 나무 · 셈 · 줄 자리 규칙(버림 · 반올림 · 올림 × 0~3) - M02_San_Stmate — 숨은 G 열 원문 → 줄 나무(계 5 단 · (-=) · 대입 · [A] · ;; · &L/M/S/X/D& · &P/R& 율 · :=A01 · -> · // 덧글) - M02_San_Est — 보이는 글 → 식 복원(단가 · 앞줄 · 상수 역할 · 값 줄 숨은 합 · 변수 얼림 · 단가 정체 · 모호 표지) - M02_San_Eval — STmate 셈(특수 % · 끝전 두 갈래 · 결과금액 조정 · 계약단가) · EST 다시 셈 · 줄마다 검산 · 단가 전파 - M02_San_Book — 같은 책 블록 · 단가표 읽기(읽기만) - test_793_1 — STmate 233/233 · 금액 줄 1,315 · 계 줄 695 · EST 222/222 · 줄 2,901/2,903 · 모호 9 · 전파 견본 7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""793-1 지킴 — 경쟁사 산근 글 식 파서 · 참조 평가기(`M02_San_*`) · 실무 여덟 파일 금액 그대로.
|
||
|
||
STmate = 숨은 G 열 원문 식 → 233/233 블록 · 금액 줄 1,315 · 계표시 줄 695 다 맞음.
|
||
EST = 보이는 글 → 식 복원 → 222/222 블록 · 돈 · 값 줄 2,901/2,903 · 모호 단가 토큰 9.
|
||
단가 바뀜 전파 견본 = `_분석_649-G2` 4 장(보통인부 → 180,000).
|
||
엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만) · 없으면 skip(다른 PC).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from functools import lru_cache
|
||
|
||
import helper_m02_hopyo_golden as golden
|
||
import pytest
|
||
|
||
from M02_MasterTemplete import M02_San_Book as book
|
||
from M02_MasterTemplete import M02_San_Est as est
|
||
from M02_MasterTemplete import M02_San_Eval as ev
|
||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||
|
||
# 파일: (블록, 금액 줄, 계표시 줄, 끝전)
|
||
STMATE = {
|
||
"봉화": (62, 349, 184, "비목버림"),
|
||
"동부청영월": (69, 411, 230, "합버림_큰나머지"),
|
||
"울진대흥": (47, 261, 128, "비목버림"),
|
||
"영덕계류": (15, 86, 41, "비목버림"),
|
||
"울진소광": (40, 208, 112, "비목버림"),
|
||
}
|
||
# 파일: (블록, 돈 · 값 줄, 틀린 줄 상한, 모호 토큰)
|
||
EST = {"북부청임도": (94, 1252, 2, 0), "북부청사방": (67, 849, 0, 7), "거창": (61, 802, 0, 2)}
|
||
LABOR = ("노임", "보통인부", "", "L")
|
||
|
||
|
||
@lru_cache(maxsize=8)
|
||
def _load(tag: str) -> dict:
|
||
path = golden.FILES[tag]
|
||
if not path.exists():
|
||
pytest.skip(f"실무 엑셀 없음 {path.name}")
|
||
return book.load(path)
|
||
|
||
|
||
def _stmate_runs(tag: str) -> list[tuple]:
|
||
data = _load(tag)
|
||
out = []
|
||
for blk in data["블록"]:
|
||
if not any(ln["G"].strip() for ln in blk["몸"]):
|
||
continue
|
||
res = ev.stmate_block(blk, stm.parse_block(blk["몸"]), data["단가"], STMATE[tag][3])
|
||
out.append((blk, res, ev.stmate_check(blk, res)))
|
||
return out
|
||
|
||
|
||
@pytest.mark.parametrize("tag", list(STMATE))
|
||
def test_stmate_blocks_equal_excel(tag: str) -> None:
|
||
blocks, amount_lines, total_lines, _ = STMATE[tag]
|
||
runs = _stmate_runs(tag)
|
||
assert len(runs) == blocks
|
||
wrong = [b["코드"] for b, _, c in runs if not c["맞음"]]
|
||
assert not wrong, wrong
|
||
assert sum(c["금액줄"] for _, _, c in runs) == amount_lines
|
||
assert [x for _, _, c in runs for x in c["틀린줄"]] == []
|
||
assert sum(c["계줄"] for _, _, c in runs) == total_lines
|
||
assert sum(c["계줄맞음"] for _, _, c in runs) == total_lines
|
||
|
||
|
||
@pytest.mark.parametrize("tag", list(EST))
|
||
def test_est_blocks_equal_excel(tag: str) -> None:
|
||
blocks, lines, bad_max, vague = EST[tag]
|
||
data = _load(tag)
|
||
n, wrong, got_lines, got_bad, got_vague = 0, [], 0, 0, 0
|
||
for blk in data["블록"]:
|
||
want = data["총괄"].get(blk["번"])
|
||
if not want or not (want["T"] or any(want[c] for c in "LMS")):
|
||
continue
|
||
parsed = est.parse_block(blk["글"], data["단가"])
|
||
chk = ev.est_check(parsed, want)
|
||
n += 1
|
||
if not chk["맞음"]:
|
||
wrong.append(blk["번"])
|
||
got_lines += chk["줄수"]
|
||
got_bad += len(chk["틀린줄"])
|
||
got_vague += sum(1 for L in parsed for r in (L.get("역할") or {}).values() if r.get("모호"))
|
||
assert (n, wrong) == (blocks, [])
|
||
assert got_lines == lines
|
||
assert got_bad <= bad_max
|
||
assert got_vague == vague
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"tag, code, labor, before, after",
|
||
[
|
||
("봉화", "D00102", "L00007", (34119, 11192, 341), (34697, 11192, 346)),
|
||
("동부청영월", "D00003", "L00002", (4764, 310, 397), (5097, 309, 397)),
|
||
("울진소광", "D01489", "L00016", (182223, 230359, 8152), (185067, 230359, 8209)),
|
||
],
|
||
)
|
||
def test_stmate_price_propagates(tag, code, labor, before, after) -> None:
|
||
data = _load(tag)
|
||
blk = next(b for b in data["블록"] if b["코드"] == code)
|
||
nodes = stm.parse_block(blk["몸"])
|
||
rule = STMATE[tag][3]
|
||
prices = dict(data["단가"])
|
||
prices[labor] = {**prices[labor], "T": 180000, "L": 180000}
|
||
for p, want in ((data["단가"], before), (prices, after)):
|
||
got = ev.stmate_block(blk, nodes, p, rule)["계"]
|
||
assert (got["L"], got["M"], got["S"]) == want
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"tag, no, after",
|
||
[
|
||
("북부청사방", 31, (14877, 5491, 0)),
|
||
("북부청임도", 2, (22774, 3235, 3830)),
|
||
("거창", 1, (3485, 832, 1049)),
|
||
("북부청사방", 3, (20147, 391, 0)),
|
||
],
|
||
)
|
||
def test_est_price_propagates(tag, no, after) -> None:
|
||
data = _load(tag)
|
||
blk = next(b for b in data["블록"] if b["번"] == no)
|
||
parsed = est.parse_block(blk["글"], data["단가"])
|
||
des = ev.designate(parsed, data["총괄"][no])
|
||
got = ev.est_block(parsed, des, {LABOR: 180000.0})["금액"]
|
||
assert (got["L"], got["M"], got["S"]) == after
|
||
|
||
|
||
# ── 엑셀 없이 도는 문법 견본(`_분석_648-B` 3-1 봉화 D00102) ──
|
||
def test_stmate_grammar_sample() -> None:
|
||
prices = {
|
||
"M00081": {"T": 11468, "L": 0, "M": 11468, "S": 0, "meta": {}},
|
||
"L00007": {"T": 165545, "L": 165545, "M": 0, "S": 0, "meta": {}},
|
||
"L00099": {"T": 2, "L": 2, "M": 0, "S": 0, "meta": {"UNT": "L%"}},
|
||
}
|
||
text = [
|
||
"'합판(내수) :'&M00081& * 1.03 * 0.327 :=A01",
|
||
";;Q = 2.5 / 3 =",
|
||
"'보통인부' &L00007& * [Q] / 4 :=A02",
|
||
"&PA01M:M& * 11'%' =",
|
||
"&PA02L:S& * 1'%' = // 공구손료",
|
||
"(=)",
|
||
"&L00099& * 2 =",
|
||
"(-=)",
|
||
]
|
||
nodes = stm.parse_block([{"행": i, "G": g} for i, g in enumerate(text)])
|
||
assert [n["종류"] for n in nodes] == ["참조", "대입", "참조", "율", "율", "계", "참조", "계"]
|
||
assert nodes[1]["숨김"] and nodes[4]["덧글"] == "공구손료"
|
||
assert nodes[3]["율"] == {"곱": False, "범위": "A", "번지": ["A01"], "비목": "M", "옮김": "M"}
|
||
lines, tot = ev.run_stmate(nodes, prices)
|
||
assert lines[0]["값"]["M"] == 3862.5
|
||
assert lines[2]["값"]["L"] == 34350.5 # 165,545 × 0.83(Q 반올림) / 4 → 0.1 버림
|
||
assert lines[3]["값"]["M"] == 424.8 # A01 재료 3,862.5 × 11 %
|
||
assert lines[4]["값"]["S"] == 343.5 # A02 노무 34,350.5 × 1 % → 경비
|
||
assert lines[5]["값"] == pytest.approx({"L": 34350.5, "M": 4287.3, "S": 343.5})
|
||
assert lines[6]["종류"] == "특수" and lines[6]["값"]["L"] == 687.0 # 노무 누계 × 2 %
|
||
assert lines[7]["값"]["L"] == 687.0 # (-=) 찍되 합에서 뺌
|
||
assert tot == pytest.approx({"L": 34350.5, "M": 4287.3, "S": 343.5})
|
||
|
||
|
||
def test_est_grammar_sample() -> None:
|
||
prices = [{"종류": "노임", "이름": "보통인부", "규격": "", "비목": "L", "값": 172068.0}]
|
||
text = [
|
||
"보통인부 : 172,068 × 0.09 인 = 15,486.1",
|
||
"공구손료 : 15,486.1 × 3 % = 464.5",
|
||
"소 계 : 15,950.6",
|
||
"노무비 : 15,950.6 × 1 = 15,950",
|
||
]
|
||
lines = est.parse_block(text, prices)
|
||
assert [L.get("종류") for L in lines] == ["단가줄", "참조줄", "값줄", "참조줄"]
|
||
assert lines[3]["규칙"] == "버림0" and lines[2]["숨은"]["갈래"] == "합"
|
||
val = ev.run_est(lines, {LABOR: 180000.0})
|
||
assert val[0] == 16200.0 and val[1] == 486.0 and val[3] == 16686.0
|