- 걷음: 43784a4c0 의 옛 수식 나무 위 고침(서버 · 화면 · css) = 그 앞 판으로 되돌림
- 남김: master_equation_sign.py · ui_template_equation_text.ts · ui_template_equation_edit.ts(다시 쓸 조각 · 지금 안 부름) · text 는 `입력글` 을 제 타입에
- 지움: test_801_1_equation_inline.py — 걷은 옛 나무 고침을 재는 시험
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
504 lines
21 KiB
Python
504 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""수식 고침 → 로직(PLAN 796-1 · 요구 657) — 나무 · 키보드 한 줄 식 → 엔진 식 · 변수 추가 · 삭제 ·
|
||
이름 · 기호 · 첨자 바꿈 · 표시 자리 · 식 검사.
|
||
|
||
로직 줄(dict) 한 벌만 고침 — 저장은 부르는 쪽(M01 = `/save` · M02 사본 = 795-1 사본 길)이 함.
|
||
고침 요청 `고침` = 목록 · 차례대로:
|
||
{할일: 식, 자리: 중간:<열쇠> | 결과 | 호표:<n> | 덧줄:<n>, 식: 글 | 나무: 나무}
|
||
{할일: 더함, 갈래: 입력 | 중간, 열쇠, 이름?, 기호?, 위?, 아래?, 단위?, 설명?, 기본값?(입력),
|
||
식(중간), 뒤?(중간 열쇠 · "" = 맨 위 · 없으면 끝)}
|
||
{할일: 지움, 열쇠} — 쓰는 식이 있으면 막고 쓰는 곳 목록
|
||
{할일: 변수, 열쇠, 이름?, 기호?, 위?, 아래?, 단위?} — 식은 열쇠로 부르니 안 깨짐
|
||
{할일: 표시, 열쇠, 표시: 0~6 | null}
|
||
{할일: 연결, 열쇠, 갈래: 입력|표|요소|로직|상수, …} — 새 기호의 연결 지정(`master_equation_link.py` 머리)
|
||
고친 중간 줄은 `표시` 없으면 `SHOW`(보이는 자리에서 잘라 셈 · 요구 658) — 고치지 않은 줄은 그대로.
|
||
키보드 한 줄 식 = 변수 글(기호_아래^위 · 이름 · 열쇠) · `× ÷ −` · 나머지는 엔진 식 말 그대로
|
||
(`[노임 …]` · `[별칭]` 은 M01 · M02 층이 `M02_Sangeun_Formula.to_engine` 으로 먼저 풂).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import re
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
import master_equation as meq
|
||
import master_equation_link as link
|
||
import master_formula as mf
|
||
|
||
KEY_FORM = re.compile(r"^[^\W\d][\w]{0,19}$") # 열쇠 = 글자로 시작 · 20 자까지
|
||
SIGN_FORM = re.compile(r"^[^\W\d_]\w{0,9}$")
|
||
SUB_FORM = re.compile(r"^[\w′'*]{1,6}$")
|
||
RESERVED = {"찾기", "로직", "그리고", "또는", "계", "줄", "번지", "단계", *mf.FUNCS, *mf.COST_ITEMS}
|
||
_TOKEN = re.compile(r"[^\W\d][\w]*(?:\^[^\s()+\-*/×÷−,=<>\[\]]+)?")
|
||
_BARE = re.compile(r"^[^\W\d][\w]*$")
|
||
_OPS = {"×": "*", "÷": "/", "−": "-"}
|
||
|
||
|
||
# ── 엔진 마디 · 나무 → 엔진 식 글 ──────────────────────────────────────
|
||
def _q(word: str) -> str:
|
||
return word if _BARE.match(word) else f"'{word}'"
|
||
|
||
|
||
def raw_text(node) -> str:
|
||
"""엔진 식 마디 → 엔진 식 글(괄호는 세기대로만)."""
|
||
kind = node[0]
|
||
if kind == "num":
|
||
return mf._plain(node[1])
|
||
if kind == "str":
|
||
return f"'{node[1]}'"
|
||
if kind in ("name", "ref"):
|
||
return node[1]
|
||
if kind == "neg":
|
||
inner = raw_text(node[1])
|
||
return f"-({inner})" if node[1][0] == "bin" else f"-{inner}"
|
||
if kind == "field":
|
||
return f"{raw_text(node[1])}.{_q(node[2])}"
|
||
if kind in ("find", "logic"):
|
||
head = "찾기" if kind == "find" else "로직"
|
||
args = "".join(f", {_q(k)}={raw_text(v)}" for k, v in node[2])
|
||
return f"{head}({node[1]}{args})"
|
||
if kind == "call":
|
||
return f"{node[1]}({', '.join(raw_text(x) for x in node[2])})"
|
||
_, op, left, right = node
|
||
return (
|
||
f"{_wrap(raw_text(left), left, op, False)} {op} {_wrap(raw_text(right), right, op, True)}"
|
||
)
|
||
|
||
|
||
def _wrap(text: str, node, op: str, right: bool) -> str:
|
||
if node[0] != "bin":
|
||
return text
|
||
mine, up = meq.PREC[node[1]], meq.PREC[op]
|
||
return f"({text})" if mine < up or (mine == up and right) else text # 왼쪽 묶임 그대로
|
||
|
||
|
||
def engine_text(tree: dict) -> str:
|
||
"""나무(이름 꼴) → 엔진 식 글 — 변수 = 열쇠 · 찾기 · 요소 · 블록 = 원문 식 · 조건 = 만약 셋."""
|
||
kind = tree.get("꼴")
|
||
if kind == "수":
|
||
return mf._plain(Decimal(str(tree["값"])))
|
||
if kind == "글":
|
||
return f"'{tree['글']}'" if tree.get("따옴") else str(tree["글"])
|
||
if kind == "변수":
|
||
return str(tree.get("식") or tree["열쇠"])
|
||
if kind == "블록":
|
||
return str(tree["식"])
|
||
if kind == "괄호":
|
||
return f"({engine_text(tree['안'])})"
|
||
if kind == "음":
|
||
return f"-{_side(tree['안'], '*', True)}"
|
||
if kind == "퍼센트":
|
||
return f"{_side(tree['안'], '/', False)} / 100"
|
||
if kind == "분수":
|
||
low = tree["아래"]
|
||
if tree.get("사슬") and low.get("꼴") == "연산": # a/(b×c) 모양 = 원래 a / b / c
|
||
parts = [tree["위"], low["좌"], low["우"]]
|
||
first = _side(parts[0], "/", False)
|
||
return " / ".join([first, *(_side(p, "/", True) for p in parts[1:])])
|
||
return f"{_side(tree['위'], '/', False)} / {_side(low, '/', True)}"
|
||
if kind == "조건":
|
||
return f"만약({', '.join(engine_text(tree[k]) for k in ('조건', '참', '거짓'))})"
|
||
if kind == "함수":
|
||
return f"{tree['이름']}({', '.join(engine_text(x) for x in tree['인자'])})"
|
||
if kind == "연산":
|
||
op = _OPS.get(tree["기호"], tree["기호"])
|
||
return f"{_side(tree['좌'], op, False)} {op} {_side(tree['우'], op, True)}"
|
||
raise mf.FormulaError(f"모르는 나무 마디 「{kind}」")
|
||
|
||
|
||
def _strength(tree: dict) -> int | None:
|
||
kind = tree.get("꼴")
|
||
if kind == "연산":
|
||
return meq.PREC[_OPS.get(tree["기호"], tree["기호"])]
|
||
return meq.PREC["/"] if kind in ("분수", "퍼센트") else None
|
||
|
||
|
||
def _side(tree: dict, op: str, right: bool) -> str:
|
||
text, mine = engine_text(tree), _strength(tree)
|
||
if mine is None:
|
||
return text
|
||
up = meq.PREC[op]
|
||
return f"({text})" if mine < up or (mine == up and right) else text # 왼쪽 묶임 그대로
|
||
|
||
|
||
# ── 키보드 한 줄 식 ↔ 엔진 식 ─────────────────────────────────────────
|
||
def _names(row: dict) -> tuple[dict, dict]:
|
||
"""(변수 글 → 열쇠, 열쇠 → 변수 글) — 겹치는 글은 뺌(열쇠로 적어야 함)."""
|
||
rows = meq.table(row)
|
||
want: dict[str, set] = {}
|
||
for v in rows:
|
||
for word in {v["열쇠"], meq.token(v), v["이름"]}:
|
||
want.setdefault(word, set()).add(v["열쇠"])
|
||
to_key = {w: next(iter(ks)) for w, ks in want.items() if len(ks) == 1}
|
||
to_word = {v["열쇠"]: meq.token(v) for v in rows if to_key.get(meq.token(v)) == v["열쇠"]}
|
||
return to_key, to_word
|
||
|
||
|
||
def _swap_words(text: str, table: dict) -> str:
|
||
"""따옴표 · 대괄호 밖 변수 글만 바꿈 — 찾기 · 로직 인자 이름(「조건=」)은 그대로."""
|
||
out, at = [], 0
|
||
while at < len(text):
|
||
ch = text[at]
|
||
if ch in "'[":
|
||
end = text.find("'" if ch == "'" else "]", at + 1)
|
||
end = len(text) if end < 0 else end + 1
|
||
out.append(text[at:end])
|
||
at = end
|
||
continue
|
||
m = _TOKEN.match(text, at) if not (text[at - 1 : at].isalnum() and at) else None
|
||
if not m:
|
||
out.append(ch)
|
||
at += 1
|
||
continue
|
||
word, rest = m.group(), text[m.end() :].lstrip()
|
||
arg = rest.startswith("=") and not rest.startswith("==")
|
||
out.append(table.get(word, word) if not arg else word)
|
||
at = m.end()
|
||
return "".join(out)
|
||
|
||
|
||
def keyboard(text: str, row: dict) -> str:
|
||
"""키보드 한 줄 식 → 엔진 식 — 변수 글(첨자 `^'` 포함) → 열쇠 먼저 · 따옴표 밖 `× ÷ −`."""
|
||
bits = _swap_words(str(text), _names(row)[0]).split("'")
|
||
return "'".join(
|
||
"".join(_OPS.get(c, c) for c in b) if i % 2 == 0 else b for i, b in enumerate(bits)
|
||
)
|
||
|
||
|
||
def keyboard_of(text: str, row: dict) -> str:
|
||
"""엔진 · 이름 꼴 식 → 키보드 한 줄 식(열쇠 → 변수 글)."""
|
||
return _swap_words(str(text), _names(row)[1])
|
||
|
||
|
||
# ── 쓰는 곳 ───────────────────────────────────────────────────────────
|
||
def _spots(row: dict):
|
||
"""(자리 글, 자리 열쇠, 식 글) — 모든 식."""
|
||
for step in row.get("중간") or []:
|
||
yield f"중간 {step['이름']}", f"중간:{step['이름']}", step["식"]
|
||
if "결과" in row:
|
||
yield "결과", "결과", row["결과"]["식"]
|
||
for at, item in enumerate(row.get("호표") or []):
|
||
where = f"호표 {at + 1} {item.get('이름', '')}"
|
||
yield f"{where} 수량", f"호표:{at}", item["수량"]
|
||
elem = item.get("요소")
|
||
if isinstance(elem, dict):
|
||
yield f"{where} 요소", f"호표:{at}", " ".join(str(v) for v in elem.values())
|
||
else:
|
||
yield f"{where} 요소", f"호표:{at}", str(elem)
|
||
for at, extra in enumerate(row.get("덧줄") or []):
|
||
yield f"덧줄 {extra['이름']}", f"덧줄:{at}", extra["식"]
|
||
|
||
|
||
def _used(text: str) -> set[str]:
|
||
out = set(mf._HOLE.findall(text))
|
||
try:
|
||
for part in mf.walk(mf.parse(text)):
|
||
if part[0] == "name":
|
||
out.add(part[1])
|
||
except (mf.FormulaError, RecursionError):
|
||
pass
|
||
return out
|
||
|
||
|
||
def uses(row: dict, key: str) -> list[str]:
|
||
"""열쇠를 쓰는 식 자리(제 줄 빼고)."""
|
||
return [
|
||
where for where, spot, text in _spots(row) if spot != f"중간:{key}" and key in _used(text)
|
||
]
|
||
|
||
|
||
# ── 고침 ──────────────────────────────────────────────────────────────
|
||
def _all_keys(row: dict) -> set[str]:
|
||
return {str(x.get("이름")) for k in ("입력", "중간", "덧줄") for x in row.get(k) or []}
|
||
|
||
|
||
def _key_ok(row: dict, key: str) -> str | None:
|
||
if not KEY_FORM.match(key) or mf.mk.KEY.fullmatch(key) or key in RESERVED:
|
||
return f"열쇠 꼴 아님 「{key}」(글자로 시작 · 20 자까지 · 예약어 · 요소 키 안 됨)"
|
||
if key in _all_keys(row):
|
||
return f"같은 열쇠 「{key}」 가 이미 있음"
|
||
return None
|
||
|
||
|
||
def _text_of(ed: dict, row: dict, words) -> tuple[str, list[str]]:
|
||
"""고침 식 → 엔진 식 — 나무 · 키보드 글(`words` = 층 번역 `[노임 …]` · `[별칭]` · 상한)."""
|
||
if isinstance(ed.get("나무"), dict):
|
||
return engine_text(ed["나무"]), []
|
||
text = keyboard(str(ed.get("식") or "").strip(), row)
|
||
return words(text, row) if words and text else (text, [])
|
||
|
||
|
||
def unknown(text: str, row: dict) -> list[str]:
|
||
"""엔진 식 글 안 모르는 변수 글(변수 · 열쇠 · 예약어 · 함수 · 칸 · 인자 이름 · 요소 키 빼고)."""
|
||
known = set(_names(row)[0]) | _all_keys(row) | RESERVED
|
||
out, at = [], 0
|
||
while at < len(text):
|
||
if text[at] in "'[":
|
||
end = text.find("'" if text[at] == "'" else "]", at + 1)
|
||
at = len(text) if end < 0 else end + 1
|
||
continue
|
||
m = None if at and text[at - 1].isalnum() else _TOKEN.match(text, at)
|
||
if not m:
|
||
at += 1
|
||
continue
|
||
word, before, rest = m.group(), text[:at].rstrip(), text[m.end() :].lstrip()
|
||
at = m.end()
|
||
arg = rest.startswith("=") and not rest.startswith("==")
|
||
skip = before.endswith(".") or rest.startswith("(") or arg
|
||
if not (skip or word in known or mf.mk.KEY.fullmatch(word) or word in out):
|
||
out.append(word)
|
||
return out
|
||
|
||
|
||
def _set_formula(row: dict, ed: dict, why: list, spots: set, words=None) -> None:
|
||
spot = str(ed.get("자리") or "")
|
||
text, bad = _text_of(ed, row, words)
|
||
if bad:
|
||
why += [f"{spot} · {w}" for w in bad]
|
||
return
|
||
if not isinstance(ed.get("나무"), dict) and (miss := unknown(text, row)):
|
||
why.append(
|
||
f"{spot} · 모르는 기호 「{'」 「'.join(miss)}」 — 변수 정의 영역에서 연결을 지정"
|
||
)
|
||
return
|
||
if not text:
|
||
why.append(f"{spot} · 식이 빔")
|
||
return
|
||
part, _, at = spot.partition(":")
|
||
if part == "중간":
|
||
step = next((s for s in row.get("중간") or [] if s["이름"] == at), None)
|
||
if step is None:
|
||
why.append(f"없는 자리 「{spot}」")
|
||
return
|
||
step["식"] = text
|
||
step.setdefault("표시", meq.SHOW)
|
||
elif part == "결과" and "결과" in row:
|
||
row["결과"]["식"] = text
|
||
elif part in ("호표", "덧줄") and at.isdigit() and int(at) < len(row.get(part) or []):
|
||
row[part][int(at)]["수량" if part == "호표" else "식"] = text
|
||
else:
|
||
why.append(f"없는 자리 「{spot}」")
|
||
return
|
||
spots.add(spot)
|
||
|
||
|
||
def _look(row: dict, key: str, ed: dict, why: list) -> None:
|
||
"""이름 · 기호 · 첨자 → 변수표 · 단위 → 입력 · 중간 칸."""
|
||
spec = next((s for k in ("입력", "중간") for s in row.get(k) or [] if s["이름"] == key), None)
|
||
if spec is None:
|
||
why.append(f"없는 변수 「{key}」")
|
||
return
|
||
look = dict((row.get(meq.FIELD) or {}).get(key) or {})
|
||
for field, form in (("이름", None), ("기호", SIGN_FORM), ("위", SUB_FORM), ("아래", SUB_FORM)):
|
||
if field not in ed:
|
||
continue
|
||
got = str(ed[field] or "").strip()
|
||
if got and form is not None and not form.match(got):
|
||
why.append(f"「{key}」 {field} 꼴 아님 「{got}」")
|
||
return
|
||
if field == "이름" and len(got) > 40:
|
||
why.append(f"「{key}」 이름 40 자까지")
|
||
return
|
||
if got and got != (key if field in ("이름", "기호") else ""):
|
||
look[field] = got
|
||
else:
|
||
look.pop(field, None)
|
||
if "단위" in ed:
|
||
unit = str(ed["단위"] or "").strip()
|
||
spec.pop("단위", None) if not unit else spec.__setitem__("단위", unit)
|
||
table = dict(row.get(meq.FIELD) or {})
|
||
table.pop(key, None) if not look else table.__setitem__(key, look)
|
||
if table:
|
||
row[meq.FIELD] = table
|
||
else:
|
||
row.pop(meq.FIELD, None)
|
||
seen: dict[str, str] = {}
|
||
for v in meq.table(row): # 변수 글 겹침 — 키보드 식이 갈리지 않게
|
||
for word in {meq.token(v), v["이름"]}:
|
||
if seen.get(word, v["열쇠"]) != v["열쇠"]:
|
||
why.append(f"「{word}」 이 「{seen[word]}」 · 「{v['열쇠']}」 둘에 겹침")
|
||
seen.setdefault(word, v["열쇠"])
|
||
|
||
|
||
def _add(row: dict, ed: dict, why: list, spots: set, words=None) -> None:
|
||
kind, key = ed.get("갈래"), str(ed.get("열쇠") or ed.get("이름") or "").strip()
|
||
if kind not in ("입력", "중간"):
|
||
why.append(f"더함 갈래 「{kind}」 — 입력 · 중간")
|
||
return
|
||
if bad := _key_ok(row, key):
|
||
why.append(bad)
|
||
return
|
||
if kind == "입력":
|
||
spec: dict = {"이름": key}
|
||
for field in ("설명", "단위"):
|
||
if ed.get(field):
|
||
spec[field] = str(ed[field])
|
||
if ed.get("기본값") not in (None, ""):
|
||
try:
|
||
spec["기본"] = Decimal(str(ed["기본값"]))
|
||
except InvalidOperation:
|
||
spec["기본"] = str(ed["기본값"])
|
||
row.setdefault("입력", []).append(spec)
|
||
else:
|
||
mids = row.setdefault("중간", [])
|
||
after = ed.get("뒤")
|
||
at = len(mids) if after is None else 0 if after == "" else None
|
||
if at is None:
|
||
at = next((i + 1 for i, s in enumerate(mids) if s["이름"] == after), None)
|
||
if at is None:
|
||
why.append(f"더함 자리 「{after}」 없는 중간")
|
||
return
|
||
mids.insert(
|
||
at, {"이름": key, "식": "0", **({"단위": ed["단위"]} if ed.get("단위") else {})}
|
||
)
|
||
job = {"자리": f"중간:{key}", **{k: ed[k] for k in ("식", "나무") if k in ed}}
|
||
_set_formula(row, job, why, spots, words)
|
||
look = {k: ed[k] for k in ("이름", "기호", "위", "아래") if ed.get(k)}
|
||
if look:
|
||
_look(row, key, look, why)
|
||
|
||
|
||
def _drop(row: dict, key: str, why: list) -> None:
|
||
users = uses(row, key)
|
||
if users:
|
||
why.append(f"「{key}」 를 쓰는 곳이 있어 못 지움 — {' · '.join(users)}")
|
||
return
|
||
for kind in ("입력", "중간"):
|
||
got = [s for s in row.get(kind) or [] if s["이름"] != key]
|
||
if len(got) != len(row.get(kind) or []):
|
||
row[kind] = got
|
||
break
|
||
else:
|
||
why.append(f"없는 변수 「{key}」")
|
||
return
|
||
for field in (meq.FIELD, "시험입력"):
|
||
if isinstance(row.get(field), dict) and key in row[field]:
|
||
row[field] = {k: v for k, v in row[field].items() if k != key}
|
||
if not row[field]:
|
||
row.pop(field)
|
||
|
||
|
||
def _show(row: dict, key: str, ed: dict, why: list) -> None:
|
||
step = next((s for s in row.get("중간") or [] if s["이름"] == key), None)
|
||
show = ed.get("표시")
|
||
if step is None:
|
||
why.append(f"표시는 중간 줄만 — 「{key}」")
|
||
elif show is None:
|
||
step.pop("표시", None)
|
||
elif not isinstance(show, int) or isinstance(show, bool) or not 0 <= show <= 6:
|
||
why.append(f"「{key}」 표시는 0~6 자리")
|
||
else:
|
||
step["표시"] = show
|
||
|
||
|
||
def apply(master, row: dict, edits: list, words=None) -> tuple[dict, list[str], set]:
|
||
"""고침 목록 → (새 로직 줄, 까닭, 식 고친 자리) — 까닭 있으면 저장 안 됨.
|
||
`words(글, 줄) → (엔진 식, 까닭)` = 층 번역(`M02_Sangeun_Formula` 상한 · to_engine)."""
|
||
new, why, spots = copy.deepcopy(row), [], set()
|
||
if not isinstance(edits, list) or not edits:
|
||
return new, ["고침 없음"], spots
|
||
for ed in edits:
|
||
ed = ed if isinstance(ed, dict) else {}
|
||
job, key = ed.get("할일"), str(ed.get("열쇠") or "")
|
||
try:
|
||
if job == "식":
|
||
_set_formula(new, ed, why, spots, words)
|
||
elif job == "더함":
|
||
_add(new, ed, why, spots, words)
|
||
elif job == "지움":
|
||
_drop(new, key, why)
|
||
elif job == "변수":
|
||
_look(new, key, ed, why)
|
||
elif job == "표시":
|
||
_show(new, key, ed, why)
|
||
elif job == "연결":
|
||
add, bad = link.plan(master, ed)
|
||
_add(new, add, why, spots, words) if add else why.append(bad)
|
||
else:
|
||
why.append(f"모르는 할일 「{job}」")
|
||
except (mf.FormulaError, KeyError, TypeError, ValueError) as e:
|
||
why.append(f"{job} · {e}")
|
||
if not why:
|
||
why += check(master, row, new, spots)
|
||
return new, why, spots
|
||
|
||
|
||
# ── 검사 ──────────────────────────────────────────────────────────────
|
||
def swap(master, row: dict):
|
||
"""그 로직만 고친 줄로 바꾼 마스터 사본(얕은 · 다른 줄은 같이 씀)."""
|
||
out = copy.copy(master)
|
||
key = str(row.get("키"))
|
||
out.index = dict(master.index)
|
||
out.index[key[:2]] = {**master.index.get(key[:2], {}), key: row}
|
||
return out
|
||
|
||
|
||
def _loop(row: dict, spots: set) -> list[str]:
|
||
"""고친 중간 줄 — 자기 · 아래 줄 · 순환."""
|
||
mids = [s["이름"] for s in row.get("중간") or []]
|
||
deps = {s["이름"]: _used(s["식"]) & set(mids) for s in row.get("중간") or []}
|
||
out = []
|
||
for i, name in enumerate(mids):
|
||
if f"중간:{name}" not in spots:
|
||
continue
|
||
for used in sorted(deps[name] - set(mids[:i])):
|
||
if used == name:
|
||
out.append(f"중간 {name} · 순환 — 「{name}」 이 자기 자신을 씀")
|
||
else:
|
||
path = _path(deps, used, name)
|
||
out.append(
|
||
f"중간 {name} · 순환 — {' → '.join([name, *path])}"
|
||
if path
|
||
else f"중간 {name} · 「{used}」 은 아래 줄 — 위로 옮길 것"
|
||
)
|
||
return out
|
||
|
||
|
||
def _path(deps: dict, start: str, goal: str) -> list[str] | None:
|
||
seen, todo = {start}, [[start]]
|
||
while todo:
|
||
path = todo.pop()
|
||
if path[-1] == goal:
|
||
return path
|
||
for nxt in sorted(deps.get(path[-1], ())):
|
||
if nxt not in seen:
|
||
seen.add(nxt)
|
||
todo.append([*path, nxt])
|
||
return None
|
||
|
||
|
||
def _zero(text: str) -> bool:
|
||
try:
|
||
node = mf.parse(text)
|
||
except (mf.FormulaError, RecursionError):
|
||
return False
|
||
return any(n[0] == "bin" and n[1] == "/" and n[3] == ("num", 0) for n in mf.walk(node))
|
||
|
||
|
||
def check(master, old: dict, new: dict, spots: set) -> list[str]:
|
||
"""새로 생긴 문제만 — 식 못 읽음 · 없는 이름 · 0 나눔 · 순환 · 엔진 검사 · 견본 셈 막힘."""
|
||
texts = {spot: text for _, spot, text in _spots(new)}
|
||
out = [f"{s} · 0 으로 나눔" for s in sorted(spots) if _zero(texts.get(s, ""))]
|
||
out += _loop(new, spots)
|
||
head = f"{new.get('키')} {new.get('원문번호')} "
|
||
base = set(mf.check_logic(master, old)[0])
|
||
out += [w.removeprefix(head) for w in mf.check_logic(master, new)[0] if w not in base]
|
||
if out:
|
||
return out
|
||
before, after = _trial(master, old), _trial(swap(master, new), new)
|
||
if after and after != before:
|
||
out.append(f"견본 셈 막힘 — {after}")
|
||
return out
|
||
|
||
|
||
def _trial(master, row: dict) -> str | None:
|
||
"""견본 입력 셈 — 막히면 까닭(빈 입력 빼고)."""
|
||
try:
|
||
mf.run(master, str(row.get("키")), meq.given_of(master, row))
|
||
except mf.EmptyInput:
|
||
return None
|
||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||
return str(e) or type(e).__name__
|
||
return None
|