New Settings checkbox: while a table isn't the selected canvas object, its title bar, column letters and row numbers disappear entirely -- not just visually, their reserved space collapses too, so the table shrinks to just its cell grid. Clicking back into it (e.g. any cell) reselects it and brings everything back. TableObject.hideChromeUnfocused (store/types.ts) + setTableHideChromeUnfocused (document.ts) + io/json.ts normalization, following the existing per-object appearance-setting pattern. CanvasObject.vue gates its title bar on a new showChrome computed and passes its existing `selected` prop down to TableGrid.vue (previously only took tableId) so it can make the same call for its own headers. TableGrid.vue's HEAD_W (a plain constant) becomes a `headW` computed, and the existing `headerH` computed now returns 0 for both whenever hidden -- sufficient on its own since rowTop()/colLeft() already sum those two values as their base offset and are the single source every other position calculation in the file (sticky-frozen offsets, the in-cell editor overlay, clip/fill marquees, scroll-into-view, row virtualization) already goes through. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1459 lines
90 KiB
Markdown
1459 lines
90 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), 331 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 zieht hier **direkt an Zeilen-/Spaltenzahl**: Vergrößern fügt
|
||
leere Zeilen/Spalten hinzu, Verkleinern entfernt sie — aber **nie** über
|
||
echten Zellinhalt hinweg. Während des Ziehens zeigt ein gestricheltes
|
||
Vorschau-Rechteck die Zielgröße (die eigentliche Änderung ist teuer —
|
||
ein voller Neuaufbau der Engine — und committet daher erst beim
|
||
Loslassen, nicht bei jeder Mausbewegung); dann wird auf die kleinste
|
||
Größe geklemmt, die den vorhandenen Inhalt noch vollständig zeigt.
|
||
- **Feste Größe** — ein fester, unabhängig wählbarer **Pixel-Viewport**
|
||
mit frei wählbarer Zahl **fixierter Kopfzeilen und fixierter Spalten**
|
||
(`position: sticky` gegen den Scroll-Container) — für eine große
|
||
Tabelle, die nur einen kleinen Ausschnitt auf dem Canvas belegen soll,
|
||
mit internem Scrollen für den Rest. Der SE-Griff zieht hier **nur den
|
||
Viewport**, unabhängig von der Zeilen-/Spaltenzahl (die bleibt exakt wie
|
||
sie ist) — dieselben Breiten-/Höhenfelder im Einstellungen-Panel (⚙, s.
|
||
u.) steuern denselben Wert. **Zeilen-Virtualisierung**: nur die
|
||
Zeilen im sichtbaren Ausschnitt (+ etwas Überhang) landen tatsächlich im
|
||
DOM — alles andere ist ein Platzhalter-`<tr>` in passender Höhe, damit
|
||
Scrollbar-Größe/-Position stimmen. Macht auch eine Tabelle mit
|
||
zehntausenden Zeilen praktikabel (gemessen: `PageDown` auf 50.000
|
||
Zeilen ging von ~7s auf ~15ms); nur für **feste** Tabellen (die einzige
|
||
Ansicht mit einem echten, begrenzten Scroll-Container) — eine **auto**-
|
||
große Tabelle dieser Größe wäre ohnehin ein unbrauchbar hohes
|
||
Canvas-Objekt, unabhängig vom DOM. Nur Zeilen, nicht Spalten (in der
|
||
Praxis wird eher die Zeilenzahl groß).
|
||
Spaltenbreite per Drag am Spaltenkopf (ab **24 px**), **Zeilenhöhe** per Drag
|
||
an der Unterkante des Zeilenkopfs (Standard + je Zeile). **„Ohne Rahmen/
|
||
Kopfzeilen, wenn nicht ausgewählt"** (Einstellungen → Objekt, je Tabelle):
|
||
solange die Tabelle nicht das ausgewählte Canvas-Objekt ist, verschwinden
|
||
Titelleiste, Spaltenbuchstaben und Zeilennummern komplett — nicht nur
|
||
optisch, ihr reservierter Platz fällt mit weg, die Tabelle schrumpft auf
|
||
reines Zellraster. Ein Klick hinein (z. B. auf eine Zelle) wählt sie
|
||
wieder aus und blendet alles zurück ein.
|
||
- **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`+`A`** (zweistufig, Excel-Verhalten): der **erste** Druck
|
||
markiert den **zusammenhängenden Bereich** um die aktive Zelle — iterative
|
||
Rechteck-Expansion, die an einer vollständig leeren Zeile/Spalte oder am
|
||
Tabellenrand stoppt (zwei durch eine leere Spalte/Zeile getrennte
|
||
Dateninseln bleiben getrennt). Ein **zweiter, unmittelbar folgender** Druck
|
||
erweitert auf **alle befüllten Zellen der Tabelle** — die tatsächlichen
|
||
Grenzen befüllter Felder, nicht die (oft größeren) Grunddimensionen der
|
||
Tabelle. Jeder weitere Druck bleibt auf dieser Vollauswahl stehen. Der
|
||
Zyklus erkennt sich selbst rein über einen Vergleich der aktuellen Auswahl
|
||
mit der beim letzten Druck gesetzten — jede andere Aktion dazwischen (Klick,
|
||
Pfeiltaste, Bearbeitung, …) setzt ihn auf Stufe 1 zurück, ganz wie in Excel.
|
||
- **`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`+`A`** | 1. Druck: zusammenhängender Bereich um die aktive Zelle · 2. Druck: alle befüllten Zellen der Tabelle (s. o. „Zellauswahl") |
|
||
| **`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 |
|
||
| **Pfeiltasten** (im Editor/in der Formelleiste, an Stellen wo ein Bezug erwartet wird) | „Point Mode" per Tastatur — zeigt auf eine Zelle statt den Text-Cursor zu bewegen, genau wie ein Klick es täte; nur direkt nach `=`, einem Operator, `(` oder `;` (sonst normale Text-Navigation, mitten in der Formel editierbar). **Shift+Pfeil** erweitert zu einem Bereich (`A1:A5`). Ein Zeichen tippen, `Tab`/`Enter` oder `;` (nächstes Argument) übernehmen den gezeigten Bezug |
|
||
| **`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 |
|
||
| `↑`/`↓`/`Tab`/`Enter`/`Esc` (bei offenem Funktions-Dropdown) | navigieren / Vorschlag übernehmen / nur das Dropdown schließen — hat Vorrang vor der sonstigen Bedeutung dieser Tasten (s. u. „Funktions-Autovervollständigung") |
|
||
|
||
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.
|
||
|
||
**Performance bei großen Tabellen**: Undo-Tracking und die (debounced)
|
||
`localStorage`-Persistenz werden **nicht** über einen `watch(doc, ...,
|
||
{deep:true})` angestoßen — Vues Deep-Watch muss dafür bei *jeder* Mutation den
|
||
kompletten reaktiven Objektgraphen erneut durchlaufen, ein Aufwand
|
||
proportional zur Gesamtgröße des Dokuments statt zur eigentlichen Änderung
|
||
(gemessen: bei einer 50.000-Zeilen-Tabelle kostete ein einzelner
|
||
Zellen-Edit dadurch ~450 ms, unabhängig vom Umfang der Änderung). Stattdessen
|
||
ruft jede inhaltsverändernde Store-Funktion explizit `markDirty()` auf (nach
|
||
demselben Muster wie `bump()`/`recalcTick` für den Engine-Cache) — reine
|
||
Auswahl-/Navigations-Funktionen rufen nur `scheduleSave()` (Persistenz ohne
|
||
Undo-Schritt). Der Kompromiss: `markDirty()` prüft nicht mehr bei jeder
|
||
Mutation per Fingerprint-Vergleich, ob wirklich etwas geändert wurde (genau
|
||
dieser Vergleich bei jeder Mutation war der teure Teil) — dieses Sicherheitsnetz
|
||
bleibt in `commitHistoryStep()` erhalten, läuft aber nur noch einmal pro
|
||
debounced Schritt statt pro Mutation. `setCellInput` hat zusätzlich eine
|
||
eigene, günstige No-Op-Prüfung (unverändert übernommener Wert, z. B. `F2`,
|
||
`Enter` ohne Änderung), damit der häufigste Fall keinen Schritt erzeugt und
|
||
den Wiederholen-Ast nicht verwirft.
|
||
|
||
**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 rein lesende 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.
|
||
|
||
**Namensfeld als Sprungmarke** (Exceleigenschaft, hier zusätzlich mit
|
||
Autovervollständigung): das Feld selbst ist editierbar. Eingetippt und mit
|
||
`Enter` bestätigt springt es zu jedem gültigen Ziel — derselben Syntax, die
|
||
auch eine Formel verwenden würde:
|
||
- eine **Zelle** (`A1`) oder ein **Bereich** (`A1:C10`) auf der gerade
|
||
aktiven Tabelle,
|
||
- ein **qualifizierter** Bezug auf eine andere Tabelle/ein anderes Blatt
|
||
(`Tabelle::A1`, `Blatt!Tabelle::A1:B5`) — wechselt Blatt/Auswahl mit,
|
||
- ein **benannter Bereich** (`Umsatz_Q1`) — markiert dessen Zielbereich,
|
||
- ein bloßer **Tabellenname** (`Kosten`) — markiert die ganze Tabelle,
|
||
- ein bloßer **Blattname** (`Blatt 2`) — wechselt dorthin.
|
||
|
||
Während des Tippens schlägt eine Dropdown-Liste passende **benannte
|
||
Bereiche**, **Tabellennamen** und **Blattnamen** vor (Präfix, groß-/
|
||
kleinschreibungsunabhängig; `↑`/`↓` navigiert, `Tab` übernimmt den markierten
|
||
Vorschlag ins Feld, ein Klick springt direkt dorthin). Ein nicht auflösbarer
|
||
Text (z. B. eine echte Formel wie `SUM(A1;A2)` — das Feld navigiert, es
|
||
wertet nicht aus) meldet sich per Toast und lässt den Text zum Korrigieren
|
||
markiert stehen; `Esc` verwirft die Eingabe und zeigt wieder die aktuelle
|
||
Auswahl.
|
||
|
||
**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 **„fx"-Button** 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.
|
||
Zellen, die die betroffene Funktion aufrufen, zeigen danach `#NAME?`.
|
||
Anders als bei einer **Tabelle** (s. u. „Referenzen bleiben nach dem
|
||
Umbenennen konsistent") — eine Funktion hat keine Referenz-Syntax, die
|
||
sich automatisch umschreiben ließe, nur ihren Aufruf-Namen.
|
||
- 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: echter Code statt einer einzelnen Formel
|
||
|
||
Der „Art"-Umschalter im Formular bietet neben **„Einfache Formel"** (s. o.)
|
||
auch **„Mit Schleife"** — echter Code mit Kontrollfluss, für Fälle, die eine
|
||
einzelne Formel nicht ausdrücken kann: über die Elemente eines Bereichs
|
||
selbst iterieren, verzweigen, vorzeitig abbrechen. Das Code-Feld akzeptiert
|
||
eine Folge aus:
|
||
|
||
- **Zuweisungen** — `name = ausdruck`.
|
||
- **FOR-Schleifen** — `FOR var IN bereich DO … END`, mit beliebig vielen
|
||
Anweisungen dazwischen (auch weiteren Schleifen/`IF`s); `bereich` ist ein
|
||
Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck).
|
||
- **IF/ELSE** — `IF bedingung THEN … END` bzw. mit `ELSE …` davor dem `END`,
|
||
verschachtelbar in **jedem** Block (auch innerhalb eines `FOR`-Körpers).
|
||
- **RETURN** — `RETURN ausdruck`, an **beliebiger Stelle**, auch mitten in
|
||
einer Schleife oder einem `IF`-Zweig. Bricht die Ausführung sofort mit
|
||
diesem Wert ab; nichts danach läuft noch, auch keine weiteren
|
||
Schleifen-Durchläufe.
|
||
|
||
— beliebig gemischt und verschachtelt. Jeder eingeführte Name (Zuweisungsziel
|
||
oder Schleifenvariable) bleibt für den **restlichen** Code sichtbar, auch
|
||
nach dem Ende „seines" `FOR`/`IF` (mit seinem zuletzt zugewiesenen Wert) —
|
||
flacher Namensraum, kein Block-Scoping. `FOR`/`IN`/`DO`/`END`/`IF`/`THEN`/
|
||
`ELSE`/`RETURN` sind reservierte Wörter, nicht als Parameter-/Variablenname
|
||
erlaubt. Für `IF`-Bedingungen gibt es Vergleichsoperatoren
|
||
(`= <> > < >= <=`, Ergebnis `1`/`0`) — in **jeder** Formel nutzbar, nicht
|
||
nur in Blöcken.
|
||
|
||
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
|
||
END
|
||
RETURN ergebnis
|
||
```
|
||
|
||
aufgerufen als `=PRODUCT(Umsatz!B2:E6)`. Ein zweites Beispiel mit `IF` und
|
||
frühem `RETURN` — die erste Zahl in einem Bereich über einer Schwelle finden,
|
||
ohne den Rest des Bereichs überhaupt anzufassen:
|
||
|
||
```
|
||
FOR x IN bereich DO
|
||
IF x > schwelle THEN
|
||
RETURN x
|
||
END
|
||
END
|
||
RETURN 0
|
||
```
|
||
|
||
**Kein Endlos-Risiko trotz echtem Kontrollfluss.** Anders als die einfache
|
||
Formel-Variante (die vollständig zu einer statischen Formel „inlined" wird)
|
||
läuft eine `FOR`/`IF`/`RETURN`-Funktion bei jeder Neuberechnung durch einen
|
||
kleinen, echten Interpreter (`evaluator.ts`) — nötig, weil ein `IF`-Zweig
|
||
bzw. ein frühes `RETURN` vom tatsächlichen Zellwert abhängt, nicht vorab
|
||
berechenbar ist. Eine `FOR`-Schleife über insgesamt mehr als 10.000 Zellen
|
||
(über alle Schleifen einer Funktion) ergibt `#ERR` statt zu hängen; eine
|
||
Funktion, deren Ausführung endet, ohne dass `RETURN` je gefeuert hat,
|
||
ebenso — das Formular warnt davor zusätzlich schon **beim Speichern**, wenn
|
||
nicht erkennbar jeder Codepfad ein `RETURN` erreicht. Details:
|
||
[`src/engine/README.md`](src/engine/README.md)s „Schleifen"-Abschnitt.
|
||
|
||
#### SUMIF/COUNTIF/AVERAGEIF nachbauen
|
||
|
||
Es gibt keine eingebauten `SUMIF`/`COUNTIF`/`AVERAGEIF` — aber die **„Mit
|
||
Schleife"**-Funktionen reichen aus, um die **selbstbezügliche** Form
|
||
nachzubauen: Kriterium und Aggregation laufen über **denselben** Bereich
|
||
(„Summe/Anzahl/Mittelwert aller Werte in `bereich`, die eine Bedingung
|
||
erfüllen"). Das deckt den häufigsten Alltagsfall ab — z. B. „Summe aller
|
||
Umsätze über 100" oder „Anzahl der Zeilen mit Region = Nord", solange
|
||
Kriterium und zu summierender Wert in derselben Zelle stehen.
|
||
|
||
```
|
||
summe = 0
|
||
FOR x IN bereich DO
|
||
IF x > schwelle THEN
|
||
summe = summe + x
|
||
END
|
||
END
|
||
RETURN summe
|
||
```
|
||
|
||
aufgerufen als `=SUMME_WENN(Umsatz!B2:B13; 100)` — das Excel-Äquivalent zu
|
||
`SUMIF(Umsatz!B2:B13; ">100")` mit gleichem Kriterien- und Summenbereich.
|
||
`COUNTIF`/`AVERAGEIF` folgen demselben Muster, nur mit `zaehler = zaehler + 1`
|
||
statt `summe = summe + x`, bzw. am Ende `RETURN summe / zaehler` (mit
|
||
`IF zaehler = 0 THEN RETURN 0 END`-Guard gegen `#DIV/0!`).
|
||
|
||
Seit dem Fix der Vergleichsoperatoren (s. u. „Formelgrammatik") funktioniert
|
||
das Kriterium auch mit **Text** — case-insensitiv und getrimmt, wie bei
|
||
`XLOOKUP`:
|
||
|
||
```
|
||
anzahl = 0
|
||
FOR zeile IN bereich DO
|
||
IF zeile = kriterium THEN
|
||
anzahl = anzahl + 1
|
||
END
|
||
END
|
||
RETURN anzahl
|
||
```
|
||
|
||
aufgerufen als `=ANZAHL_WENN(Umsatz!A2:A13; "Nord")` — zählt alle Zellen in
|
||
`Umsatz!A2:A13`, deren Text „Nord" ist (unabhängig von Groß-/Kleinschreibung
|
||
und Leerzeichen).
|
||
|
||
**Grenze, die (noch) bleibt:** ein Excel-`SUMIF(Kriterienbereich; Kriterium;
|
||
Summenbereich)` mit **getrennten** Bereichen (Kriterium in Spalte A, zu
|
||
summierender Wert in Spalte B) ist damit **nicht** nachbaubar — die Sprache
|
||
hat kein `OFFSET`/`INDEX`-Äquivalent, um „die Zelle an derselben Position im
|
||
anderen Bereich" zu adressieren; eine `FOR`-Schleife erreicht nur den
|
||
Bereich, über den sie selbst iteriert. Siehe „Bekannte Grenzen" unten.
|
||
|
||
### Benannte Bereiche
|
||
|
||
Der **„Benannte Bereiche"-Button** (Tag-Symbol) in der Symbolleiste, direkt
|
||
neben dem **„fx"-Button**, öffnet die einzige Stelle, an der dokumentweite Namen für
|
||
eine feste Zelle/einen festen Bereich angelegt, bearbeitet und gelöscht
|
||
werden — z. B. `Umsatz_Q1` statt `Umsatz!B2:B6`. Danach in **jeder**
|
||
Top-Level-Formel unqualifiziert nutzbar, ohne Tabellen-Präfix:
|
||
`=SUM(Umsatz_Q1)` oder `=Umsatz_Q1 + 1` für eine einzelne Zelle. Das
|
||
vorbefüllte Dokument bringt `Umsatz_Q1` (→ `Umsatz!B2:B6`) bereits als
|
||
Beispiel mit.
|
||
|
||
- **Liste** zeigt jeden Namen als `Name → Tabelle (Blatt)!Bereich` — mit
|
||
**Bearb.** und **🗑** wie bei den Funktionen.
|
||
- **Formular**: Name, eine Tabelle (Auswahl über alle Blätter hinweg) und
|
||
zwei Zellfelder (Start/Ende als A1-Notation, z. B. `B2` und `B6`); ein
|
||
leeres zweites Feld bedeutet „nur die erste Zelle". **„+ Neuer Name"**
|
||
übernimmt automatisch die aktuelle Zellauswahl (Tabelle + Bereich) als
|
||
Vorbelegung, wenn eine Zelle/ein Bereich gerade markiert ist — analog zu
|
||
Excels „Namen aus Auswahl erstellen".
|
||
- **Validierung beim Speichern**: gültiger Name (keine Zahl/Zellform wie
|
||
`A1`), kein reserviertes Schlüsselwort (`FOR`/`IN`/`DO`/`RETURN`), keine
|
||
Kollision mit einem eingebauten Namen, einer benutzerdefinierten Funktion
|
||
oder einem anderen benannten Bereich (case-insensitiv). **Kein
|
||
Zyklus-Check nötig** — anders als eine benutzerdefinierte Funktion hat ein
|
||
benannter Bereich keinen eigenen Ausdruck, nur feste Koordinaten, kann sich
|
||
also nie selbst referenzieren.
|
||
- **Überlebt eine Tabellen-Umbenennung.** Intern wird die Tabellen-*ID*
|
||
gespeichert, nicht ihr Name — anders als ein normaler `Tabelle!Zelle`-Bezug
|
||
im Formeltext (der bricht bei Umbenennung, wie eh und je) wird ein
|
||
benannter Bereich beim Nachschlagen live auf den *aktuellen* Tabellennamen
|
||
aufgelöst. Wird die Tabelle dagegen **gelöscht**, bleibt der Name bestehen
|
||
und zeigt in Formeln danach `#NAME?` — „nichts wird automatisch
|
||
umgeschrieben", dieselbe Konvention wie beim Löschen einer benutzerdefinierten
|
||
Funktion oder Tabelle.
|
||
- Vollständig **undo-/redo-fähig** und Teil des JSON-Exports/-Imports.
|
||
|
||
**Bewusst ohne** (Stand jetzt):
|
||
- **Nicht innerhalb einer benutzerdefinierten Funktion nutzbar** — im
|
||
Funktionskörper selbst bleibt ein unqualifizierter, nicht-zellförmiger
|
||
Name ein harter Parse-Fehler, genau wie vor diesem Feature. Grund: sonst
|
||
würde z. B. eine lokale Variable, die versehentlich vor ihrer eigenen
|
||
Zuweisung verwendet wird, nicht mehr sofort beim Speichern auffallen,
|
||
sondern erst zur Laufzeit als `#NAME?` — oder, schlimmer, sie würde
|
||
still passend zu einem gleichnamigen benannten Bereich irgendwo im
|
||
Dokument auflösen. Ein **Aufrufer** kann einen benannten Bereich aber
|
||
ganz normal als Argument übergeben (`=MEINEFUNKTION(Umsatz_Q1)`) — die
|
||
Einschränkung gilt nur fürs *Eintippen des bloßen Namens* im Funktionskörper
|
||
selbst.
|
||
- **Keine eigene Formel/kein Ausdruck** — nur eine feste Zelle/ein fester
|
||
Bereich, kein benannter *berechneter* Wert (dafür gibt es weiterhin
|
||
benutzerdefinierte Funktionen ohne Parameter, s. o.).
|
||
- **Nicht in der Funktions-Autovervollständigung** — das Dropdown beim
|
||
Tippen nach `=` schlägt bisher nur Funktionsnamen vor, keine benannten
|
||
Bereiche.
|
||
- **Kein „Bereich" als `Name:B2`** — ein benannter Bereich ist immer die
|
||
ganze, vordefinierte Adresse; er kann nicht als Startpunkt einer neu
|
||
getippten `:`-Bereichsangabe kombiniert werden.
|
||
|
||
### Makros
|
||
|
||
Ein **USP gegenüber gewöhnlichen Tabellenkalkulationen**: Zellen, Zeilen/
|
||
Spalten, Tabellen und Blätter dokumentweit **per Skript adressieren und
|
||
verändern** — Struktur *und* Inhalt — statt nur per Formel zu lesen. Anders
|
||
als eine Formel läuft ein Makro **außerhalb der Neuberechnung**: es wird nie
|
||
aus einer Zelle heraus aufgerufen, sondern immer **explizit ausgelöst** —
|
||
über **„▶ Ausführen"** in der neuen **„▶ Makros"**-Symbolleisten-Ansicht
|
||
oder per Klick auf ein **Button-Objekt** auf der Fläche (einfügbar über das
|
||
Symbol neben „Text einfügen"; welches Makro der Klick auslöst, wird im
|
||
Inspektor **oder** per Rechtsklick auf den Button direkt gewählt). Diese
|
||
Trennung ist bewusst: eine Formel, die als Nebeneffekt andere Zellen
|
||
verändert, würde die Neuberechnungsreihenfolge undefiniert machen (genau
|
||
das vermeidet dieses Feature) — ein Makro ist stattdessen dieselbe Art
|
||
Mutation, die auch ein Klick in der UI auslösen würde, nur mit
|
||
`FOR`/`IF`/`RETURN`-Kontrollfluss verkettet (dieselbe Sprache wie „Mit
|
||
Schleife"-Funktionen, s. o.).
|
||
|
||
**Elf Schreib-Aktionen**, adressiert mit der gewohnten
|
||
`Blatt!Tabelle::Zelle`-Syntax:
|
||
|
||
| Aktion | Wirkung |
|
||
|---|---|
|
||
| `SETVALUE(Ziel; Wert)` | schreibt `Wert` in eine Zelle oder — als Ziel ein Bereich — denselben Wert in jede Zelle darin (wie Excels `Strg+Enter`). Beginnt der (ggf. per `&` zusammengesetzte) Text mit `=`, wird er wie getippt zur echten Formel. |
|
||
| `INSERTROW`/`DELETEROW`/`INSERTCOL`/`DELETECOL(TabellenRef; Index)` | dieselbe dokumentweite Formel-Referenz-Umschreibung wie das manuelle Einfügen/Löschen (s. o.) |
|
||
| `ADDTABLE(BlattRef; Name)`, `RENAMETABLE`/`DELETETABLE(TabellenRef)` | Tabellen anlegen/umbenennen/löschen |
|
||
| `ADDSHEET(Name)`, `RENAMESHEET`/`DELETESHEET(BlattRef)` | Blätter anlegen/umbenennen/löschen |
|
||
|
||
`TabellenRef`/`BlattRef` sind Text (`"Umsatz"` oder `"Blatt2!Kosten"`,
|
||
identisch zur Formel-Qualifizierung aufgelöst) — auch berechenbar, z. B.
|
||
`ADDTABLE("Blatt1"; "Kopie_" & COUNT(Umsatz!A:A))`. Nur `SETVALUE`s erstes
|
||
Argument ist ein echter Zell-/Bereichsbezug (Literal, Parameter,
|
||
Schleifenvariable); alle anderen Argumente sind normale Ausdrücke.
|
||
|
||
- **Jeder Zell-/Bereichsbezug muss die Tabelle nennen** (`Umsatz::A1`, nie
|
||
bloß `A1`) — anders als eine Zelle hat ein Makro keine „Heimat-Tabelle",
|
||
die ein unqualifizierter Bezug meinen könnte. Beim Speichern geprüft, mit
|
||
klarer Fehlermeldung.
|
||
- **`RETURN`** beendet die Ausführung vorzeitig (z. B. um eine Schleife
|
||
früh zu verlassen); ein `RETURN "text"` wird zur Erfolgsmeldung im Toast
|
||
statt des generischen „Makro ausgeführt.".
|
||
- **Ein ganzer Lauf zählt als ein Undo-Schritt** — läuft er synchron durch
|
||
(immer der Fall), fasst die bestehende, zeitbasierte Undo/Redo-Historie
|
||
ihn automatisch zu einem `Strg`/`Cmd`+`Z` zusammen, ohne eigene
|
||
Transaktionslogik.
|
||
- **Ein Fehler mitten im Lauf bricht sofort ab** (unbekannte Tabelle,
|
||
fehlerhafter Wert, …) und zeigt die Ursache als Fehler-Toast — bereits
|
||
ausgeführte Schritte bleiben stehen (kein automatisches Rollback,
|
||
bewusst; per Undo rückgängig machbar).
|
||
- **Eigenes, kleineres Schrittbudget** (1.000 statt der 10.000 einer
|
||
gewöhnlichen `FOR`-Schleife) — eine Makro-Schleife kann pro Durchlauf
|
||
eine volle Neuberechnung auslösen (bei einer Struktur-Aktion), ein zu
|
||
großzügiges Budget wäre dort ein echtes Einfrier-Risiko.
|
||
- **Kein Makro ruft ein anderes Makro auf** (keine solche Aktion existiert)
|
||
— eliminiert jedes Makro-triggert-Makro-Zyklus-Risiko von vornherein.
|
||
- Eine im Makro aufgerufene **benutzerdefinierte Funktion** funktioniert
|
||
normal (berechnet einen Wert), kann aber selbst nie schreiben — ihr
|
||
Funktionskörper wird immer ohne Schreib-Rechte geparst.
|
||
|
||
**GET/POST + JSON-Ergebnis mit Iteration**: `r = GET(url)` und
|
||
`r = POST(url; body)` rufen eine externe Adresse auf und binden die
|
||
geparste JSON-Antwort an `r` — genau wie jede andere Zuweisung, aber die
|
||
einzige Stelle in der Sprache, die wirklich auf ein Netzwerk-Roundtrip
|
||
wartet. `FOR item IN r DO … END` (oder direkt `FOR item IN JSONGET(r;
|
||
"pfad") DO … END`) iteriert, sobald `r` ein JSON-**Array** ist, über dessen
|
||
Elemente — dieselbe `FOR`-Syntax wie bei einem Zell-/Bereichsbezug, nur
|
||
mit einer neuen Quellart. **`JSONGET(wert; pfad)`** liest ein Feld heraus
|
||
(`"a.b.0.c"` — punktgetrennt, eine Zahl indiziert ein Array, alles andere
|
||
einen Objekt-Schlüssel; ein fehlender Pfad ergibt JSON-`null`, kein
|
||
Fehler) — anders als `GET`/`POST` rein synchron und deshalb überall in
|
||
einem Ausdruck einsetzbar (`SETVALUE(Ziel; JSONGET(item; "name"))`,
|
||
verschachtelt in einer `IF`-Bedingung, …), nicht nur als ganze rechte
|
||
Seite einer Zuweisung. Ein JSON-Wert, direkt (ohne `JSONGET`) an einer
|
||
gewöhnlichen Werteposition verwendet, kollabiert automatisch: Text/Zahl
|
||
bleiben sich selbst, ein Bool wird `1`/`0`, ein Objekt/Array wird sein
|
||
JSON-Text — praktisch, um z. B. die rohe Antwort unverändert in eine Zelle
|
||
zu schreiben.
|
||
|
||
- **Verbindung wird pro Makro einmalig bestätigt.** Beim ersten Kontakt
|
||
eines Makros zu einem neuen Host (Domain **inkl. Port**) erscheint ein
|
||
Bestätigungsdialog mit Makro-Name und Ziel-Host; wird zugestimmt, ist
|
||
dieser Host für **dieses** Makro dauerhaft freigegeben (gespeichert,
|
||
Teil des JSON-Exports) — kein erneutes Nachfragen bei künftigen Läufen,
|
||
auch nicht in einer neuen Sitzung. Bewusst **pro Makro statt pro
|
||
Dokument**: ein importiertes Dokument mit mehreren kleinen Makros, die
|
||
denselben Host kontaktieren, soll nicht durch eine einzige Bestätigung
|
||
alle auf einmal freischalten — jedes Makro trifft seine eigene, einmalige
|
||
Entscheidung. Eine Ablehnung bricht das Makro ab, ohne den Host zu
|
||
speichern.
|
||
- **`GET`/`POST` sind nur als ganze rechte Seite einer Zuweisung gültig**
|
||
(`x = GET(url)`), nicht verschachtelt in einem Ausdruck — sie brauchen
|
||
einen echten `await`, den nur diese eine Stelle in der Sprache erlaubt.
|
||
Innerhalb einer normalen benutzerdefinierten Funktion sind `GET`/`POST`/
|
||
`JSONGET` von vornherein unerreichbar (Speicher-Zeit-Fehler mit klarer
|
||
Meldung) — Netzwerkzugriff bleibt strikt auf Makros beschränkt.
|
||
- **Nicht-2xx-Antwort oder ungültiges JSON** brechen den Lauf mit einer
|
||
klaren Fehlermeldung ab, wie jede andere Aktion.
|
||
|
||
**Bewusst ohne** (Stand jetzt): Makros haben keine Parameter (ein Klick
|
||
übergibt keine Argumente); mehrere Struktur-Aktionen in einer Schleife
|
||
lösen weiterhin je eine eigene Neuberechnung aus statt gebündelt einmal am
|
||
Ende (das kleinere Schrittbudget hält das Restrisiko klein, spätere
|
||
Optimierung möglich); ein `TabellenRef`-String wird am *ersten* `!`
|
||
gesplittet (ein Tabellenname mit eigenem `!` darin ist so nicht
|
||
adressierbar); `POST` hat keine eigenen Header/Authentifizierung (immer
|
||
`Content-Type: application/json`, der Body wird vom Makro-Autor selbst als
|
||
Text — z. B. per `&`-Verkettung — zusammengesetzt), `GET` hat keinen Body;
|
||
kein eigenes Timeout (folgt dem Browser-`fetch()`-Standard — ein
|
||
hängender Request blockiert den Makrolauf unbegrenzt); `FOR` über ein
|
||
JSON-*Objekt* (statt Array) ist ein klarer Fehler, keine implizite
|
||
„über die Werte/Einträge iterieren"-Zweitbedeutung; GET/POST teilen sich
|
||
das normale Schrittbudget (kein eigenes, kleineres Netzwerk-Kontingent).
|
||
|
||
### Zeilen & Spalten einfügen/löschen
|
||
|
||
Rechtsklick auf einen **Zeilen-** oder **Spaltenkopf** öffnet ein Kontextmenü
|
||
mit **„Zeile oberhalb/unterhalb einfügen"**, **„Zeile löschen"** (analog für
|
||
Spalten) — anders als „Zeile/Spalte hinzufügen" (die bestehenden Buttons/
|
||
Menüpunkte, die nur am **Rand** wachsen/schrumpfen, reines Resizing) wird
|
||
hier **mitten im Gitter** eingefügt/gelöscht: Zellinhalte, Formatierungen und
|
||
Zeilenhöhen-/Spaltenbreiten-Overrides ab der betroffenen Zeile/Spalte
|
||
verschieben sich, und **jede Formel im gesamten Dokument**, die eine
|
||
Referenz auf die verschobene Tabelle enthält — nicht nur innerhalb der
|
||
bearbeiteten Tabelle selbst, auch über `Tabelle!Zelle`/
|
||
`Blatt!Tabelle::Zelle`-Qualifier aus anderen Tabellen — wird automatisch
|
||
mitgezogen:
|
||
|
||
- Ein Bereich, der über die Einfüge-/Löschstelle hinausragt, **wächst bzw.
|
||
schrumpft** entsprechend (`=SUM(B2:B6)` bleibt beim Einfügen einer Zeile
|
||
mittendrin ein gültiger, nur größerer Bereich) — genau wie in Excel/Numbers.
|
||
- Eine Referenz, die **genau** auf die gelöschte Zeile/Spalte zeigt (eine
|
||
Einzelzelle, oder ein Bereich, der komplett darin lag), wird zu einem
|
||
echten **`#REF`**-Literal *in der Formel selbst* (z. B. `=B2+1` wird zu
|
||
`=#REF+1`) — dasselbe, was Excel inline anzeigt. `#REF` ist dabei keine
|
||
Notation, sondern ein echtes Grammatik-Element (`ErrorLiteralNode`, s.
|
||
„Formelgrammatik" unten): jeder der sieben Fehlercodes (`#REF`, `#DIV/0!`,
|
||
`#NAME?`, `#ERR`, `#CIRC`, `#VALUE!`, `#N/A`) lässt sich jetzt auch direkt
|
||
in eine Formel **tippen**, z. B. `=IF(A1=0;#N/A;A1)`.
|
||
- `$`-gesperrte Referenzen (`$B$5`) verschieben sich **trotzdem** — anders
|
||
als beim Kopieren ist `$` hier irrelevant: die referenzierte Zelle hat sich
|
||
strukturell bewegt, unabhängig davon, ob ihr Bezug gesperrt war.
|
||
- Eine Referenz **vor** der Einfüge-/Löschstelle bleibt **byte-identisch**
|
||
stehen (auch Groß-/Kleinschreibung, Leerzeichen) — nur das Notwendige wird
|
||
angefasst.
|
||
- Die aktuelle **Auswahl** wandert mit, falls sie auf der bearbeiteten
|
||
Tabelle liegt.
|
||
- **Undo/Redo-fähig** wie jede andere Änderung.
|
||
|
||
### Abhängigkeiten erkunden (🕸)
|
||
|
||
Rechtsklick auf eine Zelle → **„🕸 Abhängigkeiten anzeigen"** öffnet ein
|
||
andockbares Panel rechts (wie das Einstellungen-Panel links) — **kein**
|
||
Vollbild-Dialog, damit Blatt-Tabs, Statusleiste und Canvas die ganze Zeit
|
||
sichtbar und bedienbar bleiben: der Blattbezug geht beim Erkunden nie
|
||
verloren. Zeigt für die Zelle:
|
||
|
||
- **Vorgänger** — jede Zelle, die ihre Formel direkt liest (bei einem
|
||
Bereich wie `SUM(B2:E2)` jede einzelne Zelle darin).
|
||
- **Nachfolger** — jede Zelle, die *diese* Zelle direkt liest, auch über
|
||
Tabellen-/Blattgrenzen hinweg.
|
||
- Jeder Eintrag zeigt **Blatt · Tabelle!Zelle** (Blattname **immer**, nicht
|
||
nur bei Mehrdeutigkeit) sowie den aktuellen Wert.
|
||
|
||
**Ein Klick auf einen Eintrag navigiert wirklich dorthin** — nicht nur
|
||
innerhalb des Panels: Er wählt die echte Zelle im Dokument aus, wechselt bei
|
||
Bedarf das Blatt-Tab, und das Panel zentriert sich neu auf sie. Formelleiste,
|
||
Statusleiste und die Tabellen selbst zeigen danach exakt dieselbe Zelle, die
|
||
im Panel zentriert ist — das Erkunden des Formel-Netzwerks *ist* Navigieren,
|
||
keine isolierte Nebenansicht. **„← Zurück"** geht die so besuchten Zellen
|
||
rückwärts durch (auch das mit echter Navigation, nicht nur Panel-intern).
|
||
|
||
Ein Zirkelbezug (`#CIRC`) wird rot markiert (⚠), sowohl als Zentrum als auch
|
||
als Vorgänger/Nachfolger-Eintrag — die Kante existiert im Abhängigkeitsgraphen
|
||
unabhängig davon, dass sie bei der Neuberechnung übersprungen wird (s.
|
||
„Fehlermodell" unten).
|
||
|
||
**Bewusst ohne**: kein automatisches Scrollen der Zieltabelle zur
|
||
navigierten Zelle, falls sie außerhalb des sichtbaren Ausschnitts liegt
|
||
(bei einer „Feste Größe"-Tabelle mit vielen Zeilen); keine Gruppierung
|
||
vieler Vorgänger/Nachfolger zu einem Bereichs-Eintrag (`Umsatz!B2:B100`
|
||
erscheint als 99 einzelne Zeilen, nicht als ein zusammengefasster Eintrag).
|
||
|
||
### 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, **Datum**/**Zeit**/**Datum + Zeit** — kombiniert mit
|
||
der Locale unten — sowie **Datum (ISO)**, das immer `JJJJ-MM-TT` zeigt,
|
||
unabhängig von der Locale), **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**, **„Ohne Rahmen/
|
||
Kopfzeilen, wenn nicht ausgewählt"** (s. u.), 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**, **Rasterweite** (`↺` 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.
|
||
|
||
Die **Rasterweite** (`Sheet.gridSize`, 4–200 px, Standard 24) ist ebenfalls
|
||
pro Blatt einstellbar (Einstellungen → Blatt) und dient **doppelt**: sie
|
||
bestimmt sowohl den Abstand des sichtbaren Punkt-Rasters (skaliert mit dem
|
||
Zoom, damit die Punkte immer exakt dort liegen, wo ein Objekt tatsächlich
|
||
einrastet) als auch die **Schrittweite beim Einrasten** (Snapping), wenn ein
|
||
Tabellen-, Diagramm-, Text- oder Button-Objekt per Maus **verschoben** oder
|
||
(bei „Feste Größe"/Diagramm/Text/Button) **frei skaliert** wird. Eine
|
||
„Automatisch"-Tabelle rastet beim Skalieren stattdessen bereits durch ihre
|
||
Zeilen-/Spaltengrenzen selbst ein und wird nicht zusätzlich pixelweise
|
||
gerastert.
|
||
|
||
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 = comparison ;
|
||
comparison = concat [ ("="|"<>"|">"|"<"|">="|"<=") concat ] ; (* höchstens
|
||
ein Vergleich, nicht
|
||
verkettbar wie
|
||
"a < b < c"; vor allem
|
||
für IF-Bedingungen in
|
||
„Mit Schleife"-Code
|
||
(s. o.), aber in jeder
|
||
Formel nutzbar *)
|
||
concat = additive { "&" additive } ; (* links-assoziativ; Text-
|
||
Verkettung, bindet
|
||
schwächer als "+ -",
|
||
stärker als ein
|
||
Vergleich -- derselbe
|
||
Platz wie bei Excel *)
|
||
additive = 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 | ERRORLIT | "(" expression ")" | funcall | reference | namedref ;
|
||
|
||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||
|
||
namedref = IDENT ; (* ein unqualifizierter,
|
||
nicht zellförmiger,
|
||
nicht in `(` mündender
|
||
Bezeichner -- ein
|
||
benannter-Bereich-
|
||
Kandidat, s. u.; nur
|
||
außerhalb eines
|
||
Funktionskörpers *)
|
||
|
||
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}/ ;
|
||
ERRORLIT = "#REF" | "#DIV/0!" | "#NAME?" | "#ERR" | "#CIRC" |
|
||
"#VALUE!" | "#N/A" ; (* ein Fehlercode, direkt
|
||
getippt oder von einem
|
||
Zeilen/Spalten-Löschen
|
||
erzeugt, s. u. *)
|
||
```
|
||
|
||
Präzedenz schwach → stark: Vergleich (`= <> > < >= <=`) < `&` < `+ -` < `* /` <
|
||
unäres `+ -` < `^`.
|
||
|
||
**Vergleiche** sind numerisch, wenn beide Seiten Zahlen sind (leere Zelle
|
||
zählt als `0`), und **text**uell — case-insensitiv, getrimmt, wie bei
|
||
`XLOOKUP` (s. u.) —, wenn beide Seiten Text sind (leere Zelle zählt dann als
|
||
`''`). Eine echte Zahl gegen echten Text: `=`/`<>` antworten eindeutig
|
||
falsch/wahr, `> < >= <=` ergeben `#VALUE!` (kein sinnvoller Vergleich über
|
||
Typgrenzen). Nützlich z. B. für `=A1="Nord"` oder als Kriterium einer
|
||
selbstgebauten `COUNTIF`/`SUMIF`-Funktion (s. „SUMIF/COUNTIF/AVERAGEIF
|
||
nachbauen" oben).
|
||
|
||
**`&` verkettet als Text**, z. B. `=A1&" "&B1` oder `="Herr "&Name`. Eine Zahl
|
||
wird dabei ohne Locale-Formatierung dargestellt (`5`, nicht `5,00`), eine
|
||
leere Zelle trägt `''` bei (nicht `'0'`). Bindet schwächer als `+`/`-`
|
||
(`="a"&1+2` = `"a3"`, nicht `"a1"+2`), aber stärker als ein Vergleich
|
||
(`=A1&B1="ab"` prüft das *Ergebnis* der Verkettung, nicht `B1="ab"` allein).
|
||
`CONCAT(Text1; Text2; …)`/`CONCATENATE(...)` machen dasselbe als Funktion,
|
||
ohne Operator-Präzedenz zu beachten — nützlich mit vielen Teilen oder
|
||
innerhalb eines „Mit Schleife"-Codes.
|
||
|
||
Ein unqualifizierter Bezeichner, der weder eine Zelle noch (mit
|
||
`paramNames`) ein Parameter ist, parst als **`namedref`** — ein Kandidat für
|
||
einen **benannten Bereich** (s. „Benannte Bereiche" oben), erst zur
|
||
Auswertung/Abhängigkeits-Ermittlung gegen das Dokument aufgelöst (genau wie
|
||
ein Tabellen-Qualifier nie beim Parsen selbst geprüft wird). Ein unbekannter
|
||
Name ergibt `#NAME?`. **Nur außerhalb eines Funktionskörpers** — mit
|
||
`paramNames` bleibt ein unqualifizierter, nicht-zellförmiger Bezeichner ein
|
||
harter Parse-Fehler wie zuvor (s. „Benannte Bereiche"s „Bewusst ohne"). Ein
|
||
*qualifizierter* nicht-zellförmiger Bezeichner (`Tabelle!Foo`) ist immer ein
|
||
Parse-Fehler — ein benannter Bereich ist nie qualifiziert.
|
||
|
||
Ein **Fehlercode lässt sich auch direkt in eine Formel tippen**, z. B.
|
||
`=IF(A1=0;#N/A;A1)` oder `=1+#REF`. Wertet unbedingt zu genau diesem Fehler
|
||
aus, wie eine Zahl- oder Text-Konstante. Alle sieben Codes sind erlaubt:
|
||
`#REF`, `#DIV/0!`, `#NAME?`, `#ERR`, `#CIRC`, `#VALUE!`, `#N/A` — **ohne**
|
||
das in Excel übliche `!` am Ende von `#REF!`, da diese Engine ihren eigenen
|
||
Fehlercode konsequent ohne `!` schreibt (`#REF`, nicht `#REF!`; siehe
|
||
Fehlermodell unten). Genau das erzeugt auch ein **Zeilen-/Spalten-Löschen**
|
||
automatisch für eine dadurch ungültig gewordene Referenz (s. „Zeilen &
|
||
Spalten einfügen/löschen" oben) — die Formel zeigt danach z. B. `=#REF+1`
|
||
statt einer stillen Fehladressierung.
|
||
|
||
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.
|
||
|
||
**Referenzen bleiben nach dem Umbenennen konsistent.** Wird eine Tabelle
|
||
umbenannt (Doppelklick auf ihren Namen in der Kopfleiste), schreibt der
|
||
Store **dokumentweit jede** `Tabelle!Zelle`-/`Tabelle::Zelle`-Referenz auf
|
||
sie um — egal in welcher anderen Tabelle sie steht, egal ob 2- oder
|
||
3-teilige Form (bei der 3-teiligen Form wird nur der Tabellen-Teil
|
||
ersetzt, der Blatt-Teil bleibt unangetastet), egal ob Einzelzelle oder
|
||
Bereich. Nur die betroffene Qualifier-Textspanne wird ersetzt (nötigenfalls
|
||
mit `'...'` gequotet, wenn der neue Name Leerzeichen/Sonderzeichen
|
||
enthält) — der Rest der Formel, inklusive Whitespace, bleibt byte-identisch,
|
||
dieselbe Technik wie beim dokumentweiten Umschreiben durch Zeilen-/
|
||
Spalten-Einfügen (s. u.). Umbenennen + Umschreiben zählen zusammen als
|
||
**ein** Undo-Schritt. Damit bleibt eine Formel nach dem Umbenennen exakt
|
||
so gültig wie vorher — ohne dieses Umschreiben würde jede Formel, die noch
|
||
den alten Namen nennt, beim nächsten Neuberechnen `#REF` zeigen (der
|
||
Tabellenname existiert für sie dann schlicht nicht mehr). Bewusst
|
||
**begrenzt auf Tabellenzellen** — Makro-Aktionen adressieren eine Tabelle
|
||
über einen zur Laufzeit aufgelösten Text-String (s. „Makros" oben), nicht
|
||
über eine Formel-Qualifier-Syntax, und ein benutzerdefinierter
|
||
Funktionskörper wird ebenfalls nicht durchsucht (dieselbe, bereits
|
||
bestehende Begrenzung wie beim dokumentweiten Zeilen-/Spalten-Umschreiben).
|
||
|
||
**`$`-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.
|
||
|
||
**Funktions-Autovervollständigung**: sobald in der Formelleiste oder im
|
||
In-Zellen-Editor ein Funktionsname zu tippen beginnt (nach `=`, einem
|
||
Operator, `(` oder `;` — nicht bei einem Zellbezug wie `A1`), erscheint ein
|
||
Dropdown mit den passenden eingebauten **und** eigenen Funktionen samt
|
||
Kurzsignatur (z. B. `XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; …)`).
|
||
`↑`/`↓` navigieren, `Tab`/`Enter` oder ein Klick übernehmen den markierten
|
||
Vorschlag (fügt `NAME(` ein, bereit für die Argumente), `Esc` schließt nur
|
||
das Dropdown, ohne die Bearbeitung selbst abzubrechen.
|
||
|
||
**Funktionen** (case-insensitiv, Trenner `;`): `SUM`, `AVG` (Alias `AVERAGE`),
|
||
`MIN`, `MAX`, `COUNT`, `CONCAT` (Alias `CONCATENATE`), `IF(Bedingung; Dann;
|
||
Sonst?)`, `AND(Bedingung1; Bedingung2; …)`, `OR(Bedingung1; Bedingung2; …)`,
|
||
`NOT(Bedingung)`, `DATE(Jahr; Monat; Tag)`, `TIME(Std; Min; Sek)`, `TODAY()`,
|
||
`NOW()`, `XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
|
||
[Vergleichsmodus]; [Suchmodus])` — plus alle über den **„fx"-Button** 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)`. Ein optionales Argument darf übersprungen
|
||
werden, indem die Stelle einfach leer bleibt (`XLOOKUP(x; a; b; ; -1)`
|
||
lässt „WennNichtGefunden" aus, um „Vergleichsmodus" zu erreichen) — wie in
|
||
Excel; eine leere Stelle zählt wie eine leere Zelle (`0` in Arithmetik,
|
||
von `COUNT` nicht mitgezählt).
|
||
|
||
`IF` ist die einzige Funktion, die ihre Argumente **nicht** vorab auswertet:
|
||
nur die Bedingung wird immer berechnet, danach ausschließlich der tatsächlich
|
||
genommene Zweig — der andere Zweig wird gar nicht ausgewertet, ein Fehler
|
||
darin (z. B. `=IF(B1=0; 0; A1/B1)`, wenn `B1` `0` ist) schlägt also nie
|
||
durch. Die Bedingung nutzt dieselbe Zahl-Semantik wie die Vergleichsoperatoren
|
||
`= <> > < >= <=` (Ergebnis `1`/`0`, keinen eigenen Wahrheitstyp): jede
|
||
Zahl außer `0` sowie jeder nicht-leere Text gilt als wahr, z. B.
|
||
`=IF(A1>=100; "groß"; "klein")`. Ohne Sonst-Zweig liefert eine falsche
|
||
Bedingung `0`. Ein benutzerdefiniertes „IF" ist nicht anlegbar — der Name
|
||
kollidiert mit der eingebauten Funktion.
|
||
|
||
**`AND`/`OR`/`NOT`** verknüpfen mehrere Bedingungen — z. B.
|
||
`=IF(AND(A1>0; A1<10); "im Bereich"; "außerhalb")`, da ein einzelner
|
||
Vergleich (`= <> > < >= <=`) nicht verkettbar ist (`1<A1<10` ist kein
|
||
gültiger Ausdruck). Anders als `IF` werten sie **alle** Argumente aus, auch
|
||
wenn das Ergebnis schon feststeht — genau wie in Excel: `=AND(1=2; 1/0)`
|
||
ergibt `#DIV/0!`, nicht `0`/falsch. `AND`/`OR` akzeptieren beliebig viele
|
||
Argumente inkl. Bereichen (`=AND(A1:A5)` prüft jede Zelle einzeln), `NOT`
|
||
genau eines. Dieselbe Wahrheits-Semantik wie überall sonst: jede Zahl außer
|
||
`0` sowie jeder nicht-leere Text gilt als wahr, eine leere Zelle als falsch.
|
||
Funktioniert genauso in einer „Mit Schleife"-`IF`-Bedingung (s. o.), z. B.
|
||
`IF AND(x > 0; x < schwelle) THEN … END`.
|
||
|
||
**Datum & Zeit**: ein Datum ist — wie in jeder Tabellenkalkulation — intern
|
||
einfach eine **Zahl** (Tage seit dem 30.12.1899, Uhrzeit als Bruchteil eines
|
||
Tages), keine eigene Zellart. Deshalb funktioniert Datums-Arithmetik ganz
|
||
ohne Zusatzaufwand: `=fällig - rechnungsdatum` zählt Tage, `=rechnungsdatum
|
||
+ 30` addiert 30 Tage. Zwei Wege zu einem Datumswert:
|
||
- **Eingabe erkennen**: ein eindeutig lesbares Datum/eine Uhrzeit als reine
|
||
Zelleingabe (nicht als Formel) wird beim Eintippen automatisch erkannt
|
||
und sofort mit dem passenden Format angezeigt — genau wie in Excel/Numbers.
|
||
Erkannt werden ISO (`2026-09-02`, auch mit `14:30`/`14:30:15` angehängt)
|
||
und Punkt-Daten (`02.09.2026`, `02.09.26`, ebenfalls mit Uhrzeit), sowie
|
||
reine Uhrzeiten (`14:30`). **Absichtlich nicht** erkannt: Schrägstrich-Daten
|
||
(`02/09/2026`) — Tag-zuerst vs. Monat-zuerst ist über die unterstützten
|
||
Locales (en-US ist monatzuerst, alle anderen tagzuerst) nicht eindeutig
|
||
auflösbar; falsches Raten würde das Datum lautlos verfälschen, deshalb
|
||
bleibt so ein Text unangetastet. Die automatische Formatierung überschreibt
|
||
nie ein bereits explizit gesetztes Zellformat.
|
||
- **Formel**: `DATE(Jahr; Monat; Tag)` und `TIME(Std; Min; Sek)` bauen einen
|
||
Datums-/Zeitwert (Überlauf wie in Excel erlaubt: `DATE(2026;13;1)` =
|
||
01.01.2027, `TIME(25;0;0)` = 01:00 des Folgetags-Anteils); `TODAY()` /
|
||
`NOW()` liefern das aktuelle Datum bzw. Datum+Uhrzeit. Formel-Ergebnisse
|
||
bekommen **kein** automatisches Zellformat (wie bei jeder anderen Formel
|
||
auch) — das Zahlenformat-Dropdown auf „Datum"/„Zeit"/„Datum + Zeit" stellen.
|
||
`TODAY`/`NOW` sind **nicht volatil**: diese Engine rechnet rein
|
||
vorgänger-getrieben neu, ohne Timer — eine Zelle mit `=TODAY()`
|
||
aktualisiert sich erst bei ihrer eigenen nächsten Neuberechnung, nicht von
|
||
selbst beim Tageswechsel (siehe „Bekannte Grenzen" unten).
|
||
|
||
Das Zahlenformat **„Datum (ISO)"** zeigt ein Datum immer als `JJJJ-MM-TT`
|
||
(`2026-09-02`) — unabhängig von der aktiven Locale (Blatt/Dokument oder
|
||
Zelle), anders als „Datum", das der jeweiligen Locale folgt (`02.09.2026`
|
||
bei de-DE, `09/02/2026` bei en-US). Nützlich für export-nahe oder
|
||
sortierbare Datumsspalten.
|
||
|
||
**XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
|
||
[Vergleichsmodus]; [Suchmodus])**: sucht `Suchwert` in `Suchbereich` und
|
||
liefert die Zelle an derselben Position in `Rückgabebereich`. Text wird
|
||
case-insensitiv und getrimmt verglichen (`"birne"` findet „Birne");
|
||
Zahl und Text gelten nie als gleich (keine automatische Typumwandlung).
|
||
`Suchbereich`/`Rückgabebereich` müssen gleich viele Zellen haben, sonst
|
||
`#VALUE!`. Kein Treffer und kein `WennNichtGefunden` angegeben → `#N/A`
|
||
(*dokumentierte Erweiterung*, wie schon `#VALUE!`); `WennNichtGefunden`
|
||
wird — wie bei `IF` — nur ausgewertet, wenn tatsächlich kein Treffer
|
||
gefunden wurde, ein Fehler darin schlägt bei einem Treffer also nie durch.
|
||
`Vergleichsmodus`: `0` (Standard) exakt, `-1`/`1` exakt oder nächstkleinerer/
|
||
-größerer **numerischer** Wert (kein Näherungsvergleich auf Text). Excels
|
||
Platzhalter-Modus (`2`) ist nicht unterstützt. `Suchmodus`: `1` (Standard)
|
||
erster Treffer zuerst, `-1` letzter Treffer zuerst; Excels binäre
|
||
Suchmodi (`2`/`-2`, setzen einen sortierten Bereich voraus) sind nicht
|
||
unterstützt — bei den Tabellengrößen dieser Studie ohne Vorteil. Beispiel:
|
||
`=XLOOKUP(B7; A2:A6; C2:C6; "unbekannt")`.
|
||
|
||
**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, oder bei „Mit Schleife"-Code eine ungültige FOR-Quelle, mehr als
|
||
10.000 Schleifenschritte, oder eine Ausführung ohne je erreichtes `RETURN`),
|
||
`#CIRC` (Zirkelbezug, auch tabellenübergreifend), `#VALUE!`
|
||
(Typfehler in Arithmetik — *dokumentierte Erweiterung*), `#N/A` (`XLOOKUP`
|
||
ohne Treffer und ohne `WennNichtGefunden` — *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 `kind: 'expression'`-Aufruf beim Parsen der aufrufenden Zelle als Kopie des (rekursiv expandierten, parametersubstituierten) Funktionskörpers in deren AST ein — kein Laufzeit-Closure. Ein `kind: 'block'`-Aufruf (FOR/IF/RETURN-Code) wird stattdessen zu einem `BlockCallNode` mit bereits aufgelösten verschachtelten Aufrufen; sein eigener Kontrollfluss läuft erst zur Auswertungszeit, im kleinen Interpreter in `evaluator.ts` (echte Zellwerte nötig für `IF`/frühes `RETURN`). |
|
||
| `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, 331 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,
|
||
`IF` (beide Zweige, 2-/3-Argument-Form, Kurzschluss-Beweis, Vergleiche als
|
||
Bedingung, Fehler in der Bedingung).
|
||
- **Leere Argumentstelle** (`;;`) — parst als eigener Knoten, verhält sich
|
||
überall wie eine leere Zelle (`SUM(1;;3)` = 4, von `COUNT` nicht
|
||
mitgezählt); am Anfang, in der Mitte und am Ende einer Argumentliste,
|
||
inkl. `f()` ganz ohne Argumente.
|
||
- **XLOOKUP** — exakter Text-/Zahl-Treffer (case-insensitiv, kein Zahl-Text-
|
||
Crossmatch), `#N/A` ohne/mit `WennNichtGefunden`, Kurzschluss-Beweis für
|
||
`WennNichtGefunden`, `Suchmodus` 1/-1 bei Duplikaten, `Vergleichsmodus`
|
||
-1/1 (exakter Treffer gewinnt immer, nächstkleiner/-größer, kein
|
||
qualifizierender Kandidat), abgelehnte Modi (`2`, `-2`), ungleich große
|
||
Bereiche → `#VALUE!`, falsche Argumentanzahl, Einzelzellen statt Bereich,
|
||
übersprungenes optionales Argument (`;;`) erreicht ein späteres.
|
||
Workbook-Ebene: Neuberechnung bei Änderung einer Zelle in *entweder*
|
||
Bereich, keine Neuberechnung bei unbeteiligten Zellen.
|
||
- **Datum & Zeit** — `dates.ts`: `dateToSerial`/`timeSerial`-Rundtrip gegen
|
||
bekannte Werte, Überlauf-Verhalten, `parseDateTimeLiteral` für jede
|
||
erkannte Form (ISO/Punkt, mit/ohne Uhrzeit) sowie Ablehnung von
|
||
Schrägstrich-Daten und ungültigen/rollierten Daten; `literalValue` wandelt
|
||
eine erkannte Eingabe in ihren Serial um; `DATE`/`TIME`/`TODAY`/`NOW` als
|
||
Formel (Basisfälle, Arity-/Typ-Fehler, Datums-Arithmetik); `formatValue`
|
||
über mehrere Locales inkl. `plain` und das locale-unabhängige „Datum
|
||
(ISO)"; Store: Auto-Format beim Eintippen (überschreibt nie ein bereits
|
||
gesetztes Format), Namenskollision mit den neuen eingebauten Funktionen.
|
||
- **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.
|
||
- **Funktions-Autovervollständigung** — Engine (`refs.spec.ts`):
|
||
`currentIdentifierAtCaret` erkennt den getippten Namen nach `=`/Operator/`(`/
|
||
`;`, ignoriert Zellbezüge (`A1`) und bereits abgeschlossene Aufrufe (`SUM(`),
|
||
schaut nie über den Cursor hinaus, verkraftet einen kaputten Formel-Präfix
|
||
ohne zu werfen. Store (`document.spec.ts`): `functionSuggestions` filtert
|
||
case-insensitiv, mischt eingebaute und eigene Funktionen sortiert.
|
||
`functions.spec.ts`: die Signatur-Tabelle deckt sich exakt mit den
|
||
registrierten eingebauten Funktionen (kein künftiges Vergessen möglich).
|
||
- **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.
|
||
- **Vergleichsoperatoren** — alle sechs (`= <> > < >= <=`), Präzedenz
|
||
unterhalb `+ -`, nicht verkettbar (`a < b < c` ist ein Parse-Fehler);
|
||
numerischer Vergleich, Text-Vergleich (case-insensitiv/getrimmt), leere
|
||
Zelle nimmt den Typ der Gegenseite an, Zahl-gegen-Text-Mismatch
|
||
(`=`/`<>` eindeutig, `> < >= <=` → `#VALUE!`).
|
||
- **`&`-Verkettung** — Präzedenz zwischen `+ -` und einem Vergleich,
|
||
links-assoziativ, Zahl rendert locale-neutral, leere Zelle trägt `''` bei,
|
||
Fehler propagieren; `CONCAT`/`CONCATENATE` als Funktions-Äquivalent.
|
||
- **`AND`/`OR`/`NOT`** — korrekte Wahrheitstabellen, Bereichsargument (jede
|
||
Zelle einzeln), **kein** Kurzschluss (ein Fehler in einem späteren
|
||
Argument schlägt durch, selbst wenn das Ergebnis schon feststeht, wie in
|
||
echtem Excel), Komposition innerhalb eines `IF` und innerhalb einer
|
||
„Mit Schleife"-`IF`-Bedingung, Arity-Fehler → `#ERR`.
|
||
- **Benannte Bereiche** — Auflösung als Einzelzelle/Bereich (Skalar- und
|
||
Funktionsargument-Position), case-insensitives/getrimmtes Namens-Matching,
|
||
Abhängigkeitsgraph-Kante (Neuberechnung bei Änderung der Zielzelle),
|
||
Überleben einer Tabellen-Umbenennung, `#NAME?` bei unbekanntem Namen und
|
||
bei gelöschter Zieltabelle, `addNamedRange`/`updateNamedRange`/
|
||
`deleteNamedRange`-Validierung (Kollision mit Built-in/eigener
|
||
Funktion/anderem Namen, Zellform), Undo/Redo, JSON-Round-Trip, Nutzung als
|
||
`FOR`-Quelle in einer benutzerdefinierten Funktion (als übergebenes
|
||
Argument), harter Parse-Fehler statt Fallback innerhalb eines
|
||
Funktionskörpers selbst.
|
||
- **Fehler-Literale** (`ErrorLiteralNode`) — alle sieben Codes tokenisieren
|
||
als ein Token, Komposition mit Operatoren/Funktionen, Fehlerpropagation
|
||
wie bei jedem anderen Fehlerwert.
|
||
- **Zeilen/Spalten einfügen/löschen** — Datenverschiebung (Zellen, Formate,
|
||
Zeilenhöhen/Spaltenbreiten) innerhalb der Tabelle, dokumentweites
|
||
Umschreiben von Formeln (auch über `Tabelle!Zelle`- und
|
||
`Blatt!Tabelle::Zelle`-Qualifier aus *anderen* Tabellen), Wachsen/
|
||
Schrumpfen eines Bereichs, der die Stelle überspannt, Kollaps zu `#REF`
|
||
bei einer exakt getroffenen Einzelzelle/einem vollständig gelöschten
|
||
Bereich, `$`-Sperren bleiben wirkungslos (anders als beim Kopieren),
|
||
unveränderte Referenzen bleiben byte-identisch, Undo/Redo, Schutz gegen
|
||
Löschen der letzten verbleibenden Zeile/Spalte.
|
||
- **Tabellen-Umbenennung schreibt Referenzen um** — ein `Tabelle!Zelle`-
|
||
und ein `Tabelle::Zelle`-Bezug aus einer *anderen* Tabelle werden beide
|
||
umgeschrieben und liefern danach weiterhin denselben Wert (kein `#REF`),
|
||
ein Bereichsbezug behält seinen Bereich (nur der Qualifier ändert sich),
|
||
bei der 3-teiligen `Blatt!Tabelle::Zelle`-Form wird ausschließlich der
|
||
Tabellen-Teil ersetzt, ein neuer Name mit Leerzeichen wird korrekt
|
||
gequotet, eine Formel, die eine *andere*, nicht umbenannte Tabelle
|
||
referenziert, bleibt byte-identisch, ein redundanter Selbst-Qualifier
|
||
innerhalb der umbenannten Tabelle selbst wird ebenfalls erfasst,
|
||
Umbenennen + Umschreiben zählen als ein Undo-Schritt.
|
||
- **Abhängigkeiten erkunden** (`graphPrecedents`/`graphDependents` in
|
||
`workbook.spec.ts`, `cellGraphSelf`/`cellGraphNeighbors`/
|
||
`openDependencyGraph`/`closeDependencyGraph` in `document.spec.ts`) —
|
||
exakte Vorgänger/Nachfolger-Kanten, Auflösung über Tabellen-/Blattgrenzen,
|
||
bleibt korrekt für eine Zelle auf einem Zirkelbezug, aktualisiert sich
|
||
nach einer Neuzuordnung der Formel, `openDependencyGraph` löst wirklich
|
||
`selectCell` aus (inkl. Blatt-Tab-Wechsel), `closeDependencyGraph` rührt
|
||
die Auswahl nicht an. Das Panel selbst (`DependencyGraphView.vue`) ist
|
||
nur live per Playwright verifiziert, kein Komponenten-Test-Setup
|
||
vorhanden (s. „Bekannte Grenzen").
|
||
- **„Mit Schleife"-Code** — Parser-Ebene (`parseFunctionBlock`): ein
|
||
`FOR...DO...END` mit mehreren Anweisungen, mehrere unabhängige Schleifen
|
||
nacheinander, `IF...THEN...END` und `IF...THEN...ELSE...END`, `IF`
|
||
verschachtelt in `FOR` mit einem `RETURN` mittendrin, `FOR` verschachtelt
|
||
in `FOR`, `RETURN` an beliebiger Stelle mit beliebigem Ausdruck, ein Name
|
||
aus einer früheren Anweisung in einer späteren Schleife nutzbar, ein Block
|
||
ganz ohne `RETURN` parst trotzdem (das ist eine Save-/Laufzeit-Prüfung,
|
||
kein Parse-Fehler); Fehler bei `FOR`/`IF` ohne passendes `END`/`DO`/`IN`/
|
||
`THEN`, fehlendem `=`, Selbstbezug im eigenen Initialisierer. Der
|
||
Interpreter (`evaluator.ts`, Workbook-Integration in
|
||
`userFunctions.spec.ts`): korrekter Wert und Neuberechnung bei Änderung
|
||
einer Zelle *innerhalb* des übergebenen Bereichs, ein `FOR`-Körper mit
|
||
mehreren Anweisungen läuft jede Iteration vollständig, `IF`/`ELSE`
|
||
verzweigt nach dem echten, aktuellen Zellwert, ein frühes `RETURN`
|
||
innerhalb eines `IF` innerhalb eines `FOR` bricht die Schleife wirklich ab
|
||
(eine erst *nach* dem Treffer geänderte Zelle bleibt folgenlos), eine Zelle,
|
||
die nur in einem **nicht** genommenen `IF`-Zweig steht, ist trotzdem eine
|
||
erfasste Abhängigkeit, verschachtelte `FOR`-Schleifen rechnen korrekt (nicht
|
||
nur parsen), ein verschachtelter Aufruf einer anderen Funktion im
|
||
Schleifenkörper wird mit-expandiert, Arity-Prüfung gilt auch hier, mehr als
|
||
10.000 Schleifenschritte (über alle Schleifen einer Auswertung) sowie eine
|
||
FOR-Quelle, die nach Parametersubstitution kein Zell-/Bereichsbezug ist,
|
||
sowie eine Ausführung ohne je erreichtes `RETURN` ergeben alle `#ERR` statt
|
||
eines Absturzes. Store-Ebene: reservierte Wörter (`FOR`/`IN`/`DO`/`END`/
|
||
`IF`/`THEN`/`ELSE`/`RETURN`) als Parameter- oder lokaler Name abgelehnt (auch
|
||
wenn der lokale Name in einem verschachtelten `IF`-Zweig deklariert wird),
|
||
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 (auch verschachtelt in einem `IF`)
|
||
wird abgelehnt, ein Codepfad ohne garantiertes `RETURN` (fehlt ganz, oder
|
||
nur ein `IF`-Zweig ohne `ELSE` returnt) wird beim Speichern abgelehnt — mit
|
||
`ELSE`, bei dem **beide** Zweige returnen, wird akzeptiert — 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.
|
||
- **Makros** — Grammatik (`parser.spec.ts`): `NAME(args)` an Anweisungs-
|
||
position nur mit `allowWrites` (nie in einem gewöhnlichen Funktionskörper),
|
||
`;`-Argumente inkl. übersprungener Slots, Zuweisung bleibt eindeutig
|
||
unterscheidbar. Interpreter (`macros.spec.ts`): `SETVALUE`-Zielauflösung
|
||
auf Literal/Bereich/Schleifenvariable, `RETURN` als Kurzschluss inkl.
|
||
String-Nachricht, Fall-Through → `null`, alle elf Aktionen gegen einen
|
||
Mock-`MacroWriteHost`, das kleinere Makro-Schrittbudget greift bei einer
|
||
strukturell-schweren Schleife, ein verschachtelter Aufruf einer
|
||
benutzerdefinierten Funktion in einem `SETVALUE`-Wert funktioniert und
|
||
kann selbst nicht schreiben. Store-Ebene (`document.spec.ts`): Validierung
|
||
(unqualifizierte Referenz abgelehnt, Namenskollision, reservierte
|
||
Aktionsnamen als lokale Variable abgelehnt), alle elf Aktionen live gegen
|
||
ein echtes Dokument inkl. dokumentweiter Referenz-Umschreibung bei
|
||
`INSERTROW`, **der wichtigste Test**: `SETVALUE` → `INSERTROW` → `SETVALUE`
|
||
in einem Lauf beweist, dass kein Host-Zugriff nach der strukturellen
|
||
Aktion auf einer veralteten Workbook-Referenz hängen bleibt, ein ganzer
|
||
synchroner Lauf ist genau ein Undo-Schritt, `DELETESHEET` verweigert das
|
||
letzte Blatt als echten Fehlschlag statt stillem No-op, `ADDTABLE` auf ein
|
||
*anderes* Blatt lässt die aktive Ansicht/Auswahl unangetastet (reiner
|
||
Seiteneffekt-Fix, live per Playwright gefunden), JSON-Round-Trip.
|
||
- **Makros: GET/POST/JSONGET** (`macros.spec.ts`) — Ergebnis-Bindung +
|
||
Kollaps-Regel (Text/Zahl/Bool/Objekt/Array) gegen einen gemockten
|
||
`MacroNetworkHost`, `JSONGET`-Pfadauflösung (verschachtelt, Array-Index,
|
||
fehlender Pfad → `null`, falscher Werttyp → Fehler), `FOR` über ein
|
||
JSON-Array direkt **und** über einen `JSONGET(...)`-Aufruf direkt als
|
||
Quelle (letzteres ein live gefundener Lücke-Fix: `substitute()` allein
|
||
wertet einen Funktionsaufruf nie aus), `FOR` über ein JSON-Objekt/
|
||
Einzelwert → klarer Fehler, `GET`/`POST` außerhalb einer reinen Zuweisung
|
||
→ `#NAME?` (belegt die eindämmungsfreie Absicherung empirisch),
|
||
`JSONGET` dagegen funktioniert überall verschachtelt (rein synchron).
|
||
Store-Ebene (`document.spec.ts`): `GET`/`POST`/`JSONGET` in einer
|
||
benutzerdefinierten Funktion (Ausdruck **und** Block) abgelehnt;
|
||
`confirm()` gemockt — erster Lauf fragt und persistiert, zweiter Lauf
|
||
zum selben Host fragt nicht mehr, ein anderer Host fragt erneut, eine
|
||
Ablehnung bricht ab, ohne den Host zu speichern; nicht-2xx-Antwort
|
||
bricht sauber ab; **Pflichttest**: `SETVALUE` → ein absichtlich
|
||
langsames `GET` (300 ms, über der 150-ms-Undo-Koaleszenz) → `SETVALUE`
|
||
erzeugt trotzdem genau einen Undo-Schritt — beweist den
|
||
`macroRunActive`-Fix direkt; JSON-Round-Trip von `approvedHosts`.
|
||
Live per Playwright gegen eine echte same-origin-Fixture
|
||
(`public/test-fixtures/sample.json`) verifiziert: echter `GET`, echter
|
||
Bestätigungsdialog mit Makro-/Host-Namen, `FOR`/`JSONGET` über die
|
||
echte Antwort, kein erneuter Dialog beim zweiten Lauf, null
|
||
Konsolenfehler.
|
||
|
||
---
|
||
|
||
## Bekannte Grenzen
|
||
|
||
- **Makros — UI-Ebene** (`ButtonView.vue`, `MacrosModal.vue`, der Button-
|
||
Zweig in `CanvasObject.vue`/`SettingsPanel.vue`) rein UI-seitig
|
||
implementiert, ohne eigene Unit-Tests — wie bei Tabellen-Größenänderung/
|
||
Abhängigkeiten-Panel/Ctrl+A oben, nur live per Playwright verifiziert
|
||
(Makro anlegen, per Listen-Button und per Button-Objekt ausführen,
|
||
Rechtsklick-Zuweisung, Fehlerfall zeigt einen Fehler-Toast statt
|
||
abzustürzen). Die Makro-*Sprache* selbst (Grammatik, Interpreter,
|
||
Store-Aktionen) ist dagegen vollständig durch Engine-/Store-Tests
|
||
abgedeckt (s. „Tests" unten).
|
||
- **Zeilen/Spalten einfügen/löschen** (s. „Zeilen & Spalten einfügen/löschen"
|
||
oben) bewusst ohne: Mehrfachauswahl (immer genau eine Zeile/Spalte pro
|
||
Aufruf), automatisches Nachziehen von `CellFormat`-Regeln über die
|
||
Grenze hinaus (eine neue Zeile ist immer unformatiert), und ein
|
||
`OFFSET`/`INDEX`-Äquivalent — ein `#REF`, das aus einer gelöschten Zeile
|
||
entsteht, muss von Hand korrigiert werden, es gibt keine „nächstbeste
|
||
Zelle"-Heuristik.
|
||
- **Größenänderung einer „Automatisch"-Tabelle** (SE-Griff → Zeilen-/
|
||
Spaltenzahl, s. o. „Das Canvas-Modell") ist rein UI-seitig (`CanvasObject.vue`)
|
||
implementiert, ohne eigene Unit-Tests — das Projekt hat (noch) kein
|
||
Komponenten-Test-Setup (z. B. Vue Test Utils), nur Engine-/Store-Tests.
|
||
Nur live per Playwright verifiziert (Wachsen, Schrumpfen mit
|
||
Inhaltsschutz, deutliches Überschreiten der bestehenden Größe).
|
||
- **Abhängigkeiten-Panel** (`DependencyGraphView.vue`) bewusst ohne: Gruppierung
|
||
vieler Vorgänger/Nachfolger zu einem Bereichs-Eintrag (jede Zelle einzeln,
|
||
ungeachtet der Anzahl), automatisches Scrollen der Zieltabelle zur
|
||
navigierten Zelle. Wie bei der Tabellen-Größenänderung oben: rein
|
||
UI-seitig, nur live per Playwright verifiziert, kein Komponenten-Test-Setup
|
||
vorhanden — die zugrundeliegenden Store-/Engine-Funktionen
|
||
(`graphPrecedents`/`graphDependents`/`cellGraphNeighbors`/
|
||
`openDependencyGraph`) sind es aber (s. „Tests" oben).
|
||
- **`Strg`/`Cmd`+`A`-Zyklus** (s. o. „Zellauswahl") ist rein UI-seitig
|
||
(`TableGrid.vue`) implementiert, ohne eigene Unit-Tests — wie bei
|
||
Tabellen-Größenänderung und Abhängigkeiten-Panel oben nur live per
|
||
Playwright verifiziert (Stufe 1 auf eine von zwei durch eine leere Spalte
|
||
getrennten Dateninseln, Eskalation auf Stufe 2, Verharren auf Stufe 2 bei
|
||
weiteren Drücken, Rücksetzen des Zyklus durch einen Klick dazwischen).
|
||
- **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 Snapping der Canvas-Objekte.
|
||
- **Benannte Bereiche** bewusst ohne: Nutzung innerhalb einer
|
||
benutzerdefinierten Funktion (ein Aufrufer kann aber einen benannten
|
||
Bereich als Argument übergeben), einen eigenen Ausdruck/berechneten Wert
|
||
(nur feste Koordinaten), Funktions-Autovervollständigungs-Vorschläge, und
|
||
`Name:B2` als kombinierten Bereichsstart — s. „Benannte Bereiche" oben.
|
||
- **Datum & Zeit**: die automatische Erkennung beim Eintippen erfasst nur ISO-
|
||
und Punkt-Daten, absichtlich **nicht** Schrägstrich-Daten (Tag-zuerst vs.
|
||
Monat-zuerst ist über die unterstützten Locales nicht eindeutig auflösbar,
|
||
s. o.). Formel-Ergebnisse (`=DATE(...)`, `=TODAY()`, `=A1+30`) bekommen kein
|
||
automatisches Zellformat — wie jedes andere Formelergebnis auch. `TODAY()`/
|
||
`NOW()` sind nicht volatil: sie aktualisieren sich erst bei der nächsten
|
||
Neuberechnung ihrer eigenen Zelle, nicht spontan mit der Zeit.
|
||
- **XLOOKUP** bewusst ohne: Näherungsvergleich (`Vergleichsmodus` -1/1) auf
|
||
Text (nur numerisch), Platzhalter-Vergleich (`Vergleichsmodus 2`), binäre
|
||
Suchmodi (`Suchmodus 2`/`-2`, würden einen sortierten Bereich voraussetzen,
|
||
den diese Engine nicht prüfen kann). Ein übersprungenes optionales
|
||
Argument (`;;`) ist eine allgemeine Grammatik-Erweiterung, nicht
|
||
XLOOKUP-spezifisch — funktioniert in jeder Funktion.
|
||
- **Benutzerdefinierte Funktionen** (Phase 1, siehe oben) bewusst ohne:
|
||
Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/
|
||
Default-Werte, und automatisches Nachführen bestehender Aufrufe bei
|
||
Umbenennung — anders als bei einer Tabelle inzwischen (s. o.) bewusst
|
||
nicht nachgezogen, da eine Funktion nur einen Aufruf-Namen hat, keine
|
||
umschreibbare Referenz-Syntax. Der Funktionsname selbst wird beim
|
||
Formeltippen aber inzwischen vervollständigt (s. o. „Funktions-
|
||
Autovervollständigung"), auch für eigene Funktionen.
|
||
- **Funktions-Autovervollständigung** bewusst ohne: Zell-/Tabellennamen-
|
||
Vorschläge (dafür existiert der Point-Mode, s. o.), Vervollständigung, die
|
||
auch Text *nach* dem Cursor berücksichtigt (nur der Text bis zum Cursor
|
||
zählt — wie in den meisten Editoren), Live-Update der Vorschlagsliste bei
|
||
reiner Cursor-Bewegung ohne Tippen.
|
||
- **„Mit Schleife"-Code** bewusst ohne: verkettete Vergleiche (`a < b < c`,
|
||
nur ein Vergleich pro Ausdruck — `AND`/`OR` lösen das aber, s. u.),
|
||
`BREAK`/`CONTINUE` (nur `RETURN` als Kontrollfluss-Ausstieg), echtes
|
||
Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar).
|
||
Verschachtelte Schleifen/`IF`s und `RETURN` an beliebiger Stelle sind
|
||
dagegen unterstützt (s. o.), ebenso Text-Vergleich in `IF`-Bedingungen
|
||
(s. „SUMIF/COUNTIF/AVERAGEIF nachbauen" oben).
|
||
- **Keine Positions-Adressierung zwischen zwei Bereichen** (kein
|
||
`OFFSET`/`INDEX`-Äquivalent) — eine `FOR`-Schleife kann nur den Bereich
|
||
erreichen, über den sie selbst iteriert, nicht „die Zelle an derselben
|
||
Position in einem anderen Bereich". Ein klassisches dreiargumentiges
|
||
`SUMIF(Kriterienbereich; Kriterium; Summenbereich)` mit **getrennten**
|
||
Bereichen ist deshalb nicht nachbaubar; nur die selbstbezügliche Form
|
||
(Kriterium und Aggregation auf demselben Bereich) geht (s. o.).
|
||
|
||
## 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, ButtonView, MacrosModal, 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)
|
||
```
|