From 560e2e09282359906a89b494a41f92e43a903c8a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 4 Nov 2026 08:49:46 +0900 Subject: [PATCH] =?UTF-8?q?fix(778-1):=20=EC=9E=A5=EB=B9=84=20=E2=86=92=20?= =?UTF-8?q?EQ=20=EA=B4=84=ED=98=B8=20=EB=82=B1=EB=A7=90=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=E2=80=94=20=EA=B5=B4=EC=B0=A9=EA=B8=B0(=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=ED=98=95)=20=3D=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EA=B8=B0=EA=B3=84=20=C2=B7=20=EB=B6=80=EC=86=8D=20?= =?UTF-8?q?=EB=BA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Elem: 괄호 낱말(타이어 · 무한궤도 · 습지 · 트럭탑재) 어긋남 · 부속(자동덮개 · 부착용 · 콤팩터 · 습지) 깎음 · narrow · machine(하나로 가름) - Fixed Linker._machine: 같은 갈래 · 규격 여럿 = 괄호 낱말로 가름 - SkPatch._add_machine: 기계 = 괄호 낱말로 가른 그 기계 · 기계경비 기초 = 그 기계(같은 이름)를 고르기에 둔 기초 먼저 - 여덟 파일 장비 줄 잇기 59 → 172 / 183(32 → 94 %) · 굴착기(타이어형) 42 줄 = 세 길 모두 타이어 · 기초 80 · SK 37 · 고름 대기 89 그대로 - 새 시험 test_778_1_hopyo_machine Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_Elem.py | 45 +++++++++++++++++-- M02_MasterTemplete/M02_Hopyo_Fixed.py | 7 ++- M02_MasterTemplete/M02_Hopyo_SkPatch.py | 32 ++++++++++++-- resources/tester/test_778_1_hopyo_machine.py | 46 ++++++++++++++++++++ 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 resources/tester/test_778_1_hopyo_machine.py diff --git a/M02_MasterTemplete/M02_Hopyo_Elem.py b/M02_MasterTemplete/M02_Hopyo_Elem.py index 86f2c99ce..bd64f52e6 100644 --- a/M02_MasterTemplete/M02_Hopyo_Elem.py +++ b/M02_MasterTemplete/M02_Hopyo_Elem.py @@ -4,7 +4,8 @@ 잇기 길 `{노임|기계|자재: 키}`). 자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위) 노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값 - 기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 단가 없음(기계경비는 로직 셈) + 기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 괄호 낱말 어긋남 깎음 · + 단가 없음(기계경비는 로직 셈) · `machine` = 하나로 가름(778-1) 차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 글 「이름 단가 × 식 = 금액」 · 모르면 점수만). 서버 단독 · 읽기만. """ @@ -19,10 +20,18 @@ from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Finger as finger from M02_MasterTemplete import M02_Hopyo_Match as hm from M02_MasterTemplete import M02_Hopyo_Material as hmat -from M02_MasterTemplete.M02_Hopyo_Words import mtypes, parse_rhs, specnums +from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, parse_rhs, specnums TOP = 3 POOL = 5 +# 괄호 낱말(778-1) — 글에 있으면 EQ 이름에도 있어야 함 · 부속 = 글에 없으면 본 장비 아님 +FORMS = ( + ("타이어", ("타이어",)), + ("무한", ("무한궤도",)), + ("습지", ("습지",)), + ("트럭탑재", ("트럭탑재",)), +) +PARTS = ("자동덮개", "부착용", "콤팩터", "습지") def prices(block: dict[str, Any] | None) -> dict[str, float]: @@ -90,8 +99,36 @@ def labor(name: str, want: float | None = None) -> list[dict[str, Any]]: return _order(rows, want) +def _form(text: str, name: Any) -> tuple[bool, bool]: + """괄호 낱말(778-1) — (글 형식 낱말이 EQ 이름에 다 있음, EQ 이름에 글에 없는 부속 있음).""" + t, n = nfkc(text), nfkc(name) + ok = all(any(w in n for w in ws) for k, ws in FORMS if k in t) + return ok, any(w in n and w not in t for w in PARTS) + + +def narrow(text: str, rows: list[tuple[str, Any]]) -> list[str]: + """EQ `[(키, 이름)]` → 괄호 낱말로 가른 키 — 형식 다름 뺌 · 부속은 남는 것 있을 때만 뺌.""" + keep = [(c, _form(text, n)[1]) for c, n in rows if _form(text, n)[0]] + return [c for c, part in keep if not part] or [c for c, _ in keep] + + +def machine(text: str, kinds: set | None = None, nums: set | None = None) -> dict | None: + """기계 글 → EQ 한 줄 — 갈래 겹침 + 규격 수 같음 · 괄호 낱말로 하나일 때만.""" + kinds = set(kinds or ()) or mtypes(text) + nums = set(nums or ()) or set(specnums(text)) + rows = { + str(r.get("키")): r + for r in m01.loaded()[1].numbers.get("EQ", {}).values() + if nums + and kinds & mtypes(f"{r.get('상세구분')} {r.get('이름')}") + and nums == set(specnums(r.get("규격"))) + } + got = narrow(text, [(k, r.get("이름")) for k, r in rows.items()]) + return rows[got[0]] if len(got) == 1 else None + + def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]: - """기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수가 없으면 글에서 읽음.""" + """기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수 없으면 글에서 읽음 · 괄호 낱말 어긋남 깎음.""" whole = m01.loaded()[1] kinds = set(kinds or ()) or mtypes(text) nums = set(nums or ()) or set(specnums(text)) @@ -105,6 +142,8 @@ def machines(text: str, kinds: set | None = None, nums: set | None = None) -> li score = len(kinds & types) / len(kinds | types) score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0 score += 0.3 * len(mine & hm.words(row.get("이름"))) + ok, part = _form(text, row.get("이름")) + score -= (0.0 if ok else 1.0) + (0.5 if part else 0.0) one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")} unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else "" rows.append( diff --git a/M02_MasterTemplete/M02_Hopyo_Fixed.py b/M02_MasterTemplete/M02_Hopyo_Fixed.py index f21f81580..b2a4ef76e 100644 --- a/M02_MasterTemplete/M02_Hopyo_Fixed.py +++ b/M02_MasterTemplete/M02_Hopyo_Fixed.py @@ -20,6 +20,7 @@ from decimal import Decimal from typing import Any from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Elem as elem from M02_MasterTemplete import M02_Hopyo_Finger as finger from M02_MasterTemplete import M02_Hopyo_Match as hm from M02_MasterTemplete import M02_Hopyo_Material as hmat @@ -86,6 +87,9 @@ class Linker: set(specnums(row.get("규격"))), mm_plain(row.get("이름")), mm_plain(row.get("규격"))) for row in self.whole.numbers.get("EQ", {}).values() ] # fmt: skip + self.eq_names = { + str(r.get("키")): r.get("이름") for r in self.whole.numbers.get("EQ", {}).values() + } self.near = 0 # 못 이은 이음 후보 수 def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]: @@ -103,13 +107,14 @@ class Linker: return out def _machine(self, line: dict[str, Any]) -> str | None: - """기계 줄 → EQ 하나 — 이름 · 규격 글 같음 › 갈래 겹침 + 규격 수 같음(하나일 때만).""" + """기계 줄 → EQ 하나 — 이름 · 규격 글 같음 › 갈래 + 규격 수 · 괄호 낱말(하나일 때만).""" name, spec = mm_plain(line.get("이름")), mm_plain(line.get("규격")) same = [c for c, _, _, n, s in self.machines if n == name and s == spec] if len(same) != 1: text = f"{line.get('이름') or ''} {line.get('규격') or ''}" kinds, nums = mtypes(text), set(specnums(text)) same = [c for c, t, s, _, _ in self.machines if kinds & t and nums and nums == s] + same = elem.narrow(text, [(c, self.eq_names.get(c)) for c in same]) if len(same) == 1: return same[0] self.near += bool(same) diff --git a/M02_MasterTemplete/M02_Hopyo_SkPatch.py b/M02_MasterTemplete/M02_Hopyo_SkPatch.py index 55f6a2fe5..bfc446977 100644 --- a/M02_MasterTemplete/M02_Hopyo_SkPatch.py +++ b/M02_MasterTemplete/M02_Hopyo_SkPatch.py @@ -54,10 +54,32 @@ LINK = { } # 잇기 칸 → 더한줄 · 키 머리 +MACH_IN = "기계" # 기계경비 기초 기계 입력 칸 + + class LinkError(ValueError): """잇기 틀림(없는 id · 갈래 다름 · 없는 키) — 부르는 쪽이 400.""" +def _mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]: + """기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면 + 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞(778-1).""" + first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY) + order = [first, *sorted(combine.MACHINE_SET - {first})] + if not eq: + return order + eqs = whole.numbers.get("EQ", {}) + + def takes(code: str) -> bool: + ins = whole.logic(code).get("입력") or [] + opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), []) + same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts) + return eq.get("원문번호") in opts or same + + hit = [c for c in order if takes(c)] + return hit + [c for c in order if c not in hit] + + def _near(a: float, b: float) -> bool: return abs(a - b) <= TINY or option.near(a, b, fit.TOL[0], option.ru(a)) @@ -293,16 +315,18 @@ class _Plan: self.note("장비", "·".join(sorted(m["갈래"])), excel, mine, 규격=spec, 단위="시간") def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None: - """엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기).""" + """엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기 · + 괄호 낱말로 하나면 그 기계 · 로직 = 그 기계를 고르기에 둔 기초 먼저(778-1)).""" fp = finger.empty(line) fp["장비"] = [{**m, "시간": 1.0, "밑시간": 1.0, "율": []}] kinds = set(m["갈래"]) - first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY) - for code in [first, *sorted(combine.MACHINE_SET - {first})]: + eq = elem.machine(str(m.get("글") or ""), kinds, set(m.get("규격") or [])) + for code in _mach_logics(self.whole, kinds, eq): got = option.find(code, fp, []) if got["수준"] <= 1: name = " ".join([*m["갈래"], *(f"{x:g}" for x in m.get("규격") or [])]) - self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name, + given = {**got["입력"], **({MACH_IN: eq["원문번호"]} if eq else {})} + self.add({"종류": "로직", "로직": code, "입력값": given, "이름": name, "단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip return spec = " ".join(f"{x:g}" for x in m.get("규격") or []) diff --git a/resources/tester/test_778_1_hopyo_machine.py b/resources/tester/test_778_1_hopyo_machine.py new file mode 100644 index 000000000..1932cf0e4 --- /dev/null +++ b/resources/tester/test_778_1_hopyo_machine.py @@ -0,0 +1,46 @@ +"""장비 → EQ 괄호 낱말(PLAN 778-1 · 분석 621-B 2-3) — 「굴착기(타이어형)」 = 타이어 기계 · 부속 뺌. + +같은 갈래 · 규격에 이름 여럿(타이어 · 무한궤도 · 습지 · 부착용)인 EQ 를 글의 괄호 낱말로 가름 — +불러오기 이음(`Linker._machine`) · 후보 1위(`Elem.machines`) · +엑셀만 장비 기계경비 기초(`SkPatch._add_machine`) 모두 같은 기계. master 읽기만. +""" + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Combine as combine +from M02_MasterTemplete import M02_Hopyo_Elem as elem +from M02_MasterTemplete import M02_Hopyo_Fixed as hfix +from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch + +TIRE = "굴착기(타이어형)(0.6㎥)" + + +def _key(code: str) -> str: + """기계 번호(0211-0060) → EQ 키.""" + return str(m01.loaded()[1].numbers["EQ"][code]["키"]) + + +def test_굴착기_타이어형_EQ000026_세_길_같음(): + linker = hfix.Linker(by_name=True) + assert linker._machine({"이름": "굴착기(타이어형)", "규격": "0.6㎥"}) == "EQ000026" + assert elem.machines(TIRE)[0]["키"] == "EQ000026" + assert elem.machine(TIRE)["키"] == "EQ000026" + # 부속 · 형식 낱말 — 덤프트럭 ≠ 자동덮개 · 굴착기 0.7 ≠ 습지 · 콤팩터(부착용) · 무한궤도 그대로 + assert linker._machine({"이름": "덤프트럭", "규격": "15톤"}) == "EQ000074" + assert linker._machine({"이름": "굴착기", "규격": "0.7㎥"}) == "EQ000020" + assert linker._machine({"이름": "굴착기(무한궤도)", "규격": "0.6㎥"}) == "EQ000019" + assert linker._machine({"이름": "굴착기", "규격": "0.6㎥"}) is None # 타이어 · 무한궤도 못 가름 + + class Fake: + tb, whole, got = 2.0, m01.loaded()[1], None + + def add(self, one): + self.got = one + + def miss(self, *a, **k): + raise AssertionError("기계 못 찾음") + + m = {"갈래": ["굴착기"], "규격": [0.6], "시간": 0.25, "글": TIRE} + fake = Fake() + skpatch._Plan._add_machine(fake, m, {"이름": "x", "단위": "㎥"}) + assert fake.got["로직"] != combine.MACHINE["굴착기"] # 굴착기(무한궤도) 기계경비 아님 + assert _key(fake.got["입력값"]["기계"]) == "EQ000026" and fake.got["수량"] == 0.5