Numbers-Studie — Tabellenkalkulation mit eigener Formel-Engine
Eine browserbasierte Tabellenkalkulation als Case- und Designstudie nach dem Vorbild von Apple Numbers. Zwei Dinge stehen im Mittelpunkt:
- Handwerk — eine von Grund auf selbst geschriebene, schlanke Formel-Engine
(Tokenizer, Parser, AST, Abhängigkeitsgraph, Neuberechnung, Zirkelbezug-
Erkennung). Dokumentiert in
src/engine/README.md. - 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.
npm install
npm run dev # Vite-Devserver
npm run test # Vitest-Suite (Engine + Store + IO), 331 Tests
npm run build # Typecheck + Produktionsbuild
npm run lint # ESLint (flat config)
Alles läuft rein clientseitig — kein Server, keine Netzwerk-Requests. Datei- I/O über File API und Blob-URLs.
Das Canvas-Modell
Wie in Numbers ist ein Blatt kein einzelnes Endlos-Raster, sondern eine scroll- und zoombare Fläche, die Objekte trägt:
- Tabellen-Objekt — ein eigenes Gitter mit einem Namen, der pro Blatt
eindeutig ist (siehe „Formelgrammatik" unten für Bezüge über mehrere Blätter
mit demselben Tabellennamen). Frei positionierbar (Ziehen an der
Kopfleiste). Zwei Darstellungen:
- Automatisch — so groß wie die Zellen, alle Zeilen sichtbar; der SE-Griff skaliert die Spaltenbreiten proportional.
- Feste Größe — fester Viewport, intern scrollbar, mit frei wählbarer
Zahl fixierter Kopfzeilen und fixierter Spalten (
position: stickygegen den Scroll-Container). Der SE-Griff zieht dann den Viewport auf. Spaltenbreite per Drag am Spaltenkopf (ab 24 px), Zeilenhöhe per Drag an der Unterkante des Zeilenkopfs (Standard + je Zeile).
- Diagramm-Objekt — Chart.js (Balken/Linie), frei positionier- und skalierbar. Datenquelle = ein Tabellen-Objekt; jede Zelländerung aktualisiert es sofort.
- Text-Objekt — ein formatierbares Textfeld als Zusatz zu Tabellen und Diagrammen. Doppelklick zum Bearbeiten; Schriftgröße, Fett/Kursiv, Ausrichtung, Text- und Hintergrundfarbe im Inspector; frei skalierbar.
Objekte kommen beim Anklicken in den Vordergrund, werden per Entf gelöscht
(Diagramm/Text) bzw. über das × der Kopfleiste, per Doppelklick auf den Namen
umbenannt. Je Tabelle, Diagramm und Textfeld sind eigene Hintergrund- und
Textfarbe einstellbar. Mehrere Blätter über die Tabs unten (hinzufügen,
umbenennen, löschen, per Drag sortieren). Zoom 40–200 % pro Blatt
(Strg/Cmd + Mausrad, Zoom-Leiste oder Einstellungen).
Zellauswahl
- Klick = eine Zelle, Shift+Klick / Ziehen = ein Rechteck. Das Ziehen ist
positions- statt event-target-basiert (
elementsFromPoint), übersteht schnelle Bewegungen und das Verlassen der Tabelle, scrollt bei festen Tabellen an der Kante automatisch mit und heilt einen verlorenenmouseupselbst. - Kopf-Klick = ganze Spalte / Zeile, Ecke = alles.
Strg/Cmd+Klick = weitere Zelle hinzufügen (erneut = abwählen),Strg/Cmd+Ziehen = weiteres Rechteck; auch am Spalten-/Zeilenkopf.- Die diskontinuierliche Mehrfachauswahl trägt alle folgenden Aktionen:
Live-Aggregat (Summe/Mittel/Anzahl) in der Statusleiste, die
Auswahl-Formatierung im Inspector und
Entf(leert alle markierten Zellen). - Pfeiltasten / Enter kollabieren wieder auf eine Zelle. Die aktive Zelle
(blauer Rahmen, Formelleiste,
F2/Tippen zum Bearbeiten) ist stets die zuerst angeklickte/gesetzte Zelle einer Auswahl — sie bleibt beim Ziehen/Erweitern eines Bereichs stehen, statt dem Cursor zu folgen (Excel-/ Numbers-Verhalten). Während einer Tab/Enter-Dateneingabefolge im Bereich wandert sie mit dem Cursor mit. - Kopieren/Ausschneiden/Einfügen (
Strg/Cmd+C/X/V, Kontextmenü): überträgt Zellinhalte und Formatierung. Einfügen auf eine Auswahl, die ein exaktes Vielfaches der kopierten Größe ist, kachelt; eine kleinere oder Einzelzell-Auswahl verankert die kopierte Fläche dort und läuft darüber hinaus. Formeln werden dabei bezugsangepasst wie in Excel/Numbers — siehe „$-Bezüge sperren" unten; ein gestrichelter Rahmen markiert die Kopierquelle (mit eigenem Rahmen um deren erste Zelle, bleibt stehen, auch wenn die aktuelle Auswahl längst woanders ist),Escverwirft den Puffer. - Ausfüllkästchen (das kleine Quadrat unten rechts an der Auswahl, wie in Excel/Numbers): ziehen wiederholt die Auswahl — Werte, Formeln (bezugsangepasst) und Formatierung — in die überstrichenen Zellen. Nur eine Achse gleichzeitig (rein runter/hoch/rechts/links, je nachdem wohin weiter gezogen wird); ein mehrzeiliger/-spaltiger Ausgangsbereich wird zyklisch wiederholt, auch über eine unvollständige letzte Wiederholung hinweg.
Tastaturnavigation (Excel-nah) — Fokus in der Tabelle:
| Taste | Verhalten |
|---|---|
| Pfeiltasten | eine Zelle; Shift+Pfeil erweitert die Auswahl |
Strg/Cmd+Pfeil |
zum Rand des Datenblocks springen (leer↔gefüllt) |
| Tab / Shift+Tab | rechts / links |
| Enter / Shift+Enter | runter / hoch — nach einer Tab-Folge zurück zur Startspalte + eine Zeile |
| bei aktivem Bereich | Tab/Enter rotieren den Cursor im Bereich (mit Umbruch), der Bereich bleibt markiert |
Tippen / F2 |
Zelle editieren; Enter/Tab committen und ziehen weiter (auch im Bereich) |
Pos1 / Strg+Pos1 / Ende / Strg+Ende |
Zeilenanfang / A1 / Zeilenende / letzte Zelle |
Bild↑ / Bild↓ |
seitenweise (bei festen Tabellen nach Viewport-Höhe) |
Strg/Cmd+C/X/V |
Kopieren / Ausschneiden / Einfügen (Zellen + Formatierung, bezugsangepasst) |
Strg/Cmd+Z · Strg/Cmd+Y / Strg/Cmd+Shift+Z |
Rückgängig · Wiederholen (auch als Symbolleisten-Buttons ↶ ↷) |
F4 (im Editor/in der Formelleiste) |
Bezug am Cursor sperren/zyklisch umschalten: A1 → $A$1 → A$1 → $A1 → A1; bei markiertem Text werden alle darin gefundenen Bezüge auf einmal umgeschaltet, jeder ausgehend vom eigenen Zustand |
Alt+Enter bzw. Cmd+Enter (im Editor) |
Zeilenumbruch in der Zelle einfügen, statt zu committen — Cmd+Enter als Mac-Fallback, da ⌥ (Option) dort eine Compose-Taste ist und nicht überall zuverlässig als reines Modifier ankommt |
↑/↓/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.
In-Zellen-Editor: das Eingabefeld liegt als Overlay über der Zelle (nicht
in ihr) und wächst mit dem Inhalt — in der Breite bis zu einer angenäherten
„sichtbaren Anzeigebreite", danach umbrechend; in der Höhe mit Alt+Enter
eingefügten Zeilenumbrüchen, bis zu einem Maximum, danach intern scrollbar.
Ein Zeilenumbruch bleibt auch nach dem Committen erhalten und wird in der
Zelle als solcher dargestellt (umbrechend statt einzeiliger Ellipse), auf die
Zeilenhöhe der Tabelle geklemmt.
Formelleiste (fx, oben): ein <textarea> statt eines einzeiligen Felds
— dadurch umbricht eine lange Formel statt seitwärts wegzuscrollen, und
an der unteren Kante lässt sie sich per Maus (natives resize: vertical)
auf beliebig viele Zeilen aufziehen, um mehr auf einmal zu sehen. Alt+Enter
bzw. Cmd+Enter, F4 und Esc verhalten sich wie im In-Zellen-Editor. Das
Namensfeld links davon (ebenso das Pendant links in der Statuszeile, die
mit den Blatt-Reitern eine gemeinsame Zeile am unteren Rand teilt) zeigt bei
einer Mehrfachauswahl nicht nur die aktive Zelle, sondern den ganzen markierten
Bereich (Umsatz::B2:E6), bei einer diskontinuierlichen Auswahl zusätzlich
+N für die weiteren Rechtecke.
Bezüge per Maus einfügen ("Point-Mode"): sobald der Editor (Zelle oder
Formelleiste) eine Formel (=…) enthält, fügt ein Klick auf eine Zelle —
in derselben Tabelle oder einer ganz anderen, mit Name!-Qualifier bei
Bedarf 'in Quotes' — deren Bezug an der Cursorposition ein; Ziehen
fügt stattdessen einen Bereichsbezug (A1:C3) ein, live wachsend während
der Zug. Ein weiterer Klick/Zug ersetzt den zuletzt eingefügten Bezug,
solange nichts dazwischen getippt wurde — jede Eingabe (ein ; als
Argumenttrennzeichen ebenso wie jedes andere Zeichen) beendet dieses Fenster,
der nächste Klick fügt dann einen neuen Bezug an. Ein Klick während des
Point-Mode ändert nie die Tabellen-/Objektauswahl im Hintergrund.
Benutzerdefinierte Funktionen
Der Button „𝑓 Funktionen" in der Symbolleiste öffnet die einzige Stelle, an
der eigene, dokumentweite Funktionen angelegt, bearbeitet und gelöscht werden
— kein JSON-Handedit, keine Konsole. Danach in jeder Zelle aufrufbar wie ein
eingebauter Funktionsname: =MARGE(Umsatz!B2; Umsatz!C2).
- Liste zeigt jede Funktion als
NAME(param1; param2)— exakt die Aufruf-Syntax — mit Bearb. (öffnet dasselbe Formular, vorausgefüllt) und 🗑 (löscht nach Bestätigung). - Formular (Neu wie Bearbeiten): Name, eine dynamische Liste von
Parameter-Feldern (+ Parameter,
×zum Entfernen; auch 0 Parameter sind gültig — dann ist es eine benannte Konstante/Formel), und ein Formel-Feld optisch identisch zur Formelleiste — ohne führendes=. Ein Parametername verhält sich im Funktionskörper wie eine Zelle mit eigenem Wert; er darf selbst nicht wie ein Zellbezug aussehen (A1,$B2o. ä. sind ungültig). Ein Parameter kann auch ein Bereich sein, solange er im Funktionskörper direkt an eine bereichsfähige Funktion übergeben wird — z. B.SPANNE(bereich) = MAX(bereich) - MIN(bereich), aufgerufen als=SPANNE(Umsatz!B2:E6); das Formular zeigt dieses Muster als Hinweis unter dem Formel-Feld. Wie bei jeder Formel ergibt ein Bereich außerhalb einer Funktionsargument-Position#VALUE!. - Validierung beim Speichern, mit Inline-Fehlermeldung direkt im
Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer
eingebauten oder einer anderen benutzerdefinierten Funktion, ungültiger
Name/Parameter, ein nicht parsender Formelkörper, und ein Funktions-
Zyklus (A ruft B, B ruft A, auch über mehrere Stufen) — wird abgelehnt,
bevor je eine
#ERR-Zelle entsteht. - Umbenennen/Löschen aktualisiert keine bestehenden Formeltexte —
genau wie beim Umbenennen/Löschen einer Tabelle. Zellen, die die
betroffene Funktion aufrufen, zeigen danach
#NAME?. - Ein Aufruf wird beim Parsen der aufrufenden Zelle in ihre AST eingesetzt
(Inlining, keine Laufzeit-Closures) — siehe
src/engine/userFunctions.tsund 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/IFs);bereichist ein Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck). - IF/ELSE —
IF bedingung THEN … ENDbzw. mitELSE …davor demEND, verschachtelbar in jedem Block (auch innerhalb einesFOR-Körpers). - RETURN —
RETURN ausdruck, an beliebiger Stelle, auch mitten in einer Schleife oder einemIF-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.mds „Schleifen"-Abschnitt.
Zell-Formatierung
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im
Inspector unter Objekt · Auswahl: horizontale Ausrichtung
(links/mitte/rechts),
Zahlenformat (Automatisch, Ohne, Ganzzahl, 1/2 Nachkommastellen,
Prozent 0/2 Stellen, 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), die die Fläche zur
Seite schiebt (auf schmalen Viewports überlagert sie). Sie bündelt alle
wichtigen Punkte:
- Objekt (das gewählte):
- Tabelle → Name, Zeilen/Spalten (exakt), Darstellung, Viewport-Größe, fixierte Kopfzeilen/Spalten, Position, Zahlenformat-Locale, Standard- Zeilenhöhe, Tabellen-Hintergrund/-Textfarbe, und – bei aktiver Zellauswahl – die Auswahl-Formatierung (s. o.).
- Diagramm → Name, Ansicht, Quelltabelle, Größe, Position, Zahlenformat-Locale, Hintergrund, Text-/Achsenfarbe.
- Textfeld → Name, Text, Schriftgröße, Fett/Kursiv, Ausrichtung, Textfarbe, Hintergrund, Größe, Position.
- Blatt: Name, Zahlenformat, Hintergrundfarbe, Schriftfarbe,
Raster ein-/ausblenden (
↺setzt eine Farbe auf den Theme-Wert zurück), neues Blatt, Blatt löschen. - Ansicht: Zoom-Regler (pro Blatt gespeichert).
- Dokument: Erscheinungsbild (Auto/Hell/Dunkel), Zahlenformat (Ländereinstellung), CSV-Standard-Trennzeichen, „Auf Beispiel zurücksetzen".
Blatt-Erscheinungsbild
Jedes Blatt kann eine eigene Hintergrundfarbe (Canvas-Fläche hinter den
Objekten) und Schriftfarbe (Standard-Textfarbe der Gitter/Labels) haben und
das Punkt-Raster ein- oder ausblenden. Die Farben überschreiben die
Theme-Variablen --bg / --text nur für dieses Blatt; ohne Wert gilt das
Theme. Der Canvas-Zoom wird ebenfalls pro Blatt gemerkt.
Alle diese Einstellungen sind Teil des Dokuments (DocumentState /
Sheet) und werden damit in localStorage gespeichert und in .json
exportiert – Round-Trip verlustfrei, ungültige Werte werden beim Import
verworfen bzw. geklemmt.
Zahlenformat (Ländereinstellung)
Die Darstellung von Zahlen (Dezimal- und Tausendertrennzeichen) ist eine
Kaskade: Objekt → Blatt → Dokument. Jede Ebene kann die darüberliegende
überschreiben oder „erben" (leere Auswahl). Auswahl: Ohne Trennzeichen,
de-DE, de-CH, en-US, en-GB, fr-FR, it-IT. Wirkt auf Zellwerte, das
Live-Aggregat der Statusleiste und die Wertelabels der Diagramme. CSV-/XLSX-
Export bleibt bewusst locale-neutral (.-Dezimal, keine Gruppierung) für einen
verlustfreien Round-Trip; der CSV-Import erkennt beide Schreibweisen ohnehin.
Das vorbefüllte „Umsatz" ist bereits als feste, scrollbare Tabelle mit fixierter Kopfzeile und fixierter Spalte A angelegt, um das Feature zu zeigen.
Formelgrammatik (EBNF)
formula = "=" expression ;
expression = comparison ;
comparison = additive [ ("="|"<>"|">"|"<"|">="|"<=") additive ] ; (* 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 *)
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 | "(" expression ")" | funcall | reference ;
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
reference = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname *)
sheetQualifier = (IDENT | QUOTED) "!" ; (* Blattname -- nur erkannt,
wenn direkt gefolgt von
einem vollen "Tabelle::"
(3-teilige Form); ein
einzelnes "X!Y" ohne
folgendes "::" bleibt
die alte 2-teilige Form
(X = Tabelle) *)
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
QUOTED = /' ( [^'] | '' )* '/ ; (* Name mit Leerzeichen *)
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
Präzedenz schwach → stark: Vergleich (= <> > < >= <=) < + - < * / <
unäres + - < ^.
Der (innere) Qualifier ist der Tabellenname. ! und der Numbers-Stil ::
sind dabei austauschbar und case-/whitespace-tolerant:
Kosten!A2 = 'Kosten'!A2 = kosten!a2 = Kosten::A2.
Tabellennamen sind nur pro Blatt eindeutig, nicht dokumentweit — zwei
verschiedene Blätter dürfen je eine Tabelle „Umsatz" haben (beim Anlegen/
Umbenennen wird nur gegen das eigene Blatt geprüft). Ein unqualifizierter
Bezug wie Umsatz!B2 (ohne Blattangabe) löst sich dann wie folgt auf:
- Gibt es die Tabelle nur auf einem Blatt (der Normalfall), wird sie genommen — unverändert wie zuvor.
- 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).
- Bleibt es danach noch mehrdeutig, ergibt sich
#REF— hier hilft die explizite 3-teilige FormBlatt!Tabelle::Zelle, z. B.=Analyse!Umsatz::B2oder mit Leerzeichen='Blatt 2'!Umsatz::B2, die immer genau die Tabelle auf dem genannten Blatt trifft, unabhängig davon, wo die Formel selbst steht. Die 2-teilige Form (Umsatz!B2/Umsatz::B2) bleibt für jede bestehende Formel exakt gültig — die 3-teilige Form ist rein additiv.
Die Formel-Point-Mode-Einfügung (s. o.) erkennt eine mehrdeutige Zieltabelle selbst und fügt automatisch die volle 3-teilige Form ein, statt einer Formel, die versehentlich die falsche Tabelle träfe.
$-Bezüge sperren (relativ/gemischt/absolut): A1 (relativ) vs. $A1 /
A$1 (Spalte bzw. Zeile gesperrt) vs. $A$1 (beide gesperrt). Für die
Auswertung selbst ist $ irrelevant — sie ist rein positionsbasiert. Es zählt
beim Kopieren: eine gesperrte Achse bleibt exakt wie eingegeben stehen,
eine ungesperrte verschiebt sich um das Einfüge-Delta (siehe „Kopieren/
Ausschneiden/Einfügen" oben). Beim Ausschneiden gilt das nicht — dort
verschiebt sich die referenzierte Zelle selbst, also folgt jede Referenz
darauf, $ hin oder her. F4 schaltet den Bezug unter dem Cursor durch die
vier Kombinationen.
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, IF(Bedingung; Dann; Sonst?), DATE(Jahr; Monat; Tag), TIME(Std; Min; Sek), TODAY(), NOW(), XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden]; [Vergleichsmodus]; [Suchmodus]) — plus alle über „𝑓 Funktionen" angelegten
benutzerdefinierten Funktionen (siehe „Benutzerdefinierte Funktionen" oben),
die sich für den Aufrufer identisch verhalten. Argumente gemischt:
Einzelzellen, Bereiche (lokal oder Name!…), Zahl-Literale — z. B.
=SUM(Umsatz!B2:E2; 100; A1). 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.
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 mit14:30/14:30:15angehängt) und Punkt-Daten (02.09.2026,02.09.26, ebenfalls mit Uhrzeit), sowie reine Uhrzeiten (14:30). Absichtlich nicht erkannt: Schrägstrich-Daten (02/09/2026) — Tag-zuerst vs. Monat-zuerst ist über die unterstützten Locales (en-US ist monatzuerst, alle anderen tagzuerst) nicht eindeutig auflösbar; falsches Raten würde das Datum lautlos verfälschen, deshalb bleibt so ein Text unangetastet. Die automatische Formatierung überschreibt nie ein bereits explizit gesetztes Zellformat. - Formel:
DATE(Jahr; Monat; Tag)undTIME(Std; Min; Sek)bauen einen Datums-/Zeitwert (Überlauf wie in Excel erlaubt:DATE(2026;13;1)= 01.01.2027,TIME(25;0;0)= 01:00 des Folgetags-Anteils);TODAY()/NOW()liefern das aktuelle Datum bzw. Datum+Uhrzeit. Formel-Ergebnisse bekommen kein automatisches Zellformat (wie bei jeder anderen Formel auch) — das Zahlenformat-Dropdown auf „Datum"/„Zeit"/„Datum + Zeit" stellen.TODAY/NOWsind nicht volatil: diese Engine rechnet rein vorgänger-getrieben neu, ohne Timer — eine Zelle mit=TODAY()aktualisiert sich erst bei ihrer eigenen nächsten Neuberechnung, nicht von selbst beim Tageswechsel (siehe „Bekannte Grenzen" unten).
Das Zahlenformat „Datum (ISO)" zeigt ein Datum immer als JJJJ-MM-TT
(2026-09-02) — unabhängig von der aktiven Locale (Blatt/Dokument oder
Zelle), anders als „Datum", das der jeweiligen Locale folgt (02.09.2026
bei de-DE, 09/02/2026 bei en-US). Nützlich für export-nahe oder
sortierbare Datumsspalten.
XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
[Vergleichsmodus]; [Suchmodus]): sucht Suchwert in Suchbereich und
liefert die Zelle an derselben Position in Rückgabebereich. Text wird
case-insensitiv und getrimmt verglichen ("birne" findet „Birne");
Zahl und Text gelten nie als gleich (keine automatische Typumwandlung).
Suchbereich/Rückgabebereich müssen gleich viele Zellen haben, sonst
#VALUE!. Kein Treffer und kein WennNichtGefunden angegeben → #N/A
(dokumentierte Erweiterung, wie schon #VALUE!); WennNichtGefunden
wird — wie bei IF — nur ausgewertet, wenn tatsächlich kein Treffer
gefunden wurde, ein Fehler darin schlägt bei einem Treffer also nie durch.
Vergleichsmodus: 0 (Standard) exakt, -1/1 exakt oder nächstkleinerer/
-größerer numerischer Wert (kein Näherungsvergleich auf Text). Excels
Platzhalter-Modus (2) ist nicht unterstützt. Suchmodus: 1 (Standard)
erster Treffer zuerst, -1 letzter Treffer zuerst; Excels binäre
Suchmodi (2/-2, setzen einen sortierten Bereich voraus) sind nicht
unterstützt — bei den Tabellengrößen dieser Studie ohne Vorteil. Beispiel:
=XLOOKUP(B7; A2:A6; C2:C6; "unbekannt").
Fehlermodell (propagiert an abhängige Zellen):
#REF (ungültiger/gelöschter Bezug, unbekannter Tabellen-/Blattname, oder ein
unqualifizierter Tabellenname, der auf mehreren Blättern existiert und auch
über die „gleiches Blatt wie die Formel"-Regel nicht eindeutig wird — siehe
oben), #DIV/0!,
#NAME? (unbekannte Funktion, eingebaut oder benutzerdefiniert), #ERR
(Parse-Fehler; auch eine falsche Argumentanzahl bei einer benutzerdefinierten
Funktion, oder bei „Mit Schleife"-Code eine ungültige FOR-Quelle, mehr als
10.000 Schleifenschritte, oder eine Ausführung ohne je erreichtes RETURN),
#CIRC (Zirkelbezug, auch tabellenübergreifend), #VALUE!
(Typfehler in Arithmetik — dokumentierte Erweiterung), #N/A (XLOOKUP
ohne Treffer und ohne WennNichtGefunden — dokumentierte Erweiterung).
Referenz auf leere Zelle = 0 in Arithmetik, von COUNT nicht gezählt. Ein Zyklus zwischen
benutzerdefinierten Funktionen selbst wird nicht über #CIRC sichtbar — er
wird schon beim Speichern der Funktion im UI abgelehnt (kein neuer
Fehlercode nötig).
Architektur der Engine-Module
Framework-agnostisches ES-Modul unter src/engine/ — 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 ist das
„eigene reactive()-Modul": es hält eine Workbook-Instanz und einen
recalcTick-Ref. Die UI liest store.cellValue(...), das recalcTick berührt —
nach jeder Neuberechnung wird der Tick erhöht, jede Formelzelle re-rendert also
wie ein computed.
Selbst gebaut vs. Library
| Selbst gebaut | Library |
|---|---|
| Formel-Engine komplett (Lexer, Parser, AST, Evaluator, dokumentweiter Abhängigkeitsgraph, Topo-Sort, Zirkel-Erkennung, Funktionen) | Vue 3 + Vite |
| Die Canvas: Objekte platzieren, verschieben, skalieren, Z-Order, Zoom | Chart.js (Balken + Linie) |
| Das Raster je Tabelle: Auswahl (Klick/Shift/Drag), Tastaturnavigation, In-Zellen-Edit, Spaltenbreite per Drag | xlsx (SheetJS) — nur .xlsx I/O, lazy geladen, nie zur Auswertung |
| Reaktiver Store / Dokumentmodell (v1 → v2 Migration) | — |
| CSV-Parser + -Writer (RFC 4180, Quoting, Delimiter-Sniffing, Locale-Zahlen) | |
| JSON-Serialisierung + Schema-Validierung | |
| Sheet-Tabs (Drag-Sortierung), Kontextmenü, Modals, Toasts, Theme |
Bewusst nicht verwendet: Handsontable, AG Grid, Univer, Luckysheet,
HyperFormula, formula-parser. @vueuse/core/splitpanes (in der Aufgabe als
optionale Komfort-Libs genannt) wurden nach dem Umbau auf das Canvas-Modell nicht
mehr gebraucht und wieder entfernt, um den Abhängigkeitsbaum ehrlich zu halten.
Import / Export
Alles clientseitig (Blob + URL.createObjectURL + <a download> + revoke).
Export
| Format | Inhalt |
|---|---|
.json |
Natives Format, verlustfreier Round-Trip: alle Blätter, alle Tabellen-/Diagramm-Objekte mit Position & Größe, Rohformeln, Spaltenbreiten, Tab-Reihenfolge, aktives Objekt, Zellauswahl. |
.csv |
Aktive Tabelle, berechnete Werte, RFC-4180-Quoting, Trennzeichen ,/; wählbar, \r\n, optional UTF-8-BOM. |
.xlsx |
Arbeitsmappe via SheetJS, ein Worksheet je Tabellen-Objekt. Formeln als echte Excel-Formeln (;→,, AVG→AVERAGE), immer mit gecachtem Wert. |
.png |
Gewähltes Diagramm-Objekt über chart.toBase64Image(). |
Import
<input type="file"> und Drag & Drop auf die Canvas; .json .csv .xlsx.
.json→ Dokument ersetzen (Rückfrage) oder Blätter anhängen. v1-Dokumente (altes Ein-Raster-Format) werden automatisch nach v2 migriert (je altes Blatt ein Blatt mit einer gleichnamigen Tabelle). Schema wird validiert; bei Fehler klare Meldung, kein Teil-Import..csv→ „als neue Tabelle (aktuelles Blatt)" / „aktive Tabelle ab A1 ersetzen". Trennzeichen-Sniffing (+ Override). Locale-Erkennung (1.234,56↔1,234.56). Werte mit führendem=werden zu Formeln..xlsx→ je Worksheet ein neues Blatt mit einer Tabelle.- Nach jedem Import: kompletter Reparse + Neuberechnung + Diagramm-Refresh. Schlägt ein Import fehl, bleibt das bestehende Dokument unverändert.
Persistenz
Das gesamte Dokument wird (debounced) nach localStorage gespiegelt, robust
gegen leeren/kaputten State — Fallback ist das Seed-Dokument. Ein v1-State im
Storage wird beim Laden migriert. Button „Zurücksetzen" stellt das
Seed-Dokument wieder her.
Vorbefülltes Dokument
Blatt „Analyse" mit drei Tabellen-Objekten (Umsatz, Kosten,
Übersicht) und einem Diagramm (Umsatz je Region), plus ein leeres Blatt
„Blatt 2". „Übersicht" demonstriert tabellenübergreifende Bezüge und die vier
Grundrechenarten: Übersicht!B2 = =SUM(Umsatz!B2:E2), D2 = =B2-C2,
E2 = =D2/B2*100. Siehe src/data/seed.ts.
Hinweis zur Aufgabenstellung: Die Akzeptanzliste nennt „Übersicht!B2 = 6070". Die explizit angegebene Nord-Zeile ergibt jedoch
1200+1450+1600+1720 = 5970. Die Studie hält sich an die explizite Tabelle; das demonstrierte Verhalten (Sofort-Propagierung durch Übersicht, „Gesamt" und das Diagramm bei Änderung vonUmsatz!B2) ist davon unberührt.
Tests
npm run test — Vitest, 331 Tests:
- lexer / parser — Präzedenz & Assoziativität (
^vor*/vor+-,^rechts,-2^2 = -(2^2)), lokale/qualifizierte/quoted Bezüge,!und::, Bereiche,;-Argumente, Parse-Fehler. - refs (
$-Sperrachsen,F4) —parseA1/toA1runden relativ/absolut/ gemischt;cycleReferenceLocksdurchläuft die vier Kombinationen, trifft bei Cursor auf einem Bereich die richtige Seite, liefertnullohne 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, vonCOUNTnicht 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/Aohne/mitWennNichtGefunden, Kurzschluss-Beweis fürWennNichtGefunden,Suchmodus1/-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,parseDateTimeLiteralfür jede erkannte Form (ISO/Punkt, mit/ohne Uhrzeit) sowie Ablehnung von Schrägstrich-Daten und ungültigen/rollierten Daten;literalValuewandelt eine erkannte Eingabe in ihren Serial um;DATE/TIME/TODAY/NOWals Formel (Basisfälle, Arity-/Typ-Fehler, Datums-Arithmetik);formatValueüber mehrere Locales inkl.plainund 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,
#REFnach 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;
setTableViewklemmt fixierte Zeilen/Spalten an die Gittergröße; Verkleinern löscht Zellen außerhalb und klemmt nach; Ansichts-Einstellungen im JSON-Round-Trip. - Zahlenformat-Kaskade —
cellDisplaynutzt 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;selectionFormatmeldet nur gemeinsame Werte; Zeilenhöhe (Standard + je Zeile), Spaltenbreite ab 24 px; Verkleinern prunedformats/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 —
addToSelectionbehält frühere Rechtecke & verschiebt die aktive Zelle, erneutes Klicken schaltet eine 1×1-Auswahl ab,extendActiveRangewä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;Escverwirft 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):currentIdentifierAtCareterkennt 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):functionSuggestionsfiltert 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 < cist ein Parse-Fehler). - „Mit Schleife"-Code — Parser-Ebene (
parseFunctionBlock): einFOR...DO...ENDmit mehreren Anweisungen, mehrere unabhängige Schleifen nacheinander,IF...THEN...ENDundIF...THEN...ELSE...END,IFverschachtelt inFORmit einemRETURNmittendrin,FORverschachtelt inFOR,RETURNan beliebiger Stelle mit beliebigem Ausdruck, ein Name aus einer früheren Anweisung in einer späteren Schleife nutzbar, ein Block ganz ohneRETURNparst trotzdem (das ist eine Save-/Laufzeit-Prüfung, kein Parse-Fehler); Fehler beiFOR/IFohne passendesEND/DO/IN/THEN, fehlendem=, Selbstbezug im eigenen Initialisierer. Der Interpreter (evaluator.ts, Workbook-Integration inuserFunctions.spec.ts): korrekter Wert und Neuberechnung bei Änderung einer Zelle innerhalb des übergebenen Bereichs, einFOR-Körper mit mehreren Anweisungen läuft jede Iteration vollständig,IF/ELSEverzweigt nach dem echten, aktuellen Zellwert, ein frühesRETURNinnerhalb einesIFinnerhalb einesFORbricht die Schleife wirklich ab (eine erst nach dem Treffer geänderte Zelle bleibt folgenlos), eine Zelle, die nur in einem nicht genommenenIF-Zweig steht, ist trotzdem eine erfasste Abhängigkeit, verschachtelteFOR-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 erreichtesRETURNergeben alle#ERRstatt 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 verschachteltenIF-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 einemIF) wird abgelehnt, ein Codepfad ohne garantiertesRETURN(fehlt ganz, oder nur einIF-Zweig ohneELSEreturnt) wird beim Speichern abgelehnt — mitELSE, bei dem beide Zweige returnen, wird akzeptiert — Funktions-Zyklus durch einen Schleifenkörper wird erkannt, Undo/Redo,kindim JSON-Round-Trip, ein älteres gespeichertes Dokument ohnekind-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 unqualifiziertesTabelle!Zellezum gleichen Blatt wie die Formel auf, die explizite Form trifft immer das genannte Blatt unabhängig vom Formel-Standort, ein unbekanntes Blatt/eine weiterhin mehrdeutige Tabelle ergibt#REF, Neuberechnung folgt korrekt der Auflösung. Store- Ebene: Tabellennamen sind nur noch pro Blatt eindeutig (Anlegen/ Umbenennen prüft nur gegen das eigene Blatt), Point-Mode fügt die volle 3-teilige Form ein, sobald ein Klick auf eine mehrdeutig benannte Tabelle auf einem anderen Blatt trifft. - IO — JSON verlustfreier Round-Trip (identisch), defekte/leere Datei →
Fehler ohne Nebenwirkung, unbekannter Tabellenname →
#REF, v1→v2 beim Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting,=1+2→3, Locale-Zahlen.
Bekannte Grenzen
- Zeilen/Spalten nur am Rand. Einfügen/Löschen innerhalb eines Gitters (mit automatischem Umschreiben relativer Bezüge) ist bewusst nicht implementiert.
- Objekt-Bezüge beim Anhängen. Beim JSON-Anhängen zweier Dokumente werden nur Blatt-/Objekt-IDs neu vergeben, nicht Tabellennamen. Kollidiert ein angehängter Tabellenname mit einem bestehenden, gewinnt bei der Auflösung der erste Treffer. „Ersetzen" (der Round-Trip-Fall) ist davon nicht betroffen.
- Kein Virtual Scrolling — „Feste Größe" scrollt intern, aber alle Zeilen bleiben im DOM (nur geclippt); sehr große Tabellen/Importe (viele Tausend Zeilen) werden träge.
- Große Dateien werden per
setTimeout-Yield verarbeitet (UI bleibt reaktiv), kein echter Web Worker. - xlsx-Formel-Übernahme ist heuristisch (Separator-Swap unter Beachtung von Quotes); exotische Excel-Formeln fallen auf den Wert zurück.
- Kein Text-Verkettungsoperator, keine benannten Bereiche (nur benutzerdefinierte Funktionen, keine benannten Zellen/Bereiche), kein Snapping der Canvas-Objekte.
- Datum & Zeit: die automatische Erkennung beim Eintippen erfasst nur ISO-
und Punkt-Daten, absichtlich nicht Schrägstrich-Daten (Tag-zuerst vs.
Monat-zuerst ist über die unterstützten Locales nicht eindeutig auflösbar,
s. o.). Formel-Ergebnisse (
=DATE(...),=TODAY(),=A1+30) bekommen kein automatisches Zellformat — wie jedes andere Formelergebnis auch.TODAY()/NOW()sind nicht volatil: sie aktualisieren sich erst bei der nächsten Neuberechnung ihrer eigenen Zelle, nicht spontan mit der Zeit. - XLOOKUP bewusst ohne: Näherungsvergleich (
Vergleichsmodus-1/1) auf Text (nur numerisch), Platzhalter-Vergleich (Vergleichsmodus 2), binäre Suchmodi (Suchmodus 2/-2, würden einen sortierten Bereich voraussetzen, den diese Engine nicht prüfen kann). Ein übersprungenes optionales Argument (;;) ist eine allgemeine Grammatik-Erweiterung, nicht XLOOKUP-spezifisch — funktioniert in jeder Funktion. - Benutzerdefinierte Funktionen (Phase 1, siehe oben) bewusst ohne: Live-Vorschau des Ergebnisses im Bearbeiten-Dialog, variadische Parameter/ Default-Werte, und automatisches Nachführen bestehender Aufrufe bei Umbenennung (Verhalten entspricht bewusst dem einer Tabellen-Umbenennung, s. o.) — 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), Text-Vergleich (Vergleichsoperatoren bleiben numerisch),AND/OR/NOTfür zusammengesetzte Bedingungen,BREAK/CONTINUE(nurRETURNals Kontrollfluss-Ausstieg), echtes Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar). Verschachtelte Schleifen/IFs undRETURNan beliebiger Stelle sind dagegen unterstützt (s. o.).
Projektstruktur
src/
engine/ # Formel-Engine (Eigenbau) + Unit-Tests
store/ # reaktiver Dokument-Store (v2 Canvas-Modell), Formatierung
io/ # csv / json (Eigenbau, inkl. v1→v2), xlsx (SheetJS, lazy)
components/ # AppToolbar, SettingsPanel, FormulaBar, SheetCanvas, CanvasObject,
# TableGrid, ChartView, TextBox, SheetTabs, StatusBar, TheLogo, …
numolus-logo.svg # Marke: drei überlappende Pop-Art-Quadrate + Schriftzug
config/brand.ts # Herausgeber-Metadaten (wicom.li) + Social-Media-Liste
composables/ # Theme, Toasts
data/seed.ts # vorbefülltes Dokument
styles/main.css # Design-Tokens (Light/Dark)