/* ============================================================================= * spreadsheet_functions_math.ts (주인 B) * 수학 · 삼각 · 끝수 함수 + 함수 표 전체가 쓰는 형 바꾸기 도우미(공용은 여기서 export). * 뜻 · 경계값은 엑셀과 같게(Formula.js 는 시험 대조 답으로만 씀 · 속에 안 넣음). * 무리수(SQRT · 삼각 · PI · 소수 거듭제곱)는 double → 유효 15 자리 십진 → 분수. * ========================================================================== */ import { ZERO, add, cmp, div, frac, mul, parseDecimal, roundAt, sub, toInteger, type Frac, } from "@ui/sheet/ui_template_sheet_frac"; import type { ErrorCode, ErrorValue, EvalResult, LazyArg, RangeValue, Scalar, SheetFunction, } from "./spreadsheet_types"; const ONE = frac(1n); // ── 공용 도우미(math · misc 둘 다 씀) ──────────────────────────────────────── export function mkErr(code: ErrorCode, why?: string): ErrorValue { return why === undefined ? { error: code } : { error: code, why }; } export function isErr(v: unknown): v is ErrorValue { return typeof v === "object" && v !== null && "error" in (v as Record); } export function isFrac(v: unknown): v is Frac { return typeof v === "object" && v !== null && "n" in (v as Record); } export function isRange(v: EvalResult): v is RangeValue { return typeof v === "object" && v !== null && (v as RangeValue).kind === "range"; } /** 범위면 1×1 일 때만 칸 하나로 · 아니면 `#VALUE!` */ export function toScalar(v: EvalResult): Scalar | ErrorValue { if (!isRange(v)) return v; if (v.rows === 1 && v.cols === 1) return v.at(0, 0); return mkErr("#VALUE!", "범위를 칸 하나 자리에 씀"); } /** 빈 칸 0 · 참거짓 1/0 · 수 글 · 아니면 `#VALUE!` */ export function toNumber(v: Scalar): Frac | ErrorValue { if (v === null) return ZERO; if (isErr(v)) return v; if (typeof v === "boolean") return v ? ONE : ZERO; if (typeof v === "string") { try { return parseDecimal(v); } catch { return mkErr("#VALUE!", `글을 수로 못 읽음: ${v}`); } } return v; } /** 인자 하나(범위는 1×1 만) → 수 */ export function num(v: EvalResult): Frac | ErrorValue { const s = toScalar(v); return isErr(s) ? s : toNumber(s); } /** 글로 — 수는 일반 형식(간단 십진) · 참거짓 TRUE/FALSE */ export function toText(v: Scalar): string | ErrorValue { if (v === null) return ""; if (isErr(v)) return v; if (typeof v === "string") return v; if (typeof v === "boolean") return v ? "TRUE" : "FALSE"; return generalNumberText(v); } export function str(v: EvalResult): string | ErrorValue { const s = toScalar(v); return isErr(s) ? s : toText(s); } export function toBool(v: Scalar): boolean | ErrorValue { if (v === null) return false; if (isErr(v)) return v; if (typeof v === "boolean") return v; if (typeof v === "string") { const up = v.trim().toUpperCase(); if (up === "TRUE") return true; if (up === "FALSE") return false; return mkErr("#VALUE!", `글을 참거짓으로 못 읽음: ${v}`); } return cmp(v, ZERO) !== 0; } export function bool(v: EvalResult): boolean | ErrorValue { const s = toScalar(v); return isErr(s) ? s : toBool(s); } /** double → 유효 15 자리 십진 → 분수 · NaN · 무한은 `#NUM!` */ export function fromDouble(x: number): Frac | ErrorValue { if (!Number.isFinite(x)) return mkErr("#NUM!"); if (x === 0) return ZERO; return parseDecimal(x.toPrecision(15)); } export function fracToDouble(x: Frac): number { return Number(x.n) / Number(x.d); } /** 엑셀 「일반」 형식과 같은 십진 글(수식 `&` · TEXT 안 형식 자리가 없을 때) */ export function generalNumberText(x: Frac): string { const scaled = toInteger(mul(x, frac(10n ** 15n)), "round"); const negative = scaled < 0n; const digits = (negative ? -scaled : scaled).toString().padStart(16, "0"); const whole = digits.slice(0, -15).replace(/^0+(?=\d)/, ""); const fraction = digits.slice(-15).replace(/0+$/, ""); const body = fraction ? `${whole}.${fraction}` : whole; return negative && body !== "0" ? `-${body}` : body; } /** 인자 목록에서 수만 모음 — 범위는 수 아닌 값 조용히 건너뜀 · 낱값은 형 바꿈(실패시 strict 면 오류) * `propagateErrors` = 범위 · 낱값 오류를 그대로 돌려줄지(COUNT 는 false) */ export function collectNumbers( args: LazyArg[], propagateErrors: boolean, strictScalar: boolean, ): Frac[] | ErrorValue { const out: Frac[] = []; for (const a of args) { const v = a(); if (isRange(v)) { for (let r = 0; r < v.rows; r++) { for (let c = 0; c < v.cols; c++) { const cell = v.at(r, c); if (isErr(cell)) { if (propagateErrors) return cell; continue; } if (isFrac(cell)) out.push(cell); } } } else if (isErr(v)) { if (propagateErrors) return v; } else if (v !== null) { const n = toNumber(v); if (isErr(n)) { if (strictScalar) return n; } else { out.push(n); } } } return out; } function digitsArg(args: LazyArg[], index: number, fallback: number): number | ErrorValue { if (index >= args.length) return fallback; const n = num(args[index]()); return isErr(n) ? n : Math.trunc(fracToDouble(n)); } function unaryDouble(fn: (x: number) => number): SheetFunction { return { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; return fromDouble(fn(fracToDouble(n))); }, }; } // ── 함수 표 ────────────────────────────────────────────────────────────── export const MATH_FUNCTIONS: Record = { SUM: { min: 0, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; return values.reduce(add, ZERO); }, }, PRODUCT: { min: 0, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; return values.length === 0 ? ZERO : values.reduce(mul, ONE); }, }, SUMPRODUCT: { min: 1, call: (args) => { const arrays = args.map((a) => a()); const grids = arrays.map((v) => isRange(v) ? v : { rows: 1, cols: 1, at: () => (isErr(v) ? v : (v as Scalar)) }, ); const rows = grids[0].rows; const cols = grids[0].cols; for (const g of grids) { if (g.rows !== rows || g.cols !== cols) return mkErr("#VALUE!", "배열 크기가 다름"); } let sum = ZERO; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { let prod = ONE; for (const g of grids) { const n = toNumber(g.at(r, c)); if (isErr(n)) return n; prod = mul(prod, n); } sum = add(sum, prod); } } return sum; }, }, ROUND: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const d = digitsArg(args, 1, 0); if (isErr(d)) return d; return roundAt(n, d, "round"); }, }, ROUNDUP: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const d = digitsArg(args, 1, 0); if (isErr(d)) return d; return roundAt(n, d, "away"); }, }, ROUNDDOWN: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const d = digitsArg(args, 1, 0); if (isErr(d)) return d; return roundAt(n, d, "trunc"); }, }, INT: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; return frac(toInteger(n, "floor")); }, }, TRUNC: { min: 1, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const d = digitsArg(args, 1, 0); if (isErr(d)) return d; return roundAt(n, d, "trunc"); }, }, CEILING: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const sig = num(args[1]()); if (isErr(sig)) return sig; if (cmp(sig, ZERO) === 0) return ZERO; if (cmp(n, ZERO) !== 0 && cmp(n, ZERO) > 0 !== cmp(sig, ZERO) > 0) { return mkErr("#NUM!", "수 · 기준의 부호가 다름"); } return mul(frac(toInteger(div(n, sig), "away")), sig); }, }, FLOOR: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const sig = num(args[1]()); if (isErr(sig)) return sig; if (cmp(sig, ZERO) === 0) return ZERO; if (cmp(n, ZERO) !== 0 && cmp(n, ZERO) > 0 !== cmp(sig, ZERO) > 0) { return mkErr("#NUM!", "수 · 기준의 부호가 다름"); } return mul(frac(toInteger(div(n, sig), "trunc")), sig); }, }, ABS: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; return frac(n.n < 0n ? -n.n : n.n, n.d); }, }, MOD: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const d = num(args[1]()); if (isErr(d)) return d; if (cmp(d, ZERO) === 0) return mkErr("#DIV/0!"); const q = frac(toInteger(div(n, d), "floor")); return sub(n, mul(d, q)); }, }, POWER: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const p = num(args[1]()); if (isErr(p)) return p; if (p.d === 1n) { const exp = p.n; if (exp >= 0n) return frac(n.n ** exp, n.d ** exp); if (n.n === 0n) return mkErr("#DIV/0!"); return frac(n.d ** -exp, n.n ** -exp); } return fromDouble(Math.pow(fracToDouble(n), fracToDouble(p))); }, }, SQRT: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; if (cmp(n, ZERO) < 0) return mkErr("#NUM!"); return fromDouble(Math.sqrt(fracToDouble(n))); }, }, PI: { min: 0, max: 0, call: () => fromDouble(Math.PI) }, SIN: unaryDouble(Math.sin), COS: unaryDouble(Math.cos), TAN: unaryDouble(Math.tan), ASIN: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const x = fracToDouble(n); if (x < -1 || x > 1) return mkErr("#NUM!"); return fromDouble(Math.asin(x)); }, }, ACOS: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const x = fracToDouble(n); if (x < -1 || x > 1) return mkErr("#NUM!"); return fromDouble(Math.acos(x)); }, }, ATAN: unaryDouble(Math.atan), RADIANS: unaryDouble((x) => (x * Math.PI) / 180), DEGREES: unaryDouble((x) => (x * 180) / Math.PI), MIN: { min: 0, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; return values.length === 0 ? ZERO : values.reduce((a, b) => (cmp(a, b) <= 0 ? a : b)); }, }, MAX: { min: 0, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; return values.length === 0 ? ZERO : values.reduce((a, b) => (cmp(a, b) >= 0 ? a : b)); }, }, AVERAGE: { min: 1, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; if (values.length === 0) return mkErr("#DIV/0!"); return div(values.reduce(add, ZERO), frac(BigInt(values.length))); }, }, MROUND: { min: 2, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; const m = num(args[1]()); if (isErr(m)) return m; if (cmp(m, ZERO) === 0) return ZERO; if (cmp(n, ZERO) !== 0 && cmp(n, ZERO) > 0 !== cmp(m, ZERO) > 0) { return mkErr("#NUM!", "수 · 배수의 부호가 다름"); } return mul(frac(toInteger(div(n, m), "round")), m); }, }, ODD: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; let v = toInteger(n, "away"); if (v === 0n) v = 1n; else if (v % 2n === 0n) v += v < 0n ? -1n : 1n; return frac(v); }, }, EVEN: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; let v = toInteger(n, "away"); if (v % 2n !== 0n) v += v < 0n ? -1n : 1n; return frac(v); }, }, LOG: { min: 1, max: 2, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; if (cmp(n, ZERO) <= 0) return mkErr("#NUM!"); const base = args.length > 1 ? num(args[1]()) : frac(10n); if (isErr(base)) return base; if (cmp(base, ZERO) <= 0 || cmp(base, ONE) === 0) return mkErr("#NUM!"); return fromDouble(Math.log(fracToDouble(n)) / Math.log(fracToDouble(base))); }, }, LOG10: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; if (cmp(n, ZERO) <= 0) return mkErr("#NUM!"); return fromDouble(Math.log10(fracToDouble(n))); }, }, LN: { min: 1, max: 1, call: (args) => { const n = num(args[0]()); if (isErr(n)) return n; if (cmp(n, ZERO) <= 0) return mkErr("#NUM!"); return fromDouble(Math.log(fracToDouble(n))); }, }, EXP: unaryDouble(Math.exp), };