- 새 Line — 상태 차례(% · 호표 · 노임 · 장부 뺌 · 별칭 · 맞춤셈 판정 · 이름 근거 후보) · 체크 제안(맞음 · 별칭 규칙 셋 · 확인된 하위 호표) · 대상(입력값 출처 별칭 > 블록 > 규격 · 환산 · 몫 · 배) · 품 비교 · 직접 줄 호표 통째 · guess(대상 목록 · 낱개도 받음 · 엔진 한 번) - Match — view(to = 만들 층 · 셈) · picks · guess 를 Line 으로 · 옛 층 기억 · confirm · 들어감 없앰 · b_order 후보에 근거 칸 - Verdict — 위 다섯 딱 맞음 먼저(조합 없는 줄) · 후보 근거 = 근거 맞춤 꼴 · 지문차례(조합 전) - Fit — 맞춤셈 캐시 이름에 셈 판(CALC_VERSION) - 골든 시험(느림 · 1 분 54 초) — 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 다른 점 틀림 1 · 옵션까지 57 · 조합 맞음 62/83 · 새 파일 51 맞음 틀림 2 · 씨앗 끔 · 북부청 조합 전 통과 - 근거 시험(나 창) 세 곳 — 옛 match_line → b_order(새 API) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
210 lines
9.7 KiB
Python
210 lines
9.7 KiB
Python
"""M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2).
|
||
|
||
블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) ·
|
||
같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve`) → 위 다섯 옵션
|
||
거꾸로(`Option`) →
|
||
위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) →
|
||
판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표).
|
||
글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||
from M02_MasterTemplete import M02_Hopyo_Combine as combine
|
||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||
|
||
|
||
def _num(x: float) -> str:
|
||
return f"{x:.4g}"
|
||
|
||
|
||
def excel_qty(efp: dict[str, Any]) -> dict[str, Any]:
|
||
"""엑셀 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}."""
|
||
return {
|
||
"인력": {k: round(x["품"], 6) for k, x in efp["인력"].items() if x.get("품")},
|
||
"장비": [{"갈래": m["갈래"], "시간": m["시간"]} for m in efp["장비"] if m.get("시간")],
|
||
"자재": [{"이름": m["이름"], "수량": m["품"]} for m in efp["자재"] if m.get("품")],
|
||
}
|
||
|
||
|
||
def _public(efp: dict[str, Any]) -> dict[str, Any]:
|
||
keep = ("인력", "장비", "자재", "출처", "조건", "옛판", "율", "계수")
|
||
return {k: efp.get(k) for k in keep}
|
||
|
||
|
||
def _label(code: str, values: dict[str, Any]) -> str:
|
||
"""옵션 요약 — 고르기 글은 그대로 · 수 칸은 「칸 값」 · 기계 코드는 고르기이름."""
|
||
specs = {s.get("이름"): s for s in hopyo_match._logic_specs(code)}
|
||
out = []
|
||
for name, v in values.items():
|
||
names = (specs.get(name) or {}).get("고르기이름") or {}
|
||
out.append(str(names.get(v) or v) if isinstance(v, str) else f"{name} {v}")
|
||
return " · ".join(out)
|
||
|
||
|
||
def _part_fp(part: dict[str, Any], x: float, line: dict[str, Any]) -> dict[str, Any]:
|
||
"""조합 몫 → 옵션 찾기용 지문(몫 x 를 풀어 100% 품)."""
|
||
fp = finger.empty(line)
|
||
k = x or 1.0
|
||
for t, q in (part.get("노임") or {}).items():
|
||
fp["인력"][t] = {"품": q / k, "밑품": q / k, "율": [], "어떻게": ["몫"]}
|
||
return fp
|
||
|
||
|
||
def _diffs(efp: dict, found: dict) -> list[str]:
|
||
"""숫자 사실 꼬리표 — 직종 품 다름 · 섞음 · 율 · 엑셀만 · 우리만 장비."""
|
||
tags = []
|
||
ours_l = (found.get("우리품") or {}).get("인력") or {}
|
||
for t, x in efp["인력"].items():
|
||
a, b = x.get("품"), ours_l.get(t)
|
||
if a and b and not option.near(a, b, fit.TOL[2]):
|
||
tags.append(f"{t} 엑셀 {_num(a)} · 우리 {_num(b)}")
|
||
elif a and not b:
|
||
tags.append(f"{t} 엑셀만 {_num(a)}")
|
||
tags += [f"{t} 우리만 {_num(q)}" for t, q in ours_l.items() if t not in efp["인력"]]
|
||
for x in efp["인력"].values():
|
||
for v, note in x.get("율") or []:
|
||
tag = f"섞음 — 엑셀이 × {_num(v)}" if note == "섞음" else f"율 {note} × {_num(v)}"
|
||
if v and tag not in tags:
|
||
tags.append(tag)
|
||
tags += [d for d in ((found.get("견줌") or {}).get("덧") or []) if d not in tags]
|
||
return tags
|
||
|
||
|
||
def _state(efp: dict, code: str, found: dict | None, theirs: list) -> str:
|
||
lvl = 9 if (found is None or fit.single(efp)) else found["수준"]
|
||
if lvl <= 1:
|
||
return "맞음"
|
||
our = hopyo_basis.ours().get(code)
|
||
clause_ok = bool(our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9)
|
||
if lvl == 2 or (found and found.get("직종같음") and clause_ok):
|
||
return "다른 점"
|
||
return "후보"
|
||
|
||
|
||
def _no_numbers(entry: dict, top: list[str], why: str) -> dict[str, Any]:
|
||
entry["순위"] = [{"로직": k, "점수": None, "B": i} for i, k in enumerate(top[: fit.TOP_OPT])]
|
||
entry["차례"] = top[: fit.TOP_OPT]
|
||
entry["판정"] = {"상태": "후보", "글": why, "꼬리표": [why]}
|
||
return entry
|
||
|
||
|
||
def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dict[str, Any]:
|
||
"""블록(또는 `{_지문}` 직접 줄 호표) + 줄 → 맞춤셈 한 칸(설계 3-2)."""
|
||
if block_ and "_지문" in block_:
|
||
efp, text = block_["_지문"], []
|
||
else:
|
||
efp, text = finger.excel(block_, line), (block_ or {}).get("글") or []
|
||
border = hopyo_match.b_order(line, got, text)
|
||
entry: dict[str, Any] = {"지문": _public(efp), "품": excel_qty(efp), "B": border["상태"]}
|
||
entry.update(옵션={}, 조합=None)
|
||
if not fit.has_numbers(efp):
|
||
return _no_numbers(entry, border["top"], fit.NO_NUMBER)
|
||
if fit.prepared() is None:
|
||
return _no_numbers(entry, border["top"], "우리 로직 준비 중")
|
||
e = fit.ex_prep(efp)
|
||
pool = fit.rank(e)
|
||
btop = border["top"]
|
||
scores = {k: s for s, k, _, _ in pool}
|
||
fused = fit.fuse(e, pool, btop)
|
||
if border["상태"] == "같음": # 같음 1위 고정 — 지문 딱 맞음이 다른 로직이면 지문 1위
|
||
first = pool[0][1] if fit.sure(e, pool) and pool[0][1] != btop[0] else btop[0]
|
||
top = [first] + [k for k in fused if k != first][:4]
|
||
else:
|
||
top = fused
|
||
entry["지문차례"] = top[: fit.TOP_OPT] # 조합 · 딱 맞음 먼저 전(시험 지표)
|
||
parts = efp.get("몫") or []
|
||
if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저
|
||
top = btop[: fit.TOP_OPT]
|
||
top, combo = combine.solve(top, parts, line, got, border["합침"])
|
||
opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]}
|
||
for c in combo:
|
||
if c["로직"] not in opts and c["체크"]:
|
||
part = next((p for p in parts if p.get("제목") == c["어디"]), None)
|
||
pfp = _part_fp(part, c["몫"], line) if part else efp
|
||
opts[c["로직"]] = option.find(c["로직"], pfp, text) if pfp["인력"] else None
|
||
theirs = efp["출처"]
|
||
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
|
||
if (
|
||
not combo and exact and exact[0] != top[0]
|
||
): # 딱 맞음 먼저(K5) — 1위가 못 맞추면 위 다섯 딱 맞음
|
||
top = [exact[0]] + [k for k in top if k != exact[0]]
|
||
chans = {k: ch for _, k, ch, _ in pool}
|
||
entry["순위"] = [
|
||
{
|
||
"로직": k,
|
||
"점수": round(scores[k], 3) if k in scores else None,
|
||
"채널": {a: round(b, 3) for a, b in (chans.get(k) or {}).items() if b is not None},
|
||
"B": btop.index(k) if k in btop else None,
|
||
}
|
||
for k in top[: fit.TOP_OPT]
|
||
]
|
||
entry["차례"] = top[: fit.TOP_OPT]
|
||
entry["옵션"] = opts
|
||
entry["조합"] = combo or None
|
||
entry["판정"] = judge(efp, top, opts, theirs, got)
|
||
entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {})
|
||
return entry
|
||
|
||
|
||
def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dict[str, Any]:
|
||
"""1위 판정 {상태, 글, 꼬리표}."""
|
||
if not top:
|
||
return {"상태": "후보", "글": "후보 없음", "꼬리표": []}
|
||
names = {x["로직"]: x["이름"] for x in got["로직"]}
|
||
t1, found = top[0], opts.get(top[0]) or {}
|
||
state = _state(efp, t1, opts.get(t1), theirs)
|
||
exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"]
|
||
tags = []
|
||
if state == "맞음":
|
||
label = _label(t1, found.get("입력") or {})
|
||
text = " · ".join(x for x in ("맞음 — " + t1, names.get(t1, ""), label) if x)
|
||
if len(exact) > 1:
|
||
tags.append("맞음 둘")
|
||
if found.get("갈리는칸"):
|
||
tags.append(f"{' · '.join(found['갈리는칸'])} 못 가름")
|
||
return {"상태": state, "글": text, "꼬리표": tags}
|
||
if state == "다른 점":
|
||
tags = _diffs(efp, found)
|
||
return {"상태": state, "글": " · ".join(["다른 점 — " + t1, *tags[:2]]), "꼬리표": tags}
|
||
return {"상태": "후보", "글": f"후보 {len(top[: fit.TOP_OPT])}", "꼬리표": []}
|
||
|
||
|
||
def candidates(
|
||
efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict, why: dict
|
||
) -> list[dict[str, Any]]:
|
||
"""후보 다섯 — 로직마다 옵션 판정 꼬리표 · 「맞음 둘」이면 둘째 맞음이 맨 앞."""
|
||
by = {x["로직"]: x for x in got["로직"]}
|
||
table = hopyo_basis.ours()
|
||
out = []
|
||
for k in top[: fit.TOP_OPT]:
|
||
found = opts.get(k) or {}
|
||
state = _state(efp, k, opts.get(k), theirs)
|
||
our = table.get(k)
|
||
sc, clauses, tag = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "")
|
||
one = by.get(k) or {"로직": k, "이름": "", "단위": ""}
|
||
unit = hopyo_match.fits(efp["단위"], one["단위"])
|
||
basis = why.get(k) or {"조항": clauses, "직종": [], "단위": unit, "점수": sc, "까닭": tag}
|
||
text = state if state != "다른 점" else " · ".join(["다른 점", *_diffs(efp, found)[:1]])
|
||
out.append(
|
||
{
|
||
"로직": k,
|
||
"이름": one["이름"],
|
||
"단위": one["단위"],
|
||
"점수": round(scores[k], 3) if k in scores else None,
|
||
"판정": state,
|
||
"글": text,
|
||
"근거": basis,
|
||
}
|
||
)
|
||
first = [c for c in out[1:] if c["판정"] == "맞음"][:1]
|
||
if out and out[0]["판정"] == "맞음" and first:
|
||
out = [out[0], *first, *[c for c in out[1:] if c is not first[0]]]
|
||
return out
|