Files
Aislo/A00_Common/master_unit_swap.ts
T
eomsangdonandClaude Opus 5.5 c0319a4f58 feat(746-1): 불러오기 고름 대기 후보 블록 줄비교 · 못옮김 · 기계 줄 M01 요소 후보 · 단위 별칭 hr = 시간 · 개소 = 개
- 고름 대기 후보 앞 셋에 그 기초와 줄마다 견준 `줄비교`(같음 · 품다름 · 엑셀만 · 기초만 · 1 단위 품 · 못옮김) — 고르면 만드는 SK 와 같은 셈(`_Plan` 기록) · 판정 · 순위 · 미리 고름 그대로
- 못옮김 `후보` · 안 이은 기계 줄 `남은줄[].후보` = M01 요소 셋(새 `M02_Hopyo_Elem` — 이름 · 규격 점수 › 엑셀 단가 가까운 차례)
- 단위 모듈 짝(master_unit.py ↔ master_unit_swap.ts) 별칭 hr = 시간 · 개수 묶음에 개소 — 거울 시험 사례 셋 더함
- 실측(여덟 파일): 고름 대기 89 중 1위 줄비교 72(셋 중 하나 82) · 골든 1위 163 그대로 · 판정 수 그대로 · 개 ↔ 개소 1 블록 견줌 풀림 · 못옮김 후보 8/9 · 기계 줄 후보 32/32 · M01 로직 3,992 값 전후 같음
- 새 지킴 시험 test_746_1_hopyo_compare

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:04:48 +09:00

134 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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: "조",
세트: "조",
hr: "시간",
};
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, 개소: 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 };
}