Files
Aislo/M02_MasterTemplete/M02_San_Eval.py
T
eomsangdonandClaude Opus 5.5 74446990f8 refactor(812-1): 옛 엑셀 엔진 걷기 1 — San_Est 글 추정 사슬 · San_Eval EST 셈 · 엑셀 검산 · San_Emit.est · San_Book.est_blocks · load
- 남김: 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>
2026-11-09 07:37:28 +09:00

180 lines
7.3 KiB
Python
Raw 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 = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림.
단가 바뀜 전파 — 단가 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}