- 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>
133 lines
4.9 KiB
TypeScript
133 lines
4.9 KiB
TypeScript
/* =============================================================================
|
||
* 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<string, string> = {
|
||
ea: "개",
|
||
ton: "톤",
|
||
t: "톤",
|
||
l: "ℓ",
|
||
리터: "ℓ",
|
||
set: "조",
|
||
세트: "조",
|
||
};
|
||
const UNITS: Record<string, Record<string, number>> = {
|
||
무게: { 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<string, [string, number]> = {};
|
||
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 };
|
||
}
|