Files
Aislo/resources/tester/test_793_1_san_parse.py
eomsangdonandClaude Opus 5.5 428d35cbd6 feat(793-1): 경쟁사 산근 글 식 파서 · 참조 평가기 · 실무 여덟 파일 지킴 시험
- 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>
2026-11-05 16:59:56 +09:00

172 lines
7.1 KiB
Python
Raw Permalink 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.
"""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