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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user