Datei › Speichern writes a .num file: a ZIP (fflate) laid out like OpenDocument -- an uncompressed "mimetype" entry first (application/x-numolus), meta.json (format version, ISO 8601 creation time) and document.json, the same JSON the .json export writes. Opening a .num (file dialog or drag & drop) unpacks it and offers the same replace/append choice as a .json. Non-ZIPs, foreign ZIPs, a missing document.json and a newer format version fail with a clear message. The zip code is loaded on first use, like xlsx, so the main bundle does not grow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1514 lines
94 KiB
Markdown
1514 lines
94 KiB
Markdown
# numolus — Tabellenkalkulation mit eigener Formel-Engine
|
||
|
||
Eine browserbasierte Tabellenkalkulation 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), 570 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 ] | COLPART ":" COLPART | ROWPART ":" ROWPART ) ;
|
||
(* B:B / A:C = ganze
|
||
Spalten, 1:1 / 2:5 =
|
||
ganze Zeilen, s. u. *)
|
||
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}/ ;
|
||
COLPART = /\$?[A-Za-z]{1,3}/ ;
|
||
ROWPART = /\$?[0-9]{1,7}/ ; (* 1-basiert, ab 1 *)
|
||
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()`, die Datums-/Zeitfunktionen `YEAR`, `MONTH`, `DAY`, `HOUR`,
|
||
`MINUTE`, `SECOND`, `WEEKDAY`, `ISOWEEKNUM`, `WEEKNUM`, `EDATE`, `EOMONTH`,
|
||
`DAYS`, `DATEDIF`, `NETWORKDAYS`, `WORKDAY`, `DATEVALUE`, `TIMEVALUE` (s. „Datum
|
||
& Zeit" unten), `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`.
|
||
|
||
**Ganze Spalten und Zeilen**: `B:B` (Spalte B), `A:C` (Spalten A bis C),
|
||
`1:1` (Zeile 1) und `2:5` (Zeilen 2 bis 5) — auch mit `$` (`$B:$B`) und mit
|
||
Tabellen-/Blattnamen (`Umsatz!B:B`, `Umsatz::1:1`, `'Blatt 2'!Kosten::A:C`).
|
||
So ein Bezug reicht immer bis zum aktuellen Ende der Tabelle: wächst die
|
||
Tabelle (neue Zeilen per Größenänderung oder durch Eintippen unterhalb),
|
||
zählt `=SUM(B:B)` die neuen Zellen automatisch mit. Einfügen/Löschen von
|
||
Spalten verschiebt `B:B` wie einen Bereich (Spalte vor B eingefügt → `C:C`,
|
||
Spalte B gelöscht → `#REF`), Zeilen-Änderungen lassen eine ganze Spalte
|
||
unberührt — für ganze Zeilen umgekehrt. Beim Kopieren verschiebt sich
|
||
`B:B` nur seitlich, `1:1` nur nach oben/unten, `$`-gesperrte Teile gar
|
||
nicht. Steht die Formel selbst in der summierten Spalte/Zeile, ist das ein
|
||
Zirkelbezug (`#CIRC`), wie in Excel — das Ergebnis also in eine andere
|
||
Spalte bzw. Zeile oder Tabelle schreiben.
|
||
|
||
**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`); ab 24 Stunden (`37:30`) wird daraus eine
|
||
**Dauer**. **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 — das Zahlenformat-Dropdown auf
|
||
„Datum"/„Zeit"/„Datum + Zeit" stellen. Einzige Ausnahme: eine Formel, deren
|
||
Zahlen-Eingaben **alle Zeiten oder Dauern** sind (`=SUM(B2:B8)` über
|
||
Arbeitszeiten, `=C2-B2` für Ende − Beginn), zeigt ihr Ergebnis als
|
||
**Dauer**, solange die Zelle auf „Automatisch" steht.
|
||
`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 **„Dauer"** zählt Stunden über 24 hinaus weiter (`37:30`
|
||
statt `13:30`) — für Summen von Arbeits- oder Laufzeiten; Sekunden
|
||
erscheinen nur, wenn welche da sind (`100:00:30`). **„Zeit"** bleibt eine
|
||
Uhrzeit und springt nach 24 Stunden wieder auf `00:00`. **„Datum + Zeit"**
|
||
zeigt bei einer reinen Uhrzeit (unter einem Tag) nur die Uhrzeit statt des
|
||
Nullpunkts 30.12.1899.
|
||
|
||
**Datums- und Zeitfunktionen**: `YEAR`/`MONTH`/`DAY`/`HOUR`/`MINUTE`/`SECOND`
|
||
lesen einen Teil aus (auf ganze Sekunden gerundet). Wo Excel eine
|
||
US-Voreinstellung hat, gilt hier **ISO 8601**: `WEEKDAY(Datum; [Typ])`
|
||
zählt Montag = 1 … Sonntag = 7 (Typ 1: Sonntag = 1, Typ 3: Montag = 0),
|
||
`ISOWEEKNUM` und `WEEKNUM(Datum; [Typ])` liefern die ISO-Kalenderwoche
|
||
(`WEEKNUM` Typ 1/2: US-Zählung mit Wochenbeginn Sonntag/Montag).
|
||
`EDATE(Datum; Monate)` verschiebt um Monate (31.01. + 1 = 28./29.02.),
|
||
`EOMONTH(Datum; Monate)` liefert den Monatsletzten, `DAYS(Ende; Start)` die
|
||
Kalendertage dazwischen, `DATEDIF(Start; Ende; Einheit)` den Abstand in
|
||
`"Y"`/`"M"`/`"D"` bzw. als Rest `"YM"`/`"MD"`/`"YD"`.
|
||
`NETWORKDAYS(Start; Ende; [Feiertage])` zählt Arbeitstage (Mo–Fr, beide
|
||
Enden eingeschlossen), `WORKDAY(Start; Tage; [Feiertage])` rechnet eine
|
||
Frist in Arbeitstagen — Feiertage als Zellen/Bereich. `DATEVALUE(Text)` und
|
||
`TIMEVALUE(Text)` lesen Datum bzw. Uhrzeit aus einem Text in jedem Format,
|
||
das auch beim Eintippen erkannt wird. Ungültige Argumente ergeben `#VALUE!`.
|
||
|
||
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 von numolus 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 |
|
||
|--------|--------|
|
||
| `.num` | **Speicherformat** (Datei › Speichern): ZIP-Container mit demselben JSON wie `.json`, nur kleiner. Aufbau wie OpenDocument: zuerst `mimetype` (`application/x-numolus`, unkomprimiert), dann `meta.json` (Format-Version, Erstellzeit als ISO 8601) und `document.json`. Eine Datei aus einer neueren Format-Version wird mit klarer Meldung abgelehnt. |
|
||
| `.json` | **Natives Format** (unkomprimiert), 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; `.num` `.json` `.csv` `.xlsx`.
|
||
|
||
- **`.num`** / **`.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 ein **leeres Dokument** (ein Blatt
|
||
„Blatt 1" mit einer leeren „Tabelle 1"). Ein v1-State im Storage wird beim
|
||
Laden migriert. **Datei › Neu** legt ein neues leeres Dokument an, **Datei ›
|
||
Muster · Umsatzanalyse** lädt das vorbefüllte Musterdokument (beides nach
|
||
Rückfrage, die bisherigen Änderungen gehen verloren).
|
||
|
||
## Vorbefülltes Dokument (Datei › Muster)
|
||
|
||
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`. numolus 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, 570 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 — außer „Dauer" für Formeln über reine Zeiten.
|
||
Das gilt auch dann, wenn die Formel die Zeiten in Stunden umrechnet
|
||
(`=A1*24`); dort das Format „Automatisch" durch „Ohne Formatierung" o. ä.
|
||
ersetzen. `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), num (ZIP via fflate, lazy), 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 # leeres Dokument + Musterdokument
|
||
styles/main.css # Design-Tokens (Light/Dark)
|
||
```
|