/* ============================================================================= * master_unit_swap.ts * SK 단위 바꿈 환산(728-1) — 짝(①): `resources/master_data/scripts/master_unit.py` `swap` · `split`. * 거울 시험 `resources/tester/test_728_1_unit_swap.py`. 단위 표 · 별칭 · 분모 읽기는 그 모듈 그대로 옮김 * (한 벌 — 고칠 때 두 파일 함께). 화면 · fetch · 저장 없음. * ========================================================================== */ const ALIAS: Record = { ea: "개", ton: "톤", t: "톤", l: "ℓ", 리터: "ℓ", set: "조", 세트: "조", }; const UNITS: Record> = { 무게: { 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 }, }; const SCALE: Record = {}; for (const [kind, group] of Object.entries(UNITS)) { for (const [u, n] of Object.entries(group)) SCALE[u] = [kind, n]; } const WEIGHT_TABLE = "CC000337"; const PER = /원\s*\/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)/; export type UnitNeed = null | "단위중량" | "사용자"; export interface UnitSwap { 가: [number, string]; 나: [number, string]; 성격: [string | null, string | null]; /** have 1 개가 want 몇 개 — 1 단위 당 값은 ÷배 · 수량은 ×배 · 계수 없으면 null(셈 막음) */ 배: number | null; 계수: UnitNeed; 근거: string; } export function plainUnit(unit: unknown): string { const s = String(unit ?? "") .normalize("NFKC") .toLowerCase() .replace(/\s+/g, "") .replace(/ℓ/g, "l"); return ALIAS[s] ?? s; } export function unitKind(unit: unknown): string | null { return SCALE[plainUnit(unit)]?.[0] ?? null; } function sameKind(a: unknown, b: unknown): boolean { const k = unitKind(a); return plainUnit(a) === plainUnit(b) || (k !== null && k === unitKind(b)); } function weighs(a: unknown, b: unknown): boolean { const ks = new Set([unitKind(a), unitKind(b)]); return ks.size === 2 && ks.has("무게") && ks.has("부피"); } /** `have` 1 단위가 `want` 몇 단위 — 부피 ↔ 무게는 단위중량(㎏/㎥) 있을 때만 · 못 하면 null */ function factor(have: string, want: string, density?: number | null): number | null { const a = plainUnit(have); const b = plainUnit(want); if (a === b) return 1; if (sameKind(a, b)) return SCALE[a][1] / SCALE[b][1]; if (density && weighs(a, b)) { const gramsPerCm3 = density / 1000; const base = SCALE[a][1] * (unitKind(a) === "부피" ? gramsPerCm3 : 1 / gramsPerCm3); return base / SCALE[b][1]; } return null; } /** 열두 자리로 다듬음 — 파이썬 Decimal 셈과 같은 값(뜬수 끝 흔들림 없앰) */ function tidy(x: number): number { return Number(x.toPrecision(12)); } function num(x: number): string { return Number.isInteger(x) ? String(x) : String(x).slice(0, 10); } /** 기준단위 앞 수 떼기 — 「100본」 → [100, 본] · 「㎥」 → [1, ㎥] */ export function splitUnit(unit: unknown): [number, string] { const text = String(unit ?? "").trim(); const got = text ? PER.exec(`원/${text}`) : null; if (!got) return [1, text]; return [got[1] ? Number(got[1].replace(/,/g, "")) : 1, got[2]]; } /** SK 단위 `have` → `want` 환산 — 같은 성격 = 배 자동 · 부피 ↔ 무게 = 단위중량 · 그 밖 = 사용자 계수 * (have 앞 수 뗀 1 단위 = want 앞 수 뗀 몇 단위 · 0 보다 커야). */ export function swapUnit( have: string, want: string, density?: number | null, coef?: number | null, ): UnitSwap { const [na, ua] = splitUnit(have); const [nb, ub] = splitUnit(want); const base = { 가: [na, ua], 나: [nb, ub], 성격: [unitKind(ua), unitKind(ub)] } as Pick< UnitSwap, "가" | "나" | "성격" >; if (!ua || !ub) return { ...base, 배: 1, 계수: null, 근거: "" }; const need: UnitNeed = sameKind(ua, ub) ? null : weighs(ua, ub) ? "단위중량" : "사용자"; const f = need === "사용자" ? (coef && coef > 0 ? coef : null) : factor(ua, ub, density); if (f === null) { const why = need === "단위중량" ? `단위중량(${WEIGHT_TABLE}) 골라야 셈` : "성격 다름 · 환산 계수 넣어야 셈"; return { ...base, 배: null, 계수: need, 근거: `${have} ↔ ${want} — ${why}` }; } const ratio = tidy((na * f) / nb); const how = need === "단위중량" ? ` · 단위중량 ${density}㎏/㎥` : need === "사용자" ? ` · 계수 1 ${ua} = ${coef} ${ub}` : ""; let step = ""; if (ratio === 1) step = plainUnit(have) === plainUnit(want) ? "" : `${have} = ${want}`; else step = `${have} 당 → ${want} 당 ${ratio > 1 ? `÷${num(ratio)}` : `×${num(tidy(1 / ratio))}`}`; return { ...base, 배: ratio, 계수: need, 근거: step + how }; }