- master_unit.swap · split — 같은 성격 = 배 자동 · 부피 ↔ 무게 = 단위중량 · 그 밖 = 사용자 계수 · 계수 없으면 배 없음 + 까닭 글 - A00_Common/master_unit_swap.ts swapUnit — 같은 답(단위 표 · 별칭 · 분모 읽기 옮김) - 지킴 시험 test_728_1_unit_swap.py(거울 20 사례) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
7.6 KiB
Python
162 lines
7.6 KiB
Python
# -*- 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 는 서버가 준 글만 씀.
|
||
- 단 SK 단위 바꿈 환산(`swap` · 728-1)은 짝(①) — `A00_Common/master_unit_swap.ts` · 거울 시험 `test_728_1_unit_swap.py`.
|
||
- 개수 묶음(개 · 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
|
||
|
||
|
||
def split(unit) -> tuple[Decimal, str]:
|
||
"""기준단위 앞 수 떼기 — 「100본」 → (100, 본) · 「㎥」 → (1, ㎥) · 결과단위 분모 읽기(`per`) 그대로."""
|
||
got = per(f"원/{unit}") if str(unit or "").strip() else []
|
||
return got[0] if got else (Decimal(1), str(unit or "").strip())
|
||
|
||
|
||
def swap(have, want, density=None, coef=None) -> dict:
|
||
"""SK 단위 `have` → `want` 환산(728-1) — `배` = have 1 개가 want 몇 개(1 단위 당 값은 ÷배 · 수량은 ×배).
|
||
같은 성격 = 배 자동 · 부피 ↔ 무게 = 단위중량(㎏/㎥) `density` · 그 밖 성격 다름 = 사용자 `coef`
|
||
(have 앞 수 뗀 1 단위 = want 앞 수 뗀 몇 단위 · 0 보다 커야) · 계수 없으면 `배` None + 까닭 글(셈 막음)."""
|
||
(na, ua), (nb, ub) = split(have), split(want)
|
||
out = {"가": [na, ua], "나": [nb, ub], "성격": [kind_of(ua), kind_of(ub)]}
|
||
if not ua or not ub:
|
||
return {**out, "배": Decimal(1), "계수": None, "근거": ""}
|
||
need = None if same_kind(ua, ub) else ("단위중량" if _weighs(ua, ub) else "사용자")
|
||
if need == "사용자":
|
||
f = Decimal(str(coef)) if coef and Decimal(str(coef)) > 0 else None
|
||
else:
|
||
f = factor(ua, ub, density)
|
||
if f is None:
|
||
why = (
|
||
f"단위중량({WEIGHT_TABLE}) 골라야 셈"
|
||
if need == "단위중량"
|
||
else "성격 다름 · 환산 계수 넣어야 셈"
|
||
)
|
||
return {**out, "배": None, "계수": need, "근거": f"{have} ↔ {want} — {why}"}
|
||
ratio = na * f / nb
|
||
how = {
|
||
"단위중량": f" · 단위중량 {density}㎏/㎥",
|
||
"사용자": f" · 계수 1 {ua} = {coef} {ub}",
|
||
}.get(need, "")
|
||
if ratio == 1:
|
||
step = "" if plain(have) == plain(want) else f"{have} = {want}"
|
||
else:
|
||
by = f"÷{_num(ratio)}" if ratio > 1 else f"×{_num(1 / ratio)}"
|
||
step = f"{have} 당 → {want} 당 {by}"
|
||
return {**out, "배": ratio, "계수": need, "근거": step + how}
|