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