- init
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* AST evaluator.
|
||||
*
|
||||
* `evaluate(ast, host)` walks the tree and returns a {@link CellValue}. It never
|
||||
* recurses into other cells directly -- it asks the {@link EngineHost} for their
|
||||
* *already computed* values, which the recalculation driver guarantees are ready
|
||||
* (topological order). Circular cells are detected up front by the graph module
|
||||
* and never reach the evaluator.
|
||||
*
|
||||
* Evaluation model
|
||||
* ----------------
|
||||
* - Arithmetic is numeric. `null` (empty cell) counts as 0. A numeric-looking
|
||||
* string is coerced; any other string yields `#VALUE!`.
|
||||
* - Any error among operands / arguments propagates unchanged (first wins).
|
||||
* - A range may only appear as a function argument. Used in scalar position it
|
||||
* yields `#VALUE!`.
|
||||
* - Unknown sheet name -> `#REF`. Unknown function -> `#NAME?`.
|
||||
* - Division by zero -> `#DIV/0!`.
|
||||
*/
|
||||
|
||||
import type { Node } from './parser';
|
||||
import { getFunction } from './functions';
|
||||
import { expandRange } from './refs';
|
||||
import { FormulaError, isError, type CellValue, type EngineHost } from './types';
|
||||
|
||||
export function evaluate(ast: Node, host: EngineHost): CellValue {
|
||||
return evalScalar(ast, host);
|
||||
}
|
||||
|
||||
function evalScalar(node: Node, host: EngineHost): CellValue {
|
||||
switch (node.kind) {
|
||||
case 'number':
|
||||
return node.value;
|
||||
case 'string':
|
||||
return node.value;
|
||||
|
||||
case 'unary': {
|
||||
const v = toNumber(evalScalar(node.operand, host));
|
||||
if (isError(v)) return v;
|
||||
return node.op === '-' ? -v : v;
|
||||
}
|
||||
|
||||
case 'binary': {
|
||||
if (node.op === '^') {
|
||||
const l = toNumber(evalScalar(node.left, host));
|
||||
if (isError(l)) return l;
|
||||
const r = toNumber(evalScalar(node.right, host));
|
||||
if (isError(r)) return r;
|
||||
return Math.pow(l, r);
|
||||
}
|
||||
const l = toNumber(evalScalar(node.left, host));
|
||||
if (isError(l)) return l;
|
||||
const r = toNumber(evalScalar(node.right, host));
|
||||
if (isError(r)) return r;
|
||||
switch (node.op) {
|
||||
case '+':
|
||||
return l + r;
|
||||
case '-':
|
||||
return l - r;
|
||||
case '*':
|
||||
return l * r;
|
||||
case '/':
|
||||
return r === 0 ? new FormulaError('#DIV/0!') : l / r;
|
||||
}
|
||||
return new FormulaError('#ERR', `unknown operator ${node.op}`);
|
||||
}
|
||||
|
||||
case 'cell': {
|
||||
const tableId =
|
||||
node.qualifier == null ? host.homeTableId : host.resolveTableName(node.qualifier);
|
||||
if (!tableId) return new FormulaError('#REF', `unknown table '${node.qualifier}'`);
|
||||
return host.getComputed(tableId, node.col, node.row);
|
||||
}
|
||||
|
||||
case 'range':
|
||||
// A bare range can't collapse to a scalar.
|
||||
return new FormulaError('#VALUE!', 'range used where a value is expected');
|
||||
|
||||
case 'func': {
|
||||
const fn = getFunction(node.name);
|
||||
if (!fn) return new FormulaError('#NAME?', `unknown function ${node.name}`);
|
||||
const values: CellValue[] = [];
|
||||
for (const arg of node.args) {
|
||||
const part = evalArg(arg, host);
|
||||
if (isError(part)) return part;
|
||||
for (const v of part) {
|
||||
if (isError(v)) return v;
|
||||
values.push(v);
|
||||
}
|
||||
}
|
||||
return fn(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Evaluate one function argument into a flat list of values (ranges expand). */
|
||||
function evalArg(node: Node, host: EngineHost): FormulaError | CellValue[] {
|
||||
if (node.kind === 'range') {
|
||||
const tableId =
|
||||
node.qualifier == null ? host.homeTableId : host.resolveTableName(node.qualifier);
|
||||
if (!tableId) return new FormulaError('#REF', `unknown table '${node.qualifier}'`);
|
||||
return expandRange(node.start, node.end).map((c) => host.getComputed(tableId, c.col, c.row));
|
||||
}
|
||||
const v = evalScalar(node, host);
|
||||
return [v];
|
||||
}
|
||||
|
||||
/** Coerce a cell value to a number for arithmetic, or return an error value. */
|
||||
function toNumber(v: CellValue): number | FormulaError {
|
||||
if (isError(v)) return v;
|
||||
if (v === null) return 0;
|
||||
if (typeof v === 'number') return v;
|
||||
const trimmed = v.trim();
|
||||
if (trimmed === '') return 0;
|
||||
const n = Number(trimmed);
|
||||
return Number.isFinite(n) ? n : new FormulaError('#VALUE!', `'${v}' is not a number`);
|
||||
}
|
||||
Reference in New Issue
Block a user