- 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>
294 lines
13 KiB
Python
294 lines
13 KiB
Python
"""경쟁사 산근 참조 평가기(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}
|