Files
Aislo/resources/master_data/scripts/master_text.py
T
eomsangdonandClaude Sonnet 5 29265815b4 fix(m01): 텍스트 수식 「단가 * (수량 합)」 괄호 — 이름 식 · 값 식
- 수량식 맨 위가 덧셈·뺄셈이면 단가 곱 뒤에 괄호(GC000467 촌락지대가)
- 번호 별칭은 이름글에서 그대로 둠 — 별칭표 이름이어야 되돌려짐(찾은 줄 이름은 되돌리기 안 됨) · 시험으로 고정
- 조각은 수량식만이라 그대로

검증: test_m01_text 25개 통과 · 1,354 금액 변화 0 · 값 없이 멈춤 0 · GC000467 식에 괄호.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
2026-09-24 10:07:32 +09:00

572 lines
26 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.
# -*- coding: utf-8 -*-
"""사람이 읽는 식 줄 — 비목 묶음마다 「이름 : 단가 * 수량식 = 금액」 과 소계.
줄마다 글 두 꼴 —
· `글` 값 꼴: 수는 화면과 같은 자릿수(소수 넷째 자리까지 · 천 단위 쉼표) · `찾기`·`로직`·
중간 값·요소는 글자로 내보내지 않고 그 자리에서 나온 수로 적음
· `이름글` 이름 꼴: 「석공노임 * 메쌓기인력.석공 * (1 + 증가율) = 석공비」 — 표 찾기·로직
부르기는 별칭 이름(`master_copy` 3-4 규칙) · 중간 값·입력은 그 이름 그대로
두 꼴은 같은 줄에 나란히 있고 `짝` 번호가 붙음 — 화면 두 칸이 줄 맞춤으로 놓임.
줄마다 `조각`(PLAN 2-1 조각 계약) 도 같이 줌 — kind(요소·표·로직·값·기호·글) · 글 · 참조.
알약(요소·표·로직)이 될 자리는 구조만 보고 짓고, `만약()` 갈래만 골라야 해서 계산이 필요함 —
계산이 안 되면(FormulaError 따위) 갈래 양쪽을 다 내보내고 깊이를 적어 접을 수 있게 함.
호표 줄 계산 자체가 막혀도(재료 단가 없음 따위) 이름 식·조각은 `row` 구조만으로 그리고
`글`·`금액`만 비움 — `만약() 과 무관` 인 그 60개 로직도 이름 식은 보이게.
끝수는 줄이 아니라 소계에 녹여 적음. 계산 자체는 `master_formula.run` 이 한 벌만 함.
"""
from __future__ import annotations
from decimal import ROUND_HALF_UP, Decimal
import master_copy as mc
import master_formula as mf
PLACES = Decimal("0.0001") # 화면 `formatNumber` 와 같음(소수 넷째 자리까지)
PRICE_TAIL = {"인력": "노임", "기계": "손료"} # 단가 자리의 이름 꼬리(그 밖은 「단가」)
ELEM_KEYS = ("LB", "EQ", "MT", "MP", "MO") # 요소 테이블 — 그 밖의 열쇠(QC · QF …)는 별칭 이름
ELEM_TAIL = {"LB": "노임", "EQ": "손료"} # 식 안 요소 키의 이름 꼬리(그 밖은 「단가」)
_PREC = {"또는": 1, "그리고": 2, "==": 3, "!=": 3, "<": 3, "<=": 3, ">": 3, ">=": 3}
_PREC |= {"+": 4, "-": 4, "*": 5, "/": 5}
_RIGHT_TIGHT = ("-", "/") # 오른쪽 짝은 같은 세기라도 괄호가 필요함
_CALC_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) # 계산 못 함
def fmt(value) -> str:
"""화면에 보이는 자릿수 그대로 — 소수 넷째 자리까지 · 뒤 0 은 떼고 · 천 단위 쉼표."""
if not isinstance(value, Decimal):
return str(value)
cut = value.quantize(PLACES) if value.as_tuple().exponent < -4 else value
sign, digits = ("-", -cut) if cut < 0 else ("", cut)
whole, _, rest = f"{digits.normalize():f}".partition(".")
if "E" in whole or "e" in whole: # 아주 큰 수 — 지수 꼴을 펴서 씀
whole, _, rest = f"{digits:f}".rstrip("0").rstrip(".").partition(".")
return sign + f"{int(whole):,}" + (f".{rest}" if rest else "")
def fmt_money(value) -> str:
"""돈(단가 · 금액)은 원 단위 정수로 보임 — 글만 그렇고 계산 값은 소수 그대로."""
if isinstance(value, Decimal):
value = value.quantize(Decimal(1), rounding=ROUND_HALF_UP)
return fmt(value)
# ── 식을 글로 — 값 꼴과 이름 꼴 ────────────────────────────────────────
def render(node, env: dict, master, depth: int = 0) -> str:
"""식 한 마디 → 값 꼴 글. 값이 정해지는 마디(요소 · 찾기 · 로직 · 이름)는 그 수로 바뀜."""
return _draw(node, node, env, master, depth)
def _draw(show, real, env: dict, master, depth: int) -> str:
kind = show[0]
if kind == "num":
return fmt(show[1])
if kind == "str":
return f"'{show[1]}'"
if kind == "neg":
return "-" + _draw(show[1], real[1], env, master, depth)
if kind == "bin":
return _bin(show, real, env, master, depth, _draw)
if kind == "call" and show[1] == "만약": # 걸린 쪽만 적음
at = 1 if mf.evaluate(real[2][0], env, master, depth) else 2
return _draw(show[2][at], real[2][at], env, master, depth)
return _value(real, env, master, depth)
def named(show, real, env: dict, master, depth: int = 0) -> str:
"""식 한 마디 → 이름 꼴 글. `show` = 부르기를 별칭으로 갈아 끼운 나무 · `real` = 원래 나무.
이름·중간 값은 그대로 · 표 찾기·로직 부르기는 별칭 이름 · 요소는 그 이름."""
kind = show[0]
if kind == "name":
return show[1]
if kind == "ref":
return _elem_name(master, show[1])
if kind == "field":
return f"{named(show[1], real[1], env, master, depth)}.{show[2]}"
if kind == "neg":
return "-" + named(show[1], real[1], env, master, depth)
if kind == "bin":
return _bin(show, real, env, master, depth, named)
if kind == "call":
if show[1] == "만약": # 값 꼴과 같은 쪽만 적어 두 칸이 짝이 되게
try:
at = 1 if mf.evaluate(real[2][0], env, master, depth) else 2
except _CALC_ERRORS: # 조건 입력이 비면 식 그대로(아래 부르기 꼴)
at = 0
if at:
return named(show[2][at], real[2][at], env, master, depth)
args = ", ".join(named(s, r, env, master, depth) for s, r in zip(show[2], real[2]))
return f"{show[1]}({args})"
if kind in ("num", "str"):
return _draw(show, real, env, master, depth)
return _value(real, env, master, depth) # 별칭이 안 붙은 부르기 — 값으로
def _elem_name(master, ref: str) -> str:
"""식 안 요소 한 줄의 이름 — 「석공노임」 꼴(같은 이름의 중간 값과 헷갈리지 않게)."""
if (
str(ref)[:2] not in ELEM_KEYS
): # 별칭 이름이 표 열쇠 그대로인 것(QC…) — 밑이름을 풀어쓰지 않음
return ref
try:
name = mc.plain(master.get(ref).get("이름"))
except mf.FormulaError:
name = ""
return f"{name}{ELEM_TAIL.get(str(ref)[:2], '단가')}" if name else ref
def _value(node, env: dict, master, depth: int) -> str:
value = mf.evaluate(node, env, master, depth)
if isinstance(value, dict): # 로직 부르기 묶음 — 줄 금액에 쓰는 것은 「계」
value = value.get("계")
return fmt(value)
def _bin(show, real, env: dict, master, depth: int, draw) -> str:
_, op, left, right = show
if op == "/" and right[0] == "num" and right[1] == 100: # 「… / 100」 은 퍼센트로
return f"{_side(left, real[2], show, env, master, depth, draw, False)} %"
return (
f"{_side(left, real[2], show, env, master, depth, draw, False)} {op} "
f"{_side(right, real[3], show, env, master, depth, draw, True)}"
)
def _side(show, real, parent, env: dict, master, depth: int, draw, right: bool) -> str:
text = draw(show, real, env, master, depth)
if show[0] != "bin":
return text
mine, up = _PREC[show[1]], _PREC[parent[1]]
if mine < up or (mine == up and right and parent[1] in _RIGHT_TIGHT):
return f"({text})"
return text
# ── 조각(알약) — PLAN 2-1 조각 계약 · 2-2 「이름 식을 계산 없이」 ──────────
_PIECE_SYM = {"+": "+", "-": "-", "*": "×", "/": "÷"}
def _p_val(text: str) -> dict:
return {"kind": "값", "글": text}
def _p_sym(text: str) -> dict:
return {"kind": "기호", "글": text}
def _alias_index(marks: list[dict]) -> dict[str, dict]:
"""별칭 이름 → 별칭 한 줄(`master_copy.aliases` 결과) — 조각이 참조를 되찾을 때 씀."""
return {m["이름"]: m for m in marks}
def _elem_piece(master, ref: str) -> dict:
"""요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시(`{…}`) 든 참조 14파일은
값(참조 키)을 못 구하므로 이름만 적고 참조 자리는 비움."""
piece = {"kind": "요소", "글": _elem_name(master, ref)}
if "{" in str(ref):
return piece
try:
row = master.get(ref)
except mf.FormulaError:
return piece
key = str(row.get("키") or ref)
piece["참조"] = {"테이블": key[:2], "키": key}
return piece
def _row_label(table: dict, row: dict, conds: dict) -> list[str]:
"""찾은 줄의 이름 — 조건마다 그 줄의 값(범위 칸은 찾은 범위)."""
out = []
for cond, want in conds.items():
have = row.get(cond, want)
if table.get("조건", {}).get(cond) == "범위" and isinstance(have, list):
low, high = ("" if x is None else fmt(x) for x in have)
out.append(f"{low}~{high}")
else:
out.append(fmt(have))
return out
def _alias_piece(mark: dict, env: dict, master, column: str | None) -> dict:
"""표 찾기·로직 부르기 알약 — 표는 글 = 「찾은 줄 · 칸」 · 참조.행 = 찾기 조건.
다시 못 찾으면(입력이 비어서 따위) 별칭 이름만 남김."""
piece = {"kind": "표" if mark["종류"] == "찾기" else "로직", "글": mark["이름"]}
try:
node = mf.parse(mark["원문"])
ref = mf.fill(node[1], env)
if mark["종류"] == "찾기":
table = master.get(ref)
conds = {k: mf.evaluate(v, env, master, 0) for k, v in node[2]}
row = mf.find(table, conds, column)
tail = column.rpartition("_")[2] if column else ""
piece["글"] = " · ".join([*_row_label(table, row, conds), *([tail] if tail else [])])
piece["참조"] = {"테이블": ref[:2], "키": ref, "행": conds, "열": column}
else:
piece["참조"] = {"테이블": ref[:2], "키": ref}
except _CALC_ERRORS:
pass # 다시 못 찾음 — 별칭 이름만 남김(값 자리 비움)
return piece
def _ga(word: str) -> str:
"""받침이 없으면 「가」 · 있으면 「이」."""
last = word[-1:]
return "이" if "가" <= last <= "힣" and (ord(last) - 0xAC00) % 28 else "가"
def _easy(show, real, env: dict, master) -> str:
if show[0] == "str":
return show[1]
try:
return named(show, real, env, master)
except _CALC_ERRORS:
return ""
def _cond_text(show, real, env: dict, master) -> str:
"""만약 조건 → 쉬운 말 — 「폼타이 가 사용 이면」(같음) · 그 밖은 「식 이면」."""
if show[0] == "bin" and show[1] == "==":
left = _easy(show[2], real[2], env, master)
return f"{left} {_ga(left)} {_easy(show[3], real[3], env, master)} 이면"
return f"{_easy(show, real, env, master)} 이면"
def _if_pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int) -> list[dict]:
"""만약() 조각 — 「조건 이면 A · 아니면 B」 · 안 걸린 갈래는 흐림 ·
조건이 안 셈해지면(입력이 비어서 따위) 흐림 없이 두 갈래 다(깊이를 적어 접을 수 있게)."""
try:
taken = bool(mf.evaluate(real[2][0], env, master, 0))
except _CALC_ERRORS:
taken = None
deeper = ifdepth if taken is not None else ifdepth + 1
then = _pieces(show[2][1], real[2][1], env, master, alias_by_name, deeper)
other = _pieces(show[2][2], real[2][2], env, master, alias_by_name, deeper)
other.insert(0, {"kind": "글", "글": "· 아니면"})
for one in then if taken is None else ():
one["깊이"] = deeper
for one in other if taken is None else ():
one["깊이"] = deeper
for one in then if taken is False else other if taken else ():
one["흐림"] = True
cond = {"kind": "글", "글": _cond_text(show[2][0], real[2][0], env, master)}
return [cond, *then, *other]
def _side_pieces(
show, real, parent_show, env: dict, master, alias_by_name: dict, ifdepth: int, right: bool
):
inner = _pieces(show, real, env, master, alias_by_name, ifdepth)
if show[0] != "bin":
return inner
mine, up = _PREC[show[1]], _PREC[parent_show[1]]
if mine < up or (mine == up and right and parent_show[1] in _RIGHT_TIGHT):
return [_p_sym("("), *inner, _p_sym(")")]
return inner
def _bin_pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int) -> list[dict]:
op = show[1]
if op == "/" and show[3][0] == "num" and show[3][1] == 100: # 「… / 100」 은 퍼센트로
left = _side_pieces(show[2], real[2], show, env, master, alias_by_name, ifdepth, False)
return [*left, {"kind": "글", "글": "%"}]
left = _side_pieces(show[2], real[2], show, env, master, alias_by_name, ifdepth, False)
right = _side_pieces(show[3], real[3], show, env, master, alias_by_name, ifdepth, True)
return [*left, _p_sym(_PIECE_SYM.get(op, op)), *right]
def _pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int = 0) -> list[dict]:
"""식 한 마디 → 조각 목록. 요소·표·로직 부르기는 알약(참조가 됨) · 나머지는 값·기호·글."""
kind = show[0]
if kind == "num":
return [_p_val(fmt(show[1]))]
if kind == "str":
return [_p_val(f"'{show[1]}'")]
if kind == "neg":
return [_p_sym("-"), *_pieces(show[1], real[1], env, master, alias_by_name, ifdepth)]
if kind == "bin":
return _bin_pieces(show, real, env, master, alias_by_name, ifdepth)
if kind == "call" and show[1] == "만약":
return _if_pieces(show, real, env, master, alias_by_name, ifdepth)
if kind == "ref":
return [_elem_piece(master, show[1])]
if kind == "field" and show[1][0] == "name" and show[1][1] in alias_by_name:
return [_alias_piece(alias_by_name[show[1][1]], env, master, show[2])]
if kind == "name" and show[1] in alias_by_name:
return [_alias_piece(alias_by_name[show[1]], env, master, None)]
piece = _p_val(show[1] if kind == "name" else "") # 중간 값은 글에 그 이름
try:
value = _value(real, env, master, 0)
piece.update({"값": value} if kind == "name" else {"글": value})
except _CALC_ERRORS:
pass # 값 계산 자체가 막힘 — 이름만(그 밖은 빈 글) 남겨 줄이 안 죽게
return [piece]
def _formula_pieces(text, env: dict, master, name_of: dict, alias_by_name: dict) -> list[dict]:
"""식 글 한 줄의 조각 — 별칭을 씌운 나무와 원래 나무를 나란히 읽음(`_names` 와 짝)."""
real = mf.parse(text)
try:
show = mf.parse(mc.mask(text, name_of))
except mf.FormulaError:
show = real
return _pieces(show, real, env, master, alias_by_name)
# ── 줄 세우기 ─────────────────────────────────────────────────────────
def _env_of(master, row: dict, given: dict, depth: int = 0) -> dict:
"""입력 + 중간 — `run` 과 같은 차례로 다시 셈(덧줄 전 상태).
아직 안 고른 입력이 있으면(값 넣기 전) 받은 것만 담아 돌려줌 — 이름 식은 값 없이도 지어짐
(고른 값이 틀린 경우(고르기·범위 밖)는 지금처럼 그대로 막힘 — `mf._inputs` 가 걸러냄)."""
specs = row.get("입력", [])
if any(spec["이름"] not in given for spec in specs):
return {spec["이름"]: given[spec["이름"]] for spec in specs if spec["이름"] in given}
env = mf._inputs(row, given)
for step in row.get("중간", []):
env[step["이름"]] = mf.evaluate(mf.parse(step["식"]), env, master, depth)
return env
def _why(item: dict, line: dict) -> str:
"""단가를 못 구했거나 0 인 줄의 까닭 한 마디."""
if line["수량"] == 0:
return "수량 0 — 이 조건에서는 안 쓰는 줄"
if line["단가"] not in (0, None):
return ""
where = item.get("요소")
where = "재료 고르기 조건" if isinstance(where, dict) else f"「{where}」"
return f"단가 0 — {where} 값이 아직 없음"
def _label(item: dict, line: dict) -> str:
spec = str(item.get("규격") or "").strip()
name = str(line.get("이름") or item.get("이름") or "")
return f"{name}({spec})" if spec else name
def _shares(line: dict) -> list[tuple[str, Decimal]]:
"""글로 적을 (비목, 몫) — 비목이 나뉜 로직 줄은 몫이 없는 비목을 건너뜀.
몫이 전부 0 이면 호표 줄이 사라지지 않게 첫 비목에 0 원 한 줄을 남김."""
parts = list(line["비목"].items())
if len(parts) <= 1:
return parts
return [(part, money) for part, money in parts if money != 0] or parts[:1]
def _loose(node, env: dict, master) -> bool:
"""수량식 맨 위가 덧셈·뺄셈이면(곱 뒤에 오면 괄호가 필요함) · 만약은 걸린 갈래 기준."""
if node[0] == "bin":
return _PREC[node[1]] < _PREC["*"]
if node[0] == "call" and node[1] == "만약":
try:
at = 1 if mf.evaluate(node[2][0], env, master, 0) else 2
except _CALC_ERRORS:
return False # 이름 꼴은 「만약(…)」 부르기 그대로라 괄호 불필요
return _loose(node[2][at], env, master)
return False
def _times(text: str, item: dict, env: dict, master) -> str:
"""「단가 * 수량식」 의 수량식 — 덧셈·뺄셈 묶음이면 괄호."""
return f"({text})" if _loose(mf.parse(item["수량"]), env, master) else text
def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> str:
"""「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만 · 빈 줄(금액 null)은 빈 글."""
if line["금액"] is None:
return ""
qty = _times(render(mf.parse(item["수량"]), env, master), item, env, master)
money = line["비목"][part] if part else line["금액"]
price = line["단가"] if part is None or line["수량"] == 0 else money / line["수량"]
return f"{fmt_money(price)} * {qty} = {fmt_money(money)}"
def _line_names(item: dict, line: dict, env: dict, master, part: str | None, name_of: dict) -> str:
"""같은 줄을 변수 이름으로 — 「석공노임 * 메쌓기인력.석공 * (1 + 증가율) = 석공비」."""
name = mc.plain(item.get("이름") or line.get("이름")) or "값"
tail = PRICE_TAIL.get(str(item.get("종류") or ""), "단가")
price = f"{name}{part or ''}{tail}"
money = f"{name}{part}" if part else f"{name}비"
qty = _times(_names(item["수량"], env, master, name_of), item, env, master)
return f"{price} * {qty} = {money}"
def _extra_text(extra: dict, line: dict, env: dict, master) -> str:
if line["금액"] is None: # 빈 덧줄 — 식을 다시 셈하지 않음
return ""
return f"{render(mf.parse(extra['식']), env, master)} = {fmt_money(line['금액'])}"
def _extra_names(extra: dict, env: dict, master, name_of: dict) -> str:
got = mc.plain(extra.get("이름")) or "덧줄"
return f"{_names(extra['식'], env, master, name_of)} = {got}"
def _names(text, env: dict, master, name_of: dict) -> str:
"""식 글 한 줄을 이름 꼴로 — 별칭을 씌운 나무와 원래 나무를 나란히 읽음."""
real = mf.parse(text)
try:
show = mf.parse(mc.mask(text, name_of))
except mf.FormulaError: # 별칭을 씌우면 못 읽는 글 — 값 꼴로 물러남
show = real
return named(show, real, env, master)
def lines(files: dict, key: str, given: dict) -> dict:
"""로직 한 줄의 읽는 식 — 비목 묶음마다 줄과 소계 · 돈 아닌 로직은 「결과」 묶음 하나.
호표 줄 계산이 막히면(재료 단가 없음 따위) `row` 구조만으로 이름 식·조각을 짓고
`글`·`금액`은 비움(입력 자체가 없는 경우는 지금처럼 그대로 막힘 — `_env_of` 에서 걸림)."""
master = mf.Master(files)
row = master.logic(key)
env = _env_of(master, row, given)
marks = mc.aliases(row, master)
name_of = mc.names(marks)
alias_by_name = _alias_index(marks)
try:
result = mf.run(master, key, given)
except _CALC_ERRORS:
result = None
if "결과" in row:
조각 = _formula_pieces(row["결과"]["식"], env, master, name_of, alias_by_name)
got = mc.plain(row.get("이름")) or "결과"
shown = (
fmt_money if str(row.get("결과단위") or "").startswith("원") else fmt
) # 돈 결과만 정수
이름글 = f"{_names(row['결과']['식'], env, master, name_of)} = {got}"
if result is not None:
one = {
"이름": row.get("이름") or "",
"글": f"{render(mf.parse(row['결과']['식']), env, master)} = "
f"{shown(result['결과'])}",
"이름글": 이름글,
"금액": result["결과"],
"조각": 조각,
}
group = {
"비목": "결과",
"줄": [one],
"소계": result["결과"],
"끝수": _cut_note(row, "결과"),
}
= result["결과"]
else:
one = {
"이름": row.get("이름") or "",
"글": "",
"이름글": 이름글,
"금액": None,
"조각": 조각,
}
group = {"비목": "결과", "줄": [one], "소계": None, "끝수": ""}
= None
return _paired({"ok": True, "groups": [group], "계": , "별칭": marks})
made: dict[str, list] = {name: [] for name in mf.COST_ITEMS}
items = row.get("호표") or []
if result is not None:
shown = result["줄"]
env["줄"] = {r["이름"]: r["금액"] for r in shown if r["이름"]}
for item, line in zip(items, shown):
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
for part, money in _shares(line):
split = part if len(line["비목"]) > 1 else None
one = {
"이름": _label(item, line),
"글": _line_text(item, line, env, master, split),
"이름글": _line_names(item, line, env, master, split, name_of),
"금액": money,
"조각": 조각,
}
why = _why(item, line)
made[part].append({**one, "까닭": why} if why else one)
_extras(row, shown[len(items) :], made, env, master, name_of, alias_by_name)
소계 = {name: result[name] for name in mf.COST_ITEMS}
끝수 = _cut_note(row)
= result["계"]
else:
for item in items:
parts = (
list(mf.COST_ITEMS)
if item.get("종류") == "로직" and "비목" not in item
else [item.get("비목")]
)
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
for part in parts:
if part not in mf.COST_ITEMS:
continue
split = part if len(parts) > 1 else None
made[part].append(
{
"이름": _label(item, {}),
"글": "",
"이름글": _line_names(item, {}, env, master, split, name_of),
"금액": None,
"조각": 조각,
}
)
for extra in row.get("덧줄") or []:
made[extra["비목"]].append(
{
"이름": extra["이름"],
"글": "",
"이름글": (
f"{_names(extra['식'], env, master, name_of)} = "
f"{mc.plain(extra.get('이름')) or '덧줄'}"
),
"금액": None,
"조각": _formula_pieces(extra["식"], env, master, name_of, alias_by_name),
}
)
소계 = dict.fromkeys(mf.COST_ITEMS, None)
끝수 = ""
= None
groups = [
{"비목": name, "줄": made[name], "소계": 소계[name], "끝수": 끝수}
for name in mf.COST_ITEMS
if made[name]
]
return _paired({"ok": True, "groups": groups, "계": , "별칭": marks})
def _paired(answer: dict) -> dict:
"""줄마다 짝 번호 — 두 칸(이름 꼴 · 값 꼴)이 같은 차례로 놓이게 1 부터."""
at = 0
for group in answer["groups"]:
for one in group["줄"]:
at += 1
one["짝"] = at
return answer
def _extras(
row: dict, shown: list, made: dict, env: dict, master, name_of: dict, alias_by_name: dict
) -> None:
"""덧줄 — 앞선 줄까지의 비목 합을 넣고 한 줄씩(식 안의 비목 이름은 그 수로 바뀜)."""
sums = {
name: sum((one["금액"] or 0 for one in made[name]), Decimal(0)) for name in mf.COST_ITEMS
}
for extra, line in zip(row.get("덧줄") or [], shown):
env.update(sums)
one = {
"이름": extra["이름"],
"글": _extra_text(extra, line, env, master),
"이름글": _extra_names(extra, env, master, name_of),
"금액": line["금액"],
"조각": _formula_pieces(extra["식"], env, master, name_of, alias_by_name),
}
made[extra["비목"]].append(one)
if line["금액"] is not None: # 빈 덧줄은 합·이름 값에 안 넣음
sums[extra["비목"]] += line["금액"]
env[extra["이름"]] = line["금액"]
def _cut_note(row: dict, on: str = "계") -> str:
"""소계에 붙일 끝수 한 마디 — 끝수는 따로 줄로 보이지 않음."""
rule = row.get("끝수")
if not isinstance(rule, dict) or rule.get("대상") != on:
return ""
place = int(rule.get("자리") or 0)
where = "원 미만" if place == 0 else f"소수 {place + 1}째 자리에서"
return f"{where} {rule.get('방법')}"