# -*- coding: utf-8 -*- """품셈재료 단가 — 연결을 따라 시중물가 줄의 시중·조달 값 가운데 낮은 값 (`_틀.md` 4장 재료 값). `master_formula` 가 요소 값을 풀 때 부름. 서로 부르는 모듈이라 `master_formula` 는 끝에서 읽음. """ from __future__ import annotations import re import unicodedata from decimal import Decimal LINKED = "재료:품셈재료:" MARKET = ("가격정보", "물가자료", "유통물가", "물가정보", "거래가격") REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀 _UNITS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"} def plain(s) -> str: """이름 맞대기 — NFKC · 소문자 · 빈칸 없앰 · 지름 기호 하나로.""" s = unicodedata.normalize("NFKC", str(s or "")).lower() return re.sub(r"\s+", "", re.sub(r"[φΦ∅⌀]", "ø", s)) def plain_spec(s) -> str: return plain(s).replace("×", "x").replace("*", "x") def plain_unit(s) -> str: s = plain(s).replace("ℓ", "l") return _UNITS.get(s, s) def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]: """시중물가 줄 하나의 (값, 출처) — 시중 다섯 칸 · 조달(나라장터 연결은 그 줄 값).""" value = master.get(f"재료:시중물가:{key}")["값"] out = [ (value["시중"][slot], f"재료:시중물가:{key}.시중.{slot}") for slot in MARKET if value["시중"].get(slot) is not None ] supply = value["조달"] if supply.get("값") is not None: out.append((supply["값"], f"재료:시중물가:{key}.조달")) elif str(supply.get("출처") or "").startswith("나라장터:"): ref = "재료:나라장터자재:" + supply["출처"].split(":", 1)[1] if master.get(ref).get("값") is not None: out.append((master.get(ref)["값"], ref)) return out def _market_index(master: mf.Master) -> list[tuple[str, str, str, str, str]]: """(품명 뿌리, 이름, 규격, 단위, 열쇠) — 「레미콘(관급)-경상북도(울진군)」 뿌리 = 레미콘.""" if not hasattr(master, "_market"): master._market = [] for key, row in master.index.get(("재료", "시중물가"), {}).items(): name = plain(row.get("이름")) root = re.sub(r"\(.*?\)", "", name.split(",")[0]).split("-")[0] master._market.append( (root, name, plain_spec(row.get("규격")), plain_unit(row.get("단위")), key) ) return master._market def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]: """후보 조건{이름 · 규격 · 지역: 입력} — 뿌리가 이름으로 끝나고 이름에 지역 글자 · 단위 같음.""" cond = row["연결"] if cond.get("지역") == "입력" and REGION not in env: raise mf.FormulaError(f"「{ref}」 입력 「{REGION}」 없음") region = plain(env.get(REGION, "")) if cond.get("지역") == "입력" else "" want, spec, unit = plain(cond["이름"]), plain_spec(cond.get("규격")), plain_unit(row["단위"]) hits = [ (s, key) for root, name, s, u, key in _market_index(master) if root.endswith(want) and region in name and u == unit and (not spec or s == spec) ] hits = [(s, key) for s, key in hits if _offers(master, key)] if not hits: raise mf.FormulaError(f"「{ref}」 지역 값 없음 — {env.get(REGION)}") if len({s for s, _ in hits}) > 1: raise mf.FormulaError(f"「{ref}」 후보 여럿 — 규격 선택 필요 ({len(hits)}줄)") return [key for _, key in hits] def element(master: mf.Master, ref: str, env: dict) -> tuple[object, str | None]: """요소 값과 출처 — 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값` 그대로(출처 None).""" row = master.get(ref) if not ref.startswith(LINKED): return row.get("값"), None link = row.get("연결") if not link: return None, None keys = [link] if isinstance(link, str) else _candidates(master, ref, row, env) offers = [x for key in keys for x in _offers(master, key)] return min(offers, key=lambda x: x[0]) if offers else (None, None) import master_formula as mf # noqa: E402 — 서로 부름 · 이 모듈 이름이 다 선 뒤에 읽음