diff --git a/M02_MasterTemplete/M02_Doc_Match.py b/M02_MasterTemplete/M02_Doc_Match.py index fbddf1bad..974901cbc 100644 --- a/M02_MasterTemplete/M02_Doc_Match.py +++ b/M02_MasterTemplete/M02_Doc_Match.py @@ -472,6 +472,7 @@ def _totals(got: dict) -> dict: EXPLAINED = ( + "이름 다름", "단가 차이", "단위 환산", "기계 규격", @@ -516,19 +517,21 @@ def _calc(doc: dict, links: Any, given: dict) -> dict | None: def try_doc( base: dict, links: Any, imported: dict, got_imp: dict, mine: list[dict], regions: set[str] ) -> dict: - """M01 문서 하나 — 옵션 풀이 · 가장 나은 조합 판정.""" + """M01 문서 하나 — 옵션 풀이 · 가장 나은 조합 판정(이름 · 절 근거 없으면 「이름 다름」 = 확정 아님).""" want = { "수량": {x["수량"] for x in mine if x["수량"] is not None}, "단가": {x["단가"] for x in mine if x["단가"] is not None}, } dom, ev = domains(base, imported, regions) dom = narrow(base, links, dom, want, ev) + named = [] if same_name(imported, base) else [_unnamed(imported, base)] best: dict | None = None for pick in combos(base, dom) or [{}]: got = _calc(base, links, _given(base, pick)) if got is None: continue off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]] + off += named res = verdict(mine, facts(base, got), got_imp, got, off) rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"])) if best is None or rank > best["순위"]: @@ -577,17 +580,43 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str, } -def _shown(doc: dict, pick: dict) -> dict[str, str]: - """고른 옵션(기본과 다른 것) — {열쇠: 값 글}.""" +def _shown(doc: dict, pick: dict) -> dict[str, Any]: + """고른 옵션(기본과 다른 것) — {열쇠: 값}(수 = 수 · 선택 목록의 수 값과 같은 꼴 · SK 입력값).""" out = {} for v in doc.get("변수") or []: vid = md.var_id(v) if vid in pick and mc.plain_text(mc.number(pick[vid])) != mc.plain_text( mc.number((v.get("연결") or {}).get("값")) ): - out[str(v.get("열쇠") or vid)] = mc.plain_text(mc.number(pick[vid])) + out[str(v.get("열쇠") or vid)] = ml._norm(mc.number(pick[vid])) return out +_STOP = {"설치", "해체", "인력", "기계", "작업", "운반", "타설", "제작"} + + +def _grams(texts: list[Any]) -> set[str]: + """이름 낱말 — 한글 두 글자 묶음 · 영문 · 수 낱말(일반 낱말 뺌).""" + out: set[str] = set() + for t in texts: + t = re.sub(r"\s|및", "", str(t or "")) + for run in re.findall(r"[가-힣]+", t): + out |= {run[i : i + 2] for i in range(len(run) - 1)} + out |= {w.lower() for w in re.findall(r"[A-Za-z]{2,}", t)} + return out - _STOP + + +def same_name(imported: dict, base: dict) -> bool: + """뜻 근거 — 품셈 절 번호가 같거나 이름 낱말이 겹침(값만 같은 남의 기초를 확정으로 안 묶음).""" + secs = sections([x.get("글") for x in (imported.get("가져온") or {}).get("추정") or []]) + if secs & sections([base.get("출처")]): + return True + return bool(_grams([imported.get("이름")]) & _grams([base.get("이름")])) + + +def _unnamed(imported: dict, base: dict) -> dict: + return {"항": "이름 다름", "글": f"{imported.get('이름')} ↔ {base.get('이름')}"} + + def _plain(d: dict) -> dict: return {k: (None if v is None else float(v)) for k, v in d.items()} diff --git a/docs/raw/요구_1104/_목록_810-4_연결판정.xlsx b/docs/raw/요구_1104/_목록_810-4_연결판정.xlsx new file mode 100644 index 000000000..52aa19e20 Binary files /dev/null and b/docs/raw/요구_1104/_목록_810-4_연결판정.xlsx differ diff --git a/resources/tester/helper_810_4_sweep.py b/resources/tester/helper_810_4_sweep.py index 049900ebf..36ee191aa 100644 --- a/resources/tester/helper_810_4_sweep.py +++ b/resources/tester/helper_810_4_sweep.py @@ -9,6 +9,7 @@ from __future__ import annotations import collections import json +import os import pickle import sys import time @@ -70,15 +71,22 @@ def file_docs(t, dim, dm, whole, kind: str, tag: str) -> dict[int, dict]: return made -def sweep(arg: tuple[str, str]) -> list[dict]: - kind, tag = arg +PARTS = 10 # 파일마다 블록을 나눠 맡김(큰 파일 한 프로세스가 끝을 붙잡지 않게) + + +def sweep(arg: tuple[str, str, int]) -> list[dict]: + kind, tag, part = arg t, dim, dm, whole, docs, index, links = _ctx() rows = [] try: made = file_docs(t, dim, dm, whole, kind, tag) except Exception as e: # noqa: BLE001 — 원파일 없음(다른 PC) = 빈 줄 + if part: + return [] return [{"파일": tag, "오류": repr(e)[:200], "판정": "오류", "없는파일": "없음" in repr(e)}] for no, doc in made.items(): + if no % PARTS != part: + continue start = time.time() try: res = dm.match(doc, dim.Links(whole, "우리"), index, links) @@ -101,7 +109,8 @@ def main() -> None: import test_810_1_stc as c tags = [("EST", k) for k in e.FILES] + [("STC", k) for k in c.FILES] - with Pool(len(tags)) as pool: + tags = [(kind, tag, part) for kind, tag in tags for part in range(PARTS)] + with Pool(min(len(tags), max(2, (os.cpu_count() or 4) - 2))) as pool: rows = [r for part in pool.map(sweep, tags) for r in part] if "--rows" in sys.argv: pickle.dump(rows, open(sys.argv[sys.argv.index("--rows") + 1], "wb")) diff --git a/resources/tester/test_810_4_match.py b/resources/tester/test_810_4_match.py index 0760de919..bb2bed4fc 100644 --- a/resources/tester/test_810_4_match.py +++ b/resources/tester/test_810_4_match.py @@ -9,7 +9,7 @@ (d) 바꾸기 — 임시 층 개인: 확정 SK = 같은 키에 M01 사본(바탕 · 입력값) · 금액 = M01 문서 셈 · 원래 몸 보관 → 되돌리기 = 원래 금액 · 근접 SK 바꾸기 = 409 · M01 문서 파일 그대로 (e) 전수(1,518 문서 × 원파일 산근 2,220 블록 · 파일마다 프로세스) — 확정은 모두 다시 셈해도 금액 같음 · - 판정 셋 · 근접 다른 항 갈래 표(S8104_ALL=1 일 때만 · 10 분 남짓) + 판정 셋 · 근접 다른 항 갈래 표(S8104_ALL=1 일 때만 · 수십 분 · S8104_ROWS= = 줄 목록 저장) 원파일 없으면 그 시험만 skip(다른 PC). """ @@ -75,7 +75,7 @@ def test_near_formwork_lists_differences() -> None: def test_req704_pipe_is_near_with_unit_rate_machine() -> None: got = judge(19) # 파형강관 매설 Φ800 ↔ GF000179(요구 704) - assert got["판정"] == dm.NEAR and got["키"] == "GF000179" and got["옵션"] == {"관경": "800"} + assert got["판정"] == dm.NEAR and got["키"] == "GF000179" and got["옵션"] == {"관경": 800} kinds = [d["항"] for d in got["다른"]] assert kinds.count("단위 환산") == 2 and "기계 규격" in kinds and "율 줄" in kinds assert any("× 6" in d["글"] for d in got["다른"]) @@ -125,6 +125,12 @@ def test_compare_kinds_and_no_tolerance() -> None: rate = {**_line("율", "율", "1", "3", "경비"), "수량": None, "단가": None} res = dm.verdict([*a, rate], a, tot, tot) assert res["다른"] == [{"항": "율 줄", "글": "가져온 쪽만 — 율"}] and res["판정"] == dm.NEAR + named = dm.verdict( + a, list(a), tot, tot, [dm._unnamed({"이름": "구조물되메우기"}, {"이름": "숲가꾸기"})] + ) + assert named["판정"] == dm.NEAR # 값만 같은 남의 기초 = 확정 아님 + assert not dm.same_name({"이름": "모르타르"}, {"이름": "기초다짐 및 뒷채움(인력)"}) + assert dm.same_name({"이름": "유로폼 설치및해체"}, {"이름": "유로폼 설치 및 해체"}) c = [_line("딴", "노임", "3", "7", code="LB9")] assert dm.verdict(a, c, tot, tot)["판정"] == dm.NONE @@ -180,8 +186,9 @@ def test_every_block_table() -> None: import collections import subprocess + rows = ["--rows", os.environ["S8104_ROWS"]] if os.environ.get("S8104_ROWS") else [] out = subprocess.run( - [sys.executable, str(HERE / "helper_810_4_sweep.py")], capture_output=True, text=True + [sys.executable, str(HERE / "helper_810_4_sweep.py"), *rows], capture_output=True, text=True ) assert out.returncode == 0, out.stderr[-2000:] import json