/* ============================================================================= * spreadsheet_functions_stat.ts (주인 B · 3차 · 2026-09-27) * 통계 함수 — MEDIAN · MODE · STDEV(.S/.P) · VAR · RANK(.EQ) · LARGE · SMALL · * PERCENTILE · QUARTILE · COUNTBLANK. 형 바꾸기 도우미는 `spreadsheet_functions_math.ts` 공용 것을 씀. * ========================================================================== */ import { ZERO, add, cmp, div, frac, mul, sub, type Frac } from "@ui/sheet/ui_template_sheet_frac"; import type { ErrorValue, EvalContext, EvalResult, LazyArg, RangeValue, SheetFunction, } from "./spreadsheet_types"; import { collectNumbers, fracToDouble, fromDouble, isErr, isFrac, isRange, mkErr, num, } from "./spreadsheet_functions_math"; function rangeNumbers(range: RangeValue): Frac[] | ErrorValue { const out: Frac[] = []; for (let r = 0; r < range.rows; r++) { for (let c = 0; c < range.cols; c++) { const cell = range.at(r, c); if (isErr(cell)) return cell; if (isFrac(cell)) out.push(cell); } } return out; } function meanAndSumSq(values: Frac[]): { mean: Frac; sumSq: Frac } { const n = frac(BigInt(values.length)); const mean = div(values.reduce(add, ZERO), n); const sumSq = values.reduce((acc, v) => add(acc, mul(sub(v, mean), sub(v, mean))), ZERO); return { mean, sumSq }; } function sampleVariance(values: Frac[]): Frac | ErrorValue { if (values.length < 2) return mkErr("#DIV/0!"); return div(meanAndSumSq(values).sumSq, frac(BigInt(values.length - 1))); } function populationVariance(values: Frac[]): Frac | ErrorValue { if (values.length < 1) return mkErr("#DIV/0!"); return div(meanAndSumSq(values).sumSq, frac(BigInt(values.length))); } function stdevOf(variance: Frac | ErrorValue): Frac | ErrorValue { if (isErr(variance)) return variance; return fromDouble(Math.sqrt(fracToDouble(variance))); } /** 정렬한 값에서 0 ≤ k ≤ 1 자리의 백분위(직선 보간 · 엑셀 PERCENTILE.INC 와 같음) */ function percentileOf(values: Frac[], k: number): Frac | ErrorValue { if (values.length === 0) return mkErr("#NUM!"); if (k < 0 || k > 1) return mkErr("#NUM!"); const sorted = [...values].sort((a, b) => cmp(a, b)); const pos = k * (sorted.length - 1); const lowIdx = Math.floor(pos); const highIdx = Math.ceil(pos); if (lowIdx === highIdx) return sorted[lowIdx]; const weight = fromDouble(pos - lowIdx); if (isErr(weight)) return weight; return add(sorted[lowIdx], mul(weight, sub(sorted[highIdx], sorted[lowIdx]))); } function nthOrderStat(range: RangeValue, k: number, largest: boolean): Frac | ErrorValue { const values = rangeNumbers(range); if (isErr(values)) return values; if (k < 1 || k > values.length) return mkErr("#NUM!"); const sorted = [...values].sort((a, b) => cmp(a, b)); return sorted[largest ? sorted.length - k : k - 1]; } function rankCall(args: LazyArg[], _ctx: EvalContext): EvalResult { const target = num(args[0]()); if (isErr(target)) return target; const range = args[1](); if (!isRange(range)) return mkErr("#VALUE!"); const orderArg = args.length > 2 ? num(args[2]()) : ZERO; if (isErr(orderArg)) return orderArg; const ascending = cmp(orderArg, ZERO) !== 0; let rank = 1; let found = false; for (let r = 0; r < range.rows; r++) { for (let c = 0; c < range.cols; c++) { const cell = range.at(r, c); if (!isFrac(cell)) continue; if (cmp(cell, target) === 0) found = true; if (ascending ? cmp(cell, target) < 0 : cmp(cell, target) > 0) rank++; } } return found ? frac(BigInt(rank)) : mkErr("#N/A"); } export const STAT_FUNCTIONS: Record = { MEDIAN: { min: 1, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; if (values.length === 0) return mkErr("#NUM!"); const sorted = [...values].sort((a, b) => cmp(a, b)); const mid = Math.floor(sorted.length / 2); if (sorted.length % 2 === 1) return sorted[mid]; return div(add(sorted[mid - 1], sorted[mid]), frac(2n)); }, }, MODE: { min: 1, call: (args) => { const values = collectNumbers(args, true, true); if (isErr(values)) return values; let bestIdx = -1; let bestCount = 1; for (let i = 0; i < values.length; i++) { let count = 0; for (const v of values) if (cmp(values[i], v) === 0) count++; if (count > bestCount) { bestCount = count; bestIdx = i; } } return bestIdx < 0 ? mkErr("#N/A") : values[bestIdx]; }, }, STDEV: { min: 1, call: (args) => { const values = collectNumbers(args, true, true); return isErr(values) ? values : stdevOf(sampleVariance(values)); }, }, "STDEV.S": { min: 1, call: (args) => { const values = collectNumbers(args, true, true); return isErr(values) ? values : stdevOf(sampleVariance(values)); }, }, "STDEV.P": { min: 1, call: (args) => { const values = collectNumbers(args, true, true); return isErr(values) ? values : stdevOf(populationVariance(values)); }, }, VAR: { min: 1, call: (args) => { const values = collectNumbers(args, true, true); return isErr(values) ? values : sampleVariance(values); }, }, RANK: { min: 2, max: 3, call: rankCall }, "RANK.EQ": { min: 2, max: 3, call: rankCall }, LARGE: { min: 2, max: 2, call: (args) => { const range = args[0](); if (!isRange(range)) return mkErr("#VALUE!"); const kArg = num(args[1]()); if (isErr(kArg)) return kArg; return nthOrderStat(range, Math.trunc(fracToDouble(kArg)), true); }, }, SMALL: { min: 2, max: 2, call: (args) => { const range = args[0](); if (!isRange(range)) return mkErr("#VALUE!"); const kArg = num(args[1]()); if (isErr(kArg)) return kArg; return nthOrderStat(range, Math.trunc(fracToDouble(kArg)), false); }, }, PERCENTILE: { min: 2, max: 2, call: (args) => { const range = args[0](); if (!isRange(range)) return mkErr("#VALUE!"); const kArg = num(args[1]()); if (isErr(kArg)) return kArg; const values = rangeNumbers(range); if (isErr(values)) return values; return percentileOf(values, fracToDouble(kArg)); }, }, QUARTILE: { min: 2, max: 2, call: (args) => { const range = args[0](); if (!isRange(range)) return mkErr("#VALUE!"); const qArg = num(args[1]()); if (isErr(qArg)) return qArg; const q = Math.trunc(fracToDouble(qArg)); if (q < 0 || q > 4) return mkErr("#NUM!"); const values = rangeNumbers(range); if (isErr(values)) return values; return percentileOf(values, q / 4); }, }, COUNTBLANK: { min: 1, max: 1, call: (args) => { const range = args[0](); if (!isRange(range)) return mkErr("#VALUE!"); let count = 0; for (let r = 0; r < range.rows; r++) { for (let c = 0; c < range.cols; c++) if (range.at(r, c) === null) count++; } return frac(BigInt(count)); }, }, };