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:
@@ -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))));
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user