Files
Aislo/M02_MasterTemplete/M02_Hopyo_Words.py
eomsangdonandClaude Opus 5.5 48848bb287 refactor(785-7): 옛 맞춤 층 Python 지움 · 서버 새 길로 정리 · 낡은 시험 정리
- 지움(git rm) 10 파일 3,374 줄: M02_Hopyo_Finger · Ours · Option · Fit · Combine · Verdict · Line · SkLink · SkPatch · Basis
- Match: 흐름 몫(candidates · name_cands · fix_typo · clause · b_order · _logic_specs · _line_mod · units_of · _state · view · guess · picks · chain) 지움 · 이름 도구 몫 · line_key 남김
- Words: conds · cond_score · close · cosine · jacc 지움 / Shares: sections 지움(trade · eq_key 남김)
- 옮김: Finger.kind_of → Fixed.kind_of · SkLink(sk_line · kept · left · LEFT · WHY) → Sangeun · SkPatch 기계 몫(MACH_IN · MACHINE · SOLO · _mach_logics) → Units.mach_logics
- Sangeun: 옛 /sangeun 몸(build · _sure · Maker.basis · judge · plan · make) 지움 · Kids: first · proposal · order 지움 · Build: build() · unlinked() 지움(모두 고정 Builder · build_all · _order · _origins 남김)
- Router: 옛 길 지움(GET hopyo/{키}/{번} · guess · hopyo-picks · hopyo-build · hopyo-unlinked · hopyo/{키}/sangeun) · GET hopyo/{키} = Judge.view(1차 줄 판정 줄상태 · 만든 · 블록 풀이 셈)
- Marks: 칠 = Judge.judged 만 · Forget: 맞춤 캐시 지움 줄 걷음 · Ledger: Ours.version 판 칸 걷음 · Item: Kids.first 걷음
- Row(덧): 단산 줄 = 장부 짝이 있어도 늘 「2차 필요」(장부 = 1차 후보 힌트만)

시험 정리(까닭 = 지운 길 · 모듈만 시험함):
- 지움 11 파일: test_m02_hopyo_finger · fit · golden · option · ours · basis · seam · flow · draft_calc · test_635 · test_628(지문 · 순위 · 옵션 거꾸로 · 맡은 절 · 옛 확인 흐름)
- 함수 지움: /sangeun · /hopyo-build · /hopyo-unlinked · hopyo-picks · 옛 줄 맞춤 보기를 부르던 것(572 · 718_1 · 729 · 739_1 · 777_1 · 778_5 · 780_1 · labor_national · 705_3 · m02_hopyo_match · 746_1)
- test_621 S2(지문 해시) 지움 · S1 남김 · test_m02_hopyo_build = 장부 길 시험만(PUT 으로 심음)
- 새 길로: 779_2(줄상태 = 1차 판정 · 칠 셈 전 None) · grid · stmate(hopyo-judge 줄 판) · 778_1 · 778_2(Units.mach_logics) · 642_6(Marks 원문) · 642_3 장부 짝 단산 줄 2차 지킴

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:15:20 +09:00

158 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 일위대가 맞춤 낱말 · 식 도우미(PLAN 338-1 · 설계 2-2) — 엑셀 지문 · 우리 지문이 같이 씀.
장비 갈래(`mtypes`) · 규격 수(`specnums`) · 조건 낱말(`conds` · `cond_score`) ·
식 풀기(`parse_rhs` — 「단가 × 식 = 금액」 → 품 · 맨 앞 단가는 뗌 · 금액 안 씀) · 닮음(`close` ·
`cosine` ·
`jacc`). 값 · 문턱은 336 실험(C4 `fp_common`) 그대로.
"""
from __future__ import annotations
import re
import unicodedata
from typing import Any
# ── 장비 갈래(엑셀 글 · 우리 EQ 상세구분 공통) ─────────────────────────────
MACH = (
("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe", "포크레인")),
("덤프트럭", ("덤프",)),
("크레인", ("크레인",)),
("브레이커", ("브레이커", "브레이카")),
("콘크리트펌프", ("펌프차", "펌프카", "콘크리트펌프", "콘크리트 펌프")),
("롤러", ("롤러", "로울러")),
("불도저", ("불도저", "불도우저")),
("집게", ("집게",)),
("살수차", ("살수", "물탱크")),
("모터그레이더", ("그레이더",)),
("래머", ("래머", "램머", "래 머")),
("콤팩터", ("콤팩터", "컴팩터")),
("진동기", ("진동기",)),
("발전기", ("발전기",)),
("공기압축기", ("공기압축기", "컴프레셔", "콤프레샤")),
("드릴", ("드릴", "천공기")),
("커터", ("커터",)),
("화물차", ("화물자동차", "화물차", "카고")),
("로더", ("로더",)),
("지게차", ("지게차",)),
("체인톱", ("체인톱",)),
("트랙터", ("트랙터",)),
("믹서", ("믹서",)),
("분무기", ("분무기",)),
)
_SPECNUM = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|m\b|㎾|kw|ℓ|mm|hp)", re.I)
def nfkc(text: Any) -> str:
return unicodedata.normalize("NFKC", str(text or ""))
def mtypes(text: Any) -> set[str]:
t = nfkc(text).lower()
return {k for k, ws in MACH if any(w in t for w in ws)}
def specnums(text: Any) -> set[float]:
return {float(a) for a, _ in _SPECNUM.findall(nfkc(text))}
# ── 식 풀기 — 「단가 × 식 = 금액」 → 품(단가로 나눈 몫 · 단가는 버림) ─────────
_UNITFRAC = re.compile(
r"(인|개|본|공|EA|ea|hr|ton|Ton|TON|kg|㎥|㎡|m3|m2|m|M|조|회)\s*/\s*"
r"(EA|ea|㎥|㎡|m3|m2|m|M|ton|Ton|TON|t|hr|일|개|본|조|공㎥|㎥)"
)
_UNITTAIL = re.compile(
r"(?<=[\d.)])\s*(hr|HR|EA|ea|kg|km|ton|Ton|TON|sec|m3|m2|mm|cm|M|m|t)(?![A-Za-z])"
)
_NOTE = re.compile(r"\(([^()]*[가-힣][^()]*)\)")
_LEAD = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*([*/])\s*(.+)$")
_SAFE = re.compile(r"^[\d.+\-*/() ]+$")
def _norm(s: str) -> str:
s = nfkc(s).replace("×", "*").replace("✕", "*").replace("÷", "/")
s = s.replace("x", "*").replace("X", "*")
s = re.sub(r"(?<=\d),(?=\d{3})", "", s)
return _UNITFRAC.sub("", s)
def _split(rest: str, first_op: str) -> list[tuple[str, str]]:
"""맨 바깥 * / 로 쪼갬 → [(연산, 조각)]."""
out, depth, cur, op = [], 0, "", first_op
for ch in rest:
depth += (ch == "(") - (ch == ")")
if depth == 0 and ch in "*/":
out.append((op, cur))
op, cur = ch, ""
continue
cur += ch
out.append((op, cur))
return out
def _factor(text: str) -> tuple[float, bool, str] | None:
"""조각 → (값, 율 표시, 메모) · 변수(P · Q …)면 None."""
notes = _NOTE.findall(text)
t = _NOTE.sub("", text)
pct = "%" in t
if re.search(r"\d\s*회", t): # 「× 2회」 — 횟수 곱(율로 가름)
notes.append("회")
t = re.sub(r"[가-힣㎥㎡㎝㎜℃˚°'\"]", "", t).replace("%", "")
t = _UNITTAIL.sub("", t).strip()
if not t or re.search(r"[A-Za-z]", t) or not _SAFE.match(t):
return None
try:
v = eval(t, {"__builtins__": {}}, {}) # noqa: S307 — 숫자 · 연산 글만(_SAFE)
except Exception: # noqa: BLE001 — 0 나눔 · 괄호 짝 틀림
return None
return float(v) / (100 if pct else 1), pct or bool(notes), " ".join(notes)
def _rates_any(parts: list[str]) -> list[tuple[float, str]]:
"""모든 조각의 율(메모 · %) — 풀 수 있는 조각만."""
out: list[tuple[float, str]] = []
for seg in parts:
m = _LEAD.match(seg)
if not m:
continue
for op, piece in _split(m.group(3), m.group(2)):
f = _factor(piece)
if f and f[1] and f[0] and all(abs(f[0] - v) > 1e-9 for v, _ in out):
out.append((f[0] if op == "*" else 1 / f[0], f[2] or "%"))
return out
def parse_rhs(rhs: str) -> dict[str, Any] | None:
"""「단가 × 식 [= 바꿔 쓴 식] = 금액」 → {단가, 품, 밑품, 율, 인자} · 못 읽으면 None."""
parts = _norm(rhs).split("=")
if len(parts) < 2:
return None
every = _rates_any(parts[:-1])
for seg in parts[:-1]:
m = _LEAD.match(seg)
if not m:
continue
facs = []
for op, piece in _split(m.group(3), m.group(2)):
f = _factor(piece)
if f is None:
break
facs.append((op, *f))
else:
if not facs or any(v == 0 and op == "/" for op, v, _, _ in facs):
continue
full = base = 1.0
rates = []
for op, v, rate, note in facs:
full = full * v if op == "*" else full / v
if rate:
rates.append([round(v if op == "*" else 1 / v, 6), note or "%"])
else:
base = base * v if op == "*" else base / v
if not rates and every: # 바꿔 쓴 식엔 메모가 빠짐 — 앞 조각의 율로 밑품
for v, _ in every:
base /= v or 1
rates = [[round(v, 6), note] for v, note in every]
plain = [v if op == "*" else 1 / v for op, v, rate, _ in facs if not rate and v]
return {"단가": float(m.group(1)), "품": full, "밑품": base, "율": rates, "인자": plain}
return None