Files
Aislo/M02_MasterTemplete/M02_Doc_Match_Note.py
T
eomsangdonandClaude Opus 5.5 3d4b2ec1d1 feat(812-9): 가져온 기계 잇기 다시 — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 범위 규격 · 별칭 먼저 · 조합 기계 둘 · 조회어 기종만
- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음)
- 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부)
- 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비
- 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음
- 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」
- 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾)
- 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭)
- 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 21:02:29 +09:00

229 lines
10 KiB
Python

"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
율 구조(있고 없음 · 대상 비목 — 율 값 % 는 참고) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 율 값(%) · 그로 인한 비목 금액 · 계.
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
"""
from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Machine_Find as mfind
md = dc.md
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
# ── 율 구조 ───────────────────────────────────────────────────────────
def _names(text: Any) -> set[str]:
if text in (None, ""):
return set()
try:
return set(md.names(md.parse(text)))
except md.DocError:
return set()
def _reach(doc: dict) -> dict[str, set[str]]:
"""기호 → 바로 부르는 이름(변수 연결 · 식 줄 · 금액 줄 수량 · 단가)."""
direct: dict[str, set[str]] = {}
for v in doc.get("변수") or []:
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
for line in doc.get("식") or []:
direct[str(line.get("기호"))] = _names(line.get("식"))
for line in doc.get("금액") or []:
if line.get("기호"):
direct[str(line["기호"])] = _names(line.get("수량")) | _names(line.get("단가"))
return direct
def _close(start: set[str], direct: dict[str, set[str]]) -> set[str]:
seen, todo = set(start), list(start)
while todo:
for n in direct.get(todo.pop(), ()):
if n not in seen:
seen.add(n)
todo.append(n)
return seen
def _short(r: Decimal) -> Decimal:
return Decimal(f"{r:.6g}").normalize()
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`({(자원 갈래, 비목)})을 채움(제자리)."""
money, rows = doc.get("금액") or [], got.get("줄") or []
direct = _reach(doc)
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
for f in facts:
if f["갈래"] != "율":
continue
line, row = money[f["번"]], rows[f["번"]]
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
costs = reach & set(md.COSTS)
hits = [c for c in core if _hit(money[c["번"]], reach)]
if hits: # 줄 기호 · 줄 수량 · 단가 이름 = 그 줄 금액 전부
parts = [(c, tuple(c["비목"])) for c in hits]
elif costs: # 비목 이름(「노무비 × 4 %」) = 그 비목 몫만
parts = [(c, tuple(k for k in c["비목"] if k in costs)) for c in core]
else: # 번지 합 · 못 찾음 = 블록 핵심 줄 전부
parts = [(c, tuple(c["비목"])) for c in core]
parts = [(c, ks) for c, ks in parts if ks]
f["대상"] = frozenset((_cls(c["갈래"]), k) for c, ks in parts for k in ks)
f["율"] = None
amount = _mul(row.get("수량"), row.get("단가"))
if amount is None:
continue
bases = [sum((_num(c["비목"][k]) for c, ks in parts for k in ks), Decimal(0))]
if hits:
each = (_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) for c in hits)
bases.append(sum((x or 0 for x in each), Decimal(0)))
found = [_short(amount / b) for b in bases if b]
if found:
f["율"] = min(found, key=lambda r: len(r.as_tuple().digits))
def _hit(line: dict, reach: set[str]) -> bool:
parts = _names(line.get("수량")) | _names(line.get("단가"))
return bool(line.get("기호") and str(line["기호"]) in reach) or bool(parts and parts <= reach)
def _cls(kind: str) -> str:
return "기계" if kind == "산출" else kind # 기계경비 묶음(코드 없음) = 기계
def _num(v: Any) -> Decimal:
if isinstance(v, dict):
return sum((_num(v.get(k)) for k in md.COSTS), Decimal(0))
try:
return Decimal(str(v)) if v is not None else Decimal(0)
except InvalidOperation:
return Decimal(0)
def _mul(q: Any, p: Any) -> Decimal | None:
if q is None or p is None:
return None
return _num(q) * _num(p)
def pct(r: Any) -> str:
"""비율 → 「9 %」."""
if not isinstance(r, Decimal):
return "?"
return f"{(r * 100).normalize():f} %"
# ── 짝 줄 ─────────────────────────────────────────────────────────────
def txt(v: Any) -> str:
"""단가 글 — 수 = 원 단위 쉼표 · 기계 묶음 = 노 · 재 · 경."""
if isinstance(v, tuple):
return " · ".join(f"{k[0]} {txt(x)}" for k, x in zip(md.COSTS, v) if x)
if isinstance(v, Decimal):
return f"{v.normalize():,f}"
return str(v)
def _plain(text: str) -> str:
"""표기 정규화(소리 표기 · 빈칸) — 기계 찾기와 같은 규칙(812-9 `M02_Machine_Find.norm`)."""
return mfind.norm(text)
def _head(text: str) -> str:
"""기계 이름 머리(괄호 · 규격 앞 한글) — 「머캐덤로울러(자주식)」 → 「머캐덤롤러」."""
m = re.match(r"[가-힣]+", _plain(text))
return m.group(0) if m else ""
def _same_machine(a: str, b: str) -> bool:
"""같은 기종 — 기계표 · 별칭 장부로 둘 다 기종을 찾으면 그 판정(812-9 · 박힌 짝 없음) ·
못 찾으면 이름 머리 품음."""
got = mfind.same_kind(a, b)
if got is not None:
return got
x, y = _head(a), _head(b)
return bool(x and y) and (x in y or y in x)
def _grams(text: str) -> set[str]:
t = _plain(text)
return {run[i : i + 2] for run in re.findall(r"[가-힣]+", t) for i in range(len(run) - 1)}
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
"""수량 같은 짝 줄 → (다른 항, 참고) — 단가만 다르면 참고 · 자원 · 비목이 다르면 다른 항."""
diffs: list[dict] = []
kinds = {a["갈래"], b["갈래"]}
# 기계경비 숫자 묶음(코드 없음 · 원파일 값) ↔ 우리 기계 = 이름 종류로 같은 기계인지
loose = kinds == {"기계", "산출"} and not (a if a["갈래"] == "산출" else b)["코드"]
if a["갈래"] == b["갈래"] == "기계" and a["코드"] != b["코드"]:
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
elif loose and not _same_machine(a["이름"], b["이름"]):
diffs.append({"항": "기계 규격", "글": f"{a['이름']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
diffs.append({"항": "자원", "글": f"{a['이름']} ↔ {b['이름']}"})
machine = a["갈래"] == b["갈래"] == "기계" or loose # 시간당 단가 구성(운전 노무비 등) = 값
priced = a["비목"] and b["비목"] # 한쪽 단가 없음(0) = 값
if priced and not machine and set(a["비목"]) != set(b["비목"]):
diffs.append(
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
)
if a["단가"] == b["단가"]:
return diffs, []
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
return diffs, [{"항": "단가 차이", "글": say}]
return [*diffs, {"항": "산출 단가", "글": say}], []
def _same_stuff(a: dict, b: dict) -> bool:
"""자재 종류 같음 — 두 코드가 있으면 코드 · 없으면 이름 낱말(한글 두 글자) 겹침."""
if a["코드"] and b["코드"] and a["코드"] == b["코드"]:
return True
return bool(_grams(a["이름"]) & _grams(b["이름"]))
def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dict], list[dict]]:
"""율 줄 짝 → (다른 항, 참고) — 구조 = 대상 · 비목 · 같으면 로직 같음(율 값 % 다름 = 참고 · 값은 업데이트됨)."""
diffs: list[dict] = []
notes: list[dict] = []
def is_rate(a: dict, b: dict) -> bool:
return a["갈래"] == b["갈래"] == "율"
def same(a: dict, b: dict, *keys: str) -> bool:
return is_rate(a, b) and all(
(set(a[k]) == set(b[k])) if k == "비목" else a.get(k) == b.get(k) for k in keys
)
def name(a: dict, b: dict) -> str:
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
def target(x: dict) -> str:
return "·".join(f"{k} {c}" for k, c in sorted(x.get("대상") or ())) or "없음"
def value(a: dict, b: dict) -> None:
if a.get("율") != b.get("율"):
notes.append({"항": "율 값", "글": f"{name(a, b)} — 바꾸면 우리 값"})
take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 · 값 같음
for a, b in take(lambda a, b: same(a, b, "대상", "비목")): # 로직 같음 · 율 값만 다름
value(a, b)
for a, b in take(lambda a, b: same(a, b, "대상")):
diffs.append({"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"})
value(a, b)
for a, b in take(lambda a, b: same(a, b, "율")):
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
for a, b in take(is_rate):
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
return diffs, notes