Files
Aislo/resources/master_data/scripts/master_unit.py
T
eomsangdonandClaude Opus 5.5 881c204ca8 feat(712-2): 단위 한 벌 master_unit — 별칭 · 개수 묶음 · 결과단위 배 읽기 · 같은 성격 환산 · 근거 글
- 별칭(master_material) · 같은 성격 환산(master_pick) · 결과단위 배 읽기 셋(Store_Combo · Hopyo_Ours · master_basis) · 보이는 꼴 별칭(master_basis) 갈아 끼움 · 엑셀 맞춤(Hopyo_Match)은 master_pick 이름 그대로 이음
- 개수 묶음(개 · EA · 본 · 매) 1:1 — 본 ↔ 개 단위경고 4 줄 사라짐 · 값 같음
- 부피 ↔ 무게 = 단위중량 표 CC000337 고른 줄 받을 때만(기본 줄 없음) · 밀도 · ㎾h 경고 글만
- B08 /1000 · /40 손 식 = 실무 엑셀 G열 식이라 그대로(시험이 같은 값 대조)
- 지킴 시험 test_712_2_unit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:03:54 +09:00

122 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""단위 한 벌 — 별칭 · 개수 묶음 · 결과단위 배 읽기 · 같은 성격 환산 · 환산 근거 글(PLAN 712-2).
쓰는 곳: 재료 단가 → 줄 단위(`master_pick`) · 자재 맞대기 별칭(`master_material.plain_unit`) ·
조합 알약 배(`M01_MasterData_Store_Combo`) · 엑셀 맞춤 배(`M02_Hopyo_Ours` · `_Match`) ·
표 기준배(`master_basis`) · 계산서 줄 환산 근거(714-1). 서버 단독(③) — TS 는 서버가 준 글만 씀.
- 개수 묶음(개 · EA · 본 · 매) = 1 : 1 같은 성격 · 조 · 세트는 묶음 밖(한 조 = 여럿).
- 부피 ↔ 무게 = 단위중량 표 CC000337 에서 고른 줄(`unit_weight`)을 받을 때만 · 기본 줄 없음
(사용자 확인 거리) · 밀도(kg ↔ ℓ) · ㎾h 등은 환산 없이 경고 글만.
"""
from __future__ import annotations
import re
import unicodedata
from decimal import Decimal
# 맞대기 꼴(`plain`) 별칭 — NFKC · 소문자 · 빈칸 없앤 뒤
ALIAS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
# 보이는 꼴(`shown`) 별칭 — 표 기준 · 결과단위 분모(원문 기호 ㎡ · ㎥ 그대로)
SHOWN = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
# 같은 성격 — {성격: {맞대기 단위: 기준 단위 몇 배}}
UNITS = {
"무게": {"g": 1, "kg": 1000, "톤": 1000000},
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
"넓이": {"cm2": 1, "m2": 10000},
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
"개수": {"개": 1, "본": 1, "매": 1},
}
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
WEIGHT_TABLE = "CC000337" # 건설품셈 공통 1-2-4 재료의 단위중량(㎏/㎥)
# 결과단위 분모 — 「원/10㎡」 → (10, ㎡) · 「원/1,000본」 → (1000, 본)
_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)")
def plain(unit) -> str:
"""맞대기 꼴 — 「㎥」 = 「m3」 · 「EA」 = 「개」 · 「Ton」 = 「톤」 · 「리터」 = 「ℓ」."""
s = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(unit or "")).lower())
s = s.replace("ℓ", "l")
return ALIAS.get(s, s)
def shown(unit) -> str:
return SHOWN.get(unit, unit)
def per(result_unit) -> list[tuple[Decimal, str]]:
"""결과단위 분모마다 (배, 단위 원문 글) — 배 없으면 1 · 분모가 여럿이면 여럿."""
return [
(Decimal(n.replace(",", "")) if n else Decimal(1), u)
for n, u in _PER.findall(str(result_unit or ""))
]
def kind_of(unit) -> str | None:
got = _SCALE.get(plain(unit))
return got[0] if got else None
def same_kind(a, b) -> bool:
"""단위가 같거나 같은 성격(무게 · 길이 · 넓이 · 부피 · 개수)."""
return plain(a) == plain(b) or (kind_of(a) is not None and kind_of(a) == kind_of(b))
def _weighs(a, b) -> bool:
return {kind_of(a), kind_of(b)} == {"무게", "부피"}
def factor(have, want, density=None) -> Decimal | None:
"""`have` 1 단위가 `want` 몇 단위인가 — 단가는 ÷, 수량은 ×.
부피 ↔ 무게는 단위중량(㎏/㎥) `density` 가 있을 때만 · 못 하면 None."""
a, b = plain(have), plain(want)
if a == b:
return Decimal(1)
if same_kind(a, b):
return Decimal(_SCALE[a][1]) / Decimal(_SCALE[b][1])
if density and _weighs(a, b):
grams_per_cm3 = Decimal(str(density)) / Decimal(1000)
base = Decimal(_SCALE[a][1]) * (
grams_per_cm3 if kind_of(a) == "부피" else 1 / grams_per_cm3
)
return base / Decimal(_SCALE[b][1])
return None
def convert(price, have, want, density=None):
"""`have` 단위 단가 → `want` 단위 단가 — 못 바꾸면 그대로(경고는 부르는 쪽)."""
if price is None or not want or not have or plain(have) == plain(want):
return price
if same_kind(have, want): # × 뒤 ÷ — 옛 셈과 같은 Decimal 꼴(자릿수)
return price * Decimal(_SCALE[plain(want)][1]) / Decimal(_SCALE[plain(have)][1])
got = factor(want, have, density) # want 1 단위 = have 몇 단위
return price if got is None or got == 1 else price * got
def note(have, want, density=None) -> str:
"""환산 근거 글 — 「kg → 톤 ×1000」 · 「본 = 개(개수 1:1)」 · 못 바꾸면 까닭."""
if not have or not want or plain(have) == plain(want):
return ""
got = factor(want, have, density)
if got is not None:
if kind_of(have) == "개수" and got == 1:
return f"{have} = {want}(개수 1:1)"
how = f" · 단위중량 {density}㎏/㎥" if _weighs(have, want) else ""
return f"{have} → {want} ×{_num(got)}{how}"
if _weighs(have, want):
return f"{have} ↔ {want} — 단위중량({WEIGHT_TABLE}) 골라야 환산 · 지금 그대로"
return f"{have} ↔ {want} — 성격 다름 · 환산 없음(경고)"
def _num(x: Decimal) -> str:
return format(x.normalize(), "f") if x == x.to_integral_value() else format(x, "f")[:10]
def unit_weight(master, kind: str, shape: str, end: str = "위") -> Decimal | None:
"""단위중량 표에서 고른 줄의 ㎏/㎥ — 기본 줄 없음(고르지 않으면 None)."""
for row in master.get(WEIGHT_TABLE).get("줄") or []:
if row.get("종별") == kind and row.get("형상") == shape:
got = row.get(end)
return None if got is None else Decimal(str(got))
return None