feat(791-1): 엔진 셈 의미 넓힘 — 금액번지 · 소계 단계 · 제외 · 줄 자리 · 블록 끝전 · 결과 조정 · 변수 표시
- 새 모듈 master_formula_sum.py — 덧줄 고리(master_formula.run 에서 옮김) · 번지 · 단계 · 합밖 · 조정 · 표시 · 검사
- master_cut.py — 줄 자리 {방법, 자리} · 블록 끝전(비목버림 | 큰나머지) · 결과 조정 · 답 단계 목록
- master_formula_check.py — 새 칸 검사(없는 번지 · 단계 순서 · 자리 범위 · 끝전 · 조정)
- _틀.md 7장 「셈 의미 넓힘」 · 8장 덧줄 이름
- 지킴 시험 test_791_1 — M01 로직 1,515 지문 전후 같음 · 새 의미 단위 · 실무 견본 넷(봉화 D00102 · 울진소광 D01489 · 동부청영월 D00003 · 북부청임도 18) = 엑셀
- master_formula.py 690 → 669 줄
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import master_formula as mf
|
||||
import master_formula_sum as ms
|
||||
|
||||
_ROOT = str(Path(__file__).resolve().parents[3])
|
||||
if _ROOT not in sys.path: # 스크립트 · 시험 길 — 서버는 이미 뿌리에서 돎
|
||||
@@ -39,9 +40,19 @@ def down(value, places: int):
|
||||
return Decimal(str(rounddown(float(value), places)))
|
||||
|
||||
|
||||
def _parts(split: dict) -> tuple[dict, Decimal | None]:
|
||||
"""줄 비목 → (비목마다 0.1 원 버림, 금액 = 그 합 · 모두 빈 값 = None)."""
|
||||
cut = {k: down(v, 1) for k, v in split.items()}
|
||||
def _line(value, rule: dict | None):
|
||||
"""줄 값 끝수 — 줄 `자리` {방법, 자리}(791-1) · 없으면 0.1 원 버림."""
|
||||
if value is None or not rule:
|
||||
return down(value, 1)
|
||||
way, places = rule.get("방법"), int(rule.get("자리") or 0)
|
||||
if way == "버림":
|
||||
return down(value, places)
|
||||
return mf._round(Decimal(str(value)), places, mf.ROUND_WAYS[way])
|
||||
|
||||
|
||||
def _parts(split: dict, rule: dict | None = None) -> tuple[dict, Decimal | None]:
|
||||
"""줄 비목 → (비목마다 0.1 원 버림 · 줄 `자리` 면 그것, 금액 = 그 합 · 모두 빈 = None)."""
|
||||
cut = {k: _line(v, rule) for k, v in split.items()}
|
||||
kept = [v for v in cut.values() if v is not None]
|
||||
return cut, (sum(kept, Decimal(0)) if kept else None)
|
||||
|
||||
@@ -84,6 +95,9 @@ def engine(master, ref: str, got: dict, depth: int = 0) -> dict:
|
||||
if isinstance(logic.get("흐름"), dict):
|
||||
return got
|
||||
items, adds = logic.get("호표") or [], logic.get("덧줄") or []
|
||||
rules = [x.get("자리") for x in (*items, *adds)] # 줄 자리(791-1)
|
||||
new = ms.uses(logic)
|
||||
skip = ms.skipped(logic) if new else set()
|
||||
env = dict(got.get("중간") or {})
|
||||
total = dict.fromkeys(COSTS, Decimal(0))
|
||||
lines = []
|
||||
@@ -100,21 +114,40 @@ def engine(master, ref: str, got: dict, depth: int = 0) -> dict:
|
||||
elif n - len(items) < len(adds) and line.get("금액") is not None:
|
||||
add = adds[n - len(items)]
|
||||
env.update(total)
|
||||
if new: # 덧줄이 부르는 번지 · 단계 = 버린 줄로
|
||||
env.update(ms.scope(logic, lines))
|
||||
try:
|
||||
value = mf.evaluate(mf.parse(add["식"]), env, master, depth)
|
||||
except _ERRORS:
|
||||
value = line["금액"]
|
||||
env[add["이름"]] = down(value, 1) # 뒤 덧줄 · 중간 칸 = 버린 값
|
||||
env[add["이름"]] = _line(value, rules[n]) # 뒤 덧줄 · 중간 칸 = 버린 값
|
||||
split = {add["비목"]: value}
|
||||
cut, money = _parts(split)
|
||||
cut, money = _parts(split, rules[n] if n < len(rules) else None)
|
||||
for k, v in cut.items():
|
||||
total[k] += v or 0
|
||||
total[k] += 0 if n in skip else v or 0
|
||||
if n >= len(items) and money is not None:
|
||||
more = {"단가": money}
|
||||
lines.append({**line, "비목": cut, "금액": money, **more})
|
||||
costs = {k: mf.cut(logic, down(v, 0), "계") for k, v in total.items()}
|
||||
# 중간 = 덧줄이 본 값(버린 줄 합 · 덧줄 값 · `줄`) — 시험 계산 중간 칸
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env}
|
||||
if not new:
|
||||
costs = {k: mf.cut(logic, down(v, 0), "계") for k, v in total.items()}
|
||||
# 중간 = 덧줄이 본 값(버린 줄 합 · 덧줄 값 · `줄`) — 시험 계산 중간 칸
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env}
|
||||
costs = settle(logic, total)
|
||||
more = {"단계": ms.steps(logic, lines)}
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env, **more}
|
||||
|
||||
|
||||
def settle(logic: dict, total: dict) -> dict:
|
||||
"""블록 끝전(791-1) — `끝전` 비목버림(비목마다 원 버림 · 옛 규칙) · 큰나머지(계 원 버림 뒤
|
||||
모자란 원을 소수 큰 비목부터 · 같으면 경 › 재 › 노) → 로직 `끝수` 묶음 → `조정` → 원 버림."""
|
||||
costs = {k: down(v, 0) for k, v in total.items()}
|
||||
if logic.get("끝전") == "큰나머지":
|
||||
rest = int(down(sum(total.values()), 0) - sum(costs.values()))
|
||||
order = sorted(COSTS, key=lambda k: (costs[k] - total[k], COSTS[::-1].index(k)))
|
||||
for k in order[:rest]:
|
||||
costs[k] += 1
|
||||
costs = ms.adjust(logic, {k: mf.cut(logic, v, "계") for k, v in costs.items()})
|
||||
return {k: down(v, 0) for k, v in costs.items()}
|
||||
|
||||
|
||||
def plain(got: dict) -> dict:
|
||||
|
||||
@@ -570,7 +570,8 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
env = _inputs(row, given, master)
|
||||
for step in row.get("중간", []):
|
||||
try:
|
||||
env[step["이름"]] = evaluate(parse(step["식"]), env, master, depth)
|
||||
value = evaluate(parse(step["식"]), env, master, depth)
|
||||
env[step["이름"]] = ms.shown(step, value) # `표시` 자리 반올림(791-1)
|
||||
except EmptyInput as e: # 못 구한 중간값 — 쓰는 줄만 비움
|
||||
env[step["이름"]] = Blank(str(e))
|
||||
if "결과" in row:
|
||||
@@ -632,34 +633,11 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
**(extra or {}),
|
||||
}
|
||||
)
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`(빈 줄은 뺌)
|
||||
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
||||
blank = {x["이름"]: x["까닭"] for x in lines if x.get("까닭")}
|
||||
for extra in row.get("덧줄", []):
|
||||
env.update(sums)
|
||||
try: # 덧줄도 값 빈 품목·빈 줄을 부르면 그 줄만 비움
|
||||
value, why = evaluate(parse(extra["식"]), env, master, depth), None
|
||||
env[extra["이름"]] = value
|
||||
except FormulaError as e:
|
||||
gone = next((n for n in blank if n in extra["식"]), None)
|
||||
value, why = None, f"「{gone}」 줄 비어 못 셈 — {blank[gone]}" if gone else str(e)
|
||||
blank[extra["이름"]] = why
|
||||
sums[extra["비목"]] += value or 0
|
||||
lines.append(
|
||||
{
|
||||
"이름": extra["이름"],
|
||||
"단위": "식",
|
||||
"수량": Decimal(1),
|
||||
"단가": value,
|
||||
"금액": value,
|
||||
"비목": {extra["비목"]: value},
|
||||
**({"까닭": why} if why else {}),
|
||||
}
|
||||
)
|
||||
sums = {k: cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합
|
||||
# 덧줄 · 비목 합 끊기 · 번지 · 단계 · 조정(791-1) = `master_formula_sum.extras`
|
||||
sums, more = ms.extras(master, row, lines, env, sums, depth)
|
||||
mb.tag(master, row, lines, env) # 줄마다 인력 · 재료 · 경비 · 하위 묶음(712-1)
|
||||
mg.tag(master, row, lines, env) # 옵션으로 빠진 줄 `옵션꺼짐`(753-1)
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": _shown(env)}
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": _shown(env), **more}
|
||||
|
||||
|
||||
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
|
||||
@@ -680,6 +658,7 @@ from master_material import ( # noqa: E402, F401
|
||||
import master_pick as mp # noqa: E402 — 검색어 · 고름 · 단위
|
||||
import master_bundle as mb # noqa: E402 — 인력 · 재료 · 경비 묶음
|
||||
import master_gate as mg # noqa: E402 — 옵션으로 빠진 줄
|
||||
import master_formula_sum as ms # noqa: E402 — 덧줄 · 번지 · 단계 · 조정 · 표시(791-1)
|
||||
from master_formula_check import ( # noqa: E402, F401 — 검사는 나눈 파일 · 이름은 여기서 그대로
|
||||
_check_cut,
|
||||
_check_node,
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
import itertools
|
||||
from decimal import Decimal
|
||||
|
||||
import master_formula_sum as ms
|
||||
import master_keys as mk
|
||||
from master_formula import (
|
||||
_HOLE,
|
||||
@@ -175,12 +176,14 @@ def check_logic(master: Master, row: dict) -> tuple[list[str], set]:
|
||||
if item.get("종류") != "로직" and group != item.get("종류"):
|
||||
out.append(f"{where} · 요소 그룹이 종류와 다름")
|
||||
names = known | set(COST_ITEMS) | {"줄"} # 「줄」 = 호표 줄 금액 묶음(덧줄에서만)
|
||||
if ms.uses(row): # 금액번지 · 소계 단계(791-1) — 덧줄에서만
|
||||
names = names | {ms.ADDR, ms.STEP}
|
||||
for extra in row.get("덧줄", []):
|
||||
one(extra["식"], f"{key} 덧줄 {extra['이름']}", names)
|
||||
names = names | {extra["이름"]}
|
||||
if extra.get("비목") not in COST_ITEMS:
|
||||
out.append(f"{key} 덧줄 {extra['이름']} · 비목 「{extra.get('비목')}」")
|
||||
out += _check_cut(row, key)
|
||||
out += _check_cut(row, key) + ms.check(row, key)
|
||||
return out, calls
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""로직 셈 의미 넓힘(PLAN 791-1 · `_틀.md` 7장 「셈 의미」) — 덧줄 고리 · 금액번지 · 소계 단계 ·
|
||||
제외 · 결과금액 조정 · 변수 표시 반올림 · 검사.
|
||||
|
||||
`master_formula.run` 이 부름(덧줄 고리를 여기로 옮김) · 끝전(줄 자리 · 블록 끝전)은 `master_cut`.
|
||||
새 칸을 안 쓰는 로직은 옛 셈 그대로 — 지킴 = M01 로직 지문(`test_791_1_engine_semantics.py`).
|
||||
eval 없음 · 수는 Decimal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
import master_formula as mf
|
||||
|
||||
COSTS = mf.COST_ITEMS
|
||||
ROW_KEYS = ("단계", "끝전", "조정") # 로직 줄 칸
|
||||
LINE_KEYS = ("번지", "합밖", "자리") # 호표 · 덧줄 칸
|
||||
ENDS = ("비목버림", "큰나머지")
|
||||
MAX_PLACE = 3 # 줄 자리 0~3
|
||||
MAX_SHOW = 6 # 변수 표시 0~6
|
||||
MAX_STEP = 5 # 소계 단 1~5(STmate `(=)`~`(=====)`)
|
||||
ADDR, STEP = "번지", "단계" # 덧줄 식 이름 — `번지.A01.재료비` · `단계.'소계'.계`
|
||||
|
||||
|
||||
def items(row: dict) -> list[dict]:
|
||||
"""엔진 줄 차례 = 호표 하나씩 뒤 덧줄 하나씩."""
|
||||
return [*row.get("호표", []), *row.get("덧줄", [])]
|
||||
|
||||
|
||||
def uses(row: dict) -> bool:
|
||||
"""새 의미(번지 · 단계 · 끝전 · 조정 · 합밖 · 줄 자리)를 쓰는 로직 — 아니면 옛 셈 그대로."""
|
||||
return any(k in row for k in ROW_KEYS) or any(k in x for x in items(row) for k in LINE_KEYS)
|
||||
|
||||
|
||||
def shown(step: dict, value):
|
||||
"""중간 `표시` = 값을 n 자리 반올림해 뒤 줄에 넘김(STmate `X = 식 =`)."""
|
||||
if step.get("표시") is None or not isinstance(value, Decimal):
|
||||
return value
|
||||
return mf._round(value, step["표시"], ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _add(acc: dict, money: dict | None) -> None:
|
||||
for k, v in (money or {}).items():
|
||||
if k in acc:
|
||||
acc[k] += v or 0
|
||||
|
||||
|
||||
def _bundle(acc: dict) -> dict:
|
||||
return {**acc, "계": sum(acc[k] for k in COSTS)}
|
||||
|
||||
|
||||
def _cover(row: dict) -> list[tuple[dict, set]]:
|
||||
"""단계마다 (단계, 담은 줄 차례) — 단 1 = 제 `줄` · 단 k = 제 `줄` + 아직 안 담긴 아래 단 단계.
|
||||
제외 단계는 위로 안 올라감."""
|
||||
where = {x.get("이름"): i for i, x in enumerate(items(row)) if x.get("이름")}
|
||||
out, open_ = [], []
|
||||
for st in row.get(STEP) or []:
|
||||
level, mine = int(st.get("단") or 1), {where[n] for n in st.get("줄") or [] if n in where}
|
||||
keep = []
|
||||
for low, got in open_:
|
||||
if low < level:
|
||||
mine |= got
|
||||
else:
|
||||
keep.append((low, got))
|
||||
open_ = keep if st.get("제외") else [*keep, (level, mine)]
|
||||
out.append((st, mine))
|
||||
return out
|
||||
|
||||
|
||||
def skipped(row: dict) -> set:
|
||||
"""비목 합에서 빼는 줄 차례 — `합밖` 줄(번지엔 쌓음) · 제외 단계가 담은 줄."""
|
||||
out = {i for i, x in enumerate(items(row)) if x.get("합밖")}
|
||||
for st, mine in _cover(row):
|
||||
if st.get("제외"):
|
||||
out |= mine
|
||||
return out
|
||||
|
||||
|
||||
def total(row: dict, lines: list[dict]) -> dict:
|
||||
"""비목 합 — 뺄 줄을 빼고 지금까지 줄."""
|
||||
skip, acc = skipped(row), dict.fromkeys(COSTS, Decimal(0))
|
||||
for i, line in enumerate(lines):
|
||||
if i not in skip:
|
||||
_add(acc, line.get("비목"))
|
||||
return acc
|
||||
|
||||
|
||||
def steps(row: dict, lines: list[dict]) -> list[dict]:
|
||||
"""단계 값 — 지금까지 셈한 줄만(뒤 줄은 0)."""
|
||||
out = []
|
||||
for st, mine in _cover(row):
|
||||
acc = dict.fromkeys(COSTS, Decimal(0))
|
||||
for i in sorted(mine):
|
||||
if i < len(lines):
|
||||
_add(acc, lines[i].get("비목"))
|
||||
out.append(
|
||||
{"이름": st.get("이름"), "단": st.get("단"), **_bundle(acc)}
|
||||
| ({"제외": True} if st.get("제외") else {})
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def scope(row: dict, lines: list[dict]) -> dict:
|
||||
"""덧줄 식 이름 — `번지` {A01: 묶음} · `단계` {이름: 묶음} (지금까지 줄)."""
|
||||
places = {}
|
||||
for item, line in zip(items(row), lines):
|
||||
if item.get(ADDR):
|
||||
_add(places.setdefault(item[ADDR], dict.fromkeys(COSTS, Decimal(0))), line.get("비목"))
|
||||
return {
|
||||
ADDR: {k: _bundle(v) for k, v in places.items()},
|
||||
STEP: {x["이름"]: {k: x[k] for k in (*COSTS, "계")} for x in steps(row, lines)},
|
||||
}
|
||||
|
||||
|
||||
def adjust(row: dict, sums: dict) -> dict:
|
||||
"""결과금액 조정 — `배율` {비목: 수} 곱 → `옮김` 비목으로 계 전부(STmate 「경비로 적용」)."""
|
||||
rule = row.get("조정")
|
||||
if not rule:
|
||||
return sums
|
||||
out = dict(sums)
|
||||
for k, rate in (rule.get("배율") or {}).items():
|
||||
out[k] = out[k] * Decimal(str(rate))
|
||||
if rule.get("옮김"):
|
||||
whole = sum(out.values())
|
||||
out = {k: whole if k == rule["옮김"] else Decimal(0) for k in COSTS}
|
||||
return out
|
||||
|
||||
|
||||
def extras(master, row: dict, lines: list[dict], env: dict, sums: dict, depth: int):
|
||||
"""덧줄 셈(옛 `run` 고리 그대로) + 새 의미 → (비목 합, 덧칸 `{단계}`)."""
|
||||
new = uses(row)
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`(빈 줄은 뺌)
|
||||
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
||||
blank = {x["이름"]: x["까닭"] for x in lines if x.get("까닭")}
|
||||
for extra in row.get("덧줄", []):
|
||||
if new:
|
||||
sums = total(row, lines)
|
||||
env.update(scope(row, lines))
|
||||
env.update(sums)
|
||||
try: # 덧줄도 값 빈 품목·빈 줄을 부르면 그 줄만 비움
|
||||
value, why = mf.evaluate(mf.parse(extra["식"]), env, master, depth), None
|
||||
env[extra["이름"]] = value
|
||||
except mf.FormulaError as e:
|
||||
gone = next((n for n in blank if n in extra["식"]), None)
|
||||
value, why = None, f"「{gone}」 줄 비어 못 셈 — {blank[gone]}" if gone else str(e)
|
||||
blank[extra["이름"]] = why
|
||||
sums[extra["비목"]] += value or 0
|
||||
lines.append(
|
||||
{
|
||||
"이름": extra["이름"],
|
||||
"단위": "식",
|
||||
"수량": Decimal(1),
|
||||
"단가": value,
|
||||
"금액": value,
|
||||
"비목": {extra["비목"]: value},
|
||||
**({"까닭": why} if why else {}),
|
||||
}
|
||||
)
|
||||
if new:
|
||||
sums = total(row, lines)
|
||||
sums = {k: mf.cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합
|
||||
if not new:
|
||||
return sums, {}
|
||||
return adjust(row, sums), {STEP: steps(row, lines)}
|
||||
|
||||
|
||||
# ── 검사 ──────────────────────────────────────────────────────────────
|
||||
def _whole(v, low: int, high: int) -> bool:
|
||||
return isinstance(v, (int, Decimal)) and v == int(v) and low <= int(v) <= high
|
||||
|
||||
|
||||
def _refs(node):
|
||||
"""식 안 `번지.X.칸` · `단계.'X'.칸` → (이름, X, 칸)."""
|
||||
for part in mf.walk(node):
|
||||
if part[0] == "field" and part[1][0] == "field" and part[1][1][0] == "name":
|
||||
head = part[1][1][1]
|
||||
if head in (ADDR, STEP):
|
||||
yield head, part[1][2], part[2]
|
||||
|
||||
|
||||
def check(row: dict, key: str) -> list[str]:
|
||||
"""새 의미 칸 — 끝전 · 조정 · 줄 자리 · 번지 · 표시 · 단계 · 덧줄이 부르는 번지 · 단계 차례."""
|
||||
out, lines = [], items(row)
|
||||
end = row.get("끝전")
|
||||
if end is not None and end not in ENDS:
|
||||
out.append(f"{key} 끝전 「{end}」 — {' · '.join(ENDS)}")
|
||||
rule = row.get("조정")
|
||||
if rule is not None:
|
||||
if not isinstance(rule, dict) or set(rule) - {"옮김", "배율"}:
|
||||
out.append(f"{key} 조정 · 칸은 옮김 · 배율만")
|
||||
else:
|
||||
if rule.get("옮김") not in (None, *COSTS):
|
||||
out.append(f"{key} 조정 옮김 「{rule['옮김']}」 — {' · '.join(COSTS)}")
|
||||
for k, rate in (rule.get("배율") or {}).items():
|
||||
if k not in COSTS or not isinstance(rate, (int, Decimal)) or rate < 0:
|
||||
out.append(f"{key} 조정 배율 「{k}」={rate}")
|
||||
for step in row.get("중간", []):
|
||||
if step.get("표시") is not None and not _whole(step["표시"], 0, MAX_SHOW):
|
||||
out.append(f"{key} 중간 {step['이름']} · 표시 「{step['표시']}」 — 0∼{MAX_SHOW} 정수")
|
||||
names = [x.get("이름") for x in lines]
|
||||
for item in lines:
|
||||
where = f"{key} 줄 {item.get('이름', '')}"
|
||||
place = item.get("자리")
|
||||
if place is not None and (
|
||||
not isinstance(place, dict)
|
||||
or set(place) - {"방법", "자리"}
|
||||
or place.get("방법") not in mf.ROUND_WAYS
|
||||
or not _whole(place.get("자리", 0), 0, MAX_PLACE)
|
||||
):
|
||||
out.append(
|
||||
f"{where} · 자리 「{place}」 — {{방법: 버림·반올림·올림, 자리: 0∼{MAX_PLACE}}}"
|
||||
)
|
||||
if ADDR in item and not (isinstance(item[ADDR], str) and mf._NAME.fullmatch(item[ADDR])):
|
||||
out.append(f"{where} · 번지 「{item[ADDR]}」 — 글자 · 숫자만")
|
||||
if "합밖" in item and not isinstance(item["합밖"], bool):
|
||||
out.append(f"{where} · 합밖 「{item['합밖']}」 — 참 · 거짓")
|
||||
out += _check_steps(row, key, names)
|
||||
return out
|
||||
|
||||
|
||||
def _check_steps(row: dict, key: str, names: list) -> list[str]:
|
||||
out, seen, taken = [], set(), {}
|
||||
for st in row.get(STEP) or []:
|
||||
name = st.get("이름") if isinstance(st, dict) else None
|
||||
where = f"{key} 단계 {name}"
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
out.append(f"{key} 단계 · 이름 없음")
|
||||
continue
|
||||
if name in seen:
|
||||
out.append(f"{where} · 같은 이름")
|
||||
seen.add(name)
|
||||
if set(st) - {"이름", "단", "줄", "제외"}:
|
||||
out.append(f"{where} · 모르는 칸 {sorted(set(st) - {'이름', '단', '줄', '제외'})}")
|
||||
if not _whole(st.get("단"), 1, MAX_STEP):
|
||||
out.append(f"{where} · 단 「{st.get('단')}」 — 1∼{MAX_STEP} 정수")
|
||||
for line in st.get("줄") or []:
|
||||
if line not in names:
|
||||
out.append(f"{where} · 없는 줄 「{line}」")
|
||||
elif names.count(line) > 1:
|
||||
out.append(f"{where} · 줄 이름 「{line}」 겹침 — 줄 이름을 가를 것")
|
||||
elif line in taken:
|
||||
out.append(f"{where} · 줄 「{line}」 이 단계 「{taken[line]}」 에도 있음")
|
||||
taken.setdefault(line, name)
|
||||
if out:
|
||||
return out
|
||||
cover = {st["이름"]: mine for st, mine in _cover(row)}
|
||||
places = {}
|
||||
for i, item in enumerate(items(row)):
|
||||
if item.get(ADDR):
|
||||
places.setdefault(item[ADDR], set()).add(i)
|
||||
first = len(row.get("호표", []))
|
||||
for n, extra in enumerate(row.get("덧줄", [])):
|
||||
try:
|
||||
node = mf.parse(extra["식"])
|
||||
except mf.FormulaError:
|
||||
continue # 못 읽는 식은 `check_logic` 이 알림
|
||||
for head, name, slot in _refs(node):
|
||||
where = f"{key} 덧줄 {extra.get('이름')}"
|
||||
group = (places if head == ADDR else cover).get(name)
|
||||
if group is None:
|
||||
out.append(f"{where} · 없는 {head} 「{name}」")
|
||||
elif any(i >= first + n for i in group):
|
||||
out.append(f"{where} · {head} 「{name}」 에 이 줄이나 뒤 줄이 듦 — 단계 순서")
|
||||
if slot not in mf.BUNDLE:
|
||||
out.append(f"{where} · {head} 칸 「{slot}」 — {' · '.join(mf.BUNDLE)}")
|
||||
return out
|
||||
Reference in New Issue
Block a user