fix(m01): 자리 표시 든 참조를 조각 · 이름 식에서 이름 + 입력값으로 (PLAN 5장 다시 돌림 2)

- MO:휘발유:{지역} 날글 대신 「휘발유 · 전국평균」 · 입력이 비면 「휘발유 · 지역」
- 조각 참조 = 입력으로 풀린 실제 키 · 못 풀면 참조 없음
- 요소조각 · 조각 · 이름 식 같은 규칙 · 값 식 · 되돌리기는 그대로
- 자동값 · 빈 입력 14,490줄: 날글 48줄(로직 13) → 0 · 금액 변화 0

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
2026-09-24 13:20:40 +09:00
co-authored by Claude Sonnet 5
parent 04bee4e4ec
commit c1181bc9b5
2 changed files with 75 additions and 10 deletions
+46 -10
View File
@@ -89,7 +89,7 @@ def named(show, real, env: dict, master, depth: int = 0) -> str:
if kind == "name":
return show[1]
if kind == "ref":
return _elem_name(master, show[1])
return _elem_name(master, show[1], env)
if kind == "field":
return f"{named(show[1], real[1], env, master, depth)}.{show[2]}"
if kind == "neg":
@@ -111,17 +111,48 @@ def named(show, real, env: dict, master, depth: int = 0) -> str:
return _value(real, env, master, depth) # 별칭이 안 붙은 부르기 — 값으로
def _elem_name(master, ref: str) -> str:
def _hole(master, ref: str, env: dict | None) -> tuple[list[str], dict | None]:
"""자리 표시(`XX:품목:{입력}` · `XX:{입력}`) 든 참조 → (읽는 글 조각들, 입력으로 풀린 요소 줄).
글 = 앞 글 + 지금 입력값(「휘발유 · 전국평균」) · 구멍뿐이면 입력 이름이 앞(「장비 · 굴착기」).
입력이 비면 입력 이름만(「휘발유 · 지역」) · 못 풀면 줄은 None."""
env = env or {}
tail = ref[3:]
holes = mf._HOLE.findall(tail)
words = [w.strip(": ") for w in mf._HOLE.split(tail)[::2] if w.strip(": ")]
try:
row = master.get(mf.fill(ref, env))
except mf.FormulaError:
row = None
pure = not words and len(holes) == 1
shown = []
for name in holes:
value = env.get(name)
if value is None:
shown.append(name)
elif pure and row is not None: # 입력 하나가 요소 자리 전부 — 그 요소 이름 + 규격
shown.append(f"{row.get('이름') or value} {row.get('규격') or ''}".strip())
else:
shown.append(fmt(value) if isinstance(value, Decimal) else str(value))
head = words or holes[:1]
return head + [one for one in shown if one not in head], row
def _elem_name(master, ref: str, env: dict | None = None) -> str:
"""식 안 요소 한 줄의 이름 — 「석공노임」 꼴(같은 이름의 중간 값과 헷갈리지 않게)."""
if (
str(ref)[:2] not in ELEM_KEYS
): # 별칭 이름이 표 열쇠 그대로인 것(QC…) — 밑이름을 풀어쓰지 않음
return ref
tail = ELEM_TAIL.get(str(ref)[:2], "단가")
try:
name = mc.short(master.get(ref).get("이름"))
if "{" in str(ref): # 입력으로 풀린 요소 이름 · 못 풀면 입력 이름
parts, row = _hole(master, ref, env)
name = mc.short(row.get("이름")) if row is not None else mc.plain("".join(parts))
else:
name = mc.short(master.get(ref).get("이름"))
except mf.FormulaError:
name = ""
return f"{name}{ELEM_TAIL.get(str(ref)[:2], '단가')}" if name else ref
return f"{name}{tail}" if name else ref
def _value(node, env: dict, master, depth: int) -> str:
@@ -168,12 +199,17 @@ def _alias_index(marks: list[dict]) -> dict[str, dict]:
return {m["이름"]: m for m in marks}
def _elem_piece(master, ref: str) -> dict:
"""요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시(`{…}`) 든 참조 14파일은
값(참조 키)을 못 구하므로 이름만 적고 참조 자리는 비움."""
piece = {"kind": "요소", "": _elem_name(master, ref)}
def _elem_piece(master, ref: str, env: dict | None = None) -> dict:
"""요소 알약 — 글은 이름(못 찾아도 이름만). 자리 표시 든 참조는 글 = 이름 + 지금 입력값 ·
참조 = 입력으로 풀린 실제 키(못 풀면 참조 없음)."""
if "{" in str(ref):
parts, row = _hole(master, ref, env)
piece = {"kind": "요소", "": " · ".join(parts)}
if row is not None:
key = str(row.get("") or ref)
piece["참조"] = {"테이블": key[:2], "": key}
return piece
piece = {"kind": "요소", "": _elem_name(master, ref)}
try:
row = master.get(ref)
except mf.FormulaError:
@@ -298,7 +334,7 @@ def _pieces(show, real, env: dict, master, alias_by_name: dict, ifdepth: int = 0
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])]
return [_elem_piece(master, show[1], env)]
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:
@@ -579,7 +615,7 @@ def _item_pieces(item: dict, env: dict, master) -> list[dict]:
name = str(item.get("이름") or "")
if isinstance(ref, dict): # 재료 고르기 조건 — 품명만
return [{"kind": "요소", "": name}]
piece = _elem_piece(master, ref)
piece = _elem_piece(master, ref, env)
return [{**piece, "": name or piece[""]}]
try:
node = mf.parse(ref)
+29
View File
@@ -308,6 +308,35 @@ def test_조각_로직_부르는_줄_990개_세_비목에_다_걸치고_계산_
assert [g["소계"] for g in out["groups"]] == [None]
def test_자리_표시_든_참조는_이름_입력값으로_읽히고_참조는_풀린_키() -> None:
"""GF000081 잡품 — 「MO:휘발유:{지역}」 날글 대신 「휘발유 · 전국평균」 · 참조 = 풀린 MO 키."""
files = store.cm.load(folder=REAL)
master = store.cm.master(REAL)
row = master.logic("GF000081")
def pills(given: dict) -> tuple[list, str]:
got = mt.lines(files, "GF000081", given)
one = next(o for o in _all_lines(got) if o["이름"] == "잡품")
return [p for p in one["조각"] if p["kind"] == "요소"], one["이름글"]
filled, name_text = pills(dict(store.cm.시험입력(row)))
assert [p[""] for p in filled] == ["휘발유 · 전국평균"]
assert filled[0]["참조"][""] == "MO000001" and "MO:" not in name_text
blank, name_text = pills({})
assert [p[""] for p in blank] == ["휘발유 · 지역"] and "참조" not in blank[0]
assert "{" not in name_text and "MO:" not in name_text
def test_자리_표시_참조가_조각_요소조각_이름식에_날글로_남지_않는다() -> None:
files = store.cm.load(folder=REAL)
master = store.cm.master(REAL)
for key in ("GC000138", "GC000211", "GC000267", "GC000559", "GF000081"):
for given in (dict(store.cm.시험입력(master.logic(key))), {}):
for one in _all_lines(mt.lines(files, key, given)):
shown = [one["이름글"], *(p[""] for p in one["조각"] + one["요소조각"])]
assert not any(re.search(r"[A-Z]{2}:|[{}]", t) for t in shown), (key, shown)
def test_표_조각_참조_줄은_엔진이_찾은_줄의_차례다() -> None:
"""GC000010 크레인 — 줄에 없는 조건 칸은 맞음이라 흉내 내면 틀림 · GF000160 합판도."""
files = store.cm.load(folder=REAL)