Add user-defined functions, editable exclusively via a new UI
Document-wide, named formula functions (e.g. MARGE(revenue; cost) = (revenue - cost) / revenue) callable from any cell like a built-in (=MARGE(Umsatz!B2; Umsatz!C2)). Created, edited and deleted only through a new "𝑓 Funktionen" toolbar button -> modal (list + shared edit form), never via JSON handedit. Engine: a call is inlined into the caller's AST at parse time (src/engine/userFunctions.ts's expandUserFunctions), so evaluator.ts and graph.ts need no changes -- dependencies on cells referenced only inside a function body (not in the call) are picked up for free. parser.ts gains an optional paramNames argument to parseFormula() producing a new ParamNode leaf, used only when parsing a function body. Store: addCustomFunction/updateCustomFunction/deleteCustomFunction with inline (not toast) validation -- name/parameter shape, no collision with a built-in or another function, the body parses, and a definition-time cycle check across the whole call graph. Renaming/deleting a function mirrors table rename/delete: existing calls are not rewritten, they become #NAME? on the next rebuild. Fully undo/redo-able and part of the JSON document (customFunctions: [], migrated for v1 and older v2 docs). 178 tests passing (32 new), vue-tsc and eslint clean. Manually verified end-to-end in the running app via Playwright: define/call/recalculate, rename -> #NAME?, a closing cycle rejected inline at save, undo restores the function library. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
import { DependencyGraph } from './graph';
|
||||
import { collectRefs, parseFormula, type Node } from './parser';
|
||||
import { expandRange } from './refs';
|
||||
import { buildFunctionIndex, expandUserFunctions, type FunctionDef } from './userFunctions';
|
||||
import {
|
||||
FormulaError,
|
||||
isError,
|
||||
@@ -66,9 +67,12 @@ export class Workbook implements EngineHost {
|
||||
private readonly asts = new Map<string, Node | FormulaError>();
|
||||
/** cellKey -> computed value */
|
||||
private readonly values = new Map<string, CellValue>();
|
||||
/** user-defined function library for this document, name.toUpperCase() -> def. */
|
||||
private readonly functionIndex: Map<string, FunctionDef>;
|
||||
|
||||
constructor(tables: TableModel[]) {
|
||||
constructor(tables: TableModel[], functionDefs: FunctionDef[] = []) {
|
||||
this.tables = tables;
|
||||
this.functionIndex = buildFunctionIndex(functionDefs);
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
@@ -150,6 +154,7 @@ export class Workbook implements EngineHost {
|
||||
let ast: Node;
|
||||
try {
|
||||
ast = parseFormula(raw);
|
||||
ast = expandUserFunctions(ast, this.functionIndex);
|
||||
} catch (e) {
|
||||
// a bad formula is a cell-level error, never a crash
|
||||
this.asts.set(key, new FormulaError('#ERR', (e as Error).message));
|
||||
|
||||
Reference in New Issue
Block a user