- 새 M02_Hopyo_Bind: B1 규격 · 품명 괄호 · B6 같은 호표 레미콘 a-b-c · B2 선택값 줄 + 글에 없는 켜기 칸 = 아님 · B3 상수(기호표 · 이름 · 90˚ · 표 칸 = 기호) · B4 대입식(분자 · 분모 자리 + 단위) · B5 표 거꾸로 + 감률 배수 · 출처 꼬리표 · 엇갈림 · 안씀 · 입력필요 · 비움 · 기본 제안 · 엔진 몫 quantities(품만) - 새 M02_Hopyo_Solve: 후보마다 바인딩 → 줄 짝(L · X · M · R) → 품 같음(자릿수 반올림 폭 · 0.5 %) · 절 · 블록 판정 · 덧 줄 Units · 금액 참고(입력 필요 없을 때 엔진 한 번) - master_auto.defaults: 기본 칸 › 고르기 첫째 › 가드 0 › 0 을 품은 범위 0 › 표 첫 줄 › None(새 맞춤만 부름 · values · run_line 그대로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
6.0 KiB
Python
153 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""견본 입력 — 마스터 화면 보기용 자동값(프로젝트 기본값 아님 · 저장 안 함).
|
|
|
|
고르기 = 목록 첫째 · 범위 = 최솟값 · 찾기 조건 값으로 쓰이는 수 = 그 표 그 칸 가장 낮은 줄 값
|
|
(범위 줄이면 그 줄에 드는 아래 끝) · 나누는 자리에 오는 수 = 1 · 그 밖 0
|
|
(「만약(X > 0, X, 기본)」 의 X 는 나누는 자리여도 0 = 기본).
|
|
찾기·나누기 쓰임은 로직 식(중간 · 결과 · 호표 수량·요소 · 덧줄)을 풀어 알아냄.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from decimal import Decimal
|
|
|
|
import master_formula as mf
|
|
|
|
|
|
def _texts(row: dict):
|
|
for step in row.get("중간", []):
|
|
yield step["식"]
|
|
if "결과" in row:
|
|
yield row["결과"]["식"]
|
|
for item in row.get("호표", []):
|
|
yield item["수량"]
|
|
if isinstance(item.get("요소"), str):
|
|
yield item["요소"]
|
|
for extra in row.get("덧줄", []):
|
|
yield extra["식"]
|
|
|
|
|
|
def _names(node) -> set:
|
|
return {part[1] for part in mf.walk(node) if part[0] == "name"}
|
|
|
|
|
|
def _uses(row: dict) -> tuple[dict, set]:
|
|
"""(찾기 조건 값으로 바로 쓰인 이름 → [(표, 조건)], 나누는 자리에 든 이름)
|
|
— 나누는 자리에 든 중간의 식에 쓰인 이름도 나누는 자리로 봄."""
|
|
finds, divs = {}, set()
|
|
for text in _texts(row):
|
|
try:
|
|
node = mf.parse(text)
|
|
except mf.FormulaError:
|
|
continue
|
|
for part in mf.walk(node):
|
|
if part[0] == "bin" and part[1] == "/":
|
|
divs |= _names(part[3])
|
|
if part[0] == "find":
|
|
for cond, arg in part[2]:
|
|
if arg[0] == "name":
|
|
finds.setdefault(arg[1], []).append((part[1], cond))
|
|
guard = guards(row) # 「0 = 기본」 칸은 나누는 자리로 안 퍼뜨림(자동값 0 = 기본)
|
|
for step in reversed(row.get("중간", [])):
|
|
if step["이름"] in divs:
|
|
divs |= _names(mf.parse(step["식"])) - guard
|
|
return finds, divs
|
|
|
|
|
|
_GUARD = re.compile(r"만약\(\s*(\w+)\s*>\s*0\s*,\s*\1\s*,")
|
|
|
|
|
|
def guards(row: dict) -> set:
|
|
"""「만약(X > 0, X, 기본)」 꼴 중간 식의 X — 0 이면 원문 기본(25㎏ · 450분 · 식 · 표)."""
|
|
return {m for s in row.get("중간", []) for m in _GUARD.findall(s["식"])}
|
|
|
|
|
|
def _fits(master: mf.Master, ref: str, cond: str) -> list:
|
|
"""표 `ref` 조건 칸 `cond` 에 드는 수 — 낮은 차례 · 범위 칸이면 줄마다 그 줄에 드는 끝
|
|
(아래 끝이 열린 줄 「60 미만」 은 0)."""
|
|
try:
|
|
table = master.get(ref)
|
|
except mf.FormulaError:
|
|
return []
|
|
ranged = table.get("조건", {}).get(cond) == "범위"
|
|
out = set()
|
|
for r in table.get("줄", []):
|
|
if r.get(cond) is None: # 빈 조건 칸은 무엇이든 맞음 — 값을 정하지 않음
|
|
continue
|
|
low, high = r[cond] if ranged else (r[cond], None)
|
|
ends = [Decimal(0) if low is None else low, high]
|
|
out |= {v for v in ends if isinstance(v, Decimal) and mf._match(table, r, cond, v)}
|
|
return sorted(out)
|
|
|
|
|
|
def _lowest(master: mf.Master, uses: list):
|
|
"""찾기 여럿이 같은 입력을 쓰면 모든 표에 드는 가장 낮은 수 · 없으면 첫 표의 가장 낮은 수."""
|
|
sets = [_fits(master, ref, cond) for ref, cond in uses]
|
|
both = sorted(set.intersection(*map(set, sets))) if sets else []
|
|
return (both or next((s for s in sets if s), [None]))[0]
|
|
|
|
|
|
def _first_row(master: mf.Master, uses: list):
|
|
"""찾기 조건 칸의 표 첫 줄 값 — 범위 줄이면 아래 끝(열린 아래 끝 = 위 끝) · 빈 칸 줄은 건넘."""
|
|
for ref, cond in uses:
|
|
try:
|
|
rows = master.get(ref).get("줄", [])
|
|
except mf.FormulaError:
|
|
continue
|
|
for r in rows:
|
|
got = r.get(cond)
|
|
if isinstance(got, list):
|
|
got = next((v for v in got if v is not None), None)
|
|
if got is not None:
|
|
return got
|
|
return None
|
|
|
|
|
|
def _holds_zero(span) -> bool:
|
|
low, high = span[0], span[1]
|
|
return (low is None or Decimal(str(low)) <= 0) and (high is None or Decimal(str(high)) >= 0)
|
|
|
|
|
|
def defaults(master: mf.Master, row: dict) -> dict:
|
|
"""「기본」 제안(PLAN 785-2 · 639-1a) — 「기본」 칸 › 고르기 첫째 › 가드 0 › 0 을 품은 범위 0 ›
|
|
찾기 조건 표 첫 줄 › None(못 정함 = 사용자 입력). 1 을 지어 넣지 않음 · `values` 와 따로."""
|
|
specs = mf.inputs_of(row) if isinstance(row.get("흐름"), dict) else row.get("입력", [])
|
|
finds = {} if isinstance(row.get("흐름"), dict) else _uses(row)[0]
|
|
guard = guards(row)
|
|
out = {}
|
|
for spec in specs:
|
|
name, span = spec["이름"], spec.get("범위")
|
|
if spec.get("기본") not in (None, ""):
|
|
out[name] = spec["기본"]
|
|
elif spec.get("고르기"):
|
|
out[name] = spec["고르기"][0]
|
|
elif name in guard:
|
|
out[name] = Decimal(0)
|
|
elif span and _holds_zero(span):
|
|
out[name] = Decimal(0)
|
|
else:
|
|
out[name] = _first_row(master, finds.get(name, []))
|
|
return out
|
|
|
|
|
|
def values(master: mf.Master, row: dict) -> dict:
|
|
"""로직 줄 하나의 입력마다 자동값 · 흐름 로직은 「설계 때 정할 것」 예시값."""
|
|
if isinstance(row.get("흐름"), dict):
|
|
return {s["이름"]: s.get("예시") for s in mf.inputs_of(row)}
|
|
finds, divs = _uses(row)
|
|
out = {}
|
|
for spec in row.get("입력", []):
|
|
name = spec["이름"]
|
|
if spec.get("고르기"):
|
|
out[name] = spec["고르기"][0]
|
|
elif spec.get("범위"):
|
|
out[name] = spec["범위"][0]
|
|
else:
|
|
got = _lowest(master, finds.get(name, []))
|
|
if got is None and "원" in str(spec.get("단위") or ""):
|
|
out[name] = None # 가격 입력 — 0 원처럼 보이지 않게 비워 둠
|
|
else:
|
|
out[name] = got if got is not None else Decimal(1 if name in divs else 0)
|
|
return out
|