/** * Built-in functions: a plain table { NAME -> fn(values) }. * * By the time a function runs, the evaluator has already: * - resolved every range argument into a flat list of cell values, * - flattened scalar arguments into that same list, * - checked for errors among the values (an error short-circuits and is * returned instead of calling the function). * * So a function only ever sees `(number | string | null)[]`. Each function * decides for itself how to treat text and empty cells. Per spec: * - empty cell -> ignored by every aggregate, never counted by COUNT * - text -> ignored by the numeric aggregates */ import { FormulaError, type CellValue } from './types'; export type FunctionFn = (values: CellValue[]) => CellValue; /** Keep only real numbers (finite). */ function nums(values: CellValue[]): number[] { const out: number[] = []; for (const v of values) { if (typeof v === 'number' && Number.isFinite(v)) out.push(v); } return out; } export const FUNCTIONS: Record = { SUM(values) { return nums(values).reduce((a, b) => a + b, 0); }, AVG(values) { const n = nums(values); if (n.length === 0) return new FormulaError('#DIV/0!', 'AVG of no numbers'); return n.reduce((a, b) => a + b, 0) / n.length; }, MIN(values) { const n = nums(values); return n.length ? Math.min(...n) : 0; }, MAX(values) { const n = nums(values); return n.length ? Math.max(...n) : 0; }, COUNT(values) { return nums(values).length; }, }; // AVERAGE is an alias of AVG. FUNCTIONS.AVERAGE = FUNCTIONS.AVG; export function hasFunction(name: string): boolean { return Object.prototype.hasOwnProperty.call(FUNCTIONS, name.toUpperCase()); } export function getFunction(name: string): FunctionFn | undefined { return FUNCTIONS[name.toUpperCase()]; }