diff --git a/M02_MasterTemplete/M02_San_Basis_Check.py b/M02_MasterTemplete/M02_San_Basis_Check.py index 39c5f3d56..38d2cbed9 100644 --- a/M02_MasterTemplete/M02_San_Basis_Check.py +++ b/M02_MasterTemplete/M02_San_Basis_Check.py @@ -322,3 +322,103 @@ def causes(v: dict, mach: list[dict], rec: dict | None) -> list[list[str]]: ] ) return out + + +# ── 판 표지(PLAN 794-2b · 분석 652-794-2a 5 장) — 근거후보 까닭 「판 차이」에 판 번호 ────────── +STRONG = ("판 차이", "현장 값", "그 밖") # 둘 이상 파일 같은 배면 「판 차이 강함」 으로 올림 +_TEXT_YEAR = re.compile(r"(? int | None: + y = int(m.group(1)) % 100 if m else None + return None if y is None else y + (2000 if y < 50 else 1900) + + +def file_year(name: Any) -> int | None: + """파일 이름 연도 — 「2024년 …」 · 「00.25년_…」 · 「20250527 2025년 …」.""" + s = str(name or "") + return _year(re.search(r"(20\d\d)\s*년", s) or re.search(r"(? int | None: + """블록 글 속 판 — 「2017년 품셈」 · 「24년 건설표준품셈」(가장 옛 것).""" + text = " ".join(str(t) for t in [blk.get("이름"), blk.get("규격"), *(blk.get("글") or [])]) + found = [_year(m) for m in _TEXT_YEAR.finditer(text)] + return min(found) if found else None + + +def edition_tag(key: str, rev: list, year: int, by: str) -> str: + """기초 절 개정 가운데 블록 판 뒤 것 — 「'24 보완 · 파일 2024」 · 없으면 빈 글. + + 글 속 판 해 = 그 해 개정을 담음(뒤 것만) · 파일 해 = 같은 해 개정이 앞일 수도(같은 해 포함).""" + later = [int(y) for y in rev or [] if int(y) > year or (by == "파일" and int(y) == year)] + if not later: + return "" + word = "제정" if key.startswith("GF") else "보완" + return f"{' · '.join(f'{chr(39)}{y % 100:02d}' for y in later)} {word} · {by} {year}" + + +def edition(out: dict, file_name: Any, blk: dict) -> dict: + """제안 근거후보마다 판 표지 `판`(덧칸) · 까닭 「판 차이」 → 「판 차이(판 표지)」. + + 셈 · 단 · 꼴 안 바뀜(표지 글만).""" + by_text, by_file = text_year(blk), file_year(file_name) + if by_text and (not by_file or by_text <= by_file): + year, by = by_text, "글" + else: + year, by = by_file, "파일" + if not year: + return out + index = hb.m01.cm.master().index + for c in out.get("근거후보") or []: + key = str(c.get("기초") or "") + tag = edition_tag(key, (index.get(key[:2], {}).get(key) or {}).get("개정"), year, by) + if tag: + c["판"] = tag + c["까닭"] = [ + [f"판 차이({tag})" if lab == "판 차이" else lab, txt] for lab, txt in c["까닭"] + ] + return out + + +def _ratios(c: dict) -> list[float]: + return [ + float(m.group(1)) + for lab, txt in c.get("까닭") or [] + if lab.startswith(STRONG) and (m := _RATIO.search(str(txt))) + ] + + +def strong(got: dict, others: list[dict]) -> dict: + """같은 기초 · 같은 배(±2 %)가 다른 파일에도 판 표지와 함께 있으면 「판 차이 강함(…)」.""" + seen = [ + (c["기초"], r) + for o in others + for p in o.values() + for c in p.get("근거후보") or [] + if c.get("판") + for r in _ratios(c) + ] + if not seen: + return got + + def hit(key: str, lab: str, text: Any) -> bool: + m = _RATIO.search(str(text)) if lab.startswith(STRONG) else None + return bool(m) and any( + k == key and r and abs(float(m.group(1)) / r - 1) <= 0.02 for k, r in seen + ) + + out = {} + for no, p in got.items(): + cs = [] + for c in p.get("근거후보") or []: + if c.get("판"): + why = [ + [f"판 차이 강함({c['판']})", t] if hit(c["기초"], lab, t) else [lab, t] + for lab, t in c["까닭"] + ] + c = {**c, "까닭": why} + cs.append(c) + out[no] = {**p, "근거후보": cs} + return out diff --git a/M02_MasterTemplete/M02_San_Basis_Store.py b/M02_MasterTemplete/M02_San_Basis_Store.py index 38e9cb77e..77841697e 100644 --- a/M02_MasterTemplete/M02_San_Basis_Store.py +++ b/M02_MasterTemplete/M02_San_Basis_Store.py @@ -20,10 +20,11 @@ from common_util import common_util_derived_store as derived from M02_MasterTemplete import M02_Hopyo_Judge as judge from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_San_Basis as basis +from M02_MasterTemplete import M02_San_Basis_Check as chk from M02_MasterTemplete import M02_San_Basis_Index as bi KIND = "san_basis_" -VERSION = "793-3a.1" # 제안 꼴이 바뀌면 올림 +VERSION = "794-2b.1" # 제안 꼴이 바뀌면 올림 SHEET = "단가산출근거" _LOCK, _RUNNING = threading.Lock(), set() @@ -50,7 +51,8 @@ def one(book: dict[str, Any], no: int) -> dict[str, Any]: blk = next(b for b in book.get("단산") or [] if int(b["번"]) == int(no)) line, same = judge._caller(book, int(no)) g = (book.get("숨은식") or {}).get(str(no)) - return basis.suggest(blk, book.get("꼴") or "", line, same, g) + out = basis.suggest(blk, book.get("꼴") or "", line, same, g) + return chk.edition(out, book.get("파일"), blk) # 794-2b 판 표지 def _stamp(key: str, book: dict[str, Any]) -> str: @@ -110,12 +112,27 @@ def shortfall(got: dict[str, Any]) -> list[dict[str, Any]]: return sorted(out.values(), key=lambda x: (-len(x["블록"]), x["이름"])) +def _others(folder: Path, key: str) -> list[dict[str, Any]]: + """같은 폴더 다른 HP 저장본(셈 끝난 것만) — 판 차이 강함(794-2b) 대조.""" + out = [] + for r in hopyo_store.rows(folder): + if r.get("키") and r["키"] != key: + try: + got = solved(folder, r["키"]) + except Exception: # noqa: BLE001 — 못 읽는 HP = 대조에서 뺌 + got = None + if got: + out.append(got) + return out + + def listing(folder: Path, key: str) -> dict[str, Any]: """{셈중, 블록 {번: 제안}, 보강} — 셈 중 = 블록 {} · 뒤 셈 시작.""" got = solved(folder, key) if got is None: ensure(folder, key) return {"키": key, "셈중": True, "블록": {}, "보강": []} + got = chk.strong(got, _others(folder, key)) return {"키": key, "셈중": False, "블록": got, "보강": shortfall(got)} @@ -123,7 +140,7 @@ def block(folder: Path, key: str, no: int) -> dict[str, Any]: """블록 하나 — 저장본 › 그 자리 셈.""" got = (solved(folder, key) or {}).get(str(no)) if got is not None: - return got + return chk.strong({str(no): got}, _others(folder, key))[str(no)] book = hopyo_store.read(folder, key) if not any(b.get("번") is not None and int(b["번"]) == int(no) for b in book.get("단산") or []): raise judge._bad(f"없는 블록 「단산{no}」", 404) diff --git a/resources/tester/test_794_2b_edition.py b/resources/tester/test_794_2b_edition.py new file mode 100644 index 000000000..d2c06acac --- /dev/null +++ b/resources/tester/test_794_2b_edition.py @@ -0,0 +1,129 @@ +"""794-2b 지킴 — 품셈 판 표지 · 잔토처리 단독 GC. + +① 로직 줄 칸 「개정」 = 빌더(`build_로직_개정.py`)가 원문 절 머리에서 뽑은 값과 같음 · 채움 수. +② 셈 값 그대로 — 791-1 지문 fixture 가 지금 마스터 지문(건너뜀 없이 1,515 전후 같음을 돎). +③ 근거 제안 까닭 판 번호 — 봉화#98 철근 현장조립 3.5 ↔ 3.4 = 「판 차이('24 보완 · 파일 2024)」 · + 거창#28 · 봉화#8 뒤채움 같은 배 ×1.98 = 「판 차이 강함」(엑셀 = `helper_m02_hopyo_golden.FILES`). +④ 잔토처리(인력) GC001194 = ㎥당 보통인부 0.2 인(건설 공통 3-2-1 비고). +""" + +import importlib +import json + +import helper_m02_hopyo_golden as golden +import pytest + +from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠 +from M02_MasterTemplete import M02_San_Basis_Check as chk +from M02_MasterTemplete import M02_San_Basis_Store as store +from M02_MasterTemplete import M02_San_Book as san_book + +rev = importlib.import_module("build_로직_개정") +mf = importlib.import_module("master_formula") +fp = importlib.import_module("test_791_1_engine_semantics") + + +def _rows() -> dict: + files, _ = m01.loaded() + return {r["키"]: r for d in files.values() if d.get("그룹") == "로직" for r in d["줄"]} + + +def test_개정_칸_빌더와_같음(): + got = rev.build(check=True) + assert got["바뀐 파일"] == 0 # 마스터 = 빌더 값(손 입력 없음) + assert got["GC 채움"] + got["GF 채움"] >= 1237 and got["GF 빈칸"] == 0 + rows = _rows() + assert rows["GC000083"]["개정"] == [2008, 2020, 2025] # 3-2-1('08, '20, 25년 보완) + assert 2024 in rows["GC000207"]["개정"] # 6-2-3 철근 현장조립 '24 보완 + assert rows["GF000123"]["개정"] == [2026] # 산림 2026-01-01 제정 + cols = list(rows["GC000083"]) + assert cols.index("개정") == cols.index("출처") + 1 # _틀.md 7 장 열 차례 + + +def test_셈_지문_건너뜀_없음(): + saved = json.loads(fp.FINGER.read_text(encoding="utf-8")) + assert saved["마스터"] == fp._master_hash() + assert len(saved["결과"]) >= 1516 + + +def test_잔토처리_단독(): + _, whole = m01.loaded() + row = _rows()["GC001194"] + assert (row["원문번호"], row["결과단위"]) == ("3-2-1", "원/㎥") + raw = mf.run(whole, "GC001194", {}) + (line,) = raw["줄"] + assert (line["이름"], float(line["수량"])) == ("보통인부", 0.2) + + +def test_연도_읽기(): + assert [chk.file_year(p.name) for p in golden.FILES.values()] == [ + 2026, + 2026, + 2025, + 2024, + 2024, + 2024, + 2025, + 2025, + ] + assert chk.text_year({"글": ["2017년 품셈 6-3-1"]}) == 2017 + assert chk.text_year({"글": ["「24년 건설표준품셈 6-2-3」"]}) == 2024 + assert chk.text_year({"글": ["건설표준품셈 6-2(철근) 적용"]}) is None + assert chk.edition_tag("GC000207", [2008, 2014, 2022, 2024], 2024, "파일") == ( + "'24 보완 · 파일 2024" + ) + assert chk.edition_tag("GC000211", [2017, 2018], 2017, "글") == "'18 보완 · 글 2017" + assert chk.edition_tag("GF000123", [2026], 2026, "글") == "" + + +def test_판_차이_강함_틀(): + a = { + "1": { + "근거후보": [ + { + "기초": "GC1", + "판": "'25 보완 · 파일 2024", + "까닭": [["그 밖", "보통인부 ×1.98"], ["단위 기준 차이", "x ×10"]], + } + ] + } + } + b = { + "9": { + "근거후보": [ + {"기초": "GC1", "판": "'25 보완 · 파일 2025", "까닭": [["그 밖", "보통인부 ×1.99"]]} + ] + } + } + got = chk.strong(a, [b])["1"]["근거후보"][0]["까닭"] + assert got == [ + ["판 차이 강함('25 보완 · 파일 2024)", "보통인부 ×1.98"], + ["단위 기준 차이", "x ×10"], + ] + assert chk.strong(a, [])["1"]["근거후보"][0]["까닭"][0][0] == "그 밖" + assert a["1"]["근거후보"][0]["까닭"][0][0] == "그 밖" # 저장본 안 고침 + + +def _one(tag: str, no: int) -> dict: + path = golden.FILES[tag] + book = golden.read(tag) if path.exists() else None + if book is None: + pytest.skip(f"실무 엑셀 없음 {tag}") + sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {} + book = {**book, "숨은식": store.hidden(book, sheets), "파일": path.name} + return store.one(book, no) + + +def test_봉화98_판_번호(): + got = {c["기초"]: c for c in _one("봉화", 98)["근거후보"]} + c = got["GC000207"] + assert c["단"] == "근접" and c["판"] == "'24 보완 · 파일 2024" + assert ["판 차이('24 보완 · 파일 2024)", "철근공 ×0.9714"] in c["까닭"] + + +def test_뒤채움_두_파일_판_차이_강함(): + a, b = _one("봉화", 8), _one("거창", 28) + marks = [ + lab for c in chk.strong({"8": a}, [{"28": b}])["8"]["근거후보"] for lab, _ in c["까닭"] + ] + assert "판 차이 강함('25 보완 · 파일 2024)" in marks