- 지움(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>
158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
"""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
|