- 새 파일 — Words(식 풀기 · 장비 갈래 · 조건 낱말) · Finger(블록 글 · STmate 자원 → 지문 · 기계 줄 · 직접 줄 호표) · Shares(절 몫 벡터) · Ours(마스터 판 열쇠 · 우리 지문 캐시 tmp/m02_hopyo · 엔진 셈 캐시 · 자재 값 찾기 건너뜀) · Option(옵션 공간 · 거꾸로 찾기 · 견줌) · Fit(손잡이 한 곳 · 순위 · 뒤 셈 · 맞춤셈 캐시) · Combine(조합 · 섞인 줄 몫 먼저) · Verdict(블록 셈 · 맞음 / 다른 점 / 후보) - Match — b_order(이름 · 근거 차례 · 같음 근거 검사 · 옛 판 글 · 조항 오타) · clause(윗마디 0.9 · 나 창 Basis.clause 있으면 그것) · 이름 후보 로직은 접지 않음 - 정답 표 fixtures/m02_hopyo_golden.json(사람 136 · 새 파일 51) · 골든 도우미 - 북부청 92 줄 실측 — 1순위 87 · 다섯 안 91 · 맞음 41 틀림 0 · 다른 점 28 틀림 0 · 조합 맞음 57/66 · 우리 지문 셈 37 초 · 캐시 읽기 0.05 초 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
498 lines
21 KiB
Python
498 lines
21 KiB
Python
"""M02 일위대가 맞춤 — 옵션 거꾸로 찾기(PLAN 338-1 · 설계 2-5): 고른 우리 로직의 입력 칸마다
|
||
후보(옵션 공간)를 엔진으로 돌려 엑셀 품(직종 품 · 장비 시간 · 자재 수량)과 같은 입력을 찾음.
|
||
|
||
공간 = 고르기 모두 · 기계(EQ) = 엑셀 규격 수와 맞는 기종(없으면 모두) · 범위 = 두 끝 + 범위 안 엑셀
|
||
수 ·
|
||
표 찾기 칸 = 그 표 줄 값 · 그 밖 수 = 자동값 + 같은 이름 계수 + 단위 붙은 엑셀 수(% 칸 = 0 덧 ·
|
||
「품」 칸 = 엑셀 품 × 결과단위) · 뺌 = 지역 · 보정작업 · 조종원직종.
|
||
조합 = 곱 ≤ CAP 모두(안 닿는 칸 · 같은 품 값 묶음 셈) · 넘으면 엑셀 조건 낱말로 고르기 좁힘 ·
|
||
그래도 넘으면 기준점 둘에서 하나씩 + 고르기 둘씩.
|
||
수준 = 0(0.5%) · 1(R = 0.5% + 엑셀 값 자릿수 반올림 폭) · 2(3%) · 9(없음) — 「딱 맞음」(핵심 + 엑셀
|
||
몫을
|
||
다 덮음)만 셈 · 같은 수준이면 거리 작은 것. 규칙 · 문턱은 336 K5(`k5_lib` · `stage_b`) 그대로.
|
||
엔진 = `M02_Hopyo_Ours.run`(읽기만).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import itertools
|
||
import json
|
||
import math
|
||
import re
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from M01_MasterData import M01_MasterData_Store as m01
|
||
from M02_MasterTemplete import M02_Hopyo_Fit as fit # 손잡이(CAP · TOL) — 부를 때만 씀
|
||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||
from M02_MasterTemplete.M02_Hopyo_Words import nfkc, specnums
|
||
|
||
FIXED = ("지역", "보정작업", "조종원직종")
|
||
_NUM = re.compile(r"(?<![\d,.])(\d+(?:\.\d+)?)(?![\d,])")
|
||
UNITS = {
|
||
"%": ("%",),
|
||
"㎝": ("㎝", "cm"),
|
||
"cm": ("㎝", "cm"),
|
||
"㎞": ("㎞", "km"),
|
||
"km": ("㎞", "km"),
|
||
"㎜": ("㎜", "mm"),
|
||
"mm": ("㎜", "mm"),
|
||
"㎥": ("㎥", "m3"),
|
||
"㎡": ("㎡", "m2"),
|
||
"m": ("m",),
|
||
"회": ("회",),
|
||
"개월": ("개월",),
|
||
"층": ("층",),
|
||
"t": ("t", "ton", "톤"),
|
||
"ton": ("t", "ton", "톤"),
|
||
"톤": ("t", "ton", "톤"),
|
||
"분": ("분",),
|
||
"초": ("초", "sec"),
|
||
"kg": ("kg", "㎏"),
|
||
"㎏": ("kg", "㎏"),
|
||
}
|
||
UNITW = {"mm", "cm", "kg", "ton", "sec", "kgf", "mpa"}
|
||
|
||
|
||
# ── 엑셀 쪽 수 ─────────────────────────────────────────────────────────
|
||
def ex_of(efp: dict[str, Any], text: list[str] | None = None) -> dict[str, Any]:
|
||
"""엑셀 지문 → 옵션 공간 재료(글 · 글머리 · 계수 · 규격 수 · 조건 · 품 값)."""
|
||
texts = [efp["이름"], efp.get("규격") or ""] + [str(t) for t in text or []]
|
||
sp: set[float] = set()
|
||
for m in efp["장비"]:
|
||
sp |= set(m.get("규격") or [])
|
||
sp |= specnums(f"{efp['이름']} {efp.get('규격') or ''}")
|
||
return {
|
||
"글": texts,
|
||
"글머리": texts[:8],
|
||
"계수": efp.get("계수") or {},
|
||
"규격수": sp,
|
||
"조건": efp.get("조건") or {},
|
||
"품값": {Decimal(str(x["품"])) for x in efp["인력"].values() if x.get("품")},
|
||
}
|
||
|
||
|
||
def excel_numbers(texts: list[str], spec: dict[str, Any] | None = None) -> set[Decimal]:
|
||
"""글의 수 — spec 이 있으면 그 칸 단위가 뒤에 붙거나 칸 이름이 앞에 붙은 수만(쉼표 단가 뺌)."""
|
||
out: set[Decimal] = set()
|
||
flat = [nfkc(t) for t in texts]
|
||
if spec is None:
|
||
pats = [_NUM]
|
||
else:
|
||
u = nfkc(spec.get("단위")).strip().lower()
|
||
vs = UNITS.get(u, (u,) if u and len(u) <= 3 and "/" not in u else ())
|
||
vs = () if u == "%" else vs # 「노무비의 2%」 같은 율이 감률 칸 후보로 드는 것을 막음
|
||
num = r"(\d+(?:\.\d+)?)"
|
||
pats = []
|
||
if vs:
|
||
alt = "|".join(re.escape(x) for x in vs)
|
||
pats.append(
|
||
re.compile(r"(?<![\d,.])" + num + r"\s*(?:" + alt + r")(?![A-Za-z가-힣])", re.I)
|
||
)
|
||
pats.append(
|
||
re.compile(re.escape(nfkc(spec["이름"])) + r"\s*[:=(]?\s*" + num + r"(?![\d,])")
|
||
)
|
||
for t in flat:
|
||
for pat in pats:
|
||
for n in pat.findall(t):
|
||
v = Decimal(n)
|
||
if 0 < v <= 2000:
|
||
out.add(v.normalize() if v == v.to_integral() else v)
|
||
return out
|
||
|
||
|
||
def eq_numbers(code: str) -> set[float]:
|
||
"""기계 규격 수 — 단위 붙은 수 + 맨 수(「41 [80∼95]」 → 41 · 80 · 95)."""
|
||
one = ours.whole().numbers.get("EQ", {}).get(str(code)) or {}
|
||
bare = {float(n) for n in re.findall(r"\d+(?:\.\d+)?", nfkc(one.get("규격")))}
|
||
return ours.eq_of(code)[1] | bare
|
||
|
||
|
||
def _uses(row: dict) -> tuple[dict, set]:
|
||
try:
|
||
return m01.mau._uses(row)
|
||
except Exception: # noqa: BLE001 — 식을 못 읽는 로직은 찾기 칸 없음
|
||
return {}, set()
|
||
|
||
|
||
def space(code: str, ex: dict[str, Any] | None = None) -> list[tuple[str, list, str]]:
|
||
"""[(입력 이름, 후보, 갈래)] — 갈래 = 기계 · 고르기 · 범위 · 표 · 수."""
|
||
whole = ours.whole()
|
||
row = whole.logic(code)
|
||
auto = m01.mau.values(whole, row)
|
||
finds, _ = _uses(row)
|
||
ex = ex or {}
|
||
texts = ex.get("글") or []
|
||
coef = {k.lower(): Decimal(str(v)) for k, v in (ex.get("계수") or {}).items()}
|
||
spn = ex.get("규격수") or set()
|
||
times = Decimal(str(ours.times_of(row)))
|
||
out = []
|
||
for s in row.get("입력") or []:
|
||
name, opts = s["이름"], s.get("고르기") or []
|
||
if name in FIXED:
|
||
continue
|
||
if opts and all(isinstance(v, str) and ours.EQ_CODE.match(v) for v in opts):
|
||
hits = {v: len(eq_numbers(v) & spn) for v in opts} if spn else {}
|
||
most = max(hits.values(), default=0)
|
||
keep = [
|
||
v for v in opts if most and hits[v] == most
|
||
] # 엑셀 규격 수가 가장 많이 겹치는 기종
|
||
out.append((name, keep or list(opts), "기계"))
|
||
elif opts:
|
||
out.append((name, list(opts), "고르기"))
|
||
elif isinstance(s.get("범위"), list) and len(s["범위"]) == 2:
|
||
lo, hi = (Decimal(str(x)) for x in s["범위"])
|
||
vals = {lo, hi} | {v for v in excel_numbers(texts, s) if lo <= v <= hi}
|
||
out.append((name, sorted(vals), "범위"))
|
||
elif name in finds:
|
||
rows = set()
|
||
for ref, cond in finds[name]:
|
||
rows |= set(m01.mau._fits(whole, ref, cond))
|
||
out.append((name, sorted(rows) or [auto.get(name)], "표"))
|
||
else:
|
||
base = auto.get(name)
|
||
vals = {base} if base is not None else set()
|
||
if name.lower() in coef:
|
||
vals.add(coef[name.lower()])
|
||
vals |= excel_numbers(texts, s) if texts else set()
|
||
if str(s.get("단위") or "").strip() == "%":
|
||
vals.add(Decimal(0))
|
||
if name == "품":
|
||
vals |= {v * times for v in ex.get("품값", set())}
|
||
vals.discard(None)
|
||
out.append((name, sorted(vals, key=lambda v: (v != base, v)) or [base], "수"))
|
||
return out
|
||
|
||
|
||
def _product(sp: list) -> list[dict]:
|
||
names = [n for n, _, _ in sp]
|
||
return [dict(zip(names, vals)) for vals in itertools.product(*[o for _, o, _ in sp])]
|
||
|
||
|
||
def narrow(sp: list, ex: dict[str, Any] | None) -> list:
|
||
"""고르기 후보를 엑셀 조건 낱말로 좁힘 — 낱말이 든 후보가 있으면 그것만."""
|
||
words = {w for ws in ((ex or {}).get("조건") or {}).values() for w in ws}
|
||
flat = re.sub(r"\s+", "", nfkc(" ".join((ex or {}).get("글머리") or [])))
|
||
out = []
|
||
for name, opts, kind in sp:
|
||
if kind == "고르기" and len(opts) > 1:
|
||
hit = [
|
||
o
|
||
for o in opts
|
||
if any(w in str(o) for w in words) or (len(str(o)) >= 2 and str(o) in flat)
|
||
]
|
||
if hit:
|
||
out.append((name, hit, kind))
|
||
continue
|
||
out.append((name, opts, kind))
|
||
return out
|
||
|
||
|
||
def modes() -> tuple[tuple[str, float, bool], ...]:
|
||
"""허용 셋 — 0.5% · R(0.5% + 엑셀 값 자릿수 반올림 폭) · 3%."""
|
||
return (("0.5%", fit.TOL[0], False), ("R", fit.TOL[0], True), ("3%", fit.TOL[2], False))
|
||
|
||
|
||
def combos(
|
||
sp: list, ex: dict[str, Any] | None = None, cap: int | None = None
|
||
) -> tuple[list[dict], str]:
|
||
"""(조합, 어떻게 — 모두 · 좁힘 · 하나씩)."""
|
||
cap = cap or fit.CAP
|
||
if (math.prod(len(o) for _, o, _ in sp) if sp else 1) <= cap:
|
||
return _product(sp), "모두"
|
||
sp2 = narrow(sp, ex)
|
||
if math.prod(len(o) for _, o, _ in sp2) <= cap:
|
||
return _product(sp2), "좁힘"
|
||
base = {n: o[0] for n, o, _ in sp2}
|
||
out = [dict(base)] + [{**base, n: v} for n, o, _ in sp2 for v in o[1:]]
|
||
small = [(n, o) for n, o, k in sp2 if k != "수"]
|
||
for (n1, o1), (n2, o2) in itertools.combinations(small, 2):
|
||
if len(o1) * len(o2) <= 400:
|
||
out += [{**base, n1: a, n2: b} for a in o1 for b in o2]
|
||
seen, uniq = set(), []
|
||
for c in out:
|
||
k = json.dumps(c, sort_keys=True, default=str, ensure_ascii=False)
|
||
if k not in seen:
|
||
seen.add(k)
|
||
uniq.append(c)
|
||
return uniq[:cap], "하나씩"
|
||
|
||
|
||
def sig(v: dict | None, nd: int = 4) -> str | None:
|
||
"""품 꼴 서명 — 직종 · 장비(갈래 · 시간) · 자재 · 딸린 로직(유효 nd 자리)."""
|
||
if v is None:
|
||
return None
|
||
r = lambda x: float(f"{x:.{nd}g}") # noqa: E731
|
||
return json.dumps(
|
||
[
|
||
sorted((k, r(q)) for k, q in v["L"].items() if q),
|
||
sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"] if q),
|
||
sorted((k, r(q)) for k, q in v["M"].items() if q),
|
||
sorted((k, r(q)) for k, q in v["S"].items() if q),
|
||
],
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
|
||
def plan(code: str, ex: dict[str, Any] | None = None) -> tuple[list[dict], str, list, list, dict]:
|
||
"""(조합, 어떻게, 움직이는 칸, 안 닿는 칸, 값 묶음)."""
|
||
sp = space(code, ex)
|
||
if (math.prod(len(o) for _, o, _ in sp) if sp else 1) <= fit.CAP:
|
||
cb = _product(sp)
|
||
sg = [sig(ours.run(code, c)) for c in cb]
|
||
active, inert, cls = [], [], {}
|
||
for n, o, k in sp:
|
||
if len(o) < 2:
|
||
continue
|
||
ctx: dict[str, dict] = {}
|
||
for c, s_ in zip(cb, sg, strict=True):
|
||
rest = {x: str(y) for x, y in c.items() if x != n}
|
||
ctx.setdefault(json.dumps(rest, sort_keys=True, ensure_ascii=False), {})[
|
||
str(c[n])
|
||
] = s_
|
||
keys = sorted(ctx)
|
||
prof = {str(v): tuple(ctx[g].get(str(v)) for g in keys) for v in o}
|
||
if len(set(prof.values())) == 1:
|
||
inert.append(n)
|
||
continue
|
||
active.append(n)
|
||
if k != "고르기":
|
||
ids: dict = {}
|
||
cls[n] = {v: ids.setdefault(pf, len(ids)) for v, pf in prof.items()}
|
||
return cb, "모두", active, inert, cls
|
||
base = {n: o[0] for n, o, _ in sp}
|
||
alt = {n: o[-1] for n, o, _ in sp}
|
||
act, inert = [], []
|
||
for n, o, k in sp:
|
||
if len(o) == 1:
|
||
continue
|
||
s0 = {sig(ours.run(code, {**base, n: v})) for v in o}
|
||
s1 = {sig(ours.run(code, {**alt, n: v})) for v in o} if len(s0) == 1 else s0
|
||
if len(s0) == 1 and len(s1) == 1:
|
||
inert.append(n)
|
||
continue
|
||
if k != "고르기": # 같은 표 줄에 드는 수는 한 값으로 — 두 기준점 품이 같으면 같은 값
|
||
keep, seen = [], set()
|
||
for v in o:
|
||
pair = (sig(ours.run(code, {**base, n: v})), sig(ours.run(code, {**alt, n: v})))
|
||
if pair not in seen:
|
||
seen.add(pair)
|
||
keep.append(v)
|
||
o = keep
|
||
act.append((n, o, k))
|
||
cb, how = combos(act, ex)
|
||
return [{**base, **c} for c in cb], how, [n for n, _, _ in act], inert, {}
|
||
|
||
|
||
def vary(hits: list[dict], act: list[str], cls: dict) -> list[str]:
|
||
"""맞은 조합들 사이에서 값이 갈리는 칸 — 수 칸은 같은 값 묶음이면 한 값."""
|
||
return [
|
||
n for n in act if len({cls.get(n, {}).get(str(g.get(n)), str(g.get(n))) for g in hits}) > 1
|
||
]
|
||
|
||
|
||
# ── 맞대기 ──────────────────────────────────────────────────────────────
|
||
def near(a: Any, b: Any, t: float, r: float = 0.0) -> bool:
|
||
"""a(엑셀) ≈ b(우리) — 비 t 안 · r = 엑셀 값 자릿수 반올림 폭."""
|
||
return bool(a and b and a > 0 and b > 0 and abs(a - b) <= max(t * max(a, b), r))
|
||
|
||
|
||
def ru(x: float) -> float:
|
||
"""엑셀 값 반올림 폭 — 소수 둘째~넷째 자리로 적힌 값만 · 값의 4% 안."""
|
||
s = repr(float(x))
|
||
if "e" in s or "." not in s:
|
||
return 0.0
|
||
d = len(s.split(".")[1].rstrip("0"))
|
||
return min(0.5 * 10 ** (-d), 0.04 * float(x)) if 2 <= d <= 4 else 0.0
|
||
|
||
|
||
def mat_words(name: Any) -> set[str]:
|
||
"""자재 이름 낱말 — 한글 두 자 넘는 덩이 · 영문 세 자 넘는 덩이 · 단위 낱말 뺌."""
|
||
t = nfkc(name).lower().replace(".", "")
|
||
return {w for w in re.findall(r"[가-힣]{2,}|[a-z]{3,}", t) if w not in UNITW}
|
||
|
||
|
||
def _named(ws: set[str], k: str) -> bool:
|
||
w = mat_words(k)
|
||
return bool(ws & w) or any(a in b or b in a for a in ws for b in w)
|
||
|
||
|
||
def compare(
|
||
e: dict, v: dict | None, t: float, base: bool = False, rnd: bool = False
|
||
) -> dict | None:
|
||
"""엑셀 지문 ↔ 우리 셈 → {직종, 장비, 자재, 핵심, 직종만, 덧} · base = 엑셀 밑품으로."""
|
||
if v is None:
|
||
return None
|
||
tol = lambda x: ru(x) if rnd else 0 # noqa: E731
|
||
E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")}
|
||
ov = {k: q for k, q in v["L"].items() if q and q > 0}
|
||
out: dict[str, Any] = {"덧": []}
|
||
if E:
|
||
same = set(E) == set(ov)
|
||
ok = same and all(near(E[k], ov[k], t, tol(E[k])) for k in E)
|
||
out["직종"] = "같음" if ok else "직종 같음" if same else "다름"
|
||
else:
|
||
out["직종"] = "우리만" if ov else "없음"
|
||
EX = [m for m in e["장비"] if m.get("시간")]
|
||
OX = [(set(a), q) for a, _, q in v["X"] if q]
|
||
if EX and OX:
|
||
ok = []
|
||
for m in EX:
|
||
mine = [q for a, q in OX if a & set(m["갈래"])]
|
||
if not mine:
|
||
out["덧"].append("엑셀만 장비 " + "·".join(sorted(m["갈래"])))
|
||
continue
|
||
h = m["밑시간"] if base and m.get("밑시간") else m["시간"]
|
||
hit = any(near(h, q, t, tol(h)) for q in mine) or near(h, sum(mine), t, tol(h))
|
||
ok.append(hit)
|
||
et = set().union(*[set(m["갈래"]) for m in e["장비"]])
|
||
out["덧"] += ["우리만 장비 " + "·".join(sorted(a)) for a, _ in OX if not a & et]
|
||
out["장비"] = ("같음" if all(ok) else "다름") if ok else "갈래 안 겹침"
|
||
else:
|
||
out["장비"] = "엑셀만" if EX else "우리만" if OX else "없음"
|
||
EM = [m for m in e["자재"] if m.get("품")]
|
||
OM = {k: q for k, q in v["M"].items() if q}
|
||
if EM and OM:
|
||
ok = []
|
||
for m in EM:
|
||
mine = [q for k, q in OM.items() if _named(mat_words(m["이름"]), k)]
|
||
if mine:
|
||
x = m["밑품"] if base and m.get("밑품") else m["품"]
|
||
ok.append(any(near(x, q, t, tol(x)) for q in mine))
|
||
out["자재"] = ("같음" if all(ok) else "다름") if ok else "이름 안 겹침"
|
||
else:
|
||
out["자재"] = "엑셀만" if EM else "없음"
|
||
core = [out["직종"] == "같음"] if E else [False] if ov else []
|
||
core += [out[k] == "같음" for k in ("장비", "자재") if out[k] in ("같음", "다름")]
|
||
out["핵심"] = bool(core) and all(core)
|
||
out["직종만"] = bool(E) and out["직종"] == "같음"
|
||
return out
|
||
|
||
|
||
def strong(e: dict, c: dict | None) -> bool:
|
||
"""딱 맞음 — 핵심 + 엑셀 몫을 다 덮음(엑셀만 · 우리만 장비 없음 · 자재 다름 없음 ·
|
||
「보통인부 하나뿐 · 장비 없음」 줄 아님)."""
|
||
if not c or not c.get("핵심"):
|
||
return False
|
||
if c.get("장비") in ("엑셀만", "우리만", "갈래 안 겹침"):
|
||
return False
|
||
if any("엑셀만 장비" in d or "우리만 장비" in d for d in c.get("덧") or []):
|
||
return False
|
||
if c.get("자재") == "다름":
|
||
return False
|
||
labor = [k for k, x in e["인력"].items() if x.get("품")]
|
||
return not (labor == ["보통인부"] and not any(m.get("시간") for m in e["장비"]))
|
||
|
||
|
||
def gap(e: dict, v: dict | None, base: bool = False) -> float:
|
||
"""닮음 거리 — 직종 묶음 어긋남 + 품 |ln 비| 평균 + 장비 시간 반 무게."""
|
||
if v is None:
|
||
return 99.0
|
||
E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")}
|
||
ov = {k: q for k, q in v["L"].items() if q and q > 0}
|
||
d = 0.0
|
||
if E or ov:
|
||
d += len(set(E) ^ set(ov)) / len(set(E) | set(ov))
|
||
both = [min(1.0, abs(math.log(E[k] / ov[k]))) for k in set(E) & set(ov)]
|
||
d += sum(both) / len(both) if both else 1.0
|
||
EX = [m for m in e["장비"] if m.get("시간")]
|
||
OX = [(set(a), q) for a, _, q in v["X"] if q]
|
||
if EX and OX:
|
||
hs = []
|
||
for m in EX:
|
||
mine = [q for a, q in OX if a & set(m["갈래"])]
|
||
h = m["밑시간"] if base and m.get("밑시간") else m["시간"]
|
||
hs.append(min(1.0, min(abs(math.log(h / q)) for q in mine)) if mine else 1.0)
|
||
d += 0.5 * sum(hs) / len(hs)
|
||
return d
|
||
|
||
|
||
def csig(e: dict, v: dict | None, nd: int = 4) -> str | None:
|
||
"""엑셀이 보이는 몫만의 품 서명 — 직종 · 엑셀 갈래와 겹치는 장비 · 엑셀 이름과 겹치는 자재."""
|
||
if v is None:
|
||
return None
|
||
r = lambda x: float(f"{x:.{nd}g}") # noqa: E731
|
||
et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set()
|
||
em = [mat_words(m["이름"]) for m in e["자재"] if m.get("품")]
|
||
return json.dumps(
|
||
[
|
||
sorted((k, r(q)) for k, q in v["L"].items() if q),
|
||
sorted((tuple(a), r(q)) for a, _, q in v["X"] if q and set(a) & et),
|
||
sorted((k, r(q)) for k, q in v["M"].items() if q and any(_named(ws, k) for ws in em)),
|
||
],
|
||
ensure_ascii=False,
|
||
)
|
||
|
||
|
||
def ours_qty(v: dict | None) -> dict[str, Any]:
|
||
"""우리 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}."""
|
||
if v is None:
|
||
return {"인력": {}, "장비": [], "자재": []}
|
||
return {
|
||
"인력": {k: round(q, 6) for k, q in v["L"].items() if q},
|
||
"장비": [{"갈래": list(a), "시간": round(q, 6)} for a, _, q in v["X"] if q],
|
||
"자재": [{"이름": k, "수량": round(q, 6)} for k, q in v["M"].items() if q],
|
||
}
|
||
|
||
|
||
def _plain(value: Any) -> Any:
|
||
if isinstance(value, Decimal):
|
||
value = value.normalize()
|
||
return int(value) if value == value.to_integral_value() else float(value)
|
||
return value
|
||
|
||
|
||
def find(code: str, efp: dict[str, Any], text: list[str] | None = None) -> dict[str, Any]:
|
||
"""로직 하나의 옵션 거꾸로 찾기 → {수준, 판정, 입력, 갈리는칸, 견줌, 우리품, 거리, 섞기전}."""
|
||
none = {"수준": 9, "판정": "없음", "입력": {}, "갈리는칸": [], "견줌": None}
|
||
none.update(우리품=ours_qty(None), 거리=99.0, 섞기전=False, 직종같음=False)
|
||
try:
|
||
ex = ex_of(efp, text)
|
||
cb, how, act, _, cls = plan(code, ex)
|
||
except Exception: # noqa: BLE001 — 로직 없음 · 식 못 읽음
|
||
return {**none, "판정": "못 셈"}
|
||
best = (9, 9, 99.0, None, None)
|
||
hits: dict[int, list] = {0: [], 1: [], 2: []}
|
||
for g in cb:
|
||
v = ours.run(code, g)
|
||
if v is None or not (v["L"] or v["X"] or v["M"]):
|
||
continue
|
||
lvl = weak = 9
|
||
for i, (_, t, rnd) in enumerate(modes()):
|
||
c = compare(efp, v, t, rnd=rnd) or {}
|
||
if c.get("핵심"):
|
||
weak = min(weak, i)
|
||
if strong(efp, c):
|
||
hits[i].append((g, csig(efp, v)))
|
||
lvl = min(lvl, i)
|
||
d = gap(efp, v)
|
||
if (lvl, weak, d) < best[:3]:
|
||
best = (lvl, weak, d, g, v)
|
||
lvl, _, d, g, v = best
|
||
if g is None:
|
||
return none
|
||
got = [h for h in hits.get(lvl, [])] if lvl < 9 else []
|
||
split = vary([h for h, _ in got], act, cls)
|
||
if not got:
|
||
verdict = "없음"
|
||
elif len({s for _, s in got}) > 1:
|
||
verdict = "여럿(다른 품)"
|
||
else:
|
||
verdict = "여럿(같은 품)" if split else "하나만"
|
||
auto = m01.mau.values(ours.whole(), ours.whole().logic(code)) # 움직인 칸 + 자동값과 다른 칸
|
||
base = compare(efp, v, fit.TOL[2], base=True) or {}
|
||
labor = {k for k, x in efp["인력"].items() if x.get("품")}
|
||
return {
|
||
"수준": lvl,
|
||
"판정": verdict,
|
||
"입력": {n: _plain(x) for n, x in g.items() if n in act or str(x) != str(auto.get(n))},
|
||
"모든입력": {n: _plain(x) for n, x in g.items()},
|
||
"갈리는칸": split,
|
||
"견줌": compare(efp, v, fit.TOL[2]),
|
||
"직종같음": bool(v["L"]) and set(k for k, q in v["L"].items() if q) == labor,
|
||
"우리품": ours_qty(v),
|
||
"거리": round(d, 4),
|
||
"섞기전": lvl == 9 and bool(base.get("핵심")),
|
||
"어떻게": how,
|
||
}
|