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:
+34
-4
@@ -16,8 +16,9 @@ löst `Umsatz!B2` (bzw. `Umsatz::B2`) von jedem Blatt aus identisch auf.
|
||||
| `evaluator.ts` | AST + `EngineHost` → `CellValue`. Numerisches Auswertungsmodell, Fehler-Propagierung. |
|
||||
| `graph.ts` | Dokumentweiter Abhängigkeitsgraph: `affected()`, `topoOrder()`, Zirkel-Erkennung (iterative DFS, white/grey/black). |
|
||||
| `refs.ts` | A1 ↔ `(col,row)` inkl. `$`-Sperrachsen, Spaltenbuchstaben, Bereichs-Expansion, `F4`-Zyklus (`cycleReferenceLocks`). |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`) als Tabelle `{ NAME → fn(values) }`. |
|
||||
| `workbook.ts` | Bindeglied: flache Liste von **Tabellen** + rohe Eingaben + ASTs + Werte + Graph. Inkrementelle Neuberechnung. Implementiert `EngineHost`. |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`) als Tabelle `{ NAME → fn(values) }`. Eingebaute Namen sind nie überschreibbar. |
|
||||
| `userFunctions.ts` | Benutzerdefinierte Funktionen (dokumentweite Bibliothek, angelegt über die UI — siehe Haupt-`README.md`). `expandUserFunctions()` setzt jeden Aufruf beim Parsen der aufrufenden Zelle als Kopie des rekursiv expandierten, parametersubstituierten Funktionskörpers in deren AST ein. |
|
||||
| `workbook.ts` | Bindeglied: flache Liste von **Tabellen** + rohe Eingaben + ASTs + Werte + Graph. Inkrementelle Neuberechnung. Implementiert `EngineHost`. Ruft `expandUserFunctions()` direkt nach `parseFormula()`. |
|
||||
| `types.ts` | Gemeinsame Typen, `FormulaError`, `keyOf()`. |
|
||||
|
||||
Die Engine kennt **kein** Vue. Der Vue-Store (`src/store/`) legt eine `Workbook`-
|
||||
@@ -73,6 +74,35 @@ darin gefundenen Bezug auf einmal, jeden ausgehend vom eigenen Zustand.
|
||||
Tabellennamen werden **case-insensitiv** und whitespace-tolerant aufgelöst:
|
||||
`Kosten!A2`, `'Kosten'!A2`, `kosten!a2`, `Kosten::A2` sind identisch.
|
||||
|
||||
`parseFormula(src, paramNames?)` akzeptiert einen optionalen zweiten
|
||||
Parameter: eine Liste von Namen, gegen die ein bare `IDENT` (das nicht wie
|
||||
eine Zelle aussieht) zusätzlich geprüft wird — ein Treffer erzeugt statt eines
|
||||
`ParseError` einen `ParamNode`. Nur beim Parsen eines **Funktionskörpers**
|
||||
verwendet (`userFunctions.ts`); ohne das Argument ist das Verhalten exakt wie
|
||||
zuvor, jeder bare Bezeichner ohne Zellform bleibt ein `ParseError`.
|
||||
|
||||
## Benutzerdefinierte Funktionen
|
||||
|
||||
Kein Laufzeit-Closure, keine Änderung an `evaluator.ts`/`graph.ts`: ein Aufruf
|
||||
einer benutzerdefinierten Funktion wird **beim Parsen der aufrufenden Zelle
|
||||
in deren AST eingesetzt** (Inlining). `userFunctions.ts#expandUserFunctions`
|
||||
läuft direkt nach `parseFormula()` in `workbook.ts#parseCell` und ersetzt
|
||||
jeden `FuncNode`, dessen Name in der Funktions-Bibliothek steht (eingebaute
|
||||
Namen haben Vorrang und werden nie ersetzt), durch eine Kopie des – selbst
|
||||
wieder rekursiv expandierten – Funktionskörpers, mit jedem `ParamNode` durch
|
||||
den zugehörigen Argument-Teilbaum des Aufrufers ersetzt. Danach sieht die
|
||||
gespeicherte AST der Zelle so aus, als wäre die Formel von Hand ausgeschrieben
|
||||
worden – `collectRefs`/`precedentsOf`/`evaluate` laufen unverändert, auch für
|
||||
Zellen, die *nur* im Funktionskörper referenziert werden, nicht im Aufruf.
|
||||
|
||||
Ein Arity-Mismatch oder eine Expansionstiefe über 32 (Sicherheitsnetz gegen
|
||||
einen Definitionszyklus, der eigentliche Schutz ist die Zyklus-Prüfung beim
|
||||
**Speichern** einer Funktion im Store) wirft in `expandUserFunctions` einen
|
||||
`Error`, den `parseCell` genau wie einen `ParseError` zu `#ERR` macht. Ein
|
||||
unbekannter Funktionsname – eingebaut oder benutzerdefiniert – bleibt exakt
|
||||
derselbe Pfad wie zuvor: der `FuncNode` bleibt unverändert stehen,
|
||||
`evaluate()`s `getFunction()` findet ihn nicht → `#NAME?`.
|
||||
|
||||
## Auswertungsmodell
|
||||
|
||||
- **Arithmetik ist numerisch.** Leere Zelle (`null`) zählt als `0`. Eine
|
||||
@@ -104,8 +134,8 @@ rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
|------------|-----------|
|
||||
| `#REF` | ungültiger/gelöschter Bezug **oder** unbekannter Tabellenname |
|
||||
| `#DIV/0!` | Division durch 0 (auch `AVG` ohne Zahlen) |
|
||||
| `#NAME?` | unbekannte Funktion |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`) |
|
||||
| `#NAME?` | unbekannte Funktion (eingebaut oder benutzerdefiniert) |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`); auch Arity-Mismatch oder Expansionstiefen-Limit bei einer benutzerdefinierten Funktion |
|
||||
| `#CIRC` | Zirkelbezug – auch tabellen-/blattübergreifend |
|
||||
| `#VALUE!` | Typfehler in Arithmetik (Text × Zahl, Bereich als Skalar). *Erweiterung über die Aufgabenstellung hinaus, dokumentiert.* |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user