- 옛 B08 식 풀이기(git 8472fc9f)의 분수 · 파서를 ui_template/sheet/ 로 되살림 — 필요한 함수만
- 열 참조 [열id] · 셀 참조 [열id@줄id] · 변수 [$이름] · SUM · INT · ROUND · ROUNDUP · ROUNDDOWN · MIN · MAX · IF
- 한 칸만 다른 식(줄.식) · 합계 줄 열마다 다른 식 · 끝수(반올림 · 올림 · 버림) · 순환은 그 칸만 오류
- 서버 껍데기 common_util_sheet_recalc.py · build:formula 가 새 진입점을 가리킴(깨진 B08 경로 고침)
- 시험 — 실무 울진 2공구 구조물집계표 합계 캐시값 · 파이썬 Decimal 거울 40문서
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tjoit7rxvpLMM7cafeVTo1
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
/* =============================================================================
|
||
* ui_template_sheet_frac.ts
|
||
* 표 식의 수 — BigInt 분수. 옛 `B08_Quantity_Formula.ts`(git 8472fc9f) 의 분수 몫을 되살림.
|
||
*
|
||
* ⚠ 부동소수는 1.15×100 = 114.999… 라 버림이 114 로 틀어짐 — 실무 엑셀 `INT(x*100)/100` 이
|
||
* 뜻한 값은 십진 값의 버림이라 분수로 풀어야 맞음. 서버(파이썬 Decimal)와 1원도 안 갈림.
|
||
* ========================================================================== */
|
||
|
||
export class FormulaError extends Error {}
|
||
|
||
export interface Frac {
|
||
n: bigint;
|
||
d: bigint;
|
||
}
|
||
|
||
/** 정수로 떨구는 법 — floor = 엑셀 INT · trunc = ROUNDDOWN · away = ROUNDUP · round = ROUND. */
|
||
export type IntMode = "floor" | "trunc" | "away" | "round";
|
||
|
||
const DIGITS = 30;
|
||
const TEN = 10n;
|
||
|
||
const abs = (x: bigint): bigint => (x < 0n ? -x : x);
|
||
|
||
function gcd(a: bigint, b: bigint): bigint {
|
||
a = abs(a);
|
||
b = abs(b);
|
||
while (b) [a, b] = [b, a % b];
|
||
return a || 1n;
|
||
}
|
||
|
||
export function frac(n: bigint, d = 1n): Frac {
|
||
if (d === 0n) throw new FormulaError("0 으로 나눔");
|
||
if (d < 0n) [n, d] = [-n, -d];
|
||
const g = gcd(n, d);
|
||
return { n: n / g, d: d / g };
|
||
}
|
||
|
||
export const ZERO = frac(0n);
|
||
export const add = (a: Frac, b: Frac): Frac => frac(a.n * b.d + b.n * a.d, a.d * b.d);
|
||
export const sub = (a: Frac, b: Frac): Frac => frac(a.n * b.d - b.n * a.d, a.d * b.d);
|
||
export const mul = (a: Frac, b: Frac): Frac => frac(a.n * b.n, a.d * b.d);
|
||
export function div(a: Frac, b: Frac): Frac {
|
||
if (b.n === 0n) throw new FormulaError("0 으로 나눔");
|
||
return frac(a.n * b.d, a.d * b.n);
|
||
}
|
||
export function cmp(a: Frac, b: Frac): number {
|
||
const left = a.n * b.d;
|
||
const right = b.n * a.d;
|
||
return left === right ? 0 : left < right ? -1 : 1;
|
||
}
|
||
|
||
/** 십진 글(`-12.5` · `3` · `1e-3` · `1,234`)을 분수로. */
|
||
export function parseDecimal(text: string): Frac {
|
||
const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text.replace(/,/g, "").trim());
|
||
if (!match || (match[2] === "" && (match[3] ?? "") === "")) {
|
||
throw new FormulaError(`수로 읽지 못함: ${text}`);
|
||
}
|
||
const [, sign, whole, fraction = "", exponent = "0"] = match;
|
||
let n = BigInt((whole || "0") + fraction);
|
||
let d = TEN ** BigInt(fraction.length);
|
||
const e = Number(exponent);
|
||
if (e > 0) n *= TEN ** BigInt(e);
|
||
if (e < 0) d *= TEN ** BigInt(-e);
|
||
return frac(sign === "-" ? -n : n, d);
|
||
}
|
||
|
||
/** 수로 온 값은 **보이는 십진 표기**로 읽음 — 0.15 를 이진 근사값으로 받지 않음. */
|
||
export function toFrac(value: number | string): Frac {
|
||
if (typeof value === "number") {
|
||
if (!Number.isFinite(value)) throw new FormulaError(`수가 아님: ${value}`);
|
||
return parseDecimal(String(value));
|
||
}
|
||
return parseDecimal(value);
|
||
}
|
||
|
||
export function toInteger(x: Frac, mode: IntMode): bigint {
|
||
const q = x.n / x.d; // 0 쪽으로 자름
|
||
const r = x.n % x.d;
|
||
if (r === 0n) return q;
|
||
const negative = x.n < 0n;
|
||
if (mode === "floor") return negative ? q - 1n : q;
|
||
if (mode === "trunc") return q;
|
||
if (mode === "away") return negative ? q - 1n : q + 1n;
|
||
// 사사오입 — 엑셀 ROUND 와 같이 0 에서 먼 쪽
|
||
return abs(r) * 2n >= x.d ? (negative ? q - 1n : q + 1n) : q;
|
||
}
|
||
|
||
/** 소수 `digits` 자리에서 떨굼(음수 자리 = 십 · 백 자리). */
|
||
export function roundAt(x: Frac, digits: number, mode: IntMode): Frac {
|
||
const places = Math.trunc(digits);
|
||
const scale = places >= 0 ? frac(TEN ** BigInt(places)) : frac(1n, TEN ** BigInt(-places));
|
||
return div(frac(toInteger(mul(x, scale), mode)), scale);
|
||
}
|
||
|
||
/** 분수를 십진 글로 — 끝나는 소수는 그대로 · 안 끝나면 30자리에서 사사오입. */
|
||
export function fracToString(x: Frac): string {
|
||
const scaled = toInteger(mul(x, frac(TEN ** BigInt(DIGITS))), "round");
|
||
const negative = scaled < 0n;
|
||
const digits = abs(scaled)
|
||
.toString()
|
||
.padStart(DIGITS + 1, "0");
|
||
const whole = digits.slice(0, -DIGITS);
|
||
const fraction = digits.slice(-DIGITS).replace(/0+$/, "");
|
||
const body = fraction ? `${whole}.${fraction}` : whole;
|
||
return negative && body !== "0" ? `-${body}` : body;
|
||
}
|