A custom function body could previously only be a single expression -- no way to iterate over a range's elements to build something like PRODUCT (multiply every cell together), since none of the built-ins (SUM/AVG/MIN/MAX/COUNT) do that and a body can only compose existing functions, not define a new reduction. This adds a second body kind, 'block': a sequence of `name = expression` assignments and `FOR var IN source DO name = expression` loops, ending in `RETURN expression`. Flat scope -- every introduced name stays visible for the rest of the body, including after its own loop, holding its last iteration's value. No runtime loop in the evaluator: a loop's source size is already known once the caller's real argument is substituted in, so userFunctions.ts's new compileBlock() unrolls every FOR loop into a single nested expression (a left fold) at the same parse-time inlining point Phase 1 already used -- the result is exactly the same shape of static AST as any other inlined function call, so evaluator.ts and graph.ts need no changes, dependency edges on every unrolled cell are picked up automatically, and a bounded MAX_UNROLL_STEPS (10,000) keeps it crash/hang-free. parser.ts's parseFunctionBlock() parses the new grammar (a purely additive EQUALS lexer token; FOR/IN/DO/RETURN are plain case-matched IDENTs, not new reserved tokens) into a BlockNode kept outside the existing Node union, so no existing switch needs a new case. Store: CustomFunction/FunctionDef gain `kind: 'expression' | 'block'` (defaulted for every function saved before this existed, including via JSON import). validateCustomFunction validates the new grammar's local/ loop-variable names the same way as parameters (not cell-shaped, no reserved words, no shadowing a parameter -- but reusing the same local across statements, the accumulator pattern, is correctly not a collision) and that every FOR source is structurally a parameter, cell, or range. UI: a "đ Funktionen" form toggle between "Einfache Formel" and "Mit Schleife", with its own placeholder/hint (a PRODUCT example) and a small badge on block-kind rows in the list. 237 tests passing (34 new), vue-tsc/eslint/build clean. Verified live in the running app: defined PRODUCT via the loop UI, called =PRODUCT(Umsatz!B2:E6), confirmed the correct value and recalculation on a change to a cell inside the range, and an inline rejection for an invalid FOR source at save time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
653 lines
38 KiB
Markdown
653 lines
38 KiB
Markdown
# Numbers-Studie â Tabellenkalkulation mit eigener Formel-Engine
|
||
|
||
Eine browserbasierte Tabellenkalkulation als **Case- und Designstudie** nach dem
|
||
Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt:
|
||
|
||
1. **Handwerk** â eine von Grund auf selbst geschriebene, schlanke Formel-Engine
|
||
(Tokenizer, Parser, AST, AbhÀngigkeitsgraph, Neuberechnung, Zirkelbezug-
|
||
Erkennung). Dokumentiert in [`src/engine/README.md`](src/engine/README.md).
|
||
2. **Produkt** â eine polierte, Numbers-nahe OberflĂ€che: jedes Blatt ist eine
|
||
**freie FlÀche (Canvas)**, auf die man beliebig viele **Tabellen- und
|
||
Diagramm-Objekte** setzt, verschiebt, skaliert und befĂŒllt.
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev # Vite-Devserver
|
||
npm run test # Vitest-Suite (Engine + Store + IO), 237 Tests
|
||
npm run build # Typecheck + Produktionsbuild
|
||
npm run lint # ESLint (flat config)
|
||
```
|
||
|
||
Alles lĂ€uft **rein clientseitig** â kein Server, keine Netzwerk-Requests. Datei-
|
||
I/O ĂŒber File API und Blob-URLs.
|
||
|
||
---
|
||
|
||
## Das Canvas-Modell
|
||
|
||
Wie in Numbers ist ein *Blatt* kein einzelnes Endlos-Raster, sondern eine
|
||
scroll- und zoombare FlÀche, die **Objekte** trÀgt:
|
||
|
||
- **Tabellen-Objekt** â ein eigenes Gitter mit einem Namen, der **pro Blatt**
|
||
eindeutig ist (siehe âFormelgrammatik" unten fĂŒr BezĂŒge ĂŒber mehrere BlĂ€tter
|
||
mit demselben Tabellennamen). Frei positionierbar (Ziehen an der
|
||
Kopfleiste). Zwei Darstellungen:
|
||
- **Automatisch** â so groĂ wie die Zellen, alle Zeilen sichtbar; der SE-Griff
|
||
skaliert die Spaltenbreiten proportional.
|
||
- **Feste GröĂe** â fester Viewport, **intern scrollbar**, mit frei wĂ€hlbarer
|
||
Zahl **fixierter Kopfzeilen und fixierter Spalten** (`position: sticky`
|
||
gegen den Scroll-Container). Der SE-Griff zieht dann den Viewport auf.
|
||
Spaltenbreite per Drag am Spaltenkopf (ab **24 px**), **Zeilenhöhe** per Drag
|
||
an der Unterkante des Zeilenkopfs (Standard + je Zeile).
|
||
- **Diagramm-Objekt** â Chart.js (Balken/Linie), frei positionier- **und**
|
||
skalierbar. Datenquelle = ein Tabellen-Objekt; jede ZellÀnderung aktualisiert
|
||
es sofort.
|
||
- **Text-Objekt** â ein formatierbares Textfeld als Zusatz zu Tabellen und
|
||
Diagrammen. Doppelklick zum Bearbeiten; SchriftgröĂe, Fett/Kursiv, Ausrichtung,
|
||
Text- und Hintergrundfarbe im Inspector; frei skalierbar.
|
||
|
||
Objekte kommen beim Anklicken in den Vordergrund, werden per `Entf` gelöscht
|
||
(Diagramm/Text) bzw. ĂŒber das `Ă` der Kopfleiste, per Doppelklick auf den Namen
|
||
umbenannt. **Je Tabelle, Diagramm und Textfeld** sind eigene Hintergrund- und
|
||
Textfarbe einstellbar. Mehrere BlĂ€tter ĂŒber die Tabs unten (hinzufĂŒgen,
|
||
umbenennen, löschen, per Drag sortieren). Zoom 40â200 % pro Blatt
|
||
(`Strg/Cmd` + Mausrad, Zoom-Leiste oder Einstellungen).
|
||
|
||
### Zellauswahl
|
||
|
||
- Klick = eine Zelle, **Shift+Klick / Ziehen** = ein Rechteck. Das Ziehen ist
|
||
positions- statt event-target-basiert (`elementsFromPoint`), ĂŒbersteht schnelle
|
||
Bewegungen und das Verlassen der Tabelle, scrollt bei festen Tabellen an der
|
||
Kante automatisch mit und heilt einen verlorenen `mouseup` selbst.
|
||
- Kopf-Klick = ganze Spalte / Zeile, Ecke = alles.
|
||
- **`Strg`/`Cmd`+Klick** = weitere Zelle **hinzufĂŒgen** (erneut = abwĂ€hlen),
|
||
**`Strg`/`Cmd`+Ziehen** = weiteres Rechteck; auch am Spalten-/Zeilenkopf.
|
||
- Die **diskontinuierliche Mehrfachauswahl** trÀgt alle folgenden Aktionen:
|
||
Live-Aggregat (Summe/Mittel/Anzahl) in der Statusleiste, die
|
||
**Auswahl-Formatierung** im Inspector und `Entf` (leert alle markierten Zellen).
|
||
- Pfeiltasten / Enter kollabieren wieder auf eine Zelle. Die **aktive Zelle**
|
||
(blauer Rahmen, Formelleiste, `F2`/Tippen zum Bearbeiten) ist stets die
|
||
**zuerst** angeklickte/gesetzte Zelle einer Auswahl â sie bleibt beim
|
||
Ziehen/Erweitern eines Bereichs stehen, statt dem Cursor zu folgen (Excel-/
|
||
Numbers-Verhalten). WĂ€hrend einer Tab/Enter-Dateneingabefolge im Bereich
|
||
wandert sie mit dem Cursor mit.
|
||
- **Kopieren/Ausschneiden/EinfĂŒgen** (`Strg`/`Cmd`+`C`/`X`/`V`, KontextmenĂŒ):
|
||
ĂŒbertrĂ€gt Zellinhalte **und** Formatierung. EinfĂŒgen auf eine Auswahl, die
|
||
ein exaktes Vielfaches der kopierten GröĂe ist, **kachelt**; eine kleinere
|
||
oder Einzelzell-Auswahl verankert die kopierte FlĂ€che dort und lĂ€uft darĂŒber
|
||
hinaus. Formeln werden dabei **bezugsangepasst** wie in Excel/Numbers â siehe
|
||
â`$`-BezĂŒge sperren" unten; ein gestrichelter Rahmen markiert die
|
||
Kopierquelle (mit eigenem Rahmen um deren erste Zelle, bleibt stehen, auch
|
||
wenn die aktuelle Auswahl lÀngst woanders ist), `Esc` verwirft den Puffer.
|
||
- **AusfĂŒllkĂ€stchen** (das kleine Quadrat unten rechts an der Auswahl, wie in
|
||
Excel/Numbers): ziehen wiederholt die Auswahl â Werte, Formeln
|
||
(bezugsangepasst) und Formatierung â in die ĂŒberstrichenen Zellen. Nur eine
|
||
Achse gleichzeitig (rein runter/hoch/rechts/links, je nachdem wohin weiter
|
||
gezogen wird); ein mehrzeiliger/-spaltiger Ausgangsbereich wird zyklisch
|
||
wiederholt, auch ĂŒber eine unvollstĂ€ndige letzte Wiederholung hinweg.
|
||
|
||
**Tastaturnavigation (Excel-nah)** â Fokus in der Tabelle:
|
||
|
||
| Taste | Verhalten |
|
||
|---|---|
|
||
| Pfeiltasten | eine Zelle; **Shift+Pfeil** erweitert die Auswahl |
|
||
| **`Strg`/`Cmd`+Pfeil** | zum Rand des Datenblocks springen (leerâgefĂŒllt) |
|
||
| **Tab / Shift+Tab** | rechts / links |
|
||
| **Enter / Shift+Enter** | runter / hoch â nach einer Tab-Folge **zurĂŒck zur Startspalte + eine Zeile** |
|
||
| bei aktivem **Bereich** | Tab/Enter **rotieren den Cursor im Bereich** (mit Umbruch), der Bereich bleibt markiert |
|
||
| Tippen / `F2` | Zelle editieren; Enter/Tab committen und ziehen weiter (auch im Bereich) |
|
||
| `Pos1` / `Strg`+`Pos1` / `Ende` / `Strg`+`Ende` | Zeilenanfang / A1 / Zeilenende / letzte Zelle |
|
||
| `Bildâ` / `Bildâ` | seitenweise (bei festen Tabellen nach Viewport-Höhe) |
|
||
| **`Strg`/`Cmd`+`C`/`X`/`V`** | Kopieren / Ausschneiden / EinfĂŒgen (Zellen + Formatierung, bezugsangepasst) |
|
||
| **`Strg`/`Cmd`+`Z`** · **`Strg`/`Cmd`+`Y`** / **`Strg`/`Cmd`+`Shift`+`Z`** | RĂŒckgĂ€ngig · Wiederholen (auch als Symbolleisten-Buttons â¶ â·) |
|
||
| **`F4`** (im Editor/in der Formelleiste) | Bezug am Cursor sperren/zyklisch umschalten: `A1` â `$A$1` â `A$1` â `$A1` â `A1`; bei markiertem Text werden **alle** darin gefundenen BezĂŒge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand |
|
||
| **`Alt`+`Enter`** bzw. **`Cmd`+`Enter`** (im Editor) | Zeilenumbruch in der Zelle einfĂŒgen, statt zu committen â `Cmd`+`Enter` als Mac-Fallback, da `â„` (Option) dort eine Compose-Taste ist und nicht ĂŒberall zuverlĂ€ssig als reines Modifier ankommt |
|
||
|
||
Bei **festen (scrollbaren) Tabellen** wird die aktive Zelle bei jeder Navigation
|
||
automatisch in den sichtbaren Bereich gescrollt (unter Beachtung fixierter
|
||
Zeilen/Spalten).
|
||
|
||
**RĂŒckgĂ€ngig/Wiederholen**: dokumentweiter Undo-Stack (Snapshot-basiert, wie
|
||
der bereits vorhandene JSON-Export/Import-Round-Trip â einfach und
|
||
nachweisbar korrekt fĂŒr ein Dokument dieser GröĂe, statt eines Patch-/
|
||
Kommando-Logs). Reine Auswahl-/Cursor-Bewegungen zÀhlen nie als Schritt; eine
|
||
schnelle Folge von Mutationen (ein Spalten-/Zeilen-Resize-Drag feuert bei
|
||
jeder Mausbewegung) wird zu **einem** Schritt zusammengefasst, sofern sie
|
||
innerhalb von 150 ms aufeinanderfolgen â knapp unter normalem Tipp-/Klick-
|
||
Tempo, damit zwei tatsÀchlich getrennte schnelle Aktionen nicht verschmelzen,
|
||
aber weit ĂŒber dem Abstand aufeinanderfolgender Drag-Ereignisse (~16â30 ms).
|
||
Ein neuer Bearbeitungsschritt verwirft sofort einen etwaigen Wiederholen-Ast.
|
||
âAuf Beispiel zurĂŒcksetzen" und JSON-Import âErsetzen" starten eine **neue**
|
||
Historie (das vorherige Dokument ist danach nicht mehr zurĂŒckholbar), Undo/
|
||
Redo selbst tun das nicht. Symbolleiste zeigt zwei Buttons (â¶ â·, deaktiviert
|
||
wenn nichts zu tun ist); greift auch ohne Tabellenfokus, auĂer wĂ€hrend eine
|
||
Texteingabe (Zell-Editor, Formelleiste, Blatt-Umbenennen, âŠ) den nativen
|
||
Undo dieses Feldes benutzen soll.
|
||
|
||
**In-Zellen-Editor**: das Eingabefeld liegt als Overlay ĂŒber der Zelle (nicht
|
||
in ihr) und wĂ€chst mit dem Inhalt â in der Breite bis zu einer angenĂ€herten
|
||
âsichtbaren Anzeigebreite", danach umbrechend; in der Höhe mit `Alt`+`Enter`
|
||
eingefĂŒgten ZeilenumbrĂŒchen, bis zu einem Maximum, danach intern scrollbar.
|
||
Ein Zeilenumbruch bleibt auch nach dem Committen erhalten und wird in der
|
||
Zelle als solcher dargestellt (umbrechend statt einzeiliger Ellipse), auf die
|
||
Zeilenhöhe der Tabelle geklemmt.
|
||
|
||
**Formelleiste** (`fx`, oben): ein `<textarea>` statt eines einzeiligen Felds
|
||
â dadurch **umbricht** eine lange Formel statt seitwĂ€rts wegzuscrollen, und
|
||
an der unteren Kante lÀsst sie sich per **Maus** (natives `resize: vertical`)
|
||
auf beliebig viele Zeilen aufziehen, um mehr auf einmal zu sehen. `Alt`+`Enter`
|
||
bzw. `Cmd`+`Enter`, `F4` und `Esc` verhalten sich wie im In-Zellen-Editor. Das
|
||
Namensfeld links davon (ebenso das Pendant links in der **Statuszeile**, die
|
||
mit den Blatt-Reitern eine gemeinsame Zeile am unteren Rand teilt) zeigt bei
|
||
einer Mehrfachauswahl nicht nur die aktive Zelle, sondern den ganzen markierten
|
||
**Bereich** (`Umsatz::B2:E6`), bei einer diskontinuierlichen Auswahl zusÀtzlich
|
||
`+N` fĂŒr die weiteren Rechtecke.
|
||
|
||
**BezĂŒge per Maus einfĂŒgen ("Point-Mode")**: sobald der Editor (Zelle oder
|
||
Formelleiste) eine Formel (`=âŠ`) enthĂ€lt, fĂŒgt ein **Klick** auf eine Zelle â
|
||
in derselben Tabelle **oder einer ganz anderen**, mit `Name!`-Qualifier bei
|
||
Bedarf `'in Quotes'` â deren Bezug an der Cursorposition ein; **Ziehen**
|
||
fĂŒgt stattdessen einen Bereichsbezug (`A1:C3`) ein, live wachsend wĂ€hrend
|
||
der Zug. Ein weiterer Klick/Zug **ersetzt** den zuletzt eingefĂŒgten Bezug,
|
||
solange nichts dazwischen getippt wurde â **jede** Eingabe (ein `;` als
|
||
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). Ein Parameter kann auch ein **Bereich** sein, solange
|
||
er im Funktionskörper direkt an eine bereichsfĂ€hige Funktion ĂŒbergeben wird
|
||
â z. B. `SPANNE(bereich) = MAX(bereich) - MIN(bereich)`, aufgerufen als
|
||
`=SPANNE(Umsatz!B2:E6)`; das Formular zeigt dieses Muster als Hinweis unter
|
||
dem Formel-Feld. Wie bei jeder Formel ergibt ein Bereich auĂerhalb einer
|
||
Funktionsargument-Position `#VALUE!`.
|
||
- **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.
|
||
|
||
#### Mit Schleife: FOR-Code statt einer einzelnen Formel
|
||
|
||
Der âArt"-Umschalter im Formular bietet neben **âEinfache Formel"** (s. o.)
|
||
auch **âMit Schleife"** â echter Code statt eines einzelnen Ausdrucks, fĂŒr
|
||
FĂ€lle, die eine einzelne Formel nicht ausdrĂŒcken kann: ĂŒber die Elemente
|
||
eines Bereichs selbst iterieren (z. B. ein Produkt aller Zellen, nicht nur
|
||
Summe/Mittel/Min/Max). Das Code-Feld akzeptiert eine Folge aus:
|
||
|
||
- **Zuweisungen** â `name = ausdruck`.
|
||
- **FOR-Schleifen** â `FOR var IN bereich DO name = ausdruck`; `bereich` ist
|
||
ein Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck).
|
||
|
||
â beliebig oft, beliebig gemischt, zuletzt **`RETURN ausdruck`**. Jeder
|
||
eingefĂŒhrte Name (Zuweisungsziel oder Schleifenvariable) bleibt fĂŒr den
|
||
**restlichen** Code sichtbar, auch nach dem Ende âseiner" Schleife (mit dem
|
||
Wert der letzten Iteration) â flacher Namensraum, kein Block-Scoping.
|
||
`FOR`/`IN`/`DO`/`RETURN` sind reservierte Wörter, nicht als Parameter-/
|
||
Variablenname erlaubt.
|
||
|
||
Beispiel â Produkt aller Elemente eines Bereichs (`PRODUCT` gibt es nicht als
|
||
eingebaute Funktion, nur `SUM`/`AVG`/`MIN`/`MAX`/`COUNT`):
|
||
|
||
```
|
||
ergebnis = 1
|
||
FOR x IN bereich DO ergebnis = ergebnis * x
|
||
RETURN ergebnis
|
||
```
|
||
|
||
aufgerufen als `=PRODUCT(Umsatz!B2:E6)`.
|
||
|
||
**Keine echte Laufzeit-Schleife im Evaluator:** Da die GröĂe eines Bereichs
|
||
beim Aufruf bereits feststeht, wird eine `FOR`-Schleife beim Einsetzen des
|
||
Funktionsaufrufs vollstĂ€ndig **abgerollt** â jede Iteration wird zu einem
|
||
eigenen, ineinander verschachtelten Ausdrucks-Teilbaum (ein Links-Fold). Am
|
||
Ende steht wieder eine ganz normale, statische Formel-AST â Evaluator und
|
||
AbhÀngigkeitsgraph sehen keinen Unterschied zu einer von Hand ausgeschriebenen
|
||
Formel, Neuberechnung bei Ănderung *jeder* im Bereich enthaltenen Zelle
|
||
funktioniert automatisch korrekt. Kein Endlos-Risiko: mehr als insgesamt
|
||
10.000 abgerollte Zellen (ĂŒber alle Schleifen einer Funktion) ergeben `#ERR`
|
||
statt zu hÀngen. Details: [`src/engine/README.md`](src/engine/README.md)s
|
||
âSchleifen"-Abschnitt.
|
||
|
||
### Zell-Formatierung
|
||
|
||
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte ĂŒber den Kopf) â im
|
||
Inspector unter **Objekt · Auswahl**: horizontale **Ausrichtung**
|
||
(links/mitte/rechts),
|
||
**Zahlenformat** (`Automatisch`, `Ohne`, Ganzzahl, 1/2 Nachkommastellen,
|
||
**Prozent** 0/2 Stellen â kombiniert mit der Locale unten), **Zell-Textfarbe**,
|
||
**Zell-FĂŒllfarbe**, **Fett**/**Kursiv**, âFormatierung entfernen". Auch exakte
|
||
**Spaltenbreite** und **Zeilenhöhe** der Fokuszelle.
|
||
|
||
### Einstellungen (â, links neben dem Titel)
|
||
|
||
Ein Zahnrad neben der Ăberschrift öffnet eine linke Inspector-Schublade
|
||
([`SettingsPanel.vue`](src/components/SettingsPanel.vue)), die die FlÀche zur
|
||
Seite schiebt (auf schmalen Viewports ĂŒberlagert sie). Sie bĂŒndelt alle
|
||
wichtigen Punkte:
|
||
|
||
- **Objekt** (das gewÀhlte):
|
||
- Tabelle â Name, Zeilen/Spalten (exakt), Darstellung, Viewport-GröĂe,
|
||
fixierte Kopfzeilen/Spalten, Position, Zahlenformat-Locale, **Standard-
|
||
Zeilenhöhe**, **Tabellen-Hintergrund/-Textfarbe**, und â bei aktiver
|
||
Zellauswahl â die **Auswahl**-Formatierung (s. o.).
|
||
- Diagramm â Name, Ansicht, Quelltabelle, GröĂe, Position, Zahlenformat-Locale,
|
||
**Hintergrund**, **Text-/Achsenfarbe**.
|
||
- Textfeld â Name, Text, SchriftgröĂe, Fett/Kursiv, Ausrichtung, **Textfarbe**,
|
||
**Hintergrund**, GröĂe, Position.
|
||
- **Blatt**: Name, Zahlenformat, **Hintergrundfarbe**, **Schriftfarbe**,
|
||
**Raster ein-/ausblenden** (`âș` setzt eine Farbe auf den Theme-Wert zurĂŒck),
|
||
neues Blatt, Blatt löschen.
|
||
- **Ansicht**: Zoom-Regler (**pro Blatt** gespeichert).
|
||
- **Dokument**: Erscheinungsbild (Auto/Hell/Dunkel), Zahlenformat
|
||
(LĂ€ndereinstellung), CSV-Standard-Trennzeichen, âAuf Beispiel zurĂŒcksetzen".
|
||
|
||
### Blatt-Erscheinungsbild
|
||
|
||
Jedes Blatt kann eine eigene **Hintergrundfarbe** (Canvas-FlÀche hinter den
|
||
Objekten) und **Schriftfarbe** (Standard-Textfarbe der Gitter/Labels) haben und
|
||
das **Punkt-Raster** ein- oder ausblenden. Die Farben ĂŒberschreiben die
|
||
Theme-Variablen `--bg` / `--text` nur fĂŒr dieses Blatt; ohne Wert gilt das
|
||
Theme. Der Canvas-**Zoom** wird ebenfalls pro Blatt gemerkt.
|
||
|
||
Alle diese Einstellungen sind Teil des Dokuments (`DocumentState` /
|
||
`Sheet`) und werden damit in **localStorage gespeichert und in `.json`
|
||
exportiert** â Round-Trip verlustfrei, ungĂŒltige Werte werden beim Import
|
||
verworfen bzw. geklemmt.
|
||
|
||
### Zahlenformat (LĂ€ndereinstellung)
|
||
|
||
Die Darstellung von Zahlen (Dezimal- und Tausendertrennzeichen) ist eine
|
||
**Kaskade**: Objekt â Blatt â Dokument. Jede Ebene kann die darĂŒberliegende
|
||
ĂŒberschreiben oder âerben" (leere Auswahl). Auswahl: `Ohne Trennzeichen`,
|
||
`de-DE`, `de-CH`, `en-US`, `en-GB`, `fr-FR`, `it-IT`. Wirkt auf Zellwerte, das
|
||
Live-Aggregat der Statusleiste und die Wertelabels der Diagramme. **CSV-/XLSX-
|
||
Export bleibt bewusst locale-neutral** (`.`-Dezimal, keine Gruppierung) fĂŒr einen
|
||
verlustfreien Round-Trip; der CSV-*Import* erkennt beide Schreibweisen ohnehin.
|
||
|
||
Das vorbefĂŒllte âUmsatz" ist bereits als **feste, scrollbare Tabelle mit
|
||
fixierter Kopfzeile und fixierter Spalte A** angelegt, um das Feature zu zeigen.
|
||
|
||
---
|
||
|
||
## Formelgrammatik (EBNF)
|
||
|
||
```ebnf
|
||
formula = "=" expression ;
|
||
|
||
expression = term { ("+" | "-") term } ; (* links-assoziativ *)
|
||
term = unary { ("*" | "/") unary } ; (* links-assoziativ *)
|
||
unary = ("+" | "-") unary
|
||
| power ;
|
||
power = primary [ "^" unary ] ; (* rechts-assoziativ; bindet
|
||
stÀrker als unÀres Minus,
|
||
d.h. -2^2 = -(2^2) = -4 *)
|
||
primary = NUMBER | STRING | "(" expression ")" | funcall | reference ;
|
||
|
||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||
|
||
reference = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
|
||
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname *)
|
||
sheetQualifier = (IDENT | QUOTED) "!" ; (* Blattname -- nur erkannt,
|
||
wenn direkt gefolgt von
|
||
einem vollen "Tabelle::"
|
||
(3-teilige Form); ein
|
||
einzelnes "X!Y" ohne
|
||
folgendes "::" bleibt
|
||
die alte 2-teilige Form
|
||
(X = Tabelle) *)
|
||
|
||
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
|
||
QUOTED = /' ( [^'] | '' )* '/ ; (* Name mit Leerzeichen *)
|
||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||
```
|
||
|
||
PrĂ€zedenz schwach â stark: `+ -` < `* /` < unĂ€res `+ -` < `^`.
|
||
|
||
Der (innere) Qualifier ist der **Tabellenname**. `!` und der Numbers-Stil `::`
|
||
sind dabei austauschbar und **case-/whitespace-tolerant**:
|
||
`Kosten!A2` = `'Kosten'!A2` = `kosten!a2` = `Kosten::A2`.
|
||
|
||
**Tabellennamen sind nur pro Blatt eindeutig**, nicht dokumentweit â zwei
|
||
verschiedene BlĂ€tter dĂŒrfen je eine Tabelle âUmsatz" haben (beim Anlegen/
|
||
Umbenennen wird nur gegen das **eigene** Blatt geprĂŒft). Ein unqualifizierter
|
||
Bezug wie `Umsatz!B2` (ohne Blattangabe) löst sich dann wie folgt auf:
|
||
|
||
1. Gibt es die Tabelle nur auf **einem** Blatt (der Normalfall), wird sie
|
||
genommen â unverĂ€ndert wie zuvor.
|
||
2. Gibt es sie auf **mehreren** BlÀttern, gewinnt die Tabelle auf **demselben
|
||
Blatt wie die Formel selbst** (nicht: dem gerade angezeigten Blatt in der
|
||
UI â das spielt fĂŒr die Formel keine Rolle).
|
||
3. Bleibt es danach noch mehrdeutig, ergibt sich `#REF` â hier hilft die
|
||
explizite **3-teilige Form** `Blatt!Tabelle::Zelle`, z. B.
|
||
`=Analyse!Umsatz::B2` oder mit Leerzeichen `='Blatt 2'!Umsatz::B2`, die
|
||
**immer** genau die Tabelle auf dem genannten Blatt trifft, unabhÀngig
|
||
davon, wo die Formel selbst steht. Die 2-teilige Form (`Umsatz!B2` /
|
||
`Umsatz::B2`) bleibt fĂŒr jede bestehende Formel exakt gĂŒltig â die
|
||
3-teilige Form ist rein additiv.
|
||
|
||
Die Formel-**Point-Mode**-EinfĂŒgung (s. o.) erkennt eine mehrdeutige
|
||
Zieltabelle selbst und fĂŒgt automatisch die volle 3-teilige Form ein, statt
|
||
einer Formel, die versehentlich die falsche Tabelle trÀfe.
|
||
|
||
**`$`-BezĂŒge sperren** (relativ/gemischt/absolut): `A1` (relativ) vs. `$A1` /
|
||
`A$1` (Spalte bzw. Zeile gesperrt) vs. `$A$1` (beide gesperrt). FĂŒr die
|
||
Auswertung selbst ist `$` irrelevant â sie ist rein positionsbasiert. Es zĂ€hlt
|
||
beim **Kopieren**: eine gesperrte Achse bleibt exakt wie eingegeben stehen,
|
||
eine ungesperrte verschiebt sich um das EinfĂŒge-Delta (siehe âKopieren/
|
||
Ausschneiden/EinfĂŒgen" oben). Beim **Ausschneiden** gilt das nicht â dort
|
||
verschiebt sich die referenzierte Zelle selbst, also folgt jede Referenz
|
||
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` â 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, unbekannter Tabellen-/Blattname, oder ein
|
||
unqualifizierter Tabellenname, der auf mehreren BlÀttern existiert und auch
|
||
ĂŒber die âgleiches Blatt wie die Formel"-Regel nicht eindeutig wird â siehe
|
||
oben), `#DIV/0!`,
|
||
`#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).
|
||
|
||
---
|
||
|
||
## Architektur der Engine-Module
|
||
|
||
Framework-agnostisches ES-Modul unter [`src/engine/`](src/engine/) â **keine**
|
||
Formel-/Spreadsheet-Library. Kennt kein Vue, kein DOM. Die adressierbare Einheit
|
||
ist eine **Tabelle**; der AbhĂ€ngigkeitsgraph lĂ€uft dokumentweit ĂŒber alle
|
||
Tabellen aller BlÀtter.
|
||
|
||
| Datei | Aufgabe |
|
||
|-------|---------|
|
||
| `lexer.ts` | Tokenizer, flacher Token-Strom (inkl. `!` und `::`). |
|
||
| `parser.ts` | Recursive-Descent â AST; EBNF im Dateikopf; `collectRefs()`. |
|
||
| `evaluator.ts` | AST + `EngineHost` â Wert; Fehler-Propagierung. |
|
||
| `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. |
|
||
| `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
|
||
betroffene Zellen behalten ihren Wert.
|
||
|
||
**Anbindung an Vue.** [`src/store/document.ts`](src/store/document.ts) ist das
|
||
âeigene `reactive()`-Modul": es hĂ€lt eine `Workbook`-Instanz und einen
|
||
`recalcTick`-Ref. Die UI liest `store.cellValue(...)`, das `recalcTick` berĂŒhrt â
|
||
nach jeder Neuberechnung wird der Tick erhöht, jede Formelzelle re-rendert also
|
||
**wie ein `computed`**.
|
||
|
||
---
|
||
|
||
## Selbst gebaut vs. Library
|
||
|
||
| Selbst gebaut | Library |
|
||
|---------------|---------|
|
||
| **Formel-Engine komplett** (Lexer, Parser, AST, Evaluator, dokumentweiter AbhÀngigkeitsgraph, Topo-Sort, Zirkel-Erkennung, Funktionen) | **Vue 3** + Vite |
|
||
| **Die Canvas**: Objekte platzieren, verschieben, skalieren, Z-Order, Zoom | **Chart.js** (Balken + Linie) |
|
||
| **Das Raster** je Tabelle: Auswahl (Klick/Shift/Drag), Tastaturnavigation, In-Zellen-Edit, Spaltenbreite per Drag | **xlsx (SheetJS)** â nur `.xlsx` I/O, lazy geladen, nie zur Auswertung |
|
||
| Reaktiver **Store** / Dokumentmodell (v1 â v2 Migration) | â |
|
||
| **CSV-Parser + -Writer** (RFC 4180, Quoting, Delimiter-Sniffing, Locale-Zahlen) | |
|
||
| **JSON**-Serialisierung + Schema-Validierung | |
|
||
| Sheet-Tabs (Drag-Sortierung), KontextmenĂŒ, Modals, Toasts, Theme | |
|
||
|
||
Bewusst **nicht** verwendet: Handsontable, AG Grid, Univer, Luckysheet,
|
||
HyperFormula, formula-parser. `@vueuse/core`/`splitpanes` (in der Aufgabe als
|
||
optionale Komfort-Libs genannt) wurden nach dem Umbau auf das Canvas-Modell nicht
|
||
mehr gebraucht und wieder entfernt, um den AbhÀngigkeitsbaum ehrlich zu halten.
|
||
|
||
---
|
||
|
||
## Import / Export
|
||
|
||
Alles clientseitig (`Blob` + `URL.createObjectURL` + `<a download>` + revoke).
|
||
|
||
### Export
|
||
| Format | Inhalt |
|
||
|--------|--------|
|
||
| `.json` | **Natives Format**, verlustfreier Round-Trip: alle BlĂ€tter, alle Tabellen-/Diagramm-Objekte mit Position & GröĂe, Rohformeln, Spaltenbreiten, Tab-Reihenfolge, aktives Objekt, Zellauswahl. |
|
||
| `.csv` | Aktive Tabelle, **berechnete Werte**, RFC-4180-Quoting, Trennzeichen `,`/`;` wÀhlbar, `\r\n`, optional UTF-8-BOM. |
|
||
| `.xlsx` | Arbeitsmappe via SheetJS, **ein Worksheet je Tabellen-Objekt**. Formeln als echte Excel-Formeln (`;`â`,`, `AVG`â`AVERAGE`), immer mit gecachtem Wert. |
|
||
| `.png` | GewĂ€hltes Diagramm-Objekt ĂŒber `chart.toBase64Image()`. |
|
||
|
||
### Import
|
||
`<input type="file">` **und** Drag & Drop auf die Canvas; `.json` `.csv` `.xlsx`.
|
||
|
||
- **`.json`** â Dokument *ersetzen* (RĂŒckfrage) oder BlĂ€tter *anhĂ€ngen*.
|
||
**v1-Dokumente** (altes Ein-Raster-Format) werden automatisch nach v2
|
||
migriert (je altes Blatt ein Blatt mit einer gleichnamigen Tabelle). Schema
|
||
wird validiert; bei Fehler klare Meldung, **kein Teil-Import**.
|
||
- **`.csv`** â âals neue Tabelle (aktuelles Blatt)" / âaktive Tabelle ab A1
|
||
ersetzen". Trennzeichen-Sniffing (+ Override). Locale-Erkennung
|
||
(`1.234,56` â `1,234.56`). Werte mit fĂŒhrendem `=` werden zu Formeln.
|
||
- **`.xlsx`** â je Worksheet ein neues Blatt mit einer Tabelle.
|
||
- Nach jedem Import: kompletter Reparse + Neuberechnung + Diagramm-Refresh.
|
||
SchlÀgt ein Import fehl, bleibt das bestehende Dokument **unverÀndert**.
|
||
|
||
## Persistenz
|
||
|
||
Das gesamte Dokument wird (debounced) nach `localStorage` gespiegelt, robust
|
||
gegen leeren/kaputten State â Fallback ist das Seed-Dokument. Ein v1-State im
|
||
Storage wird beim Laden migriert. Button **âZurĂŒcksetzen"** stellt das
|
||
Seed-Dokument wieder her.
|
||
|
||
## VorbefĂŒlltes Dokument
|
||
|
||
Blatt **âAnalyse"** mit drei Tabellen-Objekten (**Umsatz**, **Kosten**,
|
||
**Ăbersicht**) und einem **Diagramm** (Umsatz je Region), plus ein leeres Blatt
|
||
**âBlatt 2"**. âĂbersicht" demonstriert tabellenĂŒbergreifende BezĂŒge und die vier
|
||
Grundrechenarten: `Ăbersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||
`E2 = =D2/B2*100`. Siehe [`src/data/seed.ts`](src/data/seed.ts).
|
||
|
||
> **Hinweis zur Aufgabenstellung:** Die Akzeptanzliste nennt âĂbersicht!B2 =
|
||
> 6070". Die explizit angegebene Nord-Zeile ergibt jedoch
|
||
> `1200+1450+1600+1720 = 5970`. Die Studie hÀlt sich an die explizite Tabelle;
|
||
> das demonstrierte Verhalten (Sofort-Propagierung durch Ăbersicht, âGesamt" und
|
||
> das Diagramm bei Ănderung von `Umsatz!B2`) ist davon unberĂŒhrt.
|
||
|
||
## Tests
|
||
|
||
`npm run test` â Vitest, 237 Tests:
|
||
|
||
- **lexer / parser** â PrĂ€zedenz & AssoziativitĂ€t (`^` vor `*/` vor `+-`, `^`
|
||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted BezĂŒge, `!` **und** `::`,
|
||
Bereiche, `;`-Argumente, Parse-Fehler.
|
||
- **refs (`$`-Sperrachsen, `F4`)** â `parseA1`/`toA1` runden relativ/absolut/
|
||
gemischt; `cycleReferenceLocks` durchlÀuft die vier Kombinationen, trifft
|
||
bei Cursor auf einem Bereich die richtige Seite, liefert `null` ohne
|
||
Treffer; bei echter Textauswahl schaltet es **alle** ĂŒberlappten BezĂŒge auf
|
||
einmal um (jeden vom eigenen Zustand aus) und lÀsst Tabellen-Qualifier, die
|
||
wie eine Zelle aussehen (`AB12!A1`), unangetastet.
|
||
- **evaluator** â Arithmetik, leere Zelle = 0, alle Funktionen inkl. Alias,
|
||
Cross-Table, Case-/`::`-Toleranz, jeder Fehlerfall, Fehler-Propagierung.
|
||
- **graph** â `affected`, topologische Ordnung, Selbst-/2er-/lĂ€ngerer Zyklus.
|
||
- **workbook / store** â Seed-Aggregate, Cross-Table-Summen, Edit-Ripple durch
|
||
die ganze Kette, `#REF` nach Tabellen-/Blatt-Löschung, neues Tabellen-Objekt
|
||
mit Cross-Sheet-Formel, `resetToSeed`, Live-Auswahl-Aggregat,
|
||
Neuberechnungs-**Reihenfolge**, **v1 â v2 Migration**.
|
||
- **Tabellen-Ansicht** â Seed-Umsatz ist fixiert/scrollbar; `setTableView`
|
||
klemmt fixierte Zeilen/Spalten an die GittergröĂe; Verkleinern löscht
|
||
Zellen auĂerhalb und klemmt nach; Ansichts-Einstellungen im JSON-Round-Trip.
|
||
- **Zahlenformat-Kaskade** â `cellDisplay` nutzt Dokument-Locale; Blatt-Override
|
||
schlĂ€gt Dokument, Objekt-Override schlĂ€gt Blatt; ZurĂŒcksetzen fĂ€llt die
|
||
Kaskade hinab; Overrides im JSON-Round-Trip, ungĂŒltige Werte werden verworfen.
|
||
- **Blatt-Erscheinungsbild** â Hintergrund/Schrift/Raster sind pro Blatt und
|
||
löschbar; Zoom pro Blatt (folgt dem Tab); Erscheinungsbild + Zoom +
|
||
CSV-Default im JSON-Round-Trip, kaputte Farb-/Zoom-Werte verworfen bzw.
|
||
geklemmt.
|
||
- **Zell-Formatierung** â per-Zelle Zahlenformat Ă Locale (inkl. Prozent);
|
||
`setRangeFormat` ĂŒber einen Bereich, Löschen per Falsy/`auto`; `selectionFormat`
|
||
meldet nur gemeinsame Werte; Zeilenhöhe (Standard + je Zeile), Spaltenbreite ab
|
||
24 px; Verkleinern pruned `formats`/`rowHeights`; alles im JSON-Round-Trip.
|
||
- **Text-Objekte** â einfĂŒgen/bearbeiten/formatieren + Round-Trip; kaputtes
|
||
Text-Objekt im Import wird abgelehnt, Dokument bleibt unverÀndert.
|
||
- **Objekt-Farben** â je Tabelle/Diagramm/Textfeld eigenes `bg`/`fg`, löschbar,
|
||
im JSON-Round-Trip.
|
||
- **Mehrfachauswahl** â `addToSelection` behĂ€lt frĂŒhere Rechtecke & verschiebt
|
||
die aktive Zelle, erneutes Klicken schaltet eine 1Ă1-Auswahl ab,
|
||
`extendActiveRange` wĂ€chst das jĂŒngste Rechteck; Aggregat & Formatierung ĂŒber
|
||
**alle** Rechtecke; `#`-genaue Zellzahl; JSON-Round-Trip inkl. `ranges`,
|
||
Klemmung beim Verkleinern; die aktive Zelle bleibt beim Erweitern eines
|
||
Bereichs der **Anker** (erste Zelle), nicht die zuletzt erreichte.
|
||
- **Zwischenablage** â Kopieren passt jede relative Referenz um das EinfĂŒge-
|
||
Delta an, `$`-gesperrte Achsen bleiben stehen; EinfĂŒgen kachelt eine exakt
|
||
passende gröĂere Auswahl bzw. verankert/ĂŒberlĂ€uft eine kleinere; Formatierung
|
||
wird mitkopiert und am Ziel ersetzt; Ausschneiden folgt trotz `$` der
|
||
verschobenen Zelle, lÀsst aber unbeteiligte und tabellenfremde Referenzen
|
||
unangetastet und leert die Quelle; `Esc` verwirft den Puffer.
|
||
- **AusfĂŒllkĂ€stchen** â fĂŒllt runter/rechts mit bezugsangepassten Formeln;
|
||
ein mehrzeiliger/-spaltiger Quellbereich wiederholt sich zyklisch, auch bei
|
||
einer unvollstĂ€ndigen letzten Wiederholung; fĂŒllt ebenso hoch/links;
|
||
Formatierung wird mitkopiert.
|
||
- **RĂŒckgĂ€ngig/Wiederholen** â Einzel-Edit rĂŒckgĂ€ng-/wiederholbar; mehrere
|
||
Edits schrittweise in der richtigen Reihenfolge zurĂŒck; ein neuer Edit nach
|
||
einem Undo verwirft den Wiederholen-Ast; reine AuswahlÀnderungen sind nie
|
||
ein Schritt; deckt Kopieren/EinfĂŒgen, AusfĂŒllen und Formatierung ab (nicht
|
||
nur `setCellInput`); âAuf Beispiel zurĂŒcksetzen" beginnt eine neue Historie.
|
||
- **Formel-Point-Mode** â Klick fĂŒgt einen lokalen Bezug an der Cursorposition
|
||
ein; ein zweiter Klick ohne Zwischen-Tippen ersetzt ihn, mit Tippen dazwischen
|
||
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; ein Bereichs-
|
||
Parameter funktioniert in Funktionsargument-Position (inkl. Neuberechnung
|
||
bei Ănderung einer Zelle *innerhalb* des ĂŒbergebenen Bereichs) und ergibt
|
||
`#VALUE!` auĂerhalb davon. 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.
|
||
- **âMit Schleife" (FOR-Code)** â Parser-Ebene (`parseFunctionBlock`):
|
||
eine Schleife (Akkumulator-Muster), mehrere unabhÀngige Schleifen
|
||
nacheinander, mehrere Zuweisungen vor/zwischen Schleifen, ein Name aus
|
||
einer frĂŒheren Anweisung in einer spĂ€teren Schleife nutzbar; Fehler bei
|
||
fehlendem `RETURN`, `FOR` ohne `DO`/`IN`, fehlendem `=`, Selbstbezug im
|
||
eigenen Initialisierer. Engine-Ebene (`compileBlock`): korrektes Abrollen
|
||
(ein Bereich â verschachtelte AusdrĂŒcke in Zellreihenfolge), ein einzelner
|
||
Zellbezug als Quelle â genau eine Iteration, die Schleifenvariable/das
|
||
letzte Zuweisungsziel bleiben danach mit ihrem letzten Wert sichtbar,
|
||
verschachtelter Aufruf einer anderen Funktion im Schleifenkörper wird mit-
|
||
expandiert, Arity-PrĂŒfung gilt auch fĂŒr âMit Schleife"-Funktionen, eine
|
||
FOR-Quelle, die nach Parametersubstitution kein Zell-/Bereichsbezug ist,
|
||
wirft, mehr als 10.000 abgerollte Zellen wirft. Workbook-Integration:
|
||
korrekter Wert und Neuberechnung bei Ănderung einer Zelle *innerhalb* des
|
||
ĂŒbergebenen Bereichs, Ăberschreitung des Abroll-Limits und eine ungĂŒltige
|
||
FOR-Quelle ergeben `#ERR` statt eines Absturzes. Store-Ebene: reservierte
|
||
Wörter (`FOR`/`IN`/`DO`/`RETURN`) als Parameter- oder lokaler Name
|
||
abgelehnt, zellartiger lokaler Name abgelehnt, Wiederverwendung desselben
|
||
lokalen Namens ĂŒber mehrere Anweisungen (das Akkumulator-Muster) ist
|
||
**keine** Kollision, ein lokaler Name, der einen Parameter ĂŒberschattet,
|
||
wird abgelehnt, eine berechnete FOR-Quelle wird abgelehnt, Funktions-Zyklus
|
||
durch einen Schleifenkörper wird erkannt, Undo/Redo, `kind` im JSON-
|
||
Round-Trip, ein Àlteres gespeichertes Dokument ohne `kind`-Feld defaultet
|
||
auf `'expression'`.
|
||
- **Blatt-Qualifier âBlatt!Tabelle::Zelle"** â Parser-Ebene: die explizite
|
||
3-teilige Form (bar und quoted, in Bereichen/Funktionsargumenten), volle
|
||
RĂŒckwĂ€rtskompatibilitĂ€t der 2-teiligen Form (`X!Y`, `X::Y`, auch ein
|
||
zweites `!` statt `::`), Fehler bei fehlender Zelle nach `::`. Workbook-
|
||
Ebene: zwei gleichnamige Tabellen auf verschiedenen BlÀttern lösen ein
|
||
unqualifiziertes `Tabelle!Zelle` zum **gleichen Blatt wie die Formel**
|
||
auf, die explizite Form trifft immer das genannte Blatt unabhÀngig vom
|
||
Formel-Standort, ein unbekanntes Blatt/eine weiterhin mehrdeutige
|
||
Tabelle ergibt `#REF`, Neuberechnung folgt korrekt der Auflösung. Store-
|
||
Ebene: Tabellennamen sind nur noch pro Blatt eindeutig (Anlegen/
|
||
Umbenennen prĂŒft nur gegen das eigene Blatt), Point-Mode fĂŒgt die volle
|
||
3-teilige Form ein, sobald ein Klick auf eine mehrdeutig benannte Tabelle
|
||
auf einem *anderen* Blatt trifft.
|
||
- **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,
|
||
`=1+2`â3, Locale-Zahlen.
|
||
|
||
---
|
||
|
||
## Bekannte Grenzen
|
||
|
||
- **Zeilen/Spalten nur am Rand.** EinfĂŒgen/Löschen *innerhalb* eines Gitters (mit
|
||
automatischem Umschreiben relativer BezĂŒge) ist bewusst nicht implementiert.
|
||
- **Objekt-BezĂŒge beim AnhĂ€ngen.** Beim JSON-*AnhĂ€ngen* zweier Dokumente werden
|
||
nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein
|
||
angehÀngter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der
|
||
erste Treffer. âErsetzen" (der Round-Trip-Fall) ist davon nicht betroffen.
|
||
- **Kein Virtual Scrolling** â âFeste GröĂe" scrollt intern, aber alle Zeilen
|
||
bleiben im DOM (nur geclippt); sehr groĂe Tabellen/Importe (viele Tausend
|
||
Zeilen) werden trÀge.
|
||
- **GroĂe Dateien** werden per `setTimeout`-Yield verarbeitet (UI bleibt
|
||
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 (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.).
|
||
- **âMit Schleife"-Code** bewusst ohne: verschachtelte Schleifen (`FOR`
|
||
innerhalb einer `DO`-Klausel), bedingte Anweisungen (`IF`), echtes
|
||
Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar),
|
||
frĂŒhzeitiges `RETURN` mitten in der Anweisungsfolge (nur am Ende erlaubt).
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
src/
|
||
engine/ # Formel-Engine (Eigenbau) + Unit-Tests
|
||
store/ # reaktiver Dokument-Store (v2 Canvas-Modell), Formatierung
|
||
io/ # csv / json (Eigenbau, inkl. v1âv2), xlsx (SheetJS, lazy)
|
||
components/ # AppToolbar, SettingsPanel, FormulaBar, SheetCanvas, CanvasObject,
|
||
# TableGrid, ChartView, TextBox, SheetTabs, StatusBar, TheLogo, âŠ
|
||
numolus-logo.svg # Marke: drei ĂŒberlappende Pop-Art-Quadrate + Schriftzug
|
||
config/brand.ts # Herausgeber-Metadaten (wicom.li) + Social-Media-Liste
|
||
composables/ # Theme, Toasts
|
||
data/seed.ts # vorbefĂŒlltes Dokument
|
||
styles/main.css # Design-Tokens (Light/Dark)
|
||
```
|