Files
Aislo/M02_MasterTemplete/M02_Hopyo_Match.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

287 lines
12 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 324-2 · 338 · 785-7) — 엑셀 구성 줄 · 우리 로직 · 자재 이름 정리 · 목록.
이름 정리(`tidy` · `words` · `near`) · 규격 수(`numbers` · `spec_inputs`) · 환산(`factor` = `M02_Hopyo_Row.factor`) ·
우리 목록(`targets` — 돈 로직 · 자재품목 · 씨앗) · 이름 같음(`_exact`) · B09 줄 열쇠(`line_key`).
규격 → 입력값 = 고르기 글이 규격(품명 괄호 글 포함)에 있으면 그 값(기계 = 고르기이름) ·
이름 붙은 수 D= H= H: B= T= L= L3= 는 입력 단위로 · 못 읽으면 빈 값(자동값) + 표시.
M01 은 읽기만(로직 별칭 안 씀). 옛 맞춤 흐름(후보 순위 · 근거 차례 · 보기 · 미리 셈)은 785-7 에서 지움.
"""
from __future__ import annotations
import json
import re
import unicodedata
from decimal import Decimal
from functools import lru_cache
from pathlib import Path
from typing import Any
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
mf = m01.cm.mf
mp = mf.mp
mm = mp.mm
SEED_NAME = "일위대가조합.json"
LOGIC_KINDS = ("단가산출", "기계", "기타")
MATERIAL_KINDS = ("자재", "별도계상")
MACH_WORDS = (
("불도우저", "불도저"),
("굴삭기", "굴착기"),
("브레이카", "브레이커"),
("로울러", "롤러"),
("크로울러", "크롤러"),
)
_SEED_TAIL = re.compile(r"\s*\(실무 수는 옛 판 값\)\s*$")
_PAREN = re.compile(r"\([^()]*\)")
_THOUSANDS = re.compile(r"(=\d{1,3}),(\d{3})(?!\d)")
_NAMED = re.compile(r"(?<![A-Za-z])([A-Za-z]\d?)\s*[=:]\s*(\d+(?:\.\d+)?)\s*(mm|cm|m)?", re.I)
# 이름 붙은 수 → 입력 이름 낱말(긴 낱말이 앞섬 · 「뒷길이」 = L3)
LETTERS = {
"D": ("관경", "직경", "구경", "지름"),
"H": ("높이",),
"B": ("폭", "너비"),
"T": ("두께",),
"L": ("거리", "길이"),
"L3": ("뒷길이",),
}
# ── 정규화 ───────────────────────────────────────────────────────────
def line_key(line: dict[str, Any]) -> str:
"""기억 열쇠 = 품명 | 규격 | 단위(정규화)."""
return "|".join(
(
mm.plain(line.get("이름")),
mm.plain_spec(line.get("규격")),
mm.plain_unit(line.get("단위")),
)
)
def tidy(name: Any) -> str:
"""정리 이름 — 괄호 글 · 기계 옛 낱말 · 빈칸."""
text = str(name or "")
for old, new in MACH_WORDS:
text = text.replace(old, new)
return mm.plain(_PAREN.sub("", text))
def words(name: Any) -> frozenset[str]:
"""뜻 낱말 — 괄호 밖 한글 덩어리 · 영문 두 글자 넘는 것
(낱글자 덩어리는 붙임 「면 목」 = 면목)."""
text = unicodedata.normalize("NFKC", str(name or "")).lower()
for old, new in MACH_WORDS:
text = text.replace(old, new)
out, single = [], ""
for piece in re.findall(r"[가-힣]+|[a-z]{2,}", _PAREN.sub(" ", text)):
if len(piece) == 1:
single += piece
continue
out += [single, piece]
single = ""
return frozenset(w for w in [*out, single] if len(w) > 1)
def near(mine: frozenset[str], theirs: frozenset[str]) -> bool:
"""뜻 낱말이 하나라도 겹침(한쪽이 다른 쪽에 듦) — 내 낱말이 없으면 거르지 않음."""
return not mine or any(a in b or b in a for a in mine for b in theirs)
def _spec_text(text: Any) -> str:
out = str(text or "")
for old, new in MACH_WORDS:
out = out.replace(old, new)
return mm.plain_spec(out).replace(",", "").replace("~", "∼")
def numbers(spec: Any) -> dict[str, tuple[Decimal, str | None]]:
"""규격의 이름 붙은 수 `{글자: (값, 단위)}` — 천 단위 쉼표(`D=1,000mm`)를 먼저 붙임."""
text = _THOUSANDS.sub(r"\1\2", unicodedata.normalize("NFKC", str(spec or "")))
return {
letter.upper(): (Decimal(value), unit.lower() if unit else None)
for letter, value, unit in _NAMED.findall(text)
}
def _in_unit(value: Decimal, have: str | None, want: Any) -> Decimal:
"""길이 값을 입력 단위로 — 한쪽이 모르는 단위면 그대로."""
scale = mp.UNITS["길이"]
want = mm.plain_unit(want) if want else None
if have in scale and want in scale:
return value * Decimal(scale[have]) / Decimal(scale[want])
return value
def _json(value: Decimal) -> int | float:
value = value.normalize()
return int(value) if value == value.to_integral_value() else float(value)
def _meters(found: tuple[Decimal, str | None] | None) -> Decimal | None:
if found is None:
return None
return _in_unit(found[0], found[1] or "m", "m")
# ── 규격 → 입력값 ─────────────────────────────────────────────────────
def spec_inputs(line: dict[str, Any], specs: list[dict[str, Any]]) -> tuple[dict, list[str]]:
"""(입력값, 못 읽은 입력 이름) — 고르기 글 · 고르기이름 · 이름 붙은 수 · 두 단위."""
raw = f"{line.get('규격') or ''} {' '.join(_PAREN.findall(str(line.get('이름'))))}"
pool = _spec_text(raw)
named = numbers(line.get("규격"))
tokens = [_spec_text(w) for w in re.split(r"[,\s/()]+", raw)]
unit = mm.plain_unit(line.get("단위"))
values, unread = {}, []
for spec in specs:
name = str(spec.get("이름") or "")
picks = spec.get("고르기") or []
labels = spec.get("고르기이름") or {}
words = [(w, letter) for letter, ws in LETTERS.items() for w in ws if w in name]
letter = max(words, key=lambda x: len(x[0]))[1] if words else None
got: Any = None
if letter == "L" and "거리" in name and named.get("L", (0, None))[1] in ("cm", "mm"):
letter = None # 「L=35cm」 = 부재 길이 — 운반 · 거리(m) 칸 아님
if letter in named:
value = _json(_in_unit(*named[letter], spec.get("단위")))
got = value if not picks or value in picks else None
if got is None and picks:
hits = []
for opt in picks:
label = str(labels.get(opt) or opt)
for text in (label, _PAREN.sub("", label)):
flat = _spec_text(text)
if (
_NUM.fullmatch(flat) and len(flat) > 1
): # 수 고르기 = 제 단위가 붙은(또는 맨) 수만
hits += (
[(len(flat), opt)]
if any(_num_in(flat, w, spec) for w in tokens)
else []
)
elif len(flat) > 1 and (flat in pool or (unit and flat == f"{unit}당")):
hits.append((len(flat), opt))
# 규격 낱말이 고르기 글 머리(「4회」 → 「4회사용시」)
hits += [(len(w), opt) for w in tokens if len(w) > 1 and flat.startswith(w)]
got = max(hits, key=lambda x: x[0])[1] if hits else None
if got is not None:
values[name] = got
elif spec.get("자동값") in (None, ""):
unread.append(name)
return values, unread
_NUM = re.compile(r"\d+(?:\.\d+)?")
def _num_in(flat: str, pool: str, spec: dict[str, Any]) -> bool:
"""규격 글에 수 `flat` 이 따로 서 있고 뒤에 다른 단위가 안 붙음(「35cm」 의 35 ≠ 경사 35°)."""
own = _spec_text(spec.get("단위"))
for m in re.finditer(rf"(?<![\d.]){re.escape(flat)}(?![\d.])", pool):
after = re.match(r"[a-z㎜㎝㎞㎡㎥°%]+", pool[m.end() :])
if not after or (own and after.group().startswith(own)):
return True
return False
# ── 환산 ─────────────────────────────────────────────────────────────
def factor(have: Any, want: Any, spec: Any = "") -> dict[str, Any]:
"""`{배, 갈래}` — `M02_Hopyo_Row.factor` 로 옮김(785-3 · 옛 길이 부름)."""
from M02_MasterTemplete import M02_Hopyo_Row as hopyo_row # Row 가 Match 를 부름 — 늦게 읽음
return hopyo_row.factor(have, want, spec)
def fits(have: Any, want: Any) -> bool:
"""단위가 맞음 — 같음 · 같은 성격 · 두 단위 중 하나(후보 점수 덤)."""
units = {mm.plain_unit(u) for u in str(want or "").split("·") if u.strip()}
return mm.plain_unit(have) in units or mp.same_kind(have, want)
# ── 우리 쪽 목록(한 번 읽어 둠) ─────────────────────────────────────────
def _grams(text: str) -> set[str]:
return {text[i : i + 2] for i in range(len(text) - 1)} or {text}
@lru_cache(maxsize=2)
def _targets(whole_id: int) -> dict[str, Any]:
"""로직(돈) · 자재품목 — 정규 이름 · 정리 이름 · 묶음 · 단위."""
files, whole = m01.loaded()
rows = [r for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄") or []]
pills = combo_store.combo_pills({"담은로직": [{"로직": r.get("키")} for r in rows]})["물량"]
unit_of = {code: pill["단위"] for pill in pills for code in pill["로직"]} # 돈 로직만
logics, mats = [], []
for row in rows:
if str(row.get("키")) in unit_of:
name = str(row.get("이름") or "")
logics.append(
{
"로직": str(row["키"]),
"이름": name,
"단위": unit_of[str(row["키"])],
"산림": "산림" in str(row.get("구분") or ""),
"plain": mm.plain(name),
"tidy": tidy(name),
}
)
for key, row in whole.index.get(mm.MARKET, {}).items():
name = str(row.get("이름") or "")
mats.append(
{
"자재": str(key),
"이름": name,
"규격": str(row.get("규격") or ""),
"단위": str(row.get("단위") or ""),
"plain": mm.plain(name),
"spec": mm.plain_spec(row.get("규격")),
"base": (base := re.sub(r"-(사급|관급)$", "", name.strip())),
"tidy": tidy(base),
}
)
names: dict[str, dict[str, list]] = {"로직": {}, "자재": {}}
for pool, group in (("로직", logics), ("자재", mats)):
for one in group:
one["grams"] = _grams(one["tidy"])
one["words"] = words(one["base"] if "base" in one else one["이름"])
names[pool].setdefault(one["plain"], []).append(one)
return {"로직": logics, "자재": mats, "이름": names, "씨앗": _seeds()}
def targets() -> dict[str, Any]:
return _targets(id(m01.loaded()[1]))
def _seeds() -> dict[str, set[str]]:
"""씨앗 `{정규 실무 글: {로직 키}}` — 일위대가조합.json 담은로직 메모(읽기만)."""
try:
doc = json.loads((Path(m01.FOLDER) / SEED_NAME).read_bytes())
except (OSError, ValueError):
return {}
out: dict[str, set[str]] = {}
for row in doc.get("줄") or []:
for one in row.get("담은로직") or []:
memo = _SEED_TAIL.sub("", str(one.get("메모") or ""))
if memo and one.get("로직"):
out.setdefault(mm.plain(memo), set()).add(str(one["로직"]))
return out
def _public(one: dict[str, Any]) -> dict[str, Any]:
keep = ("로직", "자재", "전국", "이름", "규격", "단위")
return {k: one[k] for k in keep if k in one}
def _exact(line: dict[str, Any], pool: str, got: dict[str, Any]) -> list[dict[str, Any]]:
"""② 이름 같음 · 씨앗 — 겹치면 여럿(부르는 쪽이 후보로 돌림)."""
name, spec = mm.plain(line.get("이름")), mm.plain_spec(line.get("규격"))
same = got["이름"][pool].get(name) or []
if pool == "자재":
return [m for m in same if m["spec"] == spec] or (same if len(same) == 1 else [])
if same:
return same
codes = got["씨앗"].get(mm.plain(f"{line.get('이름')}{line.get('규격')}")) or got["씨앗"].get(
name
)
return [x for x in got["로직"] if x["로직"] in (codes or ())]