feat(M01): 식으로 새 로직 만들기 — 줄로 적은 식의 변수 뽑기·검사·단위 맞춤과 로직 틀로 옮기기
· 해석기 scripts/master_draft.py — 「이름 = 식」 줄에서 변수(처음 나온 줄 · 정의됨/미정)와 변수마다 정할 것(무엇 = 마스터요소·설계입력·고정값·표찾기 · 단위 · 비목)을 뽑음 · 검사 — 두 번 정의 · 서로 물고 돎 · 안 쓰임(결과가 하나인지) · 못 읽음 · 비목 합 이름 · 단위 검사 — 곱셈은 곱하고 나눗셈은 나누고 덧셈·뺄셈·만약·최소·최대는 같아야 함 · 어긋나면 그 줄과 글자 자리 · 「m * m」 과 「㎡」 는 같게 봄 · 비율(%)은 무차원 · 옮기기 — 돈 로직은 결과 식을 항으로 갈라 호표 줄(마스터요소 하나 + 나머지 곱이 수량)과 덧줄로 · 나머지 정의 줄은 중간(쓰는 차례) · 돈 아닌 로직은 중간 + 결과 식 · 표 찾기는 글자로 안 침 — 변수 하나로 두고 「찾기」 묶음을 모달이 채울 자리만 · 서버 길 POST /api/m01/logic/draft — 검사·미리보기 · save 면 자체 로직(키 GX)으로 저장 · 계약 _화면_계약.md 7장 · 시험 test_m01_draft.py 13개(자재비 식 · 단위 어긋난 식 · 물고 도는 식) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""줄로 적은 식 → 변수 뽑기 · 검사 · 단위 맞춤 · 로직 틀로 옮기기 (`_화면_계약.md` 7장).
|
||||
|
||||
수량 = 길이 * 폭
|
||||
자재비 = 수량 * 시멘트
|
||||
|
||||
· 줄마다 「이름 = 식」 · 빈 줄과 `#` 는 건너뜀 · 마지막 줄 왼쪽이 결과 변수
|
||||
· 식 글자는 `_틀.md` 8장 범위만 — 읽기는 `master_formula` 가 함
|
||||
· 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 「무엇」 을 `표찾기` 로 정할 때 이음
|
||||
· 값은 안 씀 — 검사만 하고 `옮기기` 가 로직 한 줄을 만듦(저장은 `Store_Make.logic_new`)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import master_formula as mf
|
||||
import master_keys as mk
|
||||
|
||||
WHAT = ("마스터요소", "설계입력", "고정값", "표찾기")
|
||||
COST_ITEMS = mf.COST_ITEMS
|
||||
BUNDLE = mf.BUNDLE # 노무비 · 재료비 · 경비 · 계 — 덧줄에서만 쓰는 비목 합
|
||||
MONEY = {"원": 1}
|
||||
# 같은 단위의 다른 표기 — 넓이·부피는 길이의 제곱으로 펴서 「m * m」 과 「㎡」 가 같아지게
|
||||
ALIAS = {
|
||||
"㎡": ("m", 2),
|
||||
"m²": ("m", 2),
|
||||
"m2": ("m", 2),
|
||||
"㎥": ("m", 3),
|
||||
"m³": ("m", 3),
|
||||
"m3": ("m", 3),
|
||||
"M": ("m", 1),
|
||||
}
|
||||
SQUARED = {2: "㎡", 3: "㎥"}
|
||||
DIMLESS = ("%", "할") # 비율은 단위가 없음 — 「1 + 증가율 / 100」 이 안 걸리게
|
||||
_ASSIGN = re.compile(r"^\s*([\w가-힣]+)\s*=(?!=)\s*(.*)$")
|
||||
|
||||
|
||||
# ── 자리를 적어 두는 읽기 ──────────────────────────────────────────────
|
||||
class _Pos(mf._Parser):
|
||||
"""`master_formula` 와 같은 식 문법 · 단위가 어긋난 자리를 짚으려고 마디에 글자 자리를 붙임."""
|
||||
|
||||
def add(self):
|
||||
node = self.mul()
|
||||
while True:
|
||||
self.ws()
|
||||
at = self.i
|
||||
if self.take("+"):
|
||||
node = ("bin", "+", node, self.mul(), at)
|
||||
elif self.take("-"):
|
||||
node = ("bin", "-", node, self.mul(), at)
|
||||
else:
|
||||
return node
|
||||
|
||||
def compare(self):
|
||||
node = self.add()
|
||||
for op in mf._OPS:
|
||||
self.ws()
|
||||
at = self.i
|
||||
if self.take(op):
|
||||
return ("bin", op, node, self.add(), at)
|
||||
return node
|
||||
|
||||
def primary(self):
|
||||
self.ws()
|
||||
at = self.i
|
||||
node = super().primary()
|
||||
return (*node, at) if node[0] == "call" else node
|
||||
|
||||
|
||||
def _at(node):
|
||||
"""마디에 붙여 둔 글자 자리 — 자리를 안 적은 마디는 None."""
|
||||
want = {"bin": 4, "call": 3}.get(node[0])
|
||||
return node[want] if want is not None and len(node) > want else None
|
||||
|
||||
|
||||
# ── 단위 셈 ───────────────────────────────────────────────────────────
|
||||
def unit_of(text) -> dict[str, int] | None:
|
||||
"""단위 글 → {밑 단위: 제곱} — 「원/㎥」 = {원: 1, ㎥: -1}.
|
||||
|
||||
빈 글은 무차원 · 안 정했으면 None.
|
||||
"""
|
||||
if text is None:
|
||||
return None
|
||||
out: dict[str, int] = {}
|
||||
sign = 1
|
||||
for token in re.split(r"([*/])", str(text).replace("·", "*")):
|
||||
token = token.strip()
|
||||
if token in ("*", "/"):
|
||||
sign = 1 if token == "*" else -1
|
||||
elif token and token not in DIMLESS:
|
||||
base, power = ALIAS.get(token, (token, 1))
|
||||
out[base] = out.get(base, 0) + sign * power
|
||||
return {k: v for k, v in out.items() if v}
|
||||
|
||||
|
||||
def _one_text(base: str, power: int) -> str:
|
||||
if base == "m" and power in SQUARED:
|
||||
return SQUARED[power]
|
||||
return base if power == 1 else f"{base}{power}"
|
||||
|
||||
|
||||
def unit_text(unit) -> str | None:
|
||||
"""{밑 단위: 제곱} → 단위 글 · 무차원은 빈 글."""
|
||||
if unit is None:
|
||||
return None
|
||||
up = [_one_text(k, v) for k, v in unit.items() if v > 0]
|
||||
down = [_one_text(k, -v) for k, v in unit.items() if v < 0]
|
||||
if not up and not down:
|
||||
return ""
|
||||
return ("*".join(up) or "1") + ("/" + "/".join(down) if down else "")
|
||||
|
||||
|
||||
def _show(unit) -> str:
|
||||
return "모름" if unit is None else (unit_text(unit) or "없음")
|
||||
|
||||
|
||||
def _merge(a: dict, b: dict, sign: int) -> dict:
|
||||
out = dict(a)
|
||||
for k, v in b.items():
|
||||
out[k] = out.get(k, 0) + sign * v
|
||||
return {k: v for k, v in out.items() if v}
|
||||
|
||||
|
||||
def _trouble(line: int, at, kind: str, say: str) -> dict:
|
||||
return {"줄": line, "자리": at, "갈래": kind, "말": say}
|
||||
|
||||
|
||||
def _same(units: list, line: int, at, where: str, out: list):
|
||||
"""여럿이 같은 단위여야 하는 자리 — 다르면 문제 한 줄 · 모르는 것은 넘김."""
|
||||
known = [u for u in units if u is not None]
|
||||
if known and any(u != known[0] for u in known):
|
||||
out.append(
|
||||
_trouble(
|
||||
line,
|
||||
at,
|
||||
"단위",
|
||||
f"「{where}」 의 단위가 서로 다름 — {' · '.join(_show(u) for u in known)}",
|
||||
)
|
||||
)
|
||||
return known[0] if known else None
|
||||
|
||||
|
||||
def _unit(node, units: dict, line: int, head: int, out: list):
|
||||
"""식 나무의 단위 — 모르면 None · 어긋나면 `out` 에 그 줄과 자리를 적음."""
|
||||
kind = node[0]
|
||||
if kind == "num":
|
||||
return {}
|
||||
if kind == "name":
|
||||
return units.get(node[1])
|
||||
if kind == "neg":
|
||||
return _unit(node[1], units, line, head, out)
|
||||
if kind in ("str", "ref", "field", "find", "logic"):
|
||||
return None # 글자 · 요소 · 표 찾기 · 로직 — 단위는 마스터가 앎
|
||||
if kind == "bin":
|
||||
op = node[1]
|
||||
a = _unit(node[2], units, line, head, out)
|
||||
b = _unit(node[3], units, line, head, out)
|
||||
at = None if _at(node) is None else head + _at(node)
|
||||
if op in ("*", "/"):
|
||||
return None if a is None or b is None else _merge(a, b, 1 if op == "*" else -1)
|
||||
if op in ("+", "-"):
|
||||
if a is None or b is None:
|
||||
return a if b is None else b
|
||||
if a != b:
|
||||
out.append(
|
||||
_trouble(
|
||||
line, at, "단위", f"「{op}」 양쪽 단위가 다름 — {_show(a)} {op} {_show(b)}"
|
||||
)
|
||||
)
|
||||
return a
|
||||
if op in mf._OPS and a and b: # 맨 수와 대보는 것은 늘 됨(「높이 <= 3」)
|
||||
_same([a, b], line, at, op, out)
|
||||
return {}
|
||||
if kind == "call":
|
||||
name, items = node[1], node[2]
|
||||
at = None if _at(node) is None else head + _at(node)
|
||||
kids = [_unit(x, units, line, head, out) for x in items]
|
||||
if name == "만약":
|
||||
return _same(kids[1:], line, at, "만약", out)
|
||||
if name in ("최소", "최대"):
|
||||
return _same(kids, line, at, name, out)
|
||||
return kids[0] # 올림 · 버림 · 반올림 — 자리 인자는 무차원
|
||||
return None
|
||||
|
||||
|
||||
# ── 식 되쓰기 ─────────────────────────────────────────────────────────
|
||||
def _text(node) -> str:
|
||||
"""나무 → 식 글자(다시 읽으면 같은 나무) — 사칙은 괄호로 묶음."""
|
||||
kind = node[0]
|
||||
if kind == "num":
|
||||
return format(node[1].normalize(), "f")
|
||||
if kind == "str":
|
||||
return f"'{node[1]}'"
|
||||
if kind in ("name", "ref"):
|
||||
return node[1]
|
||||
if kind == "neg":
|
||||
return f"-{_text(node[1])}"
|
||||
if kind == "field":
|
||||
tail = node[2] if mf._NAME.fullmatch(node[2]) else f"'{node[2]}'"
|
||||
return f"{_text(node[1])}.{tail}"
|
||||
if kind == "bin":
|
||||
return f"({_text(node[2])} {node[1]} {_text(node[3])})"
|
||||
if kind == "call":
|
||||
return f"{node[1]}({', '.join(_text(x) for x in node[2])})"
|
||||
if kind in ("find", "logic"):
|
||||
args = "".join(f", {k}={_text(v)}" for k, v in node[2])
|
||||
return f"{'찾기' if kind == 'find' else '로직'}({node[1]}{args})"
|
||||
raise mf.FormulaError(f"모르는 마디 {kind}")
|
||||
|
||||
|
||||
def _expr(node) -> str:
|
||||
"""식 한 벌 — 맨 바깥 괄호는 뺌(사람이 읽는 자리에 그대로 들어감)."""
|
||||
text = _text(node)
|
||||
return text[1:-1] if node[0] == "bin" else text
|
||||
|
||||
|
||||
def find_text(pick: dict) -> str:
|
||||
"""표 찾기 자리 → `찾기(표, 조건=값).값칸` — 모달이 채운 묶음을 글자로."""
|
||||
conds = "".join(f", {k}={v}" for k, v in (pick.get("조건") or {}).items())
|
||||
col = str(pick.get("값칸") or "")
|
||||
tail = col if mf._NAME.fullmatch(col) else f"'{col}'"
|
||||
return f"찾기({pick.get('표')}{conds}).{tail}"
|
||||
|
||||
|
||||
# ── 읽기 · 검사 ───────────────────────────────────────────────────────
|
||||
def _cut(text: str):
|
||||
"""(줄 번호, 왼쪽 이름, 식, 식이 시작하는 자리) — 빈 줄과 `#` 는 건너뜀."""
|
||||
for at, raw in enumerate(str(text).split("\n"), 1):
|
||||
if not raw.strip() or raw.lstrip().startswith("#"):
|
||||
continue
|
||||
m = _ASSIGN.match(raw)
|
||||
yield (
|
||||
at,
|
||||
(m.group(1) if m else ""),
|
||||
(m.group(2).strip() if m else raw.strip()),
|
||||
(m.start(2) if m else len(raw) - len(raw.lstrip())),
|
||||
)
|
||||
|
||||
|
||||
def _order(defined: dict, deps: dict) -> list[str]:
|
||||
"""중간 차례 — 먼저 쓰는 값이 앞에 오게(돌고 도는 것은 검사가 이미 잡음)."""
|
||||
out, seen = [], set()
|
||||
|
||||
def visit(name: str, path: tuple):
|
||||
if name in seen or name in path or name not in defined:
|
||||
return
|
||||
for nxt in deps.get(name, ()):
|
||||
visit(nxt, path + (name,))
|
||||
seen.add(name)
|
||||
out.append(name)
|
||||
|
||||
for name in sorted(defined, key=lambda n: defined[n]):
|
||||
visit(name, ())
|
||||
return out
|
||||
|
||||
|
||||
def _scan(text: str, 정함: dict | None = None) -> dict:
|
||||
"""줄 가르기 · 변수 · 검사 · 단위 — `read` 와 `옮기기` 가 같이 씀(나무를 들고 있음)."""
|
||||
정함 = 정함 or {}
|
||||
lines, 문제 = [], []
|
||||
for at, name, body, head in _cut(text):
|
||||
one = {"줄": at, "이름": name, "식": body, "앞": head}
|
||||
if not name:
|
||||
문제.append(_trouble(at, head, "못읽음", "「이름 = 식」 이 아님"))
|
||||
elif not body:
|
||||
문제.append(_trouble(at, head, "못읽음", f"「{name}」 오른쪽이 빔"))
|
||||
else:
|
||||
try:
|
||||
one["나무"] = _Pos(body).parse()
|
||||
except mf.FormulaError as e:
|
||||
문제.append(_trouble(at, head, "못읽음", str(e)))
|
||||
lines.append(one)
|
||||
|
||||
정의: dict[str, int] = {}
|
||||
for one in lines:
|
||||
if "나무" not in one:
|
||||
continue
|
||||
if one["이름"] in BUNDLE:
|
||||
문제.append(
|
||||
_trouble(
|
||||
one["줄"], 0, "이름", f"「{one['이름']}」 은 비목 합 이름 — 다른 이름을 쓸 것"
|
||||
)
|
||||
)
|
||||
continue
|
||||
if one["이름"] in 정의:
|
||||
문제.append(
|
||||
_trouble(
|
||||
one["줄"],
|
||||
0,
|
||||
"두번정의",
|
||||
f"「{one['이름']}」 을 {정의[one['이름']]}줄에서 이미 정함",
|
||||
)
|
||||
)
|
||||
continue
|
||||
정의[one["이름"]] = one["줄"]
|
||||
|
||||
쓰임: dict[str, list[int]] = {}
|
||||
deps: dict[str, set[str]] = {}
|
||||
for one in lines:
|
||||
if "나무" not in one:
|
||||
continue
|
||||
used = {p[1] for p in mf.walk(one["나무"]) if p[0] == "name"}
|
||||
if 정의.get(one["이름"]) == one["줄"]: # 두 번 정의한 뒷줄은 딸림에 안 넣음
|
||||
deps[one["이름"]] = used
|
||||
for name in sorted(used):
|
||||
쓰임.setdefault(name, []).append(one["줄"])
|
||||
|
||||
돎 = mf.find_loops({k: v & set(정의) for k, v in deps.items()})
|
||||
for loop in 돎:
|
||||
문제.append(
|
||||
_trouble(정의.get(loop[0], 0), 0, "돌고돎", "서로 물고 돎 — " + " → ".join(loop))
|
||||
)
|
||||
|
||||
성함 = [one for one in lines if "나무" in one]
|
||||
결과 = 성함[-1]["이름"] if 성함 else ""
|
||||
if not 성함:
|
||||
문제.append(_trouble(0, 0, "빔", "읽을 식이 없음"))
|
||||
for name, at in sorted(정의.items(), key=lambda kv: kv[1]):
|
||||
if name != 결과 and not [x for x in 쓰임.get(name, []) if x != at]:
|
||||
문제.append(
|
||||
_trouble(at, 0, "안쓰임", f"「{name}」 을 어디서도 안 씀 — 결과는 「{결과}」 하나")
|
||||
)
|
||||
|
||||
units: dict[str, dict | None] = {k: MONEY for k in BUNDLE}
|
||||
for name in set(쓰임) | set(정의):
|
||||
if name in 정의 or name in BUNDLE:
|
||||
continue
|
||||
spec = 정함.get(name) or {}
|
||||
one = unit_of(spec.get("단위"))
|
||||
# 마스터요소 변수는 식에서 단가로 셈 — 정한 단위가 「포」 면 식에서는 「원/포」
|
||||
units[name] = (
|
||||
_merge(MONEY, one, -1) if one is not None and spec.get("무엇") == "마스터요소" else one
|
||||
)
|
||||
loops = {n for loop in 돎 for n in loop}
|
||||
order = _order(정의, deps)
|
||||
by = {one["이름"]: one for one in 성함}
|
||||
for name in order:
|
||||
if name in loops:
|
||||
continue
|
||||
pinned = unit_of((정함.get(name) or {}).get("단위"))
|
||||
one = by[name]
|
||||
got = _unit(one["나무"], units, one["줄"], one["앞"], 문제)
|
||||
units[name] = pinned if pinned is not None else got
|
||||
|
||||
변수 = []
|
||||
for name in sorted(
|
||||
set(쓰임) | set(정의), key=lambda n: (min([쓰임.get(n, [10**9])[0], 정의.get(n, 10**9)]), n)
|
||||
):
|
||||
spec = 정함.get(name) or {}
|
||||
갈래 = "비목합" if name in BUNDLE else ("정의됨" if name in 정의 else "미정")
|
||||
변수.append(
|
||||
{
|
||||
"이름": name,
|
||||
"처음": min([*쓰임.get(name, []), 정의.get(name, 10**9)]),
|
||||
"갈래": 갈래,
|
||||
"정의줄": 정의.get(name),
|
||||
"쓰임": 쓰임.get(name, []),
|
||||
"무엇": spec.get("무엇") if 갈래 == "미정" else None,
|
||||
"단위": unit_text(units.get(name)),
|
||||
"비목": spec.get("비목"),
|
||||
"정할것": _todo(갈래, spec, units.get(name)),
|
||||
}
|
||||
)
|
||||
return {"줄": lines, "변수": 변수, "결과": 결과, "문제": 문제, "차례": order, "정의": 정의}
|
||||
|
||||
|
||||
def _todo(갈래: str, spec: dict, unit) -> list[str]:
|
||||
"""그 변수에 아직 안 정한 것 — 무엇 · 요소·찾기·값 · 단위 · 비목."""
|
||||
if 갈래 != "미정":
|
||||
return []
|
||||
what, need = spec.get("무엇"), []
|
||||
if what not in WHAT:
|
||||
need.append("무엇")
|
||||
elif what == "마스터요소":
|
||||
if not spec.get("요소"):
|
||||
need.append("요소")
|
||||
if spec.get("비목") not in COST_ITEMS:
|
||||
need.append("비목")
|
||||
elif what == "표찾기" and not (spec.get("찾기") or {}).get("표"):
|
||||
need.append("찾기")
|
||||
elif what == "고정값" and spec.get("값") in (None, ""):
|
||||
need.append("값")
|
||||
if unit is None:
|
||||
need.append("단위")
|
||||
return need
|
||||
|
||||
|
||||
def read(text: str, 정함: dict | None = None) -> dict:
|
||||
"""줄로 적은 식 → `{줄, 변수, 결과, 문제, 다됨}` — 화면이 그대로 받는 모양(나무는 뺌)."""
|
||||
got = _scan(text, 정함)
|
||||
다됨 = not got["문제"] and not any(v["정할것"] for v in got["변수"])
|
||||
return {
|
||||
"줄": [{k: v for k, v in one.items() if k != "나무"} for one in got["줄"]],
|
||||
"변수": got["변수"],
|
||||
"결과": got["결과"],
|
||||
"문제": got["문제"],
|
||||
"다됨": 다됨,
|
||||
}
|
||||
|
||||
|
||||
# ── 로직 틀로 옮기기 ───────────────────────────────────────────────────
|
||||
def _terms(node) -> list:
|
||||
return _terms(node[2]) + _terms(node[3]) if node[0] == "bin" and node[1] == "+" else [node]
|
||||
|
||||
|
||||
def _factors(node) -> list:
|
||||
return _factors(node[2]) + _factors(node[3]) if node[0] == "bin" and node[1] == "*" else [node]
|
||||
|
||||
|
||||
def _spread(node, by: dict, 정함: dict, 편: set) -> list:
|
||||
"""결과 항 — 항이 이름 하나이고 비목을 안 정했으면 그 식으로 펼침.
|
||||
|
||||
호표 줄을 항 안에서 찾으려는 것.
|
||||
"""
|
||||
out = []
|
||||
for term in _terms(node):
|
||||
name = term[1] if term[0] == "name" else ""
|
||||
if name in by and name not in 편 and not (정함.get(name) or {}).get("비목"):
|
||||
편.add(name)
|
||||
out += _spread(by[name]["나무"], by, 정함, 편)
|
||||
else:
|
||||
out.append(term)
|
||||
return out
|
||||
|
||||
|
||||
def _kind_of(요소) -> str:
|
||||
if isinstance(요소, dict):
|
||||
return "재료"
|
||||
return mk.GROUP_OF.get(mk.id_of(str(요소)), "")
|
||||
|
||||
|
||||
def _input_of(name: str, spec: dict, unit) -> dict:
|
||||
one = {"이름": name}
|
||||
for k in ("설명", "단위", "고르기", "범위", "출처"):
|
||||
if spec.get(k) not in (None, ""):
|
||||
one[k] = spec[k]
|
||||
one.setdefault("단위", unit_text(unit) or "")
|
||||
return {k: one[k] for k in ("이름", "설명", "단위", "고르기", "범위", "출처") if k in one}
|
||||
|
||||
|
||||
def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None) -> tuple[dict, list]:
|
||||
"""(로직 한 줄, 문제) — 다 정해졌으면 `_틀.md` 7장 모양 · 문제가 있으면 줄은 빈 묶음.
|
||||
|
||||
돈 로직(`결과단위` 가 「원」 으로 시작)은 결과 식을 `+` 로 갈라 항마다 —
|
||||
마스터요소 변수 하나면 호표 줄(나머지 곱이 수량) · 비목만 정한 변수면 덧줄.
|
||||
돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
|
||||
"""
|
||||
정함, 머리 = 정함 or {}, dict(머리 or {})
|
||||
got = _scan(text, 정함)
|
||||
문제 = list(got["문제"])
|
||||
for v in got["변수"]:
|
||||
if v["정할것"]:
|
||||
문제.append(
|
||||
_trouble(
|
||||
v["처음"], 0, "덜정함", f"「{v['이름']}」 에 {' · '.join(v['정할것'])} 안 정함"
|
||||
)
|
||||
)
|
||||
if 문제:
|
||||
return {}, 문제
|
||||
|
||||
by, 결과 = {one["이름"]: one for one in got["줄"] if "나무" in one}, got["결과"]
|
||||
돈 = str(머리.get("결과단위") or "").startswith("원")
|
||||
입력, 중간, 호표, 덧줄 = [], [], [], []
|
||||
for v in got["변수"]:
|
||||
spec = 정함.get(v["이름"]) or {}
|
||||
if v["갈래"] != "미정":
|
||||
continue
|
||||
if spec["무엇"] == "설계입력":
|
||||
입력.append(_input_of(v["이름"], spec, unit_of(v["단위"])))
|
||||
elif spec["무엇"] == "고정값":
|
||||
중간.append({"이름": v["이름"], "식": str(spec["값"])})
|
||||
elif spec["무엇"] == "표찾기":
|
||||
중간.append({"이름": v["이름"], "식": find_text(spec["찾기"])})
|
||||
|
||||
덧줄이름, 편 = set(), set()
|
||||
if 돈:
|
||||
for term in _spread(by[결과]["나무"], by, 정함, 편):
|
||||
facts = _factors(term)
|
||||
picks = [
|
||||
f
|
||||
for f in facts
|
||||
if f[0] == "name" and (정함.get(f[1]) or {}).get("무엇") == "마스터요소"
|
||||
]
|
||||
if len(picks) > 1:
|
||||
문제.append(
|
||||
_trouble(
|
||||
by[결과]["줄"],
|
||||
0,
|
||||
"항",
|
||||
f"한 항에 마스터요소가 둘 — {' · '.join(p[1] for p in picks)}",
|
||||
)
|
||||
)
|
||||
elif picks:
|
||||
spec = 정함[picks[0][1]]
|
||||
rest = [f for f in facts if f is not picks[0]]
|
||||
호표.append(
|
||||
{
|
||||
"종류": _kind_of(spec["요소"]),
|
||||
"요소": spec["요소"],
|
||||
"이름": spec.get("이름") or picks[0][1],
|
||||
"단위": spec.get("단위") or "",
|
||||
"수량": (
|
||||
_expr(rest[0]) if len(rest) == 1 else " * ".join(_text(f) for f in rest)
|
||||
)
|
||||
or "1",
|
||||
"비목": spec["비목"],
|
||||
}
|
||||
)
|
||||
elif (
|
||||
len(facts) == 1
|
||||
and facts[0][0] == "name"
|
||||
and (정함.get(facts[0][1]) or {}).get("비목")
|
||||
):
|
||||
name = facts[0][1]
|
||||
if name not in by:
|
||||
문제.append(_trouble(by[결과]["줄"], 0, "항", f"덧줄 「{name}」 의 식이 없음"))
|
||||
continue
|
||||
덧줄.append(
|
||||
{"이름": name, "식": _expr(by[name]["나무"]), "비목": 정함[name]["비목"]}
|
||||
)
|
||||
덧줄이름.add(name)
|
||||
else:
|
||||
문제.append(
|
||||
_trouble(
|
||||
by[결과]["줄"],
|
||||
0,
|
||||
"항",
|
||||
f"「{_text(term)}」 을 호표·덧줄로 못 옮김 — 마스터요소나 비목을 정할 것",
|
||||
)
|
||||
)
|
||||
|
||||
쓰임 = {v["이름"]: v["쓰임"] for v in got["변수"]}
|
||||
for name in got["차례"]:
|
||||
if name in 덧줄이름 or name == 결과: # 결과 줄은 호표·덧줄로 갈랐거나 「결과」 식이 됨
|
||||
continue
|
||||
if name in 편 and 쓰임.get(name) == [by[결과]["줄"]]:
|
||||
continue # 결과 항으로 펼쳐 넣었고 다른 데서 안 씀 — 중간에 두지 않음
|
||||
쓴비목 = {p[1] for p in mf.walk(by[name]["나무"]) if p[0] == "name"} & set(BUNDLE)
|
||||
if 쓴비목:
|
||||
문제.append(
|
||||
_trouble(
|
||||
by[name]["줄"],
|
||||
0,
|
||||
"비목합",
|
||||
f"「{' · '.join(sorted(쓴비목))}」 은 덧줄에서만 씀",
|
||||
)
|
||||
)
|
||||
중간.append({"이름": name, "식": _expr(by[name]["나무"])})
|
||||
if 문제:
|
||||
return {}, 문제
|
||||
|
||||
row = {
|
||||
"원문번호": 머리.get("원문번호", ""),
|
||||
"이름": 머리.get("이름", ""),
|
||||
"결과단위": 머리.get("결과단위", ""),
|
||||
"출처": 머리.get("출처", "자체"),
|
||||
"입력": 입력,
|
||||
"중간": 중간,
|
||||
}
|
||||
row.update({"호표": 호표, "덧줄": 덧줄} if 돈 else {"결과": {"식": _expr(by[결과]["나무"])}})
|
||||
row["끝수"] = 머리.get("끝수") # 틀이 늘 두는 칸 — 안 정했으면 null
|
||||
for k in ("비고", "시험입력"):
|
||||
if 머리.get(k) not in (None, ""):
|
||||
row[k] = 머리[k]
|
||||
return row, []
|
||||
|
||||
|
||||
if __name__ == "__main__": # 견본 한 벌 — `python scripts/master_draft.py`
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
print(json.dumps(read(sys.stdin.read()), ensure_ascii=False, indent=2, default=str))
|
||||
Reference in New Issue
Block a user