/* ============================================================================= * spreadsheet_eval.ts (주인 A) * 나무 풀기 · 오류 값 전파 · 형 바꾸기(엑셀 규칙). B 의 함수들이 형 바꾸기 도우미를 씀. * 빈 칸 = 수에서 0 · 글에서 "" · `"12"+1 = 13` · `TRUE+1 = 2` · 글이 수로 안 읽히면 `#VALUE!`. * 무리수(SQRT · 삼각 · PI · 소수 거듭제곱)는 double → 유효 15 자리 십진 → 분수(B · A 같이 `fromDouble`). * 오류 전파 = 왼쪽부터 처음 만난 오류. 비교 = 수 < 글 < 참거짓 · 글은 대소문자 가리지 않음. * ========================================================================== */ import { add, cmp, div, frac, fracToString, mul, parseDecimal, roundAt, sub, toFrac, ZERO, } from "@ui/sheet/ui_template_sheet_frac"; import { normRange } from "./spreadsheet_address"; import { getFunction } from "./spreadsheet_functions"; import type { CalcValue, ErrorCode, ErrorValue, EvalContext, EvalResult, FormulaNode, Frac, RangeValue, Scalar, } from "./spreadsheet_types"; export function err(code: ErrorCode, why?: string): ErrorValue { return why === undefined ? { error: code } : { error: code, why }; } export function isError(value: unknown): value is ErrorValue { return typeof value === "object" && value !== null && "error" in value; } export function isFrac(value: unknown): value is Frac { return typeof value === "object" && value !== null && "n" in value && "d" in value; } export function isRange(value: unknown): value is RangeValue { return typeof value === "object" && value !== null && (value as RangeValue).kind === "range"; } const ONE = frac(1n); const HUNDRED = frac(100n); const BIG_D = 10n ** 40n; /** 분모가 너무 커지면(나눗셈 사슬) 30 자리 십진으로 굳힘 */ export function tame(x: Frac): Frac { return x.d > BIG_D ? parseDecimal(fracToString(x)) : x; } /** 범위 → 칸 하나(1×1 일 때만) · 아니면 `#VALUE!` */ function single(value: EvalResult): Scalar { if (!isRange(value)) return value; return value.rows === 1 && value.cols === 1 ? value.at(0, 0) : err("#VALUE!", "범위를 값 하나로 못 씀"); } /** 수로 — 빈 칸 0 · 참거짓 1/0 · 수 글 · 아니면 `#VALUE!` · 범위는 칸 하나일 때만 */ export function asNumber(value: EvalResult): Frac | ErrorValue { const v = single(value); if (v === null) return ZERO; if (typeof v === "boolean") return v ? ONE : ZERO; if (typeof v === "string") { const t = v.trim(); const pct = t.endsWith("%"); try { const n = parseDecimal(pct ? t.slice(0, -1) : t); return pct ? div(n, HUNDRED) : n; } catch { return err("#VALUE!", `수로 못 읽는 글: "${v}"`); } } return v; } /** 일반 형식 글 — 유효 15 자리 · 1e15 넘거나 1e-9 밑이면 `1.5E+18` 꼴(엑셀 `&` 와 같게) */ export function numberText(x: Frac): string { if (x.n === 0n) return "0"; const d = parseFloat(fracToString(x)); const e = Math.floor(Math.log10(Math.abs(d))); if (e >= 15 || e < -9) { const [m, ex] = d.toExponential(14).split("e"); const mant = m.replace(/\.?0+$/, ""); const n = Number(ex); return `${mant}E${n < 0 ? "-" : "+"}${String(Math.abs(n)).padStart(2, "0")}`; } return fracToString(roundAt(x, 14 - e, "round")); } /** 글로 — 수는 일반 형식 글(엑셀 `&` 와 같게) · 참거짓 `TRUE`/`FALSE` */ export function asText(value: EvalResult): string | ErrorValue { const v = single(value); if (v === null) return ""; if (typeof v === "string" || isError(v)) return v; if (typeof v === "boolean") return v ? "TRUE" : "FALSE"; return numberText(v); } export function asBool(value: EvalResult): boolean | ErrorValue { const v = single(value); if (v === null) return false; if (typeof v === "boolean" || isError(v)) return v; if (typeof v === "string") { const t = v.toUpperCase(); return t === "TRUE" ? true : t === "FALSE" ? false : err("#VALUE!", `참거짓으로 못 읽는 글: "${v}"`); } return v.n !== 0n; } /** double → 유효 15 자리 십진 → 분수 · NaN · 무한은 `#NUM!` */ export function fromDouble(x: number): Frac | ErrorValue { if (!Number.isFinite(x)) return err("#NUM!", "수가 너무 크거나 뜻이 없음"); return x === 0 ? ZERO : parseDecimal(x.toPrecision(15)); } export const toDouble = (x: Frac): number => parseFloat(fracToString(x)); /** 저장 모양 — 수는 십진 글(30 자리까지) */ export function toCalcValue(value: Scalar): CalcValue { if (value === null) return { 수: "0" }; if (typeof value === "string") return { 글: value }; if (typeof value === "boolean") return { 참: value }; if (isError(value)) return value.why ? { 오류: value.error, 까닭: value.why } : { 오류: value.error }; return { 수: fracToString(value) }; } // ── 연산 ──────────────────────────────────────────────────────────────────── function power(a: Frac, b: Frac): Frac | ErrorValue { if (a.n === 0n) { if (b.n === 0n) return err("#NUM!", "0^0"); return b.n < 0n ? err("#DIV/0!", "0 의 음수 거듭제곱") : ZERO; } if (b.d === 1n) { const e = b.n < 0n ? -b.n : b.n; const bits = BigInt(a.n.toString(2).length + a.d.toString(2).length); // ponytail: 4,000 비트 넘는 정확 거듭제곱은 double 로 — 실무 식엔 없음 if (bits * e <= 4000n) { const r = frac(a.n ** e, a.d ** e); return tame(b.n < 0n ? div(ONE, r) : r); } } else if (a.n < 0n) return err("#NUM!", "음수의 소수 거듭제곱"); return fromDouble(Math.pow(toDouble(a), toDouble(b))); } function arith(op: string, a: Frac, b: Frac): Frac | ErrorValue { switch (op) { case "+": return tame(add(a, b)); case "-": return tame(sub(a, b)); case "*": return tame(mul(a, b)); case "/": return b.n === 0n ? err("#DIV/0!", "0 으로 나눔") : tame(div(a, b)); default: return power(a, b); } } const RANK = (v: Scalar) => (isFrac(v) ? 0 : typeof v === "string" ? 1 : 2); /** 엑셀 비교 — 빈 칸은 맞은편 형의 빈 값(0 · "" · FALSE) · 형이 다르면 수 < 글 < 참거짓 */ export function compareValues(a: Scalar, b: Scalar): number { const blank = (other: Scalar): Scalar => isFrac(other) ? ZERO : typeof other === "string" ? "" : typeof other === "boolean" ? false : ZERO; if (a === null) a = blank(b); if (b === null) b = blank(a); const ra = RANK(a); const rb = RANK(b); if (ra !== rb) return ra < rb ? -1 : 1; if (isFrac(a)) return cmp(a, b as Frac); if (typeof a === "string") { const x = a.toLowerCase(); const y = (b as string).toLowerCase(); return x === y ? 0 : x < y ? -1 : 1; } return a === b ? 0 : a ? 1 : -1; } function compareOp(op: string, a: Scalar, b: Scalar): boolean { const c = compareValues(a, b); return op === "=" ? c === 0 : op === "<>" ? c !== 0 : op === "<" ? c < 0 : op === "<=" ? c <= 0 : op === ">" ? c > 0 : c >= 0; } // ── 풀기 ──────────────────────────────────────────────────────────────────── export function evaluate(node: FormulaNode, ctx: EvalContext): EvalResult { switch (node.type) { case "number": return node.value; case "string": return node.value; case "bool": return node.value; case "error": return err(node.code); case "name": return err("#NAME?", `모르는 이름: ${node.name}`); case "cell": case "range": { const sheet = node.sheet === null ? ctx.sheet : ctx.sheetId(node.sheet); if (sheet === null) return err("#REF!", `없는 시트: ${node.sheet}`); if (node.type === "cell") return ctx.cell(sheet, node.ref.r, node.ref.c); return ctx.range(sheet, normRange(node.from.r, node.from.c, node.to.r, node.to.c)); } case "unary": { const v = evaluate(node.arg, ctx); if (node.op === "+") return single(v); const n = asNumber(v); if (isError(n)) return n; return node.op === "-" ? frac(-n.n, n.d) : div(n, HUNDRED); } case "binary": { const left = evaluate(node.left, ctx); const right = evaluate(node.right, ctx); if (node.op === "&") { const a = asText(left); if (isError(a)) return a; const b = asText(right); return isError(b) ? b : a + b; } if (PLAIN_OPS.has(node.op)) { const a = asNumber(left); if (isError(a)) return a; const b = asNumber(right); return isError(b) ? b : arith(node.op, a, b); } const a = single(left); if (isError(a)) return a; const b = single(right); return isError(b) ? b : compareOp(node.op, a, b); } case "call": { const fn = getFunction(node.name); if (!fn) return err("#NAME?", `모르는 함수: ${node.name}`); const n = node.args.length; if (n < fn.min || (fn.max !== undefined && n > fn.max)) { const want = fn.max === fn.min ? `${fn.min}` : `${fn.min}~${fn.max ?? ""}`; return err("#VALUE!", `${node.name} 인자 수 ${n} (받는 수 ${want})`); } return fn.call( node.args.map((a) => () => evaluate(a, ctx)), ctx, ); } } } const PLAIN_OPS = new Set(["+", "-", "*", "/", "^"]); /** 수로 온 입력 값 → 풀이 값 */ export function inputValue(v: number | string | boolean): Scalar { return typeof v === "number" ? toFrac(v) : v; }