"""M01 요소 「별칭」 칸 풀이 정본(PLAN 815-1 · 요구 764) — 인력 · 기계 · 재료 · 유가 줄의 `별칭` 글 하나. 별칭 글 = 콤마(「,」 · 전각 「,」)로 여러 개 · 앞뒤 빈칸 뗌 · 큰따옴표(" · “ ”)로 감싼 조각 = 통째 한 별칭(속 콤마 보존 — 이름과 규격을 같이 적을 때 「"결속선(철선) #20, 0.9M/M"」) · 빈 칸 = 없음. `norm` = 표기 정규화(빈칸 · 전각 · 대소문자 · 괄호 · 단위 M/M = mm · 곱셈표 · 지름 Φ · D= · 번호 #) — 이름 낱말 순서(「결속선(철선)」 ↔ 「철선(결속선)」)는 맞추지 않음(별칭 몫). 뜻: 별칭 = 그 줄을 가리키는 다른 이름 — 이름만 적은 별칭은 줄 규격이 정규화해 같을 때 그 줄 · 이름 + 규격을 적은 별칭은 「이름 규격」 통째가 같을 때 그 줄 · 여럿 = 부르는 쪽이 「후보 여럿」. 쓰는 곳 = M02 불러오기 요소 잇기(`M02_Elem_Alias`) · M01 별칭 칸 저장 · 보기. 서버 단독 · 읽기만. """ from __future__ import annotations import re import unicodedata from collections.abc import Callable, Iterable from typing import Any _QUOTES = {'"': '"', "“": "”", "”": "”"} _SEP = (",", ",") _TIMES = re.compile(r"(?<=\d)\s*[x×*]\s*(?=\d)") _DIAM = re.compile(r"[φϕΦøØ∅]|(?]") def names(text: Any) -> list[str]: """별칭 글 → 별칭 목록(차례 그대로 · 같은 것 한 번).""" out: list[str] = [] piece, close = "", None for ch in f"{text or ''},": if close: if ch == close or (close == "”" and ch == '"'): close = None else: piece += ch elif ch in _QUOTES and not piece.strip(): close, piece = _QUOTES[ch], "" elif ch in _SEP: one = piece.strip() if one and one not in out: out.append(one) piece = "" else: piece += ch if close and piece.strip() and piece.strip() not in out: # 닫는 따옴표 빠짐 = 끝까지 한 조각 out.append(piece.strip()) return out def norm(text: Any) -> str: """표기 정규화 — 같은 글이면 같은 값(맞대기 열쇠).""" t = unicodedata.normalize("NFKC", str(text or "")).lower() t = t.replace("m/m", "mm") t = _TIMES.sub("x", t).replace("×", "x") t = _DIAM.sub("φ", t) t = _BRACKETS.sub("", t) return re.sub(r"\s+", "", t) def index(rows: Iterable[dict[str, Any]]) -> dict[str, list[str]]: """요소 줄들(`키` · `별칭`) → {정규화 별칭: [키 …]}(줄 차례).""" out: dict[str, list[str]] = {} for row in rows: key = row.get("키") for one in names(row.get("별칭")) if key else (): got = out.setdefault(norm(one), []) if str(key) not in got: got.append(str(key)) return out def match( idx: dict[str, list[str]], spec_of: Callable[[str], Any], name: Any, spec: Any = "" ) -> list[str]: """가져온 줄(이름 · 규격) → 별칭으로 이어지는 키들 — 「이름 규격」 통째 별칭 › 이름 별칭 + 줄 규격 정규화 같음 › (가져온 규격 빔) 이름 별칭 줄 하나 · 없음 = [].""" mine = norm(spec) if mine and (whole := idx.get(norm(name) + mine)): return list(whole) named = idx.get(norm(name)) or [] same = [k for k in named if norm(spec_of(k)) == mine] if same: return same return list(named) if not mine and len(named) == 1 else [] if __name__ == "__main__": assert names('철선, 결속선 ,"결속선(철선) #20, 0.9M/M" , ,철선') == [ "철선", "결속선", "결속선(철선) #20, 0.9M/M" ] # fmt: skip assert ( norm("결속선(철선) #20, 0.9M/M") == norm("결속선 철선#20,0.9㎜") == "결속선철선#20,0.9mm" ) assert norm("600×1200") == norm("600 X 1200") == norm("600*1200") == "600x1200" assert norm("PVC D=100") == norm("PVC Φ100") == norm("pvc ø 100") == "pvcφ100" idx = index([{"키": "A", "별칭": "철선"}, {"키": "B", "별칭": '"철선 #8"'}]) specs = {"A": "#20, 0.9mm", "B": "#8"} assert match(idx, specs.get, "철선", "#20, 0.9M/M") == ["A"] assert match(idx, specs.get, "철선", "#8") == ["B"] assert match(idx, specs.get, "철선", "#10") == [] print("ok")