feat(728-1): SK 단위 바꿈 환산 순수 함수 · 파이썬 · TS 짝 · 거울 시험

- 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>
This commit is contained in:
2026-11-02 10:08:02 +09:00
co-authored by Claude Opus 5.5
parent 638d3f446d
commit c201a1269d
4 changed files with 298 additions and 0 deletions
@@ -4,6 +4,7 @@
쓰는 곳: 재료 단가 → 줄 단위(`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 등은 환산 없이 경고 글만.
@@ -119,3 +120,42 @@ def unit_weight(master, kind: str, shape: str, end: str = "위") -> Decimal | No
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}
@@ -0,0 +1,12 @@
/* PLAN 728-1 거울 헬퍼 — `A00_Common/master_unit_swap.ts` 를 Node 로 돌려 표준입력 사례마다 답 JSON.
* 짝 = `test_728_1_unit_swap.py`(같은 사례를 `master_unit.swap` 으로 돌려 맞댐).
*/
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const { load } = makeLoader({ root: ROOT });
const mod = load("A00_Common", "master_unit_swap.ts");
const cases = JSON.parse(fs.readFileSync(0, "utf-8"));
process.stdout.write(JSON.stringify(cases.map(([a, b, d, c]) => mod.swapUnit(a, b, d, c))));
+114
View File
@@ -0,0 +1,114 @@
"""728-1 지킴 — SK 단위 바꿈 환산 짝: `master_unit.swap` ↔ `A00_Common/master_unit_swap.ts` `swapUnit` 거울.
같은 성격 = 배 자동 · 부피 ↔ 무게 = 단위중량 · 그 밖 = 사용자 계수 · 계수 없으면 배 없음 + 까닭 글.
"""
from __future__ import annotations
import json
import subprocess
import sys
from decimal import Decimal
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
import master_unit as mu # noqa: E402
CASES = [
["100본", "본", None, None],
["10㎡", "㎡", None, None],
["1,000본", "100본", None, None],
["㎡", "10㎡", None, None],
["m", "km", None, None],
["kg", "톤", None, None],
["본", "개", None, None],
["m3", "㎥", None, None],
["㎥", "㎥", None, None],
["식", "식", None, None],
["", "본", None, None],
["㎥", "m", None, None],
["㎥", "m", None, 2.5],
["㎥", "m", None, -1],
["100m", "㎡", None, 0.5],
["㎥", "톤", None, None],
["㎥", "톤", 1600, None],
["톤", "㎥", 1600, None],
["10㎥", "kg", 2400, None],
["조", "개", None, None],
]
def _py(case) -> dict:
got = mu.swap(*case)
num = lambda x: None if x is None else float(x) # noqa: E731
return {
**got,
"가": [num(got["가"][0]), got["가"][1]],
"나": [num(got["나"][0]), got["나"][1]],
"배": num(got["배"]),
}
@pytest.fixture(scope="module")
def ts() -> list[dict]:
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HERE / "helper_728_1_unit_swap.cjs")],
input=json.dumps(CASES),
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_거울_파이썬과_TS_같은_답(ts) -> None:
for case, t in zip(CASES, ts, strict=True):
p = _py(case)
assert (t["가"], t["나"], t["성격"], t["계수"], t["근거"]) == (
p["가"],
p["나"],
p["성격"],
p["계수"],
p["근거"],
), case
assert (t["배"] is None) == (p["배"] is None), case
if p["배"] is not None:
assert t["배"] == pytest.approx(p["배"], rel=1e-12), case
def test_같은_성격은_배_자동() -> None:
assert mu.swap("100본", "본")["배"] == 100 # 단목베기 100본 당 153,889.4 → 본 당 1,538.894
assert Decimal("153889.4") / mu.swap("100본", "본")["배"] == Decimal("1538.894")
assert Decimal("447913.20") / mu.swap("10㎡", "㎡")["배"] == Decimal("44791.32")
assert mu.swap("100본", "본")["근거"] == "100본 당 → 본 당 ÷100"
assert mu.swap("m", "km")["근거"] == "m 당 → km 당 ×1000"
assert mu.swap("본", "개")["계수"] is None and mu.swap("본", "개")["근거"] == "본 = 개"
assert mu.split("1,000본") == (Decimal(1000), "본") and mu.split("㎥") == (Decimal(1), "㎥")
def test_다른_성격은_계수_없으면_셈_막음() -> None:
got = mu.swap("㎥", "m")
assert got["배"] is None and got["계수"] == "사용자" and "환산 계수" in got["근거"]
assert mu.swap("㎥", "m", coef=2.5)["배"] == Decimal("2.5")
assert mu.swap("㎥", "m", coef=0)["배"] is None # 0 · 음수 = 계수 없음
got = mu.swap("㎥", "톤")
assert got["배"] is None and got["계수"] == "단위중량" and "CC000337" in got["근거"]
assert mu.swap("㎥", "톤", density=1600)["배"] == Decimal("1.6")
assert mu.swap("조", "개")["계수"] == "사용자" # 한 조 = 여럿 — 묶음 밖
def test_TS_는_새_정규식_없이_모듈_한_벌() -> None:
src = (ROOT / "A00_Common/master_unit_swap.ts").read_text(encoding="utf-8")
assert "master_unit.py" in src and "test_728_1_unit_swap.py" in src
py = (ROOT / "resources/master_data/scripts/master_unit.py").read_text(encoding="utf-8")
assert "master_unit_swap.ts" in py and "test_728_1_unit_swap.py" in py
for kind, group in mu.UNITS.items(): # 단위 표가 두 파일에 같게
for u, n in group.items():
assert f"{u}: {n}" in src or f'"{u}": {n}' in src, (kind, u)