feat(M01): /text 조각 목록 더함 · 이름 식 계산 없이(PLAN 2-1 첫줄·2-2)
- master_text.py — 조각(요소·표·로직·값·기호·글) 생성기 추가, 이름글·글·금액·
까닭은 그대로 두고 줄마다 「조각」 칸만 더함
- 만약() 은 계산되면 걸린 가지만 · 계산 안 되면(FormulaError 따위) 양쪽 다
내보내고 깊이를 적어 접을 수 있게 함
- 자리 표시({…}) 든 요소 참조는 이름만 적고 참조(키) 자리는 비움
- mf.run 이 막혀도(재료 단가 없음 따위) row 구조만으로 이름 식·조각을 짓고
글·금액만 비움 — 로직 부르는 줄(비목 미정)은 세 비목에 다 걸쳐 보임
- test_m01_text.py — 새 시험 6개(계약 확인·요소 알약·자리 표시·만약 둘·
계산 막힘 no-calc 경로), 기존 10개 그대로 통과
- 검증: pytest resources/tester/test_m01_text.py 16 passed · 13-4-1·12-4·
12-17-1 금액이 앞과 같음(calc 엔드포인트와 대조, 기존 시험 그대로) ·
ruff format/check 통과
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -7,6 +7,11 @@
|
||||
· `이름글` 이름 꼴: 「석공노임 * 메쌓기인력.석공 * (1 + 증가율) = 석공비」 — 표 찾기·로직
|
||||
부르기는 별칭 이름(`master_copy` 3-4 규칙) · 중간 값·입력은 그 이름 그대로
|
||||
두 꼴은 같은 줄에 나란히 있고 `짝` 번호가 붙음 — 화면 두 칸이 줄 맞춤으로 놓임.
|
||||
줄마다 `조각`(PLAN 2-1 조각 계약) 도 같이 줌 — kind(요소·표·로직·값·기호·글) · 글 · 참조.
|
||||
알약(요소·표·로직)이 될 자리는 구조만 보고 짓고, `만약()` 갈래만 골라야 해서 계산이 필요함 —
|
||||
계산이 안 되면(FormulaError 따위) 갈래 양쪽을 다 내보내고 깊이를 적어 접을 수 있게 함.
|
||||
호표 줄 계산 자체가 막혀도(재료 단가 없음 따위) 이름 식·조각은 `row` 구조만으로 그리고
|
||||
`글`·`금액`만 비움 — `만약() 과 무관` 인 그 60개 로직도 이름 식은 보이게.
|
||||
끝수는 줄이 아니라 소계에 녹여 적음. 계산 자체는 `master_formula.run` 이 한 벌만 함.
|
||||
"""
|
||||
|
||||
@@ -24,6 +29,7 @@ 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:
|
||||
@@ -125,6 +131,133 @@ def _side(show, real, parent, env: dict, master, depth: int, draw, right: bool)
|
||||
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 _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)
|
||||
piece["참조"] = {
|
||||
"테이블": ref[:2],
|
||||
"키": ref,
|
||||
"행": row.get("키") or row.get("원문번호"),
|
||||
"열": column,
|
||||
}
|
||||
else:
|
||||
piece["참조"] = {"테이블": ref[:2], "키": ref}
|
||||
except _CALC_ERRORS:
|
||||
pass # 다시 못 찾음 — 별칭 이름만 남김(값 자리 비움)
|
||||
return piece
|
||||
|
||||
|
||||
def _if_pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int) -> list[dict]:
|
||||
"""만약() 조각 — 계산되면 걸린 가지만 · 안 되면 양쪽 다(깊이를 적어 접을 수 있게)."""
|
||||
try:
|
||||
taken = mf.evaluate(real[2][0], env, master, 0)
|
||||
except _CALC_ERRORS:
|
||||
deeper = ifdepth + 1
|
||||
left = _pieces(show[2][1], real[2][1], env, master, alias_by_name, deeper)
|
||||
right = _pieces(show[2][2], real[2][2], env, master, alias_by_name, deeper)
|
||||
for one in (*left, *right):
|
||||
one["깊이"] = deeper
|
||||
return [*left, _p_sym("/"), *right]
|
||||
at = 1 if taken else 2
|
||||
return _pieces(show[2][at], real[2][at], env, master, alias_by_name, ifdepth)
|
||||
|
||||
|
||||
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)]
|
||||
try:
|
||||
return [_p_val(_value(real, env, master, 0))]
|
||||
except _CALC_ERRORS:
|
||||
return [_p_val("")] # 그 밖(값 계산 자체가 막힘) — 빈 값 조각으로 줄이 안 죽게
|
||||
|
||||
|
||||
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` 과 같은 차례로 다시 셈(덧줄 전 상태)."""
|
||||
@@ -197,56 +330,117 @@ def _names(text, env: dict, master, name_of: dict) -> str:
|
||||
|
||||
|
||||
def lines(files: dict, key: str, given: dict) -> dict:
|
||||
"""로직 한 줄의 읽는 식 — 비목 묶음마다 줄과 소계 · 돈 아닌 로직은 「결과」 묶음 하나."""
|
||||
"""로직 한 줄의 읽는 식 — 비목 묶음마다 줄과 소계 · 돈 아닌 로직은 「결과」 묶음 하나.
|
||||
호표 줄 계산이 막히면(재료 단가 없음 따위) `row` 구조만으로 이름 식·조각을 짓고
|
||||
`글`·`금액`은 비움(입력 자체가 없는 경우는 지금처럼 그대로 막힘 — `_env_of` 에서 걸림)."""
|
||||
master = mf.Master(files)
|
||||
row = master.logic(key)
|
||||
result = mf.run(master, key, given)
|
||||
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:
|
||||
text = f"{render(mf.parse(row['결과']['식']), env, master)} = {fmt(result['결과'])}"
|
||||
조각 = _formula_pieces(row["결과"]["식"], env, master, name_of, alias_by_name)
|
||||
got = mc.plain(row.get("이름")) or "결과"
|
||||
one = {
|
||||
"이름": row.get("이름") or "",
|
||||
"글": text,
|
||||
"이름글": f"{_names(row['결과']['식'], env, master, name_of)} = {got}",
|
||||
"금액": result["결과"],
|
||||
}
|
||||
group = {
|
||||
"비목": "결과",
|
||||
"줄": [one],
|
||||
"소계": result["결과"],
|
||||
"끝수": _cut_note(row, "결과"),
|
||||
}
|
||||
return _paired({"ok": True, "groups": [group], "계": result["결과"], "별칭": marks})
|
||||
made: dict[str, list] = {name: [] for name in mf.COST_ITEMS}
|
||||
shown = result["줄"]
|
||||
env["줄"] = {r["이름"]: r["금액"] for r in shown if r["이름"]}
|
||||
items = row.get("호표") or []
|
||||
for item, line in zip(items, shown):
|
||||
for part, money in _shares(line):
|
||||
split = part if len(line["비목"]) > 1 else None
|
||||
이름글 = f"{_names(row['결과']['식'], env, master, name_of)} = {got}"
|
||||
if result is not None:
|
||||
one = {
|
||||
"이름": _label(item, line),
|
||||
"글": _line_text(item, line, env, master, split),
|
||||
"이름글": _line_names(item, line, env, master, split, name_of),
|
||||
"금액": money,
|
||||
"이름": row.get("이름") or "",
|
||||
"글": f"{render(mf.parse(row['결과']['식']), env, master)} = {fmt(result['결과'])}",
|
||||
"이름글": 이름글,
|
||||
"금액": result["결과"],
|
||||
"조각": 조각,
|
||||
}
|
||||
why = _why(item, line)
|
||||
made[part].append({**one, "까닭": why} if why else one)
|
||||
_extras(row, shown[len(items) :], made, env, master, name_of)
|
||||
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],
|
||||
"소계": result[name],
|
||||
"끝수": _cut_note(row),
|
||||
}
|
||||
{"비목": name, "줄": made[name], "소계": 소계[name], "끝수": 끝수}
|
||||
for name in mf.COST_ITEMS
|
||||
if made[name]
|
||||
]
|
||||
return _paired({"ok": True, "groups": groups, "계": result["계"], "별칭": marks})
|
||||
return _paired({"ok": True, "groups": groups, "계": 계, "별칭": marks})
|
||||
|
||||
|
||||
def _paired(answer: dict) -> dict:
|
||||
@@ -259,7 +453,9 @@ def _paired(answer: dict) -> dict:
|
||||
return answer
|
||||
|
||||
|
||||
def _extras(row: dict, shown: list, made: dict, env: dict, master, name_of: dict) -> None:
|
||||
def _extras(
|
||||
row: dict, shown: list, made: dict, env: dict, master, name_of: dict, alias_by_name: dict
|
||||
) -> None:
|
||||
"""덧줄 — 앞선 줄까지의 비목 합을 넣고 한 줄씩(식 안의 비목 이름은 그 수로 바뀜)."""
|
||||
sums = {name: sum((one["금액"] for one in made[name]), Decimal(0)) for name in mf.COST_ITEMS}
|
||||
for extra, line in zip(row.get("덧줄") or [], shown):
|
||||
@@ -269,6 +465,7 @@ def _extras(row: dict, shown: list, made: dict, env: dict, master, name_of: dict
|
||||
"글": _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)
|
||||
sums[extra["비목"]] += line["금액"]
|
||||
|
||||
Reference in New Issue
Block a user