Files
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

294 lines
13 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.
"""경쟁사 산근 참조 평가기(PLAN 793-1 · 요구서_652) — 줄 나무 + 같은 책 단가 →
줄 · 블록 금액 · 검산.
STmate 셈 규칙(`_분석_649-G2` 1 장) — 줄 금액 = 단가 × (계수 + 1e-13) → 소수 1 버림 ·
변수 `X = 식 =` = 소수 2 반올림 · 율 `&P..&` = 범위 비목 금액 × n % → 소수 1 버림 ·
`&R..&` = × n · 특수 % L% · M% = 처음부터 그 비목 누계 × n % ·
T% = 바로 앞 계표시 값 × 수량 · 계표시 = 아래 단 누계 · `(-=)` = 찍되 위 합에서 뺌 ·
`->` = 번지엔 쌓되 합에서 뺌 · 블록 끝전 두 갈래(비목 버림 / 계 버림 + 큰 나머지) ·
결과금액 조정(SBM = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림.
EST 셈 규칙(같은 문서 2 장) — 줄 = 복원 식(단가 · 앞줄 결과 · 상수) → 줄 자리 속성 ·
값 줄 = 숨은 복사 · 합 → 자리 속성 · 블록 성분 = 그 값을 낸 마지막 줄.
단가 바뀜 전파 — STmate = 단가 dict 를 바꿔 다시 셈 · EST = `run_est(lines, {정체 열쇠: 새 값})`.
"""
from __future__ import annotations
import collections
import math
from typing import Any
from M02_MasterTemplete import M02_San_Expr as ex
COMPS = ("L", "M", "S")
SPECIAL = ("L%", "M%", "S%", "T%")
TOL_LINE = 0.05
def _zero() -> dict[str, float]:
return dict.fromkeys(COMPS, 0.0)
# ── STmate ─────────────────────────────────────────────────────────────
def _rate_amount(node: dict, coef: float, A: dict, T: dict, since: dict, total: dict) -> dict:
rate = node["율"]
area = rate["범위"]
if area == "A":
base = _zero()
for spot in rate["번지"]:
for c in COMPS:
base[c] += A[spot][c]
elif area == "T":
base = T.get(rate["번지"][0]) or _zero()
else:
base = since if area == "C" else total
pct = coef if rate["곱"] else coef / 100
src, dst = rate["비목"], rate["옮김"]
amt = _zero()
if src:
amt[dst or src] = ex.fl1(base[src] * pct)
elif dst:
amt[dst] = ex.fl1(sum(base.values()) * pct)
else:
for c in COMPS:
amt[c] = ex.fl1(base[c] * pct)
return amt
def _ref_amount(
node: dict, coef: float, prices: dict, total: dict, last_tot: Any, res: dict
) -> dict:
src_ = node["자원"]
code = src_["코드"]
if code not in prices:
raise ex.ExprError(f"단가 없음 {code}")
pr = prices[code]
src, dst = src_["비목"], src_["옮김"]
amt = _zero()
unt = (pr.get("meta") or {}).get("UNT")
res["단가"] = {c: pr[c] for c in COMPS}
if unt in SPECIAL:
dst2 = dst or {"L": "L", "M": "M", "S": "S"}.get(code[0])
if unt == "T%":
b0 = last_tot if last_tot is not None else sum(total.values())
amt[dst2] = ex.fl1(b0 * coef)
else:
b0 = total[unt[0]]
amt[dst2] = ex.fl1(b0 * coef / 100)
res["기준"] = b0
res["종류"] = "특수"
return amt
c_eps = coef + 1e-13
if src:
amt[dst or src] = ex.fl1(pr[src] * c_eps)
else:
for c in COMPS:
if pr[c]:
amt[c] = ex.fl1(pr[c] * c_eps)
return amt
def run_stmate(nodes: list[dict], prices: dict, dec: int = 2) -> tuple[list[dict], dict]:
"""줄 나무 → (줄 결과 [{종류, 값 {L, M, S}, 계수, 오류 …}], 블록 소수 합 {L, M, S})."""
V: dict[str, float] = {}
A: dict[str, dict] = collections.defaultdict(_zero) # 금액번지
T: dict[str, dict] = {} # 누계번지
acc = [_zero() for _ in range(6)] # 계 단 1~5 · 0 = 전체
since = _zero() # 마지막 계표시 뒤
last_tot = None
out = []
for node in nodes:
kind = node["종류"]
res: dict[str, Any] = {"행": node.get("행"), "종류": kind, "값": None, "오류": None}
try:
if kind == "계":
lvl = node["단"]
val = dict(acc[lvl])
res |= {"단": lvl, "제외": node["제외"], "값": val}
for k in range(1, lvl + 1):
acc[k] = _zero()
if node["제외"]:
for k in [*range(lvl + 1, 6), 0]:
for c in COMPS:
acc[k][c] -= val[c]
if node["누계번지"]:
T[node["누계번지"]] = val
last_tot = sum(val.values())
since = _zero()
elif kind in ("참조", "율"):
if node.get("오류"):
raise ex.ExprError(node["오류"])
coef = ex.value(node["식"], {"V": V, "R": 1.0})
if kind == "율":
amt = _rate_amount(node, coef, A, T, since, acc[0])
else:
amt = _ref_amount(node, coef, prices, acc[0], last_tot, res)
res |= {"값": amt, "계수": coef, "번지": node["번지"], "빼기": node["빼기"]}
addr = node["번지"]
if addr and addr.startswith("A"):
for c in COMPS:
A[addr][c] += amt[c]
if not node["빼기"]:
for k in range(6):
for c in COMPS:
acc[k][c] += amt[c]
for c in COMPS:
since[c] += amt[c]
elif kind == "대입":
for part in node["대입"]:
if part["이름"] is None:
try:
ex.value(part["식"], {"V": V, "R": 1.0})
except ex.ExprError:
pass
continue
v = ex.value(part["식"], {"V": V, "R": 1.0})
V[part["이름"]] = ex.rnd(v, dec) if part["찍기"] else v
except ex.ExprError as error:
res["오류"] = str(error)
out.append(res)
return out, acc[0]
def block_total(tot: dict, rule: str) -> dict[str, float]:
"""블록 끝전 — 비목버림(비목 원 버림 · 계 = 합) / 합버림_큰나머지(계 원 버림 · 나머지를 소수 큰
차례 경 › 재 › 노 로 1 원씩)."""
c = {k: math.floor(round(tot[k], 6)) for k in COMPS}
if rule == "비목버림":
c["T"] = c["L"] + c["M"] + c["S"]
return c
total = math.floor(round(sum(tot.values()), 6))
order = sorted(COMPS, key=lambda k: (-round(tot[k] - c[k], 6), "SML".index(k)))
for k in order[: total - sum(c.values())]:
c[k] += 1
c["T"] = total
return c
def stmate_block(block: dict, nodes: list[dict], prices: dict, rule: str) -> dict[str, Any]:
"""블록 → {줄, 계(끝전 뒤 · 계약 전), 금액(조정 · 계약 뒤), 낙찰}."""
lines, tot = run_stmate(nodes, prices)
meta = (prices.get(block["코드"]) or {}).get("meta", {})
kye = block_total(tot, rule)
sbm = (meta.get("SBM") or "").strip()
if meta.get("ADJ") == "T" and sbm in COMPS:
kye = {"L": 0, "M": 0, "S": 0, "T": kye["T"], sbm: kye["T"]}
nag = float(meta["NAG"]) if meta.get("UC") == "T" and meta.get("NAG") else None
fin = dict(kye)
if nag:
fin = {c: math.floor(round(kye[c] * nag / 100, 6)) for c in COMPS}
fin["T"] = fin["L"] + fin["M"] + fin["S"]
return {"줄": lines, "합": tot, "계": kye, "금액": fin, "낙찰": nag}
def stmate_check(block: dict, result: dict) -> dict[str, Any]:
"""줄마다 · 블록 검산 — 금액 줄(참조 · 율 · 특수) C~F 대조 · 계표시 줄 · 블록 머리 · 꼬리 계."""
bad, n_amt, n_tot, ok_tot = [], 0, 0, 0
for r, ln in zip(result["줄"], block["몸"], strict=True):
want = {"L": ln["D"] or 0, "M": ln["E"] or 0, "S": ln["F"] or 0}
if r["종류"] in ("참조", "율", "특수"):
n_amt += 1
if not r["값"] or any(abs(r["값"][c] - want[c]) >= TOL_LINE for c in COMPS):
bad.append({"행": ln["행"], "엑셀": want, "셈": r["값"], "오류": r["오류"]})
elif r["종류"] == "계" and ln["C"] is not None:
n_tot += 1
ok_tot += all(abs(r["값"][c] - want[c]) < 0.15 for c in COMPS)
head = block["머리"]
want = {"T": head["C"] or 0, "L": head["D"] or 0, "M": head["E"] or 0, "S": head["F"] or 0}
kye = next((t for t in block["꼬리"] if t["A"] == "계"), None)
want_k = {"T": kye["C"], "L": kye["D"], "M": kye["E"], "S": kye["F"]} if kye else None
same = lambda x, y: all(abs((x[k] or 0) - (y[k] or 0)) < 0.5 for k in "TLMS") # noqa: E731
ok = same(result["금액"], want) and (want_k is None or same(result["계"], want_k))
return {
"맞음": ok,
"엑셀": want,
"엑셀계": want_k,
"금액줄": n_amt,
"틀린줄": bad,
"계줄": n_tot,
"계줄맞음": ok_tot,
}
# ── EST ────────────────────────────────────────────────────────────────
def key_of(ident: dict[str, Any]) -> tuple:
"""EST 단가 정체 → 전파 열쇠 (종류, 이름, 규격, 비목)."""
return (ident["종류"], ident["이름"], ident["규격"], ident["비목"])
def run_est(lines: list[dict], newp: dict[tuple, float] | None = None) -> dict[int, float | None]:
"""복원 줄 → {차례: 다시 셈 값}(줄 자리 속성 그대로) · newp = {정체 열쇠: 새 단가}."""
newp = newp or {}
val: dict[int, float | None] = {}
for L in lines:
k = L["차례"]
if L.get("꼴") == "돈" and L.get("규칙"):
sub = {}
for i, r in L["역할"].items():
if r["갈래"] == "단가" and key_of(r["정체"]) in newp:
sub[i] = newp[key_of(r["정체"])]
elif r["갈래"] == "앞줄" and val.get(r["줄"]) is not None:
sub[i] = val[r["줄"]]
v = _est_calc(L["식"], sub)
val[k] = ex.RULE[L["규칙"]](v) if v is not None else None
elif L.get("종류") == "값줄" and L.get("숨은"):
how = L["숨은"]
s = sum(
(val.get(j) if val.get(j) is not None else lines[j]["값"]) or 0 for j in how["줄"]
)
val[k] = ex.RULE[how["규칙"]](s)
return val
def _est_calc(node: tuple, sub: dict) -> float | None:
try:
return ex.value(node, {"V": {}, "sub": sub})
except ex.ExprError:
return None
def designate(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
"""블록 성분(총괄표) → 그 값을 낸 줄(마지막 · 라벨 비목 우선) · 0 = 「영」 · 없음 = None."""
out: dict[str, Any] = {}
kinds = ("단가줄", "참조줄", "상수줄", "값줄")
for c in COMPS:
w = want[c]
if not w:
out[c] = "영"
continue
cand = [L for L in lines if L.get("종류") in kinds and abs(L["값"] - w) < ex.EPS]
pick = [L for L in cand if L.get("비목") == c] or cand
out[c] = pick[-1]["차례"] if pick else None
return out
def est_block(lines: list[dict], des: dict[str, Any], newp: dict | None = None) -> dict[str, Any]:
"""성분 줄 → 블록 금액(성분 원 버림 · 계 = 합) · 줄 값."""
val = run_est(lines, newp)
raw = {
c: (0.0 if k == "영" else (val.get(k) if k is not None else None)) for c, k in des.items()
}
if any(v is None for v in raw.values()):
return {"줄": val, "성분": raw, "금액": None}
fin = {c: math.floor(round(raw[c], 6)) for c in COMPS}
fin["T"] = fin["L"] + fin["M"] + fin["S"]
return {"줄": val, "성분": raw, "금액": fin}
def est_check(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
"""블록 검산 — 다시 셈 성분 = 총괄표 · 줄마다 보인 값 대조(돈 · 값 줄)."""
des = designate(lines, want)
got = est_block(lines, des)
val = got["줄"]
n, bad = 0, []
for L in lines:
if L.get("꼴") == "돈" or L.get("종류") == "값줄":
n += 1
v = val.get(L["차례"])
if v is None or abs(v - L["값"]) >= ex.EPS:
bad.append({"차례": L["차례"], "글": L["원글"].strip(), "보인": L["값"], "셈": v})
fin = got["금액"]
ok = fin is not None and all(abs(fin[c] - want[c]) < 0.5 for c in COMPS)
ok = ok and abs(want["L"] + want["M"] + want["S"] - want["T"]) < 0.5
return {"맞음": ok, "성분줄": des, "금액": fin, "줄수": n, "틀린줄": bad}