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:
@@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite-Devserver
|
||||
npm run test # Vitest-Suite (Engine + Store + IO), 146 Tests
|
||||
npm run test # Vitest-Suite (Engine + Store + IO), 178 Tests
|
||||
npm run build # Typecheck + Produktionsbuild
|
||||
npm run lint # ESLint (flat config)
|
||||
```
|
||||
@@ -147,6 +147,40 @@ Argumenttrennzeichen ebenso wie jedes andere Zeichen) beendet dieses Fenster,
|
||||
der nächste Klick fügt dann einen neuen Bezug an. Ein Klick während des
|
||||
Point-Mode ändert nie die Tabellen-/Objektauswahl im Hintergrund.
|
||||
|
||||
### Benutzerdefinierte Funktionen
|
||||
|
||||
Der Button **„𝑓 Funktionen"** in der Symbolleiste öffnet die einzige Stelle, an
|
||||
der eigene, dokumentweite Funktionen angelegt, bearbeitet und gelöscht werden
|
||||
— kein JSON-Handedit, keine Konsole. Danach in jeder Zelle aufrufbar wie ein
|
||||
eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`.
|
||||
|
||||
- **Liste** zeigt jede Funktion als `NAME(param1; param2)` — exakt die
|
||||
Aufruf-Syntax — mit **Bearb.** (öffnet dasselbe Formular, vorausgefüllt)
|
||||
und **🗑** (löscht nach Bestätigung).
|
||||
- **Formular** (Neu wie Bearbeiten): Name, eine dynamische Liste von
|
||||
Parameter-Feldern (**+ Parameter**, `×` zum Entfernen; auch 0 Parameter
|
||||
sind gültig — dann ist es eine benannte Konstante/Formel), und ein
|
||||
Formel-Feld optisch identisch zur Formelleiste — **ohne** führendes `=`.
|
||||
Ein Parametername verhält sich im Funktionskörper wie eine Zelle mit
|
||||
eigenem Wert; er darf selbst nicht wie ein Zellbezug aussehen (`A1`, `$B2`
|
||||
o. ä. sind ungültig).
|
||||
- **Validierung beim Speichern**, mit **Inline-Fehlermeldung** direkt im
|
||||
Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer
|
||||
eingebauten oder einer anderen benutzerdefinierten Funktion, ungültiger
|
||||
Name/Parameter, ein nicht parsender Formelkörper, und ein **Funktions-
|
||||
Zyklus** (A ruft B, B ruft A, auch über mehrere Stufen) — wird abgelehnt,
|
||||
bevor je eine `#ERR`-Zelle entsteht.
|
||||
- **Umbenennen/Löschen** aktualisiert **keine** bestehenden Formeltexte —
|
||||
genau wie beim Umbenennen/Löschen einer Tabelle. Zellen, die die
|
||||
betroffene Funktion aufrufen, zeigen danach `#NAME?`.
|
||||
- Ein Aufruf wird **beim Parsen der aufrufenden Zelle in ihre AST eingesetzt**
|
||||
(Inlining, keine Laufzeit-Closures) — siehe
|
||||
[`src/engine/userFunctions.ts`](src/engine/userFunctions.ts) und die
|
||||
Architektur-Tabelle unten. Dadurch braucht Evaluator/Graph keine Änderung,
|
||||
und auch Zellen, die *nur* im Funktionskörper referenziert werden (nicht im
|
||||
Aufruf), lösen bei Änderung korrekt eine Neuberechnung aus.
|
||||
- Vollständig **undo-/redo-fähig** und Teil des JSON-Exports/-Imports.
|
||||
|
||||
### Zell-Formatierung
|
||||
|
||||
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im
|
||||
@@ -250,15 +284,22 @@ darauf, `$` hin oder her. `F4` schaltet den Bezug unter dem Cursor durch die
|
||||
vier Kombinationen.
|
||||
|
||||
**Funktionen** (case-insensitiv, Trenner `;`): `SUM`, `AVG` (Alias `AVERAGE`),
|
||||
`MIN`, `MAX`, `COUNT`. Argumente gemischt: Einzelzellen, Bereiche (lokal oder
|
||||
`Name!…`), Zahl-Literale — z. B. `=SUM(Umsatz!B2:E2; 100; A1)`.
|
||||
`MIN`, `MAX`, `COUNT` — plus alle über **„𝑓 Funktionen"** angelegten
|
||||
benutzerdefinierten Funktionen (siehe „Benutzerdefinierte Funktionen" oben),
|
||||
die sich für den Aufrufer identisch verhalten. Argumente gemischt: Einzelzellen,
|
||||
Bereiche (lokal oder `Name!…`), Zahl-Literale — z. B.
|
||||
`=SUM(Umsatz!B2:E2; 100; A1)`.
|
||||
|
||||
**Fehlermodell** (propagiert an abhängige Zellen):
|
||||
`#REF` (ungültiger/gelöschter Bezug oder unbekannter Tabellenname), `#DIV/0!`,
|
||||
`#NAME?` (unbekannte Funktion), `#ERR` (Parse-Fehler), `#CIRC` (Zirkelbezug,
|
||||
auch tabellenübergreifend), `#VALUE!` (Typfehler in Arithmetik — *dokumentierte
|
||||
Erweiterung*). Referenz auf leere Zelle = `0` in Arithmetik, von `COUNT` nicht
|
||||
gezählt.
|
||||
`#NAME?` (unbekannte Funktion, eingebaut oder benutzerdefiniert), `#ERR`
|
||||
(Parse-Fehler; auch eine falsche Argumentanzahl bei einer benutzerdefinierten
|
||||
Funktion), `#CIRC` (Zirkelbezug, auch tabellenübergreifend), `#VALUE!`
|
||||
(Typfehler in Arithmetik — *dokumentierte Erweiterung*). Referenz auf leere
|
||||
Zelle = `0` in Arithmetik, von `COUNT` nicht gezählt. Ein Zyklus zwischen
|
||||
benutzerdefinierten Funktionen selbst wird nicht über `#CIRC` sichtbar — er
|
||||
wird schon beim **Speichern** der Funktion im UI abgelehnt (kein neuer
|
||||
Fehlercode nötig).
|
||||
|
||||
---
|
||||
|
||||
@@ -277,7 +318,8 @@ Tabellen aller Blätter.
|
||||
| `graph.ts` | `DependencyGraph`: `affected()`, `topoOrder()`, Zirkel-DFS. |
|
||||
| `refs.ts` | A1 ↔ `(col,row)`, Bereichs-Expansion. |
|
||||
| `functions.ts` | `{ NAME → fn(values) }` für SUM/AVG/MIN/MAX/COUNT. |
|
||||
| `workbook.ts` | Flache Tabellenliste + ASTs + Werte + Graph; inkrementelle Neuberechnung; implementiert `EngineHost`. |
|
||||
| `userFunctions.ts` | Benutzerdefinierte Funktionen: `expandUserFunctions()` setzt einen Aufruf beim Parsen der aufrufenden Zelle als Kopie des (rekursiv expandierten, parametersubstituierten) Funktionskörpers in deren AST ein — kein Laufzeit-Closure, Evaluator/Graph sehen danach eine ganz normale AST. |
|
||||
| `workbook.ts` | Flache Tabellenliste + ASTs + Werte + Graph; inkrementelle Neuberechnung; implementiert `EngineHost`; verdrahtet `userFunctions.ts` direkt nach `parseFormula()`. |
|
||||
|
||||
**Neuberechnung.** `setCell` reparst eine Zelle → `graph.affected(dirty)` →
|
||||
`graph.topoOrder()` → Zyklus-Zellen `#CIRC`, Rest topologisch auswerten, nicht
|
||||
@@ -359,7 +401,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test` — Vitest, 146 Tests:
|
||||
`npm run test` — Vitest, 178 Tests:
|
||||
|
||||
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
||||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
|
||||
@@ -421,6 +463,17 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
hängt er einen neuen an; Ziehen fügt/erweitert einen Bereichsbezug; ein Klick
|
||||
in eine andere Tabelle qualifiziert mit deren Namen (Quoting bei Leerzeichen);
|
||||
ohne Formel im Puffer greift nichts.
|
||||
- **Benutzerdefinierte Funktionen** — Engine-Ebene (`userFunctions.spec.ts`):
|
||||
Parametersubstitution, verschachtelte Aufrufe (eingebaut + benutzerdefiniert,
|
||||
auch bei kollidierenden Parameternamen zwischen den Scopes), Arity-Mismatch →
|
||||
`#ERR`, unbekannter Name → `#NAME?`, Rekursionstiefen-Sicherheitsnetz,
|
||||
Abhängigkeits-Edges für Zellen, die nur im Funktionskörper vorkommen (nicht
|
||||
im Aufruf) — inkl. korrekter Neuberechnungs-Reihenfolge. Store-Ebene: Name-/
|
||||
Parameter-Validierung, Kollision mit eingebauten/anderen Funktionen,
|
||||
Zyklus-Ablehnung beim Speichern (direkt und über mehrere Stufen), Umbenennen/
|
||||
Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer
|
||||
Funktionsänderung, voller JSON-Round-Trip, `customFunctions: []` bei
|
||||
v1→v2-Migration.
|
||||
- **IO** — JSON verlustfreier Round-Trip (identisch), defekte/leere Datei →
|
||||
Fehler ohne Nebenwirkung, unbekannter Tabellenname → `#REF`, v1→v2 beim
|
||||
Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting,
|
||||
@@ -443,9 +496,14 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
reaktiv), **kein** echter Web Worker.
|
||||
- **xlsx-Formel-Übernahme** ist heuristisch (Separator-Swap unter Beachtung von
|
||||
Quotes); exotische Excel-Formeln fallen auf den Wert zurück.
|
||||
- Kein Text-Verkettungs-/Vergleichsoperator, keine benannten Bereiche, kein
|
||||
Kopieren/Einfügen von Zellbereichen, keine Undo-Historie, kein Snapping der
|
||||
Canvas-Objekte.
|
||||
- Kein Text-Verkettungs-/Vergleichsoperator, keine benannten Bereiche (nur
|
||||
benutzerdefinierte *Funktionen*, keine benannten *Zellen/Bereiche*), kein
|
||||
Snapping der Canvas-Objekte.
|
||||
- **Benutzerdefinierte Funktionen** (Phase 1, siehe oben) bewusst ohne:
|
||||
Autovervollständigen des Funktionsnamens im Editor, Live-Vorschau des
|
||||
Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und
|
||||
automatisches Nachführen bestehender Aufrufe bei Umbenennung (Verhalten
|
||||
entspricht bewusst dem einer Tabellen-Umbenennung, s. o.).
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
|
||||
Reference in New Issue
Block a user