- M01 값 역색인 · 숫자 정규화 한 벌(N1~N10 · 단위 기준 · % ↔ 소수 · t ↔ kg · 인/양 몫) · M01 지문마다 한 번 · 서버 시작 때 뒤에서 지음
- 값 길(상수 지문 → 로직 · 우연 보정 유의 4) · 합친 길(분석 648-D2 차례 · 후보 2~6 엔진 품 대조) · 검산 · 옵션(B1~B6 → 값 길 V → 고르기 맞댐 → 입력 필요)
- 세 단 확정 · 근접(판 차이 · 현장 값 · 감률 · 할증 배수 · 단위 기준 차이) · 사람(+ 값 약한 1 등) · 없음(M01 보강 목록)
- 길 GET hopyo-basis/{키} · hopyo-basis/{키}/{번} · 파생 저장본 · 올림 때 HP 숨은식(STmate G 식) · 끝내기 때 지움 · 판정 길 그대로
- 보정 자료 = 여덟 파일 상수 1,094 · 시험 test_793_3a 11(459 블록 기초 260 · 옵션 전부 52 · 품 같음 141 · 없음 85)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
325 lines
13 KiB
Python
325 lines
13 KiB
Python
"""근거 제안 4 — 기초 하나 검산 · 옵션 · 까닭(PLAN 793-3a · 분석 648-D2 1 장) · 읽기만 · 지어 넣는
|
||
값 없음.
|
||
|
||
옵션 = 기존 `M02_Hopyo_Bind.bind`(B1 부르는 줄 규격 · B2 선택값 + 블록 제목 낱말 · B3 기호표 · B4
|
||
대입식 ·
|
||
B5 표 거꾸로 · B6 레미콘 규격) → 못 읽은 칸 = 값 길 옵션(「V」) → 그래도 고르기 칸이면 고르기
|
||
값마다 넣어
|
||
엑셀 품과 맞댐(조합 60 까지 · 다 같음 하나 = 「검산」 · ±15 % 근접 하나 = 「근접」) → 남은 칸 =
|
||
입력 필요.
|
||
감률 · 할증 · 가산 · 보정 · 구멍땜 수 칸은 셈 때만 0(검산 몫) · 옵션으로 안 냄(입력 필요 · 요구
|
||
647-1).
|
||
품 대조 = `M02_Hopyo_Solve.pair`(엑셀 표시 자릿수 반올림 폭) · 기계 상수(q · K · f · E · E0 · Cm) =
|
||
그 로직(하위 포함)이 닿는 표의 같은 기호 칸 · 같은 기호 줄 값과 대조(`mach_check`).
|
||
까닭(`causes`) = b 판 차이 · c 현장 값 · d 감률 · 할증 배수 · e 단위 기준 차이 · f 그 밖(분석
|
||
648-D2 5-2 규칙).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import itertools
|
||
import math
|
||
import re
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
||
from M02_MasterTemplete import M02_Hopyo_Solve as solve
|
||
from M02_MasterTemplete import M02_San_Basis_Index as bi
|
||
|
||
RATIOS = {
|
||
8: "일↔시간 ×8",
|
||
1 / 8: "일↔시간 ÷8",
|
||
100: "% ×100",
|
||
0.01: "% ÷100",
|
||
1000: "t↔kg ×1000",
|
||
0.001: "t↔kg ÷1000",
|
||
10: "×10",
|
||
0.1: "÷10",
|
||
60: "분↔시간 ×60",
|
||
1 / 60: "분↔시간 ÷60",
|
||
}
|
||
CAUSE = {
|
||
"b": "판 차이",
|
||
"c": "현장 값",
|
||
"d": "감률 · 할증 배수",
|
||
"e": "단위 기준 차이",
|
||
"f": "그 밖",
|
||
}
|
||
ZERO = re.compile(r"감률|할증|가산|구멍땜|보정|증가|추가")
|
||
|
||
|
||
# ── 기계 상수 ↔ 그 로직 표 ──────────────────────────────────────────
|
||
def _norm(s: Any) -> str:
|
||
s = re.sub(r"\(.*?\)", "", str(s or "").replace("㎝", "cm").lower())
|
||
return re.sub(r"[^a-z0-9]", "", s)
|
||
|
||
|
||
def base_sym(s: str) -> str:
|
||
m = re.match(r"(cm|e0|q|k|f|e)\d*$", _norm(s))
|
||
return m.group(1) if m else ""
|
||
|
||
|
||
def mach_consts(cons: dict) -> list[dict]:
|
||
out = list(cons.get("상수", []))
|
||
for f in cons.get("식", []): # 식 끝값(Cm = t1+…+t4 = 10.7)은 셈 결과 — 대입 수만
|
||
out += [{"기호": k, "값": v} for k, v in (f.get("대입") or {}).items()]
|
||
return out
|
||
|
||
|
||
def mach_check(idx: bi.Index, key: str, consts: list[dict]) -> list[dict]:
|
||
out, seen = [], set()
|
||
tables = idx.item_tables.get(key, set())
|
||
for c in consts:
|
||
sym, v = base_sym(c.get("기호") or ""), c.get("값")
|
||
if not sym or not isinstance(v, (int, float)) or (c.get("기호"), v) in seen:
|
||
continue
|
||
seen.add((c.get("기호"), v))
|
||
pool = []
|
||
for t in tables:
|
||
tb = idx.tables[t]
|
||
for row in tb.get("줄", []):
|
||
row_sym = any(isinstance(x, str) and base_sym(x) == sym for x in row.values())
|
||
for col in tb.get("값칸", {}):
|
||
if base_sym(col) == sym or row_sym:
|
||
pool += bi.nums_of(row.get(col))
|
||
if not pool:
|
||
out.append({"기호": c["기호"], "값": v, "대조": "칸 없음"})
|
||
continue
|
||
d = len(str(v).split(".")[1]) if "." in str(v) else 0
|
||
near = min(pool, key=lambda x: abs(x - v))
|
||
hit = abs(near - v) <= max(0.5 * 10 ** (-d), abs(v) * 0.005)
|
||
out.append(
|
||
{
|
||
"기호": c["기호"],
|
||
"값": v,
|
||
"대조": "표에 있음" if hit else "표에 없음",
|
||
"가까운": near,
|
||
}
|
||
)
|
||
return out
|
||
|
||
|
||
# ── 옵션 · 검산 ─────────────────────────────────────────────────────
|
||
def zeroable(m: Any, key: str, name: str) -> bool:
|
||
"""글에 없으면 0(없음)으로 볼 만한 수 칸 — 고르기 없음 · 이름이 감률 · 할증 · 가산 · 보정 류."""
|
||
spec = next((x for x in hb.specs_of(m.logic(key)) if x["이름"] == name), {})
|
||
return not spec.get("고르기") and bool(ZERO.search(name))
|
||
|
||
|
||
def _conv(v: Any) -> Any:
|
||
if isinstance(v, list):
|
||
ends = [x for x in v if isinstance(x, (int, float))]
|
||
return sum(ends) / len(ends) if ends else None
|
||
return v
|
||
|
||
|
||
def probe(
|
||
m: Any, key: str, specs: dict, hard: list, base: dict, items: list, share: Any
|
||
) -> dict | None:
|
||
"""못 읽은 고르기 칸을 고르기 값마다 넣어 엑셀 품과 맞댐 — 「검산」 · 「근접」 옵션."""
|
||
if not hard or not all(specs.get(n, {}).get("고르기") for n in hard):
|
||
return None
|
||
opts = [specs[n]["고르기"] for n in hard]
|
||
if math.prod(len(o) for o in opts) > 60:
|
||
return None
|
||
exact, close = [], []
|
||
|
||
def sig(rr):
|
||
return tuple(round(r["우리"]["품"] or 0, 6) for r in rr if r.get("우리"))
|
||
|
||
for combo in itertools.product(*opts):
|
||
g2 = dict(zip(hard, combo))
|
||
try:
|
||
rr = solve.pair(hb.quantities(m, key, {**base, **g2}), solve._usable(items), share)
|
||
except Exception: # noqa: BLE001 — 그 조합은 셈 못 함
|
||
continue
|
||
same = sum(r["판정"] == solve.SAME for r in rr)
|
||
if any(r["판정"] == solve.NEED for r in rr) or not same:
|
||
continue
|
||
ratios = [
|
||
r["엑셀"]["품"] / r["우리"]["품"]
|
||
for r in rr
|
||
if r["판정"] == solve.DIFF and r["엑셀"]["품"] and r["우리"]["품"]
|
||
]
|
||
if not ratios:
|
||
exact.append((g2, rr))
|
||
elif all(abs(x - 1) <= 0.15 for x in ratios):
|
||
close.append((same, -len(ratios), g2, rr))
|
||
if len(exact) == 1:
|
||
return {"값": exact[0][0], "줄": exact[0][1], "길": "검산"}
|
||
if len(exact) > 1 and len({sig(rr) for _, rr in exact}) == 1:
|
||
return {"값": exact[0][0], "줄": exact[0][1], "길": "검산(같은 품 여럿)"}
|
||
if not exact and close:
|
||
close.sort(key=lambda x: (-x[0], -x[1]))
|
||
top = [c for c in close if (c[0], c[1]) == (close[0][0], close[0][1])]
|
||
if len(top) == 1 or len({sig(c[3]) for c in top}) == 1:
|
||
return {
|
||
"값": close[0][2],
|
||
"줄": close[0][3],
|
||
"길": "근접" + ("" if len(top) == 1 else "(같은 품 여럿)"),
|
||
}
|
||
return None
|
||
|
||
|
||
def verify(m: Any, key: str, items: list, consts: dict, src: dict, share: Any, vopts: dict) -> dict:
|
||
"""기초 하나 검산 — bind(B1~B6) · 값 길 옵션 · 고르기 맞댐 · 엑셀 품 대조."""
|
||
mine = {**src, **consts}
|
||
if src.get("제목줄"):
|
||
mine["선택값"] = list(consts.get("선택값") or []) + [src["제목줄"]]
|
||
try:
|
||
b = hb.bind(m, key, mine, solve.fitter(m, key, items, share))
|
||
specs = {s["이름"]: s for s in hb.specs_of(m.logic(key))}
|
||
given = {**b["입력값"], **{n: v[0]["값"] for n, v in b["엇갈림"].items()}}
|
||
fromv = {
|
||
n: _conv(vopts[n])
|
||
for n in b["입력필요"] + b["안씀"]
|
||
if n in vopts and _conv(vopts[n]) is not None
|
||
}
|
||
need = [n for n in b["입력필요"] if n not in fromv]
|
||
zero = {n: 0 for n in need if zeroable(m, key, n)}
|
||
idle = {
|
||
n: b["기본"][n] for n in b["안씀"] if b["기본"].get(n) is not None and n not in fromv
|
||
}
|
||
base = {**idle, **given, **fromv, **zero}
|
||
rows = solve.pair(hb.quantities(m, key, base), solve._usable(items), share)
|
||
got = probe(m, key, specs, [n for n in need if n not in zero], base, items, share)
|
||
if got:
|
||
fromv |= got["값"]
|
||
need = [n for n in need if n not in got["값"]]
|
||
rows = got["줄"]
|
||
same_q = [] # 엇갈림인데 어느 값이든 품이 같음 = 문제 아님
|
||
for n, alts in b["엇갈림"].items():
|
||
sigs = set()
|
||
for alt in alts:
|
||
q = hb.quantities(m, key, {**idle, **given, **fromv, **zero, n: alt["값"]})
|
||
sigs.add(
|
||
tuple(round(float(x["품"]), 6) if x.get("품") is not None else None for x in q)
|
||
)
|
||
if len(sigs) == 1:
|
||
same_q.append(n)
|
||
except Exception as e: # noqa: BLE001 — 검산 못 함 = 오류 표지(확정 안 함)
|
||
return {"키": key, "오류": repr(e)[:160]}
|
||
blk = src["블록"]
|
||
text = " ".join(
|
||
[str(blk.get("이름") or ""), str(blk.get("규격") or "")]
|
||
+ [str(t) for t in blk.get("글") or []]
|
||
)
|
||
words = {"소량": "소량|소형|소규모", "높이": "높이|수직고"}
|
||
absorbed = [
|
||
[n, hb.plain(given[n])]
|
||
for n, w in b["출처"].items()
|
||
if w["길"] == "B5"
|
||
and n.endswith("감률")
|
||
and n in given
|
||
and not re.search(words.get(n.replace("감률", ""), n.replace("감률", "") or "소량"), text)
|
||
]
|
||
cnt = {
|
||
v: sum(r["판정"] == v for r in rows)
|
||
for v in (solve.SAME, solve.DIFF, solve.NEED, solve.EXCEL, solve.OURS)
|
||
}
|
||
diff = []
|
||
for r in rows:
|
||
if r["판정"] == solve.DIFF:
|
||
ex, us = r["엑셀"]["품"], r["우리"]["품"]
|
||
diff.append(
|
||
{
|
||
"줄": r["엑셀"].get("이름") or r["우리"].get("이름"),
|
||
"엑셀": ex,
|
||
"우리": us,
|
||
"배": round(ex / us, 4) if ex and us else None,
|
||
"묶음": r["엑셀"].get("묶음"),
|
||
}
|
||
)
|
||
outside = [
|
||
r["엑셀"].get("이름")
|
||
for r in rows
|
||
if r["판정"] == solve.EXCEL and r["엑셀"] and r["엑셀"].get("품")
|
||
]
|
||
way = {n: alts[0]["길"] for n, alts in b["엇갈림"].items()} # 엇갈림 = 첫 값
|
||
way |= {k: v["길"] for k, v in b["출처"].items()}
|
||
way |= {k: "V" for k in fromv if not got or k not in got["값"]}
|
||
way |= {k: got["길"] for k in got["값"]} if got else {}
|
||
return {
|
||
"키": key,
|
||
"셈": cnt,
|
||
"기초밖": outside,
|
||
"감률흡수": absorbed,
|
||
"입력값": {
|
||
**{k: hb.plain(v) for k, v in given.items()},
|
||
**{k: hb.plain(v) for k, v in fromv.items()},
|
||
},
|
||
"길": way,
|
||
"입력필요": [n for n in need if n not in zero],
|
||
"0간주": list(zero),
|
||
"엇갈림": {
|
||
k: [hb.plain(x["값"]) for x in v] for k, v in b["엇갈림"].items() if k not in same_q
|
||
},
|
||
"품다름": diff,
|
||
}
|
||
|
||
|
||
def judge_one(v: dict, mach: list[dict]) -> tuple[str, list, list]:
|
||
"""(확정·전부 | 확정·일부, 글에 없음, 값이 M01 과 다름)."""
|
||
if "오류" in v:
|
||
return "확정·일부", [], [f"검산 오류 {v['오류'][:60]}"]
|
||
missing = list(v["입력필요"]) + [f"옵션 여럿 {k} {vals}" for k, vals in v["엇갈림"].items()]
|
||
missing += [f"기초 밖 줄 {x}" for x in v.get("기초밖", [])]
|
||
differ = [
|
||
f"품 {d['줄']} 엑셀 {d['엑셀']:.4g} ≠ 우리 {d['우리']:.4g}"
|
||
for d in v["품다름"]
|
||
if d["엑셀"] and d["우리"]
|
||
]
|
||
differ += [
|
||
f"기계 {x['기호']}={x['값']} 표에 없음(가까운 {x['가까운']:g})"
|
||
for x in mach
|
||
if x["대조"] == "표에 없음"
|
||
]
|
||
differ += [f"감률 {n}={x} 로 흡수(글에 그 말 없음) — 품 배수" for n, x in v.get("감률흡수", [])]
|
||
differ += [
|
||
f"근접 옵션 {k}={v['입력값'].get(k)}"
|
||
for k, w in (v.get("길") or {}).items()
|
||
if str(w).startswith("근접")
|
||
]
|
||
if not v["셈"].get(solve.SAME) and not v["셈"].get(solve.DIFF):
|
||
missing.append("(품 짝 없음)")
|
||
return ("확정·전부" if not missing and not differ else "확정·일부"), missing, differ
|
||
|
||
|
||
def causes(v: dict, mach: list[dict], rec: dict | None) -> list[list[str]]:
|
||
"""값 다름 → [갈래 b~f, 글] 【추정 규칙 · 648-D2 5-2】."""
|
||
out = []
|
||
user = bool(rec) and (
|
||
bool(rec.get("입력"))
|
||
or any(
|
||
"kg/" in str(k.get("단위", "")).lower() or "㎏/" in str(k.get("단위", ""))
|
||
for k in rec.get("K", [])
|
||
)
|
||
)
|
||
labor = [d["배"] for d in v.get("품다름", []) if d["배"] and d["묶음"] in ("L", "X")]
|
||
uniform = len(labor) >= 2 and all(abs(x / labor[0] - 1) <= 0.02 for x in labor)
|
||
for d in v.get("품다름", []):
|
||
r = d["배"]
|
||
if not r:
|
||
out.append(["f", f"{d['줄']} 품 0"])
|
||
continue
|
||
norm = next((name for k, name in RATIOS.items() if abs(r / k - 1) <= 0.01), None)
|
||
if norm:
|
||
out.append(["e", f"{d['줄']} {norm}"])
|
||
elif uniform and abs(r - 1) > 0.15:
|
||
out.append(["d", f"{d['줄']} 같은 배 ×{r:g}"])
|
||
elif abs(r - 1) <= 0.15:
|
||
out.append(["c" if user else "b", f"{d['줄']} ×{r:g}"])
|
||
else:
|
||
out.append(["c" if user else "f", f"{d['줄']} ×{r:g}"])
|
||
for n, x in v.get("감률흡수", []):
|
||
out.append(["b" if isinstance(x, (int, float)) and x < 15 else "d", f"감률 {n}={x} 흡수"])
|
||
for x in mach:
|
||
if x["대조"] == "표에 없음":
|
||
out.append(
|
||
[
|
||
"b" if x["가까운"] and abs(x["값"] / x["가까운"] - 1) <= 0.15 else "c",
|
||
f"기계 {x['기호']}={x['값']}",
|
||
]
|
||
)
|
||
return out
|