AND/OR/NOT, Fehler-Literale und Zeilen/Spalten einfügen/löschen

- AND(...)/OR(...)/NOT(...) als gewöhnliche Funktionen (kein Kurzschluss-
  Sonderfall wie IF/XLOOKUP nötig, werten wie in echtem Excel alle
  Argumente eager aus). Dabei die bisher dreifach duplizierte
  Wahrheits-Prüfung zu functions.ts#isTruthy() zusammengezogen, jetzt
  von FUNCTIONS.IF, evalIf() und dem Block-IF-Statement gemeinsam genutzt.
  Schließt "kein AND/OR für zusammengesetzte Bedingungen" aus den
  Bekannten Grenzen, auch nutzbar in "Mit Schleife"-IF-Bedingungen.

- Neue Grammatik-Ergänzung: ErrorLiteralNode. Jeder der sieben Fehlercodes
  (#REF, #DIV/0!, #NAME?, #ERR, #CIRC, #VALUE!, #N/A) lässt sich jetzt
  direkt in eine Formel tippen (=IF(A1=0;#N/A;A1)) und wertet unbedingt zu
  genau diesem Fehler aus.

- Zeilen/Spalten einfügen/löschen mitten im Gitter (Rechtsklick auf einen
  Zeilen-/Spaltenkopf), nicht nur am Rand wie bisher: Zellinhalte, Formate
  und Zeilenhöhen/Spaltenbreiten verschieben sich innerhalb der Tabelle,
  und jede Formel im gesamten Dokument, die die Tabelle referenziert (auch
  über Tabelle!Zelle/Blatt!Tabelle::Zelle-Qualifier aus anderen Tabellen),
  wird automatisch mitgezogen. Ein Bereich, der die Stelle überspannt,
  wächst/schrumpft entsprechend; eine Referenz, die exakt die gelöschte
  Zeile/Spalte trifft, wird zu einem echten #REF-Literal im Formeltext
  (nutzt die neue ErrorLiteralNode-Grammatik). $-Sperren bleiben dabei
  bewusst wirkungslos (anders als beim Kopieren), unveränderte Referenzen
  bleiben byte-identisch. Token-Level-Splice wie shiftFormula (Kopieren/
  Ausschneiden), keine AST-Neuserialisierung.

406 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-08 22:34:33 +02:00
co-authored by Claude Sonnet 5
parent 2c54537427
commit aef8f1349c
14 changed files with 937 additions and 22 deletions
+97 -11
View File
@@ -367,6 +367,41 @@ Beispiel mit.
ganze, vordefinierte Adresse; er kann nicht als Startpunkt einer neu
getippten `:`-Bereichsangabe kombiniert werden.
### 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.
### Zell-Formatierung
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im
@@ -457,7 +492,7 @@ unary = ("+" | "-") unary
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 | namedref ;
primary = NUMBER | STRING | ERRORLIT | "(" expression ")" | funcall | reference | namedref ;
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
@@ -484,6 +519,11 @@ sheetQualifier = (IDENT | QUOTED) "!" ; (* Blattname -- nur erkannt,
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
QUOTED = /' ( [^'] | '' )* '/ ; (* Name mit Leerzeichen *)
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
ERRORLIT = "#REF" | "#DIV/0!" | "#NAME?" | "#ERR" | "#CIRC" |
"#VALUE!" | "#N/A" ; (* ein Fehlercode, direkt
getippt oder von einem
Zeilen/Spalten-Löschen
erzeugt, s. u. *)
```
Präzedenz schwach → stark: Vergleich (`= <> > < >= <=`) < `&` < `+ -` < `* /` <
@@ -518,6 +558,17 @@ 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`.
@@ -565,8 +616,9 @@ 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?)`, `DATE(Jahr; Monat; Tag)`, `TIME(Std; Min; Sek)`, `TODAY()`, `NOW()`,
`XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
Sonst?)`, `AND(Bedingung1; Bedingung2; …)`, `OR(Bedingung1; Bedingung2; …)`,
`NOT(Bedingung)`, `DATE(Jahr; Monat; Tag)`, `TIME(Std; Min; Sek)`, `TODAY()`,
`NOW()`, `XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
[Vergleichsmodus]; [Suchmodus])` — plus alle über **„𝑓 Funktionen"** angelegten
benutzerdefinierten Funktionen (siehe „Benutzerdefinierte Funktionen" oben),
die sich für den Aufrufer identisch verhalten. Argumente gemischt:
@@ -588,6 +640,18 @@ Zahl außer `0` sowie jeder nicht-leere Text gilt als wahr, z. B.
Bedingung `0`. Ein benutzerdefiniertes „IF" ist nicht anlegbar — der Name
kollidiert mit der eingebauten Funktion.
**`AND`/`OR`/`NOT`** verknüpfen mehrere Bedingungen — z. B.
`=IF(AND(A1>0; A1<10); "im Bereich"; "außerhalb")`, da ein einzelner
Vergleich (`= <> > < >= <=`) nicht verkettbar ist (`1<A1<10` ist kein
gültiger Ausdruck). Anders als `IF` werten sie **alle** Argumente aus, auch
wenn das Ergebnis schon feststeht — genau wie in Excel: `=AND(1=2; 1/0)`
ergibt `#DIV/0!`, nicht `0`/falsch. `AND`/`OR` akzeptieren beliebig viele
Argumente inkl. Bereichen (`=AND(A1:A5)` prüft jede Zelle einzeln), `NOT`
genau eines. Dieselbe Wahrheits-Semantik wie überall sonst: jede Zahl außer
`0` sowie jeder nicht-leere Text gilt als wahr, eine leere Zelle als falsch.
Funktioniert genauso in einer „Mit Schleife"-`IF`-Bedingung (s. o.), z. B.
`IF AND(x > 0; x < schwelle) THEN … END`.
**Datum & Zeit**: ein Datum ist — wie in jeder Tabellenkalkulation — intern
einfach eine **Zahl** (Tage seit dem 30.12.1899, Uhrzeit als Bruchteil eines
Tages), keine eigene Zellart. Deshalb funktioniert Datums-Arithmetik ganz
@@ -872,6 +936,11 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
- **`&`-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),
@@ -882,6 +951,18 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
`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.
- **„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`
@@ -940,8 +1021,13 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
## Bekannte Grenzen
- **Zeilen/Spalten nur am Rand.** Einfügen/Löschen *innerhalb* eines Gitters (mit
automatischem Umschreiben relativer Bezüge) ist bewusst nicht implementiert.
- **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.
- **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
@@ -985,12 +1071,12 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
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`/`NOT` für zusammengesetzte
Bedingungen, `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).
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