- Bind: B2 선택값 줄 = 낱말 같음 고름(「사용횟수」 ≠ 「사용」) · B3 상수 이름 · 덧(90˚) · 단위 맞춤(sec = 초 · km → m) · 경사도 → 경사각도 · B4 식 분자 · 분모 {수|기호} + 대입
- Solve.parts: Formula.works 절 + Clause 후보 {키, 절} → 풂 절(절 표 없는 후보뿐이면 블록 통째)
- 새 test_642_2_bind_solve: 예시 1 · 2 · 3 + 단산46 · 39 입력값 · 출처 꼬리표 · 입력 필요 · 엇갈림 · 지어 넣은 값 0 · 품 같음 · defaults 규칙
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
280 lines
13 KiB
Python
280 lines
13 KiB
Python
"""M02 일위대가 실무자료 블록 풀이 검산 · 판정(PLAN 785-2 · 요구 642 · 설계 642-A 2-5 ④-4 · ④-5) —
|
|
후보 기초마다 옵션값 읽기(`M02_Hopyo_Bind`) → 엔진 한 번(품 · 금액 참고) → 엑셀 단위 줄과 짝 · 판정.
|
|
|
|
짝(같은 묶음끼리): L 노무 = 직종 이름 · X 기계 = 시간 단위 로직 줄 · M 자재 = 뜻 낱말(남은 하나끼리) ·
|
|
R 율 = 덧줄 율(같은 율 › 남은 하나끼리) · E 값 · S 하위 = 엑셀만.
|
|
품 같음(`same`) = 엑셀 자릿수 반올림 폭 안(0.67 ↔ 0.6667) 또는 0.5 % · 금액은 참고(노임 · 단가 해가 다름).
|
|
줄 판정 = 품 같음 · 품 다름 · 입력 필요 · 엑셀만 · 우리만(우리 줄 품 0 = 꺼진 줄 · 안 보임).
|
|
절 판정 = 기초 풂(품 다름 없는 후보 하나) · 후보 여럿 · 기초 없음 /
|
|
블록 판정 = 기초로 풂 · 기초+덧(엑셀만 줄 → `Units` 잇기) · 줄 조합(기초 없음뿐) · 고를 것 n.
|
|
서버 단독 · 읽기만 · 자동 고름 없음(제안 · 사용자 확인).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
from statistics import median
|
|
from typing import Any, Callable
|
|
|
|
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
|
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
|
|
|
mf, mm, ma = hb.mf, hb.mm, hb.ma
|
|
SAME, DIFF, NEED, EXCEL, OURS = "품 같음", "품 다름", "입력 필요", "엑셀만", "우리만"
|
|
SOLVED, SOME, NONE = "기초 풂", "후보 여럿", "기초 없음"
|
|
BY_BASE, BASE_PLUS, BY_LINES, PICK = "기초로 풂", "기초+덧", "줄 조합", "고를 것"
|
|
SHARE = Decimal("0.005")
|
|
HOURS = {"시간", "hr", "h", "시"}
|
|
|
|
|
|
def _d(value: Any) -> Decimal | None:
|
|
try:
|
|
return None if value is None else Decimal(str(round(float(value), 10)))
|
|
except (ArithmeticError, TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def same(excel: Any, ours: Any) -> bool:
|
|
"""품 같음 — 엑셀 적힌 자릿수의 반올림 폭 안 또는 0.5 % 안."""
|
|
e, o = _d(excel), _d(ours)
|
|
if e is None or o is None:
|
|
return False
|
|
gap = abs(e - o)
|
|
places = -e.normalize().as_tuple().exponent
|
|
width = Decimal(5) * Decimal(10) ** -(places + 1) if 0 <= places <= 4 else Decimal(0)
|
|
return gap <= width or gap <= abs(e) * SHARE
|
|
|
|
|
|
def _kind(line: dict) -> str | None:
|
|
kind = line.get("종류")
|
|
if kind in ("로직", "기계"):
|
|
return "X" if str(line.get("단위") or "").strip() in HOURS else "S"
|
|
return {"인력": "L", "재료": "M", "율": "R"}.get(kind)
|
|
|
|
|
|
def _strict(item: dict, line: dict) -> bool:
|
|
kind = item.get("묶음")
|
|
if kind == "L":
|
|
return hm.tidy(line.get("이름")) == hm.tidy(item.get("직종") or item.get("이름"))
|
|
if kind == "M":
|
|
return hm.near(hm.words(item.get("이름")), hm.words(line.get("이름")))
|
|
if kind == "R":
|
|
return same(item.get("품"), line.get("품"))
|
|
return kind == "X"
|
|
|
|
|
|
def _excel(item: dict, share: Decimal) -> dict:
|
|
q = _d(item.get("품"))
|
|
return {
|
|
"묶음": item.get("묶음"),
|
|
"이름": item.get("이름"),
|
|
"품": hb.plain(q / share if q is not None and item.get("묶음") != "R" else q),
|
|
"행": item.get("행"),
|
|
**({"별도": True} if item.get("어떻게") == "별도" else {}),
|
|
}
|
|
|
|
|
|
def _row(item: dict | None, line: dict | None, share: Decimal) -> dict:
|
|
ex = _excel(item, share) if item else None
|
|
us = {k: hb.plain(line.get(k)) for k in ("이름", "종류", "단위", "품")} if line else None
|
|
if line and line.get("까닭"):
|
|
us["까닭"] = line["까닭"]
|
|
if ex is None:
|
|
verdict = NEED if line.get("까닭") else OURS
|
|
elif us is None:
|
|
verdict = EXCEL
|
|
elif us["품"] is None:
|
|
verdict = NEED if line.get("까닭") else DIFF
|
|
else:
|
|
verdict = SAME if same(ex["품"], us["품"]) else DIFF
|
|
out = {"엑셀": ex, "우리": us, "판정": verdict}
|
|
names = f"{(ex or {}).get('이름') or ''}|{(us or {}).get('이름') or ''}"
|
|
if item and line and item.get("묶음") == "X" and "타이어" in names and "무한궤도" in names:
|
|
out["표지"] = "기계 갈래 엇갈림"
|
|
return out
|
|
|
|
|
|
def pair(lines: list[dict], items: list[dict], share: Any = 1) -> list[dict]:
|
|
"""엑셀 단위 줄 ↔ 우리 줄 짝 → 줄 판정(엑셀 차례 · 남은 우리 줄 뒤)."""
|
|
share = _d(share) or Decimal(1)
|
|
used: set = set()
|
|
got: dict = {}
|
|
late = []
|
|
for i, item in enumerate(items):
|
|
pool = [j for j, ln in enumerate(lines) if j not in used and _kind(ln) == item.get("묶음")]
|
|
hits = [j for j in pool if _strict(item, lines[j])]
|
|
exact = [j for j in hits if lines[j].get("이름") == (item.get("직종") or item.get("이름"))]
|
|
if hits:
|
|
got[i] = (exact or hits)[0]
|
|
used.add(got[i])
|
|
else:
|
|
late.append(i)
|
|
for i in late: # 남은 하나끼리(결속선 ↔ 어닐링철선)
|
|
kind = items[i].get("묶음")
|
|
pool = [j for j, ln in enumerate(lines) if j not in used and _kind(ln) == kind]
|
|
rest = [k for k in late if items[k].get("묶음") == kind and k not in got]
|
|
if len(pool) == 1 and len(rest) == 1 and kind in ("M", "R"):
|
|
got[i] = pool[0]
|
|
used.add(pool[0])
|
|
rows = [_row(item, lines[got[i]] if i in got else None, share) for i, item in enumerate(items)]
|
|
for j, line in enumerate(lines):
|
|
if (
|
|
j not in used
|
|
and line.get("품") != 0
|
|
and (line.get("품") is not None or line.get("까닭"))
|
|
):
|
|
rows.append(_row(None, line, share))
|
|
return rows
|
|
|
|
|
|
def _usable(items: list[dict]) -> list[dict]:
|
|
return [x for x in items if not x.get("밖")]
|
|
|
|
|
|
def fitter(master: mf.Master, key: str, items: list[dict], share: Any = 1) -> Callable:
|
|
"""입력값 → `{맞음, 배}` — 맞음 = 짝 하나 이상 · 모두 품 같음 / 배 = 노무 · 기계 줄이 모두 같은 배
|
|
(자재 · 율 줄은 같음) — 표 거꾸로 · 감률 읽기(B5)가 씀."""
|
|
items = _usable(items)
|
|
|
|
def fit(given: dict) -> dict:
|
|
rows = pair(hb.quantities(master, key, given), items, share)
|
|
both = [r for r in rows if r["엑셀"] and r["우리"] and r["엑셀"]["품"] and r["우리"]["품"]]
|
|
ok = bool(both) and all(r["판정"] == SAME for r in both)
|
|
labor = [r for r in both if r["엑셀"]["묶음"] in ("L", "X")]
|
|
k = None
|
|
if labor:
|
|
k = median(_d(r["엑셀"]["품"]) / _d(r["우리"]["품"]) for r in labor)
|
|
flat = all(same(r["엑셀"]["품"], _d(r["우리"]["품"]) * k) for r in labor)
|
|
rest = all(r["판정"] == SAME for r in both if r not in labor)
|
|
k = float(k) if flat and rest else None
|
|
return {"맞음": ok, "배": k}
|
|
|
|
return fit
|
|
|
|
|
|
def _money(master: mf.Master, key: str, bound: dict, given: dict) -> dict:
|
|
"""금액 참고 — 입력 필요가 없을 때 엔진 한 번(안 씀 · 비움 = 기본 제안 · 저장 안 함)."""
|
|
if bound["입력필요"]:
|
|
return {"계": None, "까닭": f"입력 필요 {len(bound['입력필요'])}"}
|
|
row = master.logic(key)
|
|
base = ma.defaults(master, row)
|
|
fill = {n: base.get(n) for n in [*bound["안씀"], *bound["비움"]]}
|
|
try:
|
|
got = mf.run(master, key, mf.fill_empty(row, {**fill, **given}))
|
|
except mf.FormulaError as e:
|
|
return {"계": None, "까닭": str(e)}
|
|
if "결과" in got:
|
|
return {"결과": hb.plain(got["결과"])}
|
|
return {k: hb.plain(got.get(k)) for k in ("노무비", "재료비", "경비", "계")}
|
|
|
|
|
|
def candidate(master: mf.Master, key: str, src: dict, items: list[dict], share: Any = 1) -> dict:
|
|
"""후보 기초 하나 — 바인딩 · 줄 대조 · 판정 · 금액 참고."""
|
|
row = master.logic(key)
|
|
bound = hb.bind(master, key, src, fitter(master, key, items, share))
|
|
given = {**bound["입력값"], **{n: v[0]["값"] for n, v in bound["엇갈림"].items()}}
|
|
idle = {n: bound["기본"][n] for n in bound["안씀"] if bound["기본"].get(n) is not None}
|
|
rows = pair(hb.quantities(master, key, {**idle, **given}), _usable(items), share)
|
|
count = {v: sum(r["판정"] == v for r in rows) for v in (SAME, DIFF, NEED, EXCEL, OURS)}
|
|
if count[DIFF]:
|
|
verdict = DIFF
|
|
elif bound["입력필요"] or count[NEED] or bound["엇갈림"]:
|
|
verdict = NEED
|
|
else:
|
|
verdict = SAME if count[SAME] else EXCEL
|
|
return {
|
|
"키": key,
|
|
"이름": row.get("이름"),
|
|
"바인딩": bound,
|
|
"줄": rows,
|
|
"셈": count,
|
|
"판정": verdict,
|
|
"금액": _money(master, key, bound, given),
|
|
}
|
|
|
|
|
|
def _section(master: mf.Master, block: dict, part: dict, src: dict) -> dict:
|
|
items = part.get("항목")
|
|
items = _usable(block.get("항목") or []) if items is None else items
|
|
mine = {**src, **{k: part[k] for k in ("선택값", "상수", "식", "대입식") if k in part}}
|
|
share = part.get("몫")
|
|
share = (share.get("값") if isinstance(share, dict) else share) or 1
|
|
cands = [candidate(master, k, mine, items, share) for k in part.get("후보") or []]
|
|
good = [c for c in cands if c["셈"][DIFF] == 0 and (c["셈"][SAME] or len(cands) == 1)]
|
|
verdict = NONE if not cands else SOLVED if len(good) == 1 else SOME
|
|
pick = good[0]["키"] if verdict == SOLVED else None
|
|
return {
|
|
"제목": part.get("제목", ""),
|
|
"판정": verdict,
|
|
"고름": pick,
|
|
"후보": cands,
|
|
"항목": items,
|
|
}
|
|
|
|
|
|
def block(
|
|
master: mf.Master, blk: dict, parts: list[dict], src: dict | None = None, units: Any = None
|
|
) -> dict:
|
|
"""블록 풀이 — 절마다 후보 검산 · 절 판정 · 덧 줄(엑셀만 → `Units`) · 블록 판정."""
|
|
src = {**(src or {}), "블록": blk}
|
|
secs = [_section(master, blk, p, src) for p in parts]
|
|
extra, n = [], 0
|
|
for sec in secs:
|
|
chosen = next((c for c in sec["후보"] if c["키"] == sec["고름"]), None)
|
|
if sec["판정"] == SOME:
|
|
n += 1
|
|
if chosen:
|
|
b = chosen["바인딩"]
|
|
n += len(b["입력필요"]) + len(b["엇갈림"]) + chosen["셈"][DIFF]
|
|
alone = [r for r in chosen["줄"] if r["판정"] == EXCEL]
|
|
lone = [x for x in sec["항목"] if any(r["엑셀"]["행"] == x.get("행") for r in alone)]
|
|
else:
|
|
lone = sec["항목"] if sec["판정"] == NONE else []
|
|
for item in lone:
|
|
if units is None:
|
|
from M02_MasterTemplete import M02_Hopyo_Units as hu
|
|
|
|
units = hu.Units()
|
|
link = units.line(item)
|
|
extra.append(
|
|
{"절": sec["제목"], "행": item.get("행"), "이름": item.get("이름"), **link}
|
|
)
|
|
n += link.get("상태") != "이어짐"
|
|
for sec in secs:
|
|
sec.pop("항목")
|
|
if secs and all(s["판정"] == NONE for s in secs):
|
|
verdict = BY_LINES
|
|
elif n:
|
|
verdict = PICK
|
|
else:
|
|
verdict = BASE_PLUS if extra else BY_BASE
|
|
return {"번": blk.get("번"), "판정": verdict, "고를수": n, "절": secs, "덧": extra}
|
|
|
|
|
|
def _gather(nodes: list[dict], key: str) -> list:
|
|
return [x for one in nodes for x in [*one.get(key, []), *_gather(one.get("아래", []), key)]]
|
|
|
|
|
|
def parts(blk: dict, cands: list[Any]) -> list[dict]:
|
|
"""절 나무(785-1 `Formula.works`) + 후보(키 · `Clause` 후보 {키, 절}) → 풂 절 — 절 표 있는
|
|
후보가 있으면 일 절마다 그 절 후보 + 절 표 없는 후보 · 없으면 블록 통째 한 절 ·
|
|
단위 줄 · 상수 · 식 · 선택값(머리 것 더함) · 몫."""
|
|
from M02_MasterTemplete import M02_Hopyo_Formula as fo
|
|
|
|
nodes = blk.get("절") or fo.tree(blk.get("글") or [], blk.get("글행"), blk.get("항목") or [])
|
|
top = fo.head(blk.get("글") or [], blk.get("글행"))
|
|
cands = [c if isinstance(c, dict) else {"키": c} for c in cands]
|
|
items = blk.get("항목") or []
|
|
work = fo.works(nodes) if any(c.get("절") for c in cands) else []
|
|
if not work: # 블록 통째 한 일(절 표 있는 후보가 없으면 한 기초가 블록 전부)
|
|
whole = {"제목": blk.get("이름", ""), "후보": [c["키"] for c in cands], "항목": None}
|
|
return [whole | {k: top[k] + _gather(nodes, k) for k in ("상수", "식", "선택값")}]
|
|
out = []
|
|
for one in work:
|
|
mine = [c["키"] for c in cands if c.get("절") in (None, one["번"])]
|
|
at = [i for i in _gather([one], "항목") if not items[i].get("밖")]
|
|
part = {"제목": one["제목"], "번": one["번"], "후보": mine, "항목": [items[i] for i in at]}
|
|
part |= {k: top[k] + _gather([one], k) for k in ("상수", "식", "선택값")}
|
|
out.append(part | {"몫": one.get("몫")})
|
|
return out
|