- 걷음: 43784a4c0 의 옛 수식 나무 위 고침(서버 · 화면 · css) = 그 앞 판으로 되돌림
- 남김: master_equation_sign.py · ui_template_equation_text.ts · ui_template_equation_edit.ts(다시 쓸 조각 · 지금 안 부름) · text 는 `입력글` 을 제 타입에
- 지움: test_801_1_equation_inline.py — 걷은 옛 나무 고침을 재는 시험
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
523 lines
23 KiB
Python
523 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""수식 나무(PLAN 796-1 · 요구 655 · 657 · 요구서_655 0 장) — 로직 줄 → 그림용 나무 · 변수 표.
|
||
|
||
수식 = 계산 로직 그 자체 — 나무는 엔진 식 나무(`master_formula.parse`)를 그대로 옮긴 것이고
|
||
`master_equation_edit.engine_text` 가 나무를 엔진 식 글로 되돌림(왕복 = 같은 금액).
|
||
|
||
줄마다 `이름식`(변수 = 이름 · 기호 · 첨자) · `값식`(같은 나무의 값 꼴) · `값` · 오른쪽 `풀이`(입력 ·
|
||
찾기 · 요소 값 + 표 · 행) · 하위 로직 부르기 = `블록`(접힘 · 노재경 소계 보이기만 · 금액 그대로).
|
||
|
||
나무 마디(`꼴`):
|
||
수 {글, 값} · 글 {글} · 변수 {열쇠, 이름, 기호, 위, 아래, 단위, 갈래, 값, 글, 식?}
|
||
(갈래 = 입력 · 중간 · 찾기 · 요소 · 합 — 찾기 · 요소 `식` = 엔진 원문 · 찾기 `표` · `행` · `칸`)
|
||
연산 {기호, 좌, 우} · 분수 {위, 아래, 사슬?} · 괄호 {안} · 퍼센트 {안} · 음 {안}
|
||
조건 {조건, 참, 거짓, 걸림(1 참 · 2 거짓 · 0 모름), 글} · 함수 {이름, 인자}
|
||
블록 {열쇠, 이름, 결과단위, 칸, 값, 묶음, 식, 접힘, 나무}
|
||
변수 표 = 입력 · 중간마다 {열쇠, 이름, 기호, 위, 아래, 단위, 갈래, 기본값, 설명} — 열쇠 = 엔진 이름
|
||
(안 바뀜) · 이름 · 기호 · 첨자 고침은 로직 줄 새 칸 `변수표` {열쇠: {이름?, 기호?, 위?, 아래?}}
|
||
(바꾼 것만 · 셈에 안 씀) · 기호 초기값 = 품셈 원문 「여기서 q : 버킷용량(㎥)」 후보(`symbols`).
|
||
셈 = `master_formula.run` · 끝수 `master_cut.engine` 한 벌 · eval 없음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import functools
|
||
import re
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
import master_auto as ma
|
||
import master_cut as mcut
|
||
import master_equation_group as grp
|
||
import master_equation_view as view
|
||
import master_formula as mf
|
||
import master_material as mm
|
||
import master_symbol as msym
|
||
|
||
from common_util.common_util_money import fmt
|
||
|
||
FIELD = "변수표" # 로직 줄 새 칸(`_틀.md` 2 장 열 차례 끝)
|
||
LOOK = ("이름", "기호", "위", "아래") # 변수표에 두는 꼴 칸
|
||
SHOW = 4 # 값 칸 보이는 자리 — 고친 중간 줄 `표시`(보이는 자리에서 잘라 셈 · 요구 658)
|
||
DEEP = 1 # 블록 안 나무를 펼칠 깊이(그 아래는 값 · 묶음만)
|
||
PREC = {"또는": 1, "그리고": 2, "==": 3, "!=": 3, "<": 3, "<=": 3, ">": 3, ">=": 3}
|
||
PREC |= {"+": 4, "-": 4, "*": 5, "/": 5}
|
||
SIGN = {"*": "×", "/": "÷", "-": "−"}
|
||
_ERR = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError, IndexError)
|
||
_BOOK = Path(mf.mk.MASTER).parents[1] / "resources/knowledge/original/원가계산"
|
||
_DEF = re.compile(r"^(?:여기서\s*[,:]?\s*)?([A-Za-z][A-Za-z0-9′'ˊ]{0,5})\s*[::]\s*(.+?)\s*$")
|
||
|
||
|
||
# ── 변수 표 ────────────────────────────────────────────────────────────
|
||
def _norm(text: str) -> str:
|
||
"""원문 풀이 · 변수 이름 맞대기 — 괄호 · 빈칸 · 「1일 · 1회」 머리 뺌 · 「횟수」 = 「회수」."""
|
||
text = re.sub(r"\([^)]*\)|([^)]*)|\s", "", str(text or ""))
|
||
return re.sub(r"^(1일|1회|일|회당)", "", text).replace("횟수", "회수")
|
||
|
||
|
||
@functools.lru_cache(maxsize=256)
|
||
def _section(book: str, number: str) -> tuple[tuple[str, str], ...]:
|
||
"""원문 절의 「여기서 기호 : 풀이」 목록 — 절 머리(`### 8-2-3 …`) 아래 다음 머리까지."""
|
||
root = _BOOK / ("산림_표준품셈/본문" if book == "산림품셈" else "건설공사_표준품셈/본문")
|
||
head = re.compile(rf"^#+\s*{re.escape(number)}\s")
|
||
out: list[tuple[str, str]] = []
|
||
for path in sorted(root.rglob("*.md")) if number else []:
|
||
try:
|
||
text = path.read_text(encoding="utf-8")
|
||
except OSError:
|
||
continue
|
||
if number not in text:
|
||
continue
|
||
inside, block = False, False
|
||
for line in text.splitlines():
|
||
if line.startswith("#"):
|
||
if inside:
|
||
break
|
||
inside = bool(head.match(line))
|
||
continue
|
||
if not inside:
|
||
continue
|
||
line = re.sub(r"<!--.*?-->", "", line).strip()
|
||
m = _DEF.match(line) if (block or line.startswith("여기서")) else None
|
||
block = bool(m) or (block and not line)
|
||
if m:
|
||
out.append((m.group(1), m.group(2)))
|
||
if out:
|
||
break
|
||
return tuple(out)
|
||
|
||
|
||
def symbols(row: dict) -> dict[str, str]:
|
||
"""{열쇠: 원문 기호 후보} — 이름 · 설명 「(q)」 · 풀이 맞대기가 하나일 때만(796-3 확정 전)."""
|
||
book = "산림품셈" if str(row.get("구분") or "").startswith("산림") else "건설품셈"
|
||
defs = _section(book, str(row.get("원문번호") or ""))
|
||
keys = {str(s.get("이름")) for k in ("입력", "중간") for s in row.get(k) or []}
|
||
got: dict[str, list[tuple[str, str]]] = {} # 기호 → [(열쇠, 갈래)]
|
||
for kind in ("입력", "중간"):
|
||
for spec in row.get(kind) or []:
|
||
key = str(spec.get("이름"))
|
||
if re.fullmatch(r"[A-Za-z][\w′']{0,5}", key):
|
||
continue # 이름이 이미 기호(K · E · f · cm)
|
||
said = re.findall(
|
||
rf"{re.escape(key)}\(([A-Za-z][\w′']{{0,5}})\)", str(spec.get("설명") or "")
|
||
)
|
||
hits = {s for s, d in defs if s in said} or {
|
||
s for s, d in defs if _norm(d) == _norm(key)
|
||
}
|
||
if len(hits) == 1 and (sign := hits.pop()) not in keys:
|
||
got.setdefault(sign, []).append((key, kind))
|
||
out = {}
|
||
for sign, owners in got.items(): # 한 기호는 한 변수 — 여럿이면 중간 하나일 때만
|
||
mids = [k for k, kind in owners if kind == "중간"]
|
||
one = owners[0][0] if len(owners) == 1 else mids[0] if len(mids) == 1 else None
|
||
if one:
|
||
out[one] = sign
|
||
if "결과" in row and defs and defs[0][0] not in keys and defs[0][0] not in out.values():
|
||
out["결과"] = defs[0][0] # 「여기서 Q : 시간당 작업량」 — 첫 풀이 = 결과
|
||
return out
|
||
|
||
|
||
def table(row: dict, master=None) -> list[dict]:
|
||
"""변수 표 — 입력 · 중간 차례 · 기본값 = 입력 `기본` › 자동 제안(`master_auto.defaults`)."""
|
||
saved = row.get(FIELD) if isinstance(row.get(FIELD), dict) else {}
|
||
auto = ma.defaults(master, row) if master is not None else {}
|
||
found = symbols(row)
|
||
out = []
|
||
for kind in ("입력", "중간"):
|
||
for spec in row.get(kind) or []:
|
||
key = str(spec.get("이름"))
|
||
look = saved.get(key) if isinstance(saved.get(key), dict) else {}
|
||
out.append(
|
||
{
|
||
"열쇠": key,
|
||
"이름": look.get("이름") or key,
|
||
"기호": look.get("기호") or found.get(key) or key,
|
||
"위": look.get("위") or "",
|
||
"아래": look.get("아래") or "",
|
||
"단위": spec.get("단위") or "",
|
||
"갈래": kind,
|
||
"기본값": spec.get("기본", auto.get(key)) if kind == "입력" else None,
|
||
"설명": spec.get("설명") or "",
|
||
**({"표시": spec["표시"]} if spec.get("표시") is not None else {}),
|
||
}
|
||
)
|
||
return out
|
||
|
||
|
||
def token(var: dict) -> str:
|
||
"""키보드 한 줄 식의 변수 글 — 기호(_아래)(^위) · 기호가 열쇠와 같고 첨자 없으면 열쇠."""
|
||
out = str(var.get("기호") or var["열쇠"])
|
||
if var.get("아래"):
|
||
out += f"_{var['아래']}"
|
||
if var.get("위"):
|
||
out += f"^{var['위']}"
|
||
return out
|
||
|
||
|
||
# ── 나무 ──────────────────────────────────────────────────────────────
|
||
class _Ctx:
|
||
def __init__(self, master, row: dict, env: dict, depth: int):
|
||
self.master, self.row, self.env, self.depth = master, row, env, depth
|
||
self.vars = {v["열쇠"]: v for v in table(row)}
|
||
self.used: dict[str, dict] = {} # 풀이 — 쓰인 입력 · 찾기 · 요소
|
||
|
||
def value(self, node):
|
||
try:
|
||
got = mf.evaluate(node, self.env, self.master, self.depth)
|
||
except _ERR:
|
||
return None
|
||
return got
|
||
|
||
|
||
def _num(value) -> dict:
|
||
if isinstance(value, Decimal):
|
||
return {"꼴": "수", "글": fmt(value), "값": value}
|
||
return {"꼴": "글", "글": "" if value is None else str(value)}
|
||
|
||
|
||
def _var(ctx: _Ctx, key: str, kind: str, value, **more) -> dict:
|
||
look = ctx.vars.get(key) or {"열쇠": key, "이름": key, "기호": key, "위": "", "아래": ""}
|
||
node = {
|
||
"꼴": "변수",
|
||
"열쇠": key,
|
||
**{k: look.get(k, "") for k in LOOK},
|
||
"단위": look.get("단위", more.pop("단위", "")),
|
||
"갈래": kind,
|
||
"값": value if not isinstance(value, dict) else None,
|
||
"글": "" if value is None or isinstance(value, dict) else fmt(value),
|
||
**more,
|
||
}
|
||
if kind in ("입력", "찾기", "요소"):
|
||
ctx.used.setdefault(more.get("식") or key, {k: v for k, v in node.items() if k != "꼴"})
|
||
return node
|
||
|
||
|
||
def _raw(node) -> str:
|
||
"""마디 → 엔진 식 글(찾기 · 요소 · 로직 부르기 원문 보관용)."""
|
||
import master_equation_edit as me # 서로 부름 — 늦게
|
||
|
||
return me.raw_text(node)
|
||
|
||
|
||
def me_keyboard(ctx: _Ctx, node) -> str:
|
||
"""조건 식 마디 → 키보드 한 줄 글(변수 = 기호) — 정의 영역 「조건 규격 = 폼타이규격」."""
|
||
import master_equation_edit as me
|
||
|
||
return me.keyboard_of(_raw(node), ctx.row)
|
||
|
||
|
||
def _find(ctx: _Ctx, node, column: str) -> dict:
|
||
"""찾기(표, 조건).칸 → 찾기 변수 · 표 · 고른 행."""
|
||
raw = _raw(("field", node, column))
|
||
more = {"식": raw, "칸": column}
|
||
more["조건식"] = {k: me_keyboard(ctx, c) for k, c in node[2]}
|
||
try:
|
||
ref = mf.fill(node[1], ctx.env)
|
||
table_row = ctx.master.get(ref)
|
||
conds = {k: mf.evaluate(v, ctx.env, ctx.master, ctx.depth) for k, v in node[2]}
|
||
more |= {"표": ref, "표이름": table_row.get("이름") or "", "행": conds}
|
||
more["단위"] = (table_row.get("값칸") or {}).get(column) or ""
|
||
except _ERR:
|
||
pass
|
||
name = f"{more.get('표이름') or node[1]}.{column}"
|
||
look = {"열쇠": raw, "이름": name, "기호": column, "위": "", "아래": ""}
|
||
ctx.vars.setdefault(raw, look)
|
||
return _var(ctx, raw, "찾기", ctx.value(("field", node, column)), **more)
|
||
|
||
|
||
def _block(ctx: _Ctx | None, node, column: str | None) -> dict:
|
||
"""로직 부르기 → 블록 — 묶음(노 · 재 · 경 · 계) · 결과 수 · 접힌 하위 나무(`DEEP` 까지)."""
|
||
raw = _raw(node if column is None else ("field", node, column))
|
||
out = {"꼴": "블록", "열쇠": node[1], "칸": column, "식": raw, "접힘": True}
|
||
if ctx is None:
|
||
return out
|
||
try:
|
||
ref = mf.fill(node[1], ctx.env)
|
||
sub = ctx.master.logic(ref)
|
||
given = {k: mf.evaluate(v, ctx.env, ctx.master, ctx.depth) for k, v in node[2]}
|
||
out |= {"열쇠": ref, "이름": sub.get("이름") or ref, "결과단위": sub.get("결과단위") or ""}
|
||
except _ERR:
|
||
return out
|
||
got = ctx.value(node)
|
||
if isinstance(got, dict):
|
||
out["묶음"] = got
|
||
got = got.get(column) if column else got.get("계")
|
||
elif column is not None:
|
||
got = None
|
||
out |= {"값": got, "글": fmt(got) if isinstance(got, Decimal) else ""}
|
||
if ctx.depth < DEEP:
|
||
try:
|
||
out["나무"] = export(ctx.master, ref, given, ctx.depth + 1)
|
||
except _ERR:
|
||
out["나무"] = None
|
||
return out
|
||
|
||
|
||
def _paren(child: dict, op: str, right: bool) -> dict:
|
||
"""연산 자식 괄호 — 낮은 세기 · 오른쪽 같은 세기(엔진은 왼쪽부터 묶음 — 나무 그대로)."""
|
||
if child.get("꼴") != "연산":
|
||
return child
|
||
mine, up = PREC[child["_op"]], PREC[op]
|
||
if mine < up or (mine == up and right):
|
||
return {"꼴": "괄호", "안": child}
|
||
return child
|
||
|
||
|
||
def build(node, ctx: _Ctx | None) -> dict:
|
||
"""엔진 식 마디 → 나무 마디(이름 꼴) · ctx 없으면 값 · 풀이 없이 꼴만."""
|
||
kind = node[0]
|
||
if kind == "num":
|
||
return _num(node[1])
|
||
if kind == "str":
|
||
return {"꼴": "글", "글": node[1], "따옴": True}
|
||
if kind == "name":
|
||
key = node[1]
|
||
if ctx is None:
|
||
return {"꼴": "변수", "열쇠": key, "갈래": "합"}
|
||
where = ctx.vars.get(key, {}).get("갈래") or "합"
|
||
return _var(ctx, key, where, ctx.env.get(key) if ctx else None)
|
||
if kind == "ref":
|
||
if ctx is None:
|
||
return {"꼴": "변수", "열쇠": node[1], "갈래": "요소", "식": node[1]}
|
||
return _elem(ctx, node[1])
|
||
if kind == "neg":
|
||
inner = build(node[1], ctx)
|
||
return {"꼴": "음", "안": {"꼴": "괄호", "안": inner} if inner["꼴"] == "연산" else inner}
|
||
if kind == "field" and node[1][0] == "find":
|
||
if ctx is None:
|
||
return {"꼴": "변수", "갈래": "찾기", "식": _raw(node), "열쇠": _raw(node)}
|
||
return _find(ctx, node[1], node[2])
|
||
if kind == "field" and node[1][0] == "logic":
|
||
return _block(ctx, node[1], node[2])
|
||
if kind == "logic":
|
||
return _block(ctx, node, None)
|
||
if kind in ("field", "find"): # 그 밖의 칸 꺼냄(요소 묶음 · 찾기 줄째) — 원문 그대로 변수
|
||
raw = _raw(node)
|
||
if ctx is None:
|
||
return {"꼴": "변수", "갈래": "요소", "식": raw, "열쇠": raw}
|
||
return _var(ctx, raw, "요소", ctx.value(node), 식=raw)
|
||
if kind == "call" and node[1] == "만약":
|
||
cond, yes, no = (build(x, ctx) for x in node[2])
|
||
hit = ctx.value(node[2][0]) if ctx else None
|
||
at = 0 if hit is None else 1 if hit else 2
|
||
return {"꼴": "조건", "조건": cond, "참": yes, "거짓": no, "걸림": at}
|
||
if kind == "call":
|
||
return {"꼴": "함수", "이름": node[1], "인자": [build(x, ctx) for x in node[2]]}
|
||
if kind == "bin":
|
||
return _bin(node, ctx)
|
||
raise mf.FormulaError(f"모르는 마디 {kind}")
|
||
|
||
|
||
def _elem(ctx: _Ctx, ref: str) -> dict:
|
||
import master_text as mt
|
||
|
||
value = ctx.value(("ref", ref))
|
||
name = mt._elem_name(ctx.master, ref, ctx.env)
|
||
look = {"열쇠": ref, "이름": name, "기호": name, "위": "", "아래": ""}
|
||
ctx.vars.setdefault(ref, look)
|
||
return _var(ctx, ref, "요소", value, 식=ref)
|
||
|
||
|
||
def _bin(node, ctx: _Ctx | None) -> dict:
|
||
_, op, left, right = node
|
||
if op == "/" and right == ("num", Decimal(100)): # 「… / 100」 = 퍼센트
|
||
inner = _paren(build(left, ctx), "*", False)
|
||
return {"꼴": "퍼센트", "안": inner}
|
||
if op == "/":
|
||
if left[0] == "bin" and left[1] == "/" and left[3] != ("num", Decimal(100)):
|
||
top, mid = build(left[2], ctx), build(left[3], ctx) # a/b/c → a/(b×c) 모양만
|
||
low = {"꼴": "연산", "기호": "×", "_op": "*", "좌": mid, "우": build(right, ctx)}
|
||
low["좌"], low["우"] = _paren(mid, "*", False), _paren(low["우"], "*", True)
|
||
return {"꼴": "분수", "위": top, "아래": low, "사슬": True}
|
||
return {"꼴": "분수", "위": build(left, ctx), "아래": build(right, ctx)}
|
||
a, b = build(left, ctx), build(right, ctx)
|
||
return {
|
||
"꼴": "연산",
|
||
"기호": SIGN.get(op, op),
|
||
"_op": op,
|
||
"좌": _paren(a, op, False),
|
||
"우": _paren(b, op, True),
|
||
}
|
||
|
||
|
||
def values(tree: dict) -> dict:
|
||
"""이름 꼴 나무 → 값 꼴 나무 — 변수 · 블록은 값 수로(열쇠 남김) · 조건은 걸린 쪽만."""
|
||
kind = tree.get("꼴")
|
||
if kind in ("변수", "블록"):
|
||
value = tree.get("값")
|
||
got = _num(value) if value is not None else {"꼴": "글", "글": "?"}
|
||
return {**got, "열쇠": tree.get("열쇠"), "갈래": tree.get("갈래") or "블록"}
|
||
if kind == "조건":
|
||
if tree.get("걸림"):
|
||
return values(tree["참" if tree["걸림"] == 1 else "거짓"])
|
||
return {**tree, **{k: values(tree[k]) for k in ("조건", "참", "거짓")}}
|
||
out = dict(tree)
|
||
for k in ("좌", "우", "위", "아래", "안"):
|
||
if k in tree:
|
||
out[k] = values(tree[k])
|
||
if "인자" in tree:
|
||
out["인자"] = [values(x) for x in tree["인자"]]
|
||
return out
|
||
|
||
|
||
def _clean(tree):
|
||
"""밖으로 내보낼 나무 — 안쪽 칸(`_op`) 뺌."""
|
||
if isinstance(tree, dict):
|
||
return {k: _clean(v) for k, v in tree.items() if k != "_op"}
|
||
if isinstance(tree, list):
|
||
return [_clean(x) for x in tree]
|
||
return tree
|
||
|
||
|
||
# ── 로직 한 줄 → 수식 ─────────────────────────────────────────────────
|
||
def given_of(master, row: dict, given: dict | None = None) -> dict:
|
||
"""셈 입력 — 자동값 · 기본 제안 · 시험입력 · 받은 값 차례로 덮음."""
|
||
out = ma.values(master, row)
|
||
out.update({k: v for k, v in ma.defaults(master, row).items() if v is not None})
|
||
out.update(row.get("시험입력") or {})
|
||
out.update(given or {})
|
||
names = {s["이름"] for s in row.get("입력") or []}
|
||
return {k: v for k, v in out.items() if k in names}
|
||
|
||
|
||
def _line(ctx: _Ctx, kind: str, key: str, text: str, value, **more) -> dict:
|
||
try:
|
||
tree = build(mf.parse(text), ctx)
|
||
except mf.FormulaError as e:
|
||
return {"갈래": kind, "열쇠": key, "식": text, "까닭": str(e), **more}
|
||
head = _var(ctx, key, kind if kind in ("중간",) else "합", value)
|
||
if kind == "결과":
|
||
head |= {"이름": "결과", "기호": symbols(ctx.row).get("결과") or "결과"}
|
||
return {
|
||
"갈래": kind,
|
||
"열쇠": key,
|
||
"식": text,
|
||
"결과": head,
|
||
"이름식": tree,
|
||
"값식": values(tree),
|
||
"값": value,
|
||
"글": fmt(value) if isinstance(value, Decimal) else "",
|
||
**more,
|
||
}
|
||
|
||
|
||
def subtotals(logic: dict, cut: dict, total: dict) -> dict:
|
||
"""호표 줄 → 소계 묶음(799-1 · 801-3 · 보이기만) — 부른 단가산출 = 묶음 하나씩 · 직접 줄 = 한 묶음 + 끝수.
|
||
|
||
값 = `master_cut.engine` 이 버린 줄 비목(0.1 원) 그대로 · 합계 = 엔진 비목 합(1 원 버림) ·
|
||
끝수 = 합계 − 묶음 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함). 묶음 꼴 = `master_equation_group`.
|
||
"""
|
||
items = logic.get("호표") or []
|
||
skip = mcut.ms.skipped(logic) if mcut.ms.uses(logic) else set()
|
||
rows: list[dict] = []
|
||
for n, line in enumerate(cut.get("줄") or []):
|
||
if n in skip:
|
||
continue
|
||
split = {k: line.get("비목", {}).get(k) or Decimal(0) for k in mf.COST_ITEMS}
|
||
item = items[n] if n < len(items) else {}
|
||
kind = "블록" if item.get("종류") == "로직" else "자원" if item else "덧줄"
|
||
rows.append(
|
||
{"갈래": kind, "이름": str(item.get("이름") or ""), "종류": item.get("종류"), **split}
|
||
| {"계": sum(split.values(), Decimal(0))}
|
||
)
|
||
got = grp.bundles(rows)
|
||
rest = {
|
||
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in got["줄"]), Decimal(0))
|
||
for k in mf.BUNDLE
|
||
}
|
||
return {"줄": got["줄"], "끝수": rest}
|
||
|
||
|
||
def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||
"""로직 키 → 수식 {열쇠, 이름, 결과단위, 줄, 풀이, 변수표, 합계 | 결과} — 셈은 엔진 한 벌."""
|
||
row = master.logic(key)
|
||
given = given_of(master, row, given) if depth == 0 else dict(given or {})
|
||
raw = mf.run(master, key, given, depth)
|
||
env = {k: v for k, v in (raw.get("중간") or {}).items()}
|
||
ctx = _Ctx(master, row, env, depth)
|
||
out_lines = []
|
||
for step in row.get("중간") or []:
|
||
name = step["이름"]
|
||
more = {"표시": step["표시"]} if step.get("표시") is not None else {}
|
||
out_lines.append(_line(ctx, "중간", name, step["식"], env.get(name), **more))
|
||
total: dict = {}
|
||
sub: dict = {}
|
||
if "결과" in row:
|
||
out_lines.append(_line(ctx, "결과", "결과", row["결과"]["식"], raw["결과"]))
|
||
total = {"결과": raw["결과"]}
|
||
else:
|
||
cut = mcut.engine(master, key, raw) if depth == 0 else raw
|
||
lines = raw.get("줄") or []
|
||
for at, item in enumerate(row.get("호표") or []):
|
||
out_lines.append(_item(ctx, at, item, lines[at] if at < len(lines) else {}))
|
||
for at, extra in enumerate(row.get("덧줄") or []):
|
||
got = env.get(extra["이름"])
|
||
one = _line(ctx, "덧줄", extra["이름"], extra["식"], got, 차례=at)
|
||
out_lines.append(one | {"비목": extra.get("비목")})
|
||
total = {k: cut.get(k) for k in mf.BUNDLE}
|
||
if depth == 0:
|
||
sub = {"소계": subtotals(row, cut, total)}
|
||
out = msym.apply( # 기호 먼저(800-1) → 정의는 기호 씌운 줄로(800-2)
|
||
{
|
||
"열쇠": row.get("키"),
|
||
"이름": row.get("이름") or "",
|
||
"결과단위": row.get("결과단위") or "",
|
||
"줄": out_lines,
|
||
"풀이": list(ctx.used.values()),
|
||
"변수표": table(row, master),
|
||
"합계": total,
|
||
**sub,
|
||
}
|
||
)
|
||
if depth == 0:
|
||
out["정의"] = view.definitions(out_lines, row, out["변수표"], _unused(row))
|
||
return _clean(out)
|
||
|
||
|
||
def _unused(row: dict):
|
||
"""열쇠 → 어느 식에도 안 쓰이나(정의 영역 · 지울 수 있는 입력)."""
|
||
import master_equation_edit as me
|
||
|
||
return lambda key: key != mm.REGION and not me.uses(row, key)
|
||
|
||
|
||
def _item(ctx: _Ctx, at: int, item: dict, line: dict) -> dict:
|
||
"""호표 줄 = 「단가 × 수량식 = 금액」 · 로직 줄 단가 = 블록."""
|
||
name = str(item.get("이름") or "")
|
||
try:
|
||
qty = build(mf.parse(item["수량"]), ctx)
|
||
except mf.FormulaError as e:
|
||
return {"갈래": "호표", "차례": at, "열쇠": name, "식": item["수량"], "까닭": str(e)}
|
||
if item.get("종류") == "로직":
|
||
price = build(mf.parse(str(item["요소"])), ctx)
|
||
elif isinstance(item.get("요소"), str):
|
||
price = _elem(ctx, item["요소"])
|
||
price |= {"값": line.get("단가"), "글": fmt(line["단가"]) if line.get("단가") else ""}
|
||
else: # 재료 고르기 조건 — 단가는 엔진 고름 값
|
||
key, label = f"단가:{name}", f"{name}단가"
|
||
ctx.vars.setdefault(key, {"열쇠": key, "이름": label, "기호": label, "위": "", "아래": ""})
|
||
price = _var(ctx, key, "요소", line.get("단가"), 고르기=item["요소"])
|
||
tree = {
|
||
"꼴": "연산",
|
||
"기호": "×",
|
||
"_op": "*",
|
||
"좌": price,
|
||
"우": _paren(qty, "*", True),
|
||
}
|
||
head = {"꼴": "변수", "열쇠": name, "이름": name, "기호": name, "갈래": "합"}
|
||
head |= {"값": line.get("금액"), "단위": item.get("단위") or ""}
|
||
return {
|
||
"갈래": "호표",
|
||
"차례": at,
|
||
"열쇠": name,
|
||
"식": item["수량"],
|
||
"결과": head,
|
||
"이름식": tree,
|
||
"값식": values(tree),
|
||
"값": line.get("금액"),
|
||
"글": fmt(line["금액"]) if isinstance(line.get("금액"), Decimal) else "",
|
||
"수량": line.get("수량"),
|
||
"비목": line.get("비목") or {},
|
||
**({"까닭": line["까닭"]} if line.get("까닭") else {}),
|
||
}
|