feat(M01): 견본 자동값 길 · 값 빈 품목은 그 줄만 비움
- GET /api/m01/logic/auto — 고르기 목록 첫째 · 범위 최솟값 · 찾기 조건 값은 그 표 가장 낮은 줄 · 나누는 수 1 · 그 밖 0 (master_auto.py) - run — 후보 0 · 값 빈 품목(덧줄 포함)은 단가·금액 null + 까닭 · 소계·계는 빈 줄 빼고 더함 · 없는 키는 지금처럼 멈춤 - 자동값으로 로직 1,354 전부 계산 · 멈춤 0 · 시험 test_m01_auto.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcqZtEzEf1sYSU4FUmVLNJ
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""견본 입력 — 마스터 화면 보기용 자동값(프로젝트 기본값 아님 · 저장 안 함).
|
||||
|
||||
고르기 = 목록 첫째 · 범위 = 최솟값 · 찾기 조건 값으로 쓰이는 수 = 그 표 그 칸 가장 낮은 줄 값
|
||||
(범위 줄이면 그 줄에 드는 아래 끝) · 나누는 자리에 오는 수 = 1 · 그 밖 0.
|
||||
찾기·나누기 쓰임은 로직 식(중간 · 결과 · 호표 수량·요소 · 덧줄)을 풀어 알아냄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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))
|
||||
for step in reversed(row.get("중간", [])):
|
||||
if step["이름"] in divs:
|
||||
divs |= _names(mf.parse(step["식"]))
|
||||
return finds, divs
|
||||
|
||||
|
||||
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 values(master: mf.Master, row: dict) -> dict:
|
||||
"""로직 줄 하나의 입력마다 자동값."""
|
||||
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, []))
|
||||
out[name] = got if got is not None else Decimal(1 if name in divs else 0)
|
||||
return out
|
||||
@@ -486,7 +486,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
lines = []
|
||||
for item in row.get("호표", []):
|
||||
qty = evaluate(parse(item["수량"]), env, master, depth)
|
||||
source = None
|
||||
source = why = None
|
||||
if item["종류"] == "로직":
|
||||
bundle = evaluate(parse(item["요소"]), env, master, depth)
|
||||
price = bundle["계"]
|
||||
@@ -503,37 +503,49 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
if isinstance(item["요소"], dict)
|
||||
else fill(item["요소"], env)
|
||||
)
|
||||
# 후보 0 · 값 빈 품목은 그 줄만 단가·금액을 비우고 까닭을 닮 — 로직은 계속 · 합에서 뺌
|
||||
# (없는 키는 틀린 로직 — 지금처럼 멈춤)
|
||||
if isinstance(ref, str) and qty != 0:
|
||||
master.get(ref)
|
||||
try:
|
||||
price, source = element(master, ref, env, item.get("단위"))
|
||||
except FormulaError:
|
||||
if qty != 0:
|
||||
raise
|
||||
price = None
|
||||
why = (
|
||||
None
|
||||
if price is not None
|
||||
else f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값"
|
||||
)
|
||||
except FormulaError as e:
|
||||
price, why = None, str(e)
|
||||
if price is None and qty == 0:
|
||||
price = Decimal(0) # 안 쓰는 재료 줄 — 값이 비어도 금액 0
|
||||
if price is None:
|
||||
raise FormulaError(f"「{master.label(ref)}」 값 없음 — 관리자가 채울 값")
|
||||
split = {item["비목"]: qty * price}
|
||||
price, why = Decimal(0), None # 안 쓰는 재료 줄 — 값이 비어도 금액 0
|
||||
split = {item["비목"]: None if price is None else qty * price}
|
||||
for k, v in split.items():
|
||||
sums[k] += v
|
||||
sums[k] += v or 0
|
||||
lines.append(
|
||||
{
|
||||
"이름": item.get("이름", ""),
|
||||
"단위": item.get("단위", ""),
|
||||
"수량": qty,
|
||||
"단가": price,
|
||||
"금액": qty * price,
|
||||
"금액": None if price is None else qty * price,
|
||||
"비목": split,
|
||||
**({"출처": source} if source else {}),
|
||||
**({"까닭": why} if why else {}),
|
||||
}
|
||||
)
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`
|
||||
env["줄"] = {item["이름"]: item["금액"] for item in lines if item["이름"]}
|
||||
# 덧줄이 호표 줄 금액 하나를 빼고 더할 수 있게 — `줄.'호표 줄 이름'`(빈 줄은 뺌)
|
||||
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)
|
||||
value = evaluate(parse(extra["식"]), env, master, depth)
|
||||
env[extra["이름"]] = value
|
||||
sums[extra["비목"]] += value
|
||||
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["이름"],
|
||||
@@ -542,6 +554,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
"단가": value,
|
||||
"금액": value,
|
||||
"비목": {extra["비목"]: value},
|
||||
**({"까닭": why} if why else {}),
|
||||
}
|
||||
)
|
||||
sums = {k: cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합
|
||||
|
||||
Reference in New Issue
Block a user