feat(815-1 별칭): 별칭 칸 풀이 정본 master_alias — 콤마 · 전각 콤마 · 큰따옴표 조각 · 표기 정규화 · 색인 · 맞대기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 14:13:52 +09:00
co-authored by Claude Opus 5.5
parent afb3fcff8d
commit 55ad1bfee0
@@ -0,0 +1,101 @@
"""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"[φϕΦøØ∅]|(?<![a-z가-힣])d\s*=\s*(?=\d)")
_BRACKETS = 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")