feat(b09): 산근 글 = 흐름도 함수 빠짐없이 · 약어 뜻 · 기계 번호 = 이름 규격(157-1 · 157-3)
- 중간 · 결과 · 호표 수량 = 「이름 = 이름식 = 값 넣은 식 = 값」 · 반올림 따위 부르기는 인자째 - 찾기 = 「표 이름(조건 = 값).칸」 · 만약 = 걸린 갈래만 · 돈 하위 로직 = 이름(산근 n호표) - 돈 아닌 하위 로직(시간당 작업량 따위) = 「└ 」 줄로 입력 · 중간 · 결과 풀이 - 입력 = 「이름 : 설명 = 값 단위」 · 기계 · 직종 원문번호 = 「이름 규격」(M02_Labor_Inputs._labels) - 함수 줄은 머리에만 — 호표 줄 · 계 · 금액 그대로 · 시험 test_157_1 새로 · 155_5 Q 줄 글 갱신 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
"""B09 산근 한 항목 → 엑셀 단가산출근거 꼴 텍스트 수식 줄들(PLAN 155-5) — 서버 단독(③ · 순수 함수).
|
||||
|
||||
꼴 = 엑셀 단가산출근거 탭 산출내역 한 칸 글:
|
||||
머리 「이름 / 단위」 · 출처 「… 적용」 · 입력 「이름 : 값 단위」 ·
|
||||
중간 「Q = 이름식 = 값」(작업량 Q = ROUND 2 — `Sangeun._env`) ·
|
||||
머리 「이름 / 단위」 · 출처 「… 적용」 · 입력 「이름 : 설명 = 값 단위」(157-1 · 설명 없으면
|
||||
「이름 : 값 단위」 · 기계 · 직종 번호 = 「이름 규격」 157-3 `M02_Labor_Inputs._labels`) ·
|
||||
함수(M01 흐름도 함수 상자 · 157-1) = 계산 차례대로 「이름 = 이름식 = 값 넣은 식 = 값」 —
|
||||
중간 · 결과(돈 아닌 로직) · 호표 수량(이름식이 값 꼴과 다를 때 「이름 수량 = …」) ·
|
||||
찾기 = 「표 이름(조건 = 값).칸」 · 만약 = 걸린 갈래만 · 하위 로직 = 로직 이름(산근 n호표) ·
|
||||
돈 아닌 하위 로직은 그 풀이를 「└ 」 줄로 이어 씀(작업량 Q = ROUND 2 — `Sangeun._env`) ·
|
||||
함수 줄은 모두 머리(호표 줄 수 그대로) ·
|
||||
줄 「보통인부 : 172,068 * 0.022 = 3,785.4」(단가 * 수량식 값 꼴[ / 배][ * 율 %] = 금액) ·
|
||||
중기 = 노무비 · 재료비 · 경비 세 줄 · 하위 산근 = 「산근 n호표 단가 * 수량」 ·
|
||||
% 덧줄 = 「이름 (노무비의 %) : 밑수 * n % = 금액」(규격 = 149-4 `pct_extras`) ·
|
||||
@@ -16,6 +21,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
@@ -23,6 +29,7 @@ from typing import Any
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
|
||||
mf = sg.mf
|
||||
mtx = m01.mtx
|
||||
@@ -56,43 +63,193 @@ def _line(name: str, price: Any, qty_text: str, q: float) -> tuple[str, float |
|
||||
return f"{name} : {_price(price)} * {qty_text} = {_money(money)}", money
|
||||
|
||||
|
||||
def _head(row: dict, env: dict, whole: Any, times: Decimal) -> list[str]:
|
||||
def _head(row: dict, env: dict, whole: Any, times: Decimal, price_of: PriceOf) -> list[str]:
|
||||
unit = str(row.get("결과단위") or "").removeprefix("원/")
|
||||
if times != 1:
|
||||
unit = unit.removeprefix(mtx.fmt(times))
|
||||
out = [f"{row.get('이름') or row.get('키')} / {unit}"]
|
||||
if row.get("출처"):
|
||||
out.append(f"{row['출처']} 적용")
|
||||
return out + _steps(whole, row, env, price_of, 0)
|
||||
|
||||
|
||||
def _word(whole: Any, name: str, value: Any) -> str:
|
||||
"""값 글 — 수 = 보이는 자릿수 · 기계 · 직종 원문번호 = 「이름 규격」(157-3 · 85-1 도우미)."""
|
||||
if isinstance(value, Decimal):
|
||||
return mtx.fmt(value)
|
||||
if isinstance(value, str):
|
||||
return labor_inputs._labels(whole, {"이름": name, "고르기": [value]}).get(value, value)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _shown(whole: Any, name: str, value: Any) -> str | None:
|
||||
if value is None or isinstance(value, mf.Blank):
|
||||
return "값 없음"
|
||||
return None if isinstance(value, dict) else _word(whole, name, value)
|
||||
|
||||
|
||||
def _marks(row: dict, whole: Any) -> list[dict]:
|
||||
"""별칭(찾기 · 로직 부르기 → 짧은 이름 · `master_copy.aliases`) — 못 지으면 빈 목록."""
|
||||
try:
|
||||
return mtx.mc.aliases(row, whole)
|
||||
except _ERRORS:
|
||||
return []
|
||||
|
||||
|
||||
def _call_text(whole: Any, mark: dict, env: dict, price_of: PriceOf) -> str | None:
|
||||
"""별칭 자리 글 — 찾기 = 「표 이름(조건 = 값)」 · 로직 = 「로직 이름(산근 n호표)」."""
|
||||
try:
|
||||
node = mf.parse(mark["원문"])
|
||||
ref = mf.fill(node[1], env)
|
||||
args = [(k, mf.evaluate(v, env, whole, 0)) for k, v in node[2]]
|
||||
except _ERRORS:
|
||||
return None
|
||||
if mark["종류"] == "찾기":
|
||||
conds = ", ".join(f"{k} = {_word(whole, k, v)}" for k, v in args)
|
||||
return f"{mark.get('밑이름') or mark['이름']}{f'({conds})' if conds else ''}"
|
||||
try:
|
||||
name = str(whole.logic(ref).get("이름") or ref)
|
||||
except mf.FormulaError:
|
||||
return None
|
||||
got = (
|
||||
price_of(sg.sangeun_key(ref, sg._clean_given(dict(args))))
|
||||
if sg.is_money(whole, ref)
|
||||
else None
|
||||
)
|
||||
return f"{name}(산근 {got['no']}호표)" if isinstance(got, dict) and got.get("no") else name
|
||||
|
||||
|
||||
def _named(text: str, env: dict, whole: Any, marks: list[dict], price_of: PriceOf) -> str:
|
||||
"""이름 꼴 식 — 별칭은 찾기 표 이름(조건) · 로직 이름으로 바꿔 씀."""
|
||||
named = mtx._names(text, env, whole, mtx.mc.names(marks))
|
||||
for mark in sorted(marks, key=lambda m: -len(m["이름"])):
|
||||
pattern = rf"(?<![\w]){re.escape(mark['이름'])}(?![\w])"
|
||||
shown = re.search(pattern, named) and _call_text(whole, mark, env, price_of)
|
||||
if shown:
|
||||
named = re.sub(pattern, lambda _m, s=shown: s, named)
|
||||
return named
|
||||
|
||||
|
||||
def _valued(show, real, env: dict, whole: Any, depth: int = 0) -> str:
|
||||
"""값 꼴 식 — 엔진 `render` 와 같되 반올림 · 최소 · 최대 부르기는 값 넣은 인자째 ·
|
||||
만약 = 걸린 갈래."""
|
||||
if show[0] == "call" and show[1] != "만약":
|
||||
args = ", ".join(_valued(s, r, env, whole, depth) for s, r in zip(show[2], real[2]))
|
||||
return f"{show[1]}({args})"
|
||||
if show[0] == "call":
|
||||
at = 1 if mf.evaluate(real[2][0], env, whole, depth) else 2
|
||||
return _valued(show[2][at], real[2][at], env, whole, depth)
|
||||
if show[0] == "bin":
|
||||
return mtx._bin(show, real, env, whole, depth, _valued)
|
||||
if show[0] == "neg":
|
||||
return "-" + _valued(show[1], real[1], env, whole, depth)
|
||||
return mtx._draw(show, real, env, whole, depth)
|
||||
|
||||
|
||||
def _formula(name, text, value, env, whole, marks, price_of, unit="") -> str:
|
||||
"""함수 한 줄 「이름 = 이름식 = 값 넣은 식 = 값」 — 같은 글이 이어지면 한 번만 ·
|
||||
글 값(기계 원문번호 따위)은 따옴표 글을 빼고 「이름 규격」 만(157-3)."""
|
||||
parts = [name]
|
||||
literal = f"'{value}'" if isinstance(value, str) else None
|
||||
for make in (
|
||||
lambda: _named(text, env, whole, marks, price_of),
|
||||
lambda: _valued(*[mf.parse(text)] * 2, env, whole),
|
||||
lambda: _shown(whole, name, value),
|
||||
):
|
||||
try:
|
||||
got = make()
|
||||
except _ERRORS:
|
||||
got = None
|
||||
if got and got != parts[-1] and got != literal:
|
||||
parts.append(got)
|
||||
tail = f" {unit}" if unit and unit != "-" and isinstance(value, Decimal) else ""
|
||||
return " = ".join(parts) + tail
|
||||
|
||||
|
||||
def _expand(whole, mark: dict, env: dict, price_of: PriceOf, depth: int) -> list[str]:
|
||||
"""돈 아닌 하위 로직 풀이 — 그 로직의 입력 · 중간 · 결과를 「└ 」 줄로(돈 로직 = 산근 호표)."""
|
||||
if mark["종류"] != "로직" or depth > 3:
|
||||
return []
|
||||
try:
|
||||
node = mf.parse(mark["원문"])
|
||||
ref = mf.fill(node[1], env)
|
||||
row = whole.logic(ref)
|
||||
if "결과" not in row or isinstance(row.get("흐름"), dict):
|
||||
return []
|
||||
sub = mf._inputs(row, {k: mf.evaluate(v, env, whole, 0) for k, v in node[2]})
|
||||
for step in row.get("중간", []):
|
||||
try:
|
||||
sub[step["이름"]] = mf.evaluate(mf.parse(step["식"]), sub, whole, depth + 1)
|
||||
except mf.EmptyInput as e:
|
||||
sub[step["이름"]] = mf.Blank(str(e))
|
||||
except _ERRORS:
|
||||
return []
|
||||
lead = " " * depth + "└ "
|
||||
return [lead + line for line in _steps(whole, row, sub, price_of, depth + 1)]
|
||||
|
||||
|
||||
def _steps(whole, row: dict, env: dict, price_of: PriceOf, depth: int) -> list[str]:
|
||||
"""흐름도 함수 상자 차례 — 입력(약어 뜻) · 중간 · 결과 · 호표 수량(맨 위만) · 하위 로직 풀이."""
|
||||
out: list[str] = []
|
||||
for spec in row.get("입력", []):
|
||||
value = env.get(spec["이름"])
|
||||
if value is None:
|
||||
continue
|
||||
shown = mtx.fmt(value) if isinstance(value, Decimal) else str(value)
|
||||
tail = f" {spec['단위']}" if spec.get("단위") else ""
|
||||
out.append(f"{spec['이름']} : {shown}{tail}")
|
||||
name_of = mf_names(row, whole)
|
||||
for step in row.get("중간", []):
|
||||
about = f"{spec['설명']} = " if spec.get("설명") else ""
|
||||
out.append(f"{spec['이름']} : {about}{_word(whole, spec['이름'], value)}{tail}")
|
||||
marks = _marks(row, whole)
|
||||
done: set[str] = set()
|
||||
|
||||
def subs(where: str) -> list[str]:
|
||||
lines = []
|
||||
for mark in marks:
|
||||
if mark["이름"] not in done and where in mark.get("자리", []):
|
||||
done.add(mark["이름"])
|
||||
lines += _expand(whole, mark, env, price_of, depth)
|
||||
return lines
|
||||
|
||||
for n, step in enumerate(row.get("중간", [])):
|
||||
value = env.get(step["이름"])
|
||||
if value is None or isinstance(value, mf.Blank):
|
||||
out.append(f"{step['이름']} = 값 없음")
|
||||
continue
|
||||
shown = mtx.fmt(value)
|
||||
unit = step.get("단위") or _find_unit(step["식"], marks)
|
||||
out.append(_formula(step["이름"], step["식"], value, env, whole, marks, price_of, unit))
|
||||
out += subs(f"중간[{n}].식")
|
||||
if "결과" in row:
|
||||
try:
|
||||
named = mtx._names(step["식"], env, whole, name_of)
|
||||
value = mf.cut(row, mf.evaluate(mf.parse(row["결과"]["식"]), env, whole, depth), "결과")
|
||||
except _ERRORS:
|
||||
named = shown
|
||||
out.append(
|
||||
f"{step['이름']} = {shown}" if named == shown else f"{step['이름']} = {named} = {shown}"
|
||||
)
|
||||
value = None
|
||||
out.append(_formula("결과", row["결과"]["식"], value, env, whole, marks, price_of))
|
||||
out += subs("결과.식")
|
||||
if depth:
|
||||
return out
|
||||
for n, item in enumerate(row.get("호표", [])):
|
||||
try:
|
||||
raw = mf.evaluate(mf.parse(item["수량"]), env, whole, 0)
|
||||
named = _named(item["수량"], env, whole, marks, price_of)
|
||||
valued = _valued(*[mf.parse(item["수량"])] * 2, env, whole)
|
||||
except _ERRORS:
|
||||
continue
|
||||
if named != valued:
|
||||
name = f"{item.get('이름', '')} 수량"
|
||||
out.append(
|
||||
_formula(name, item["수량"], raw, env, whole, marks, price_of, item.get("단위"))
|
||||
)
|
||||
out += subs(f"호표[{n}].수량")
|
||||
return out
|
||||
|
||||
|
||||
def mf_names(row: dict, whole: Any) -> dict:
|
||||
"""별칭 이름표(표 찾기 · 로직 부르기 → 짧은 이름) — 못 지으면 빈 표(값 꼴로 물러남)."""
|
||||
try:
|
||||
return mtx.mc.names(mtx.mc.aliases(row, whole))
|
||||
except _ERRORS:
|
||||
return {}
|
||||
def _find_unit(text: str, marks: list[dict]) -> str:
|
||||
"""식이 표 찾기 한 칸뿐이면 그 칸 단위(별칭 `값칸`) · 그 밖 = 빈 글."""
|
||||
for mark in marks:
|
||||
head = mark["원문"]
|
||||
if mark["종류"] == "찾기" and text.replace(" ", "").startswith(head.replace(" ", "") + "."):
|
||||
unit = (mark.get("값칸") or {}).get(text.rpartition(".")[2], "")
|
||||
return "" if unit in ("글", "구분 따름") else unit
|
||||
return ""
|
||||
|
||||
|
||||
def _material_key(whole: Any, item: dict, ref: Any, env: dict, n: int) -> tuple[str, str]:
|
||||
@@ -205,7 +362,7 @@ def render_sangeun_text(
|
||||
env = sg._env(whole, row, sg._clean_given(inputs))
|
||||
except _ERRORS as e:
|
||||
return [f"{row.get('이름') or logic_id} : 입력을 못 읽음 — {e}"]
|
||||
out = _head(row, env, whole, times)
|
||||
out = _head(row, env, whole, times, price_of)
|
||||
amounts: list[dict] = []
|
||||
with mf.mp.choosing(picks):
|
||||
for n, item in enumerate(row.get("호표", [])):
|
||||
|
||||
Reference in New Issue
Block a user