XLOOKUP als Formel, plus überspringbare optionale Argumente (;;)

Neue eingebaute Funktion XLOOKUP(Suchwert; Suchbereich; Rückgabebereich;
[WennNichtGefunden]; [Vergleichsmodus]; [Suchmodus]).

- Grammatik-Erweiterung (allgemein, nicht XLOOKUP-spezifisch): eine leere
  Argumentstelle zwischen zwei ";" (oder vor der schließenden ")") parst
  jetzt als neuer OmittedNode statt eines Parse-Fehlers -- Excels
  "dieses optionale Argument auslassen"-Idiom, z. B.
  XLOOKUP(x;a;b;;-1). Wertet überall wie eine leere Zelle aus (null);
  expand()/substitute() (userFunctions.ts) behandeln ihn als Blatt,
  collectRefs() ignoriert ihn stillschweigend.
- evaluator.ts: XLOOKUP wird -- wie IF -- vor dem generischen eager/
  flatten-Dispatch abgefangen (evalXLookup()), aus zwei Gründen: (1)
  WennNichtGefunden muss lazy ausgewertet werden (Kurzschluss wie bei
  IF), (2) Suchbereich/Rückgabebereich dürfen nicht in eine gemeinsame
  flache Werteliste geraten -- der generische Pfad würde die Grenze
  zwischen beiden Bereichen zerstören. Eigene xlookupEquals()
  (case-insensitiv/getrimmt für Text, kein Zahl-Text-Crossmatch) statt
  des `=`-Operators, der über toNumber() koerziert und nie auf Text
  träfe. Vergleichsmodus -1/1 (exakt/nächstkleiner/-größer, numerisch),
  Suchmodus 1/-1 (Richtung); Excels Platzhalter- und Binärsuch-Modi
  bewusst nicht unterstützt.
- functions.ts: Platzhalter-Eintrag für Namensreservierung (analog IF).
- types.ts: neuer Fehlercode #N/A (kein Treffer, kein
  WennNichtGefunden) -- dokumentierte Erweiterung, wie zuvor #VALUE!.
- collectRefs() brauchte keine Änderung -- sein generischer case 'func'
  läuft schon konservativ durch alle Argumente.

Tests: 22 neue (parser.spec.ts: OmittedNode leading/middle/trailing;
evaluator.spec.ts: SUM(1;;3)-Smoke plus 15 XLOOKUP-Fälle; workbook.spec.ts:
Neuberechnung bei Änderung in Such- ODER Rückgabebereich, keine bei
unbeteiligten Zellen; document.spec.ts: Namenskollision). 298 -> 318
Tests, alle grün. vue-tsc/eslint/build sauber. Live per Playwright
verifiziert (Treffer, #N/A, WennNichtGefunden, Kurzschluss-Beweis,
übersprungenes Argument für Vergleichsmodus, Neuberechnung nach Umbenennen
einer Suchbereich-Zelle inkl. nachträglich ausgelöstem #DIV/0! im vorher
nicht ausgewerteten WennNichtGefunden-Zweig).

READMEs aktualisiert (Funktionen, Fehlermodell, neue Abschnitte "Leere
Argumentstelle" und "Nachschlagen (XLOOKUP)" in engine/README.md,
Bekannte Grenzen, Testzahl; EBNF-Grammatik in parser.ts kommentiert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-03 08:21:11 +02:00
co-authored by Claude Sonnet 5
parent af01aa5e53
commit 552f1cfae8
11 changed files with 451 additions and 21 deletions
+57 -13
View File
@@ -13,7 +13,7 @@ Vorbild von **Apple Numbers**. Zwei Dinge stehen im Mittelpunkt:
```bash
npm install
npm run dev # Vite-Devserver
npm run test # Vitest-Suite (Engine + Store + IO), 298 Tests
npm run test # Vitest-Suite (Engine + Store + IO), 318 Tests
npm run build # Typecheck + Produktionsbuild
npm run lint # ESLint (flat config)
```
@@ -402,11 +402,17 @@ vier Kombinationen.
**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()` — 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)`.
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
@@ -441,16 +447,34 @@ ohne Zusatzaufwand: `=fällig - rechnungsdatum` zählt Tage, `=rechnungsdatum
`NOW()` liefern das aktuelle Datum bzw. Datum+Uhrzeit. Formel-Ergebnisse
bekommen **kein** automatisches Zellformat (wie bei jeder anderen Formel
auch) — das Zahlenformat-Dropdown auf „Datum"/„Zeit"/„Datum + Zeit" stellen.
`TODAY`/`NOW` sind **nicht volatil**: diese Engine rechnet rein
vorgänger-getrieben neu, ohne Timer — eine Zelle mit `=TODAY()`
aktualisiert sich erst bei ihrer eigenen nächsten Neuberechnung, nicht von
selbst beim Tageswechsel (siehe „Bekannte Grenzen" unten).
Das Zahlenformat **„Datum (ISO)"** zeigt ein Datum immer als `JJJJ-MM-TT`
(`2026-09-02`) — unabhängig von der aktiven Locale (Blatt/Dokument oder
Zelle), anders als „Datum", das der jeweiligen Locale folgt (`02.09.2026`
bei de-DE, `09/02/2026` bei en-US). Nützlich für export-nahe oder
sortierbare Datumsspalten.
`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).
**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
@@ -462,8 +486,9 @@ oben), `#DIV/0!`,
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*). Referenz auf leere
Zelle = `0` in Arithmetik, von `COUNT` nicht gezählt. Ein Zyklus zwischen
(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).
@@ -568,7 +593,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
## Tests
`npm run test` — Vitest, 298 Tests:
`npm run test` — Vitest, 318 Tests:
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
@@ -583,6 +608,19 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
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
@@ -738,6 +776,12 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
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:
Autovervollständigen des Funktionsnamens im Editor, Live-Vorschau des
Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und