- 남김: San_Est.tokens · num · NUMRE(NativeEst 가 씀) · San_Eval.run_stmate · stmate_block(STC · STmate) · San_Emit.stmate - 시험 고침: 802_4b1 · 797_1 = 세 줄 → 묶음 한 줄(원파일 길 꼴) · 797_2 = 블록 번은 엑셀 둘 꼴에서 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.3 KiB
Python
180 lines
7.3 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 = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림.
|
||
단가 바뀜 전파 — 단가 dict 를 바꿔 다시 셈. 옛 EST 엑셀 셈 · 엑셀 검산(`run_est` · `est_check` ·
|
||
`stmate_check`)은 812-1 엑셀 읽기기로 걷음(EST = `M02_Doc_NativeEst`).
|
||
"""
|
||
|
||
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%")
|
||
|
||
|
||
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}
|