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:
+58
-2
@@ -19,7 +19,7 @@ genau die genannte Tabelle. Siehe "Blatt-Qualifier" weiter unten.
|
||||
| `evaluator.ts` | AST + `EngineHost` → `CellValue`. Numerisches Auswertungsmodell, Fehler-Propagierung. Enthält auch den kleinen Interpreter für `kind: 'block'`-Funktionsaufrufe (`BlockCallNode`) – siehe „Schleifen" unten. |
|
||||
| `graph.ts` | Dokumentweiter Abhängigkeitsgraph: `affected()`, `topoOrder()`, Zirkel-Erkennung (iterative DFS, white/grey/black). |
|
||||
| `refs.ts` | A1 ↔ `(col,row)` inkl. `$`-Sperrachsen, Spaltenbuchstaben, Bereichs-Expansion, `F4`-Zyklus (`cycleReferenceLocks`). |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`), `DATE/TIME/TODAY/NOW` als Tabelle `{ NAME → fn(values) }`. `IF` ist hier nur als Platzhalter registriert (für `hasFunction()`/Namensreservierung); die echte, kurzschließende Auswertung sitzt in `evaluator.ts`, s. u. Eingebaute Namen sind nie überschreibbar. |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`), `DATE/TIME/TODAY/NOW` als Tabelle `{ NAME → fn(values) }`. `IF` und `XLOOKUP` sind hier nur als Platzhalter registriert (für `hasFunction()`/Namensreservierung); die echte Auswertung sitzt in `evaluator.ts`, s. u. Eingebaute Namen sind nie überschreibbar. |
|
||||
| `dates.ts` | Datum/Zeit als Zahl ("Serial", Tage seit 1899-12-30): `dateToSerial`/`timeSerial` (Bausteine für `DATE`/`TIME`), `todaySerial`/`nowSerial`, `serialToUtcDate` (für die Anzeige), `parseDateTimeLiteral` (Eingabe-Erkennung, s. u. „Datum & Zeit"). |
|
||||
| `userFunctions.ts` | Benutzerdefinierte Funktionen (dokumentweite Bibliothek, angelegt über die UI — siehe Haupt-`README.md`). `expandUserFunctions()` setzt einen `kind: 'expression'`-Aufruf beim Parsen der aufrufenden Zelle als Kopie des rekursiv expandierten, parametersubstituierten Funktionskörpers in deren AST ein; einen `kind: 'block'`-Aufruf (FOR/IF/RETURN-Code) verpackt sie stattdessen in einen `BlockCallNode` mit bereits aufgelösten verschachtelten Aufrufen, siehe „Schleifen" unten. |
|
||||
| `workbook.ts` | Bindeglied: flache Liste von **Tabellen** + rohe Eingaben + ASTs + Werte + Graph. Inkrementelle Neuberechnung. Implementiert `EngineHost`. Ruft `expandUserFunctions()` direkt nach `parseFormula()`. `literalValue()` erkennt Datums-/Zeit-Eingaben (`dates.ts`) vor dem generischen Zahl-Test. |
|
||||
@@ -374,6 +374,61 @@ eine Zahl ist, funktionieren `+`/`-`/Vergleiche darauf automatisch — keine
|
||||
passende `numFmt`, aber **nur** solange die Zelle noch `'auto'` ist; ein
|
||||
bereits explizit gesetztes Format wird nie überschrieben.
|
||||
|
||||
## Leere Argumentstelle (`OmittedNode`)
|
||||
|
||||
`funcall`s Grammatik (s. o.) erlaubt jetzt eine leere Stelle zwischen zwei
|
||||
`;` (oder zwischen einem `;` und dem schließenden `)`) statt eines
|
||||
Ausdrucks — Excels "dieses optionale Argument einfach auslassen"-Idiom,
|
||||
z. B. `XLOOKUP(x; a; b; ; -1)`, das `WennNichtGefunden` überspringt, um
|
||||
`Vergleichsmodus` zu erreichen. Der Parser produziert dafür einen eigenen
|
||||
`OmittedNode` (Teil des `Node`-Unions, s. o.) statt eines Parse-Fehlers.
|
||||
`evalScalar` wertet ihn wie eine leere Zelle aus (`null`) — für die meisten
|
||||
Funktionen (`SUM`, `COUNT`, …) verhält er sich also genauso wie eine echte
|
||||
leere Zelle in einem Bereich. Eine Funktion, die "explizit ausgelassen"
|
||||
von "wertet zu leer aus" unterscheiden muss (nur `XLOOKUP`, bisher), prüft
|
||||
`arg.kind === 'omitted'` auf dem rohen Argument-Knoten, **bevor** sie ihn
|
||||
auswertet (`evalXLookup`s `given()`-Hilfsfunktion in `evaluator.ts`).
|
||||
`expand()`/`substitute()` (`userFunctions.ts`) behandeln ihn als Blatt ohne
|
||||
weitere Rekursion, `collectRefs()` ignoriert ihn stillschweigend (kein
|
||||
Bezug enthalten).
|
||||
|
||||
## Nachschlagen (XLOOKUP)
|
||||
|
||||
`XLOOKUP(Suchwert; Suchbereich; Rückgabebereich; [WennNichtGefunden];
|
||||
[Vergleichsmodus]; [Suchmodus])` ist – wie `IF` – eine Funktion, die
|
||||
`evalScalar`s `case 'func':` **vor** dem generischen eager/flatten-Pfad
|
||||
abfängt (`evalXLookup()`), aus zwei Gründen, nicht nur einem:
|
||||
|
||||
1. **`WennNichtGefunden` muss lazy ausgewertet werden** — nur wenn
|
||||
tatsächlich kein Treffer gefunden wurde (derselbe Kurzschluss-Grundsatz
|
||||
wie bei `IF`s Zweigen).
|
||||
2. **`Suchbereich` und `Rückgabebereich` dürfen nicht zusammen flach
|
||||
geklopft werden.** Der generische Pfad (jede Funktion außer `IF` und
|
||||
`XLOOKUP`) wertet jedes Argument über `evalArg()` aus und hängt alle
|
||||
Ergebnisse an **eine gemeinsame** flache Liste an (siehe
|
||||
`functions.ts`s Modul-Kommentar) — dabei geht die Grenze zwischen zwei
|
||||
Bereichs-Argumenten komplett verloren. `evalArg()` selbst liefert pro
|
||||
Argument aber bereits ein eigenes Array zurück; `evalXLookup()` ruft es
|
||||
für `Suchbereich` und `Rückgabebereich` separat auf und hält die beiden
|
||||
Arrays getrennt, statt sie (wie der generische Pfad) zusammenzuführen.
|
||||
|
||||
Trefferlogik: Text wird über eine eigene `xlookupEquals()` verglichen
|
||||
(case-insensitiv, getrimmt) statt über den `=`-Operator (der über
|
||||
`toNumber()` koerziert und deshalb nie auf Text träfe – XLOOKUPs
|
||||
häufigster Anwendungsfall). Zahl und Text gelten nie als gleich. Bei
|
||||
`Vergleichsmodus -1`/`1` gewinnt ein exakter Treffer immer sofort, sonst
|
||||
wird der nächstkleinere/-größere **numerische** Kandidat gesucht (Text
|
||||
wird dabei übersprungen, nicht verglichen). `Suchmodus -1` kehrt einfach
|
||||
die Durchlaufreihenfolge um (kein Binärsuche-Algorithmus, kein sortierter
|
||||
Bereich vorausgesetzt).
|
||||
|
||||
`#N/A` (kein Treffer, kein `WennNichtGefunden`) ist eine dokumentierte
|
||||
Erweiterung des ursprünglichen 5-Fehlercode-Sets, mit derselben Begründung
|
||||
wie seinerzeit `#VALUE!`. `collectRefs()` braucht **keine** Änderung –
|
||||
sein generischer `case 'func':` läuft bereits durch alle Argumente
|
||||
(inkl. `Suchbereich`/`Rückgabebereich`), unabhängig davon, ob ein Treffer
|
||||
gefunden wird — dieselbe konservative Konvention wie bei `IF`.
|
||||
|
||||
## Neuberechnung
|
||||
|
||||
1. `setCell` reparst **eine** Zelle, aktualisiert ihre Graph-Kanten.
|
||||
@@ -396,7 +451,8 @@ rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
| `#NAME?` | unbekannte Funktion (eingebaut oder benutzerdefiniert) |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`); auch Arity-Mismatch/Expansionstiefen-Limit bei einer benutzerdefinierten Funktion, oder bei `kind: 'block'` eine ungültige FOR-Quelle, mehr als `MAX_LOOP_STEPS` Schleifenschritte, oder eine Ausführung ohne je erreichtes `RETURN` |
|
||||
| `#CIRC` | Zirkelbezug – auch tabellen-/blattübergreifend |
|
||||
| `#VALUE!` | Typfehler in Arithmetik (Text × Zahl, Bereich als Skalar). *Erweiterung über die Aufgabenstellung hinaus, dokumentiert.* |
|
||||
| `#VALUE!` | Typfehler in Arithmetik (Text × Zahl, Bereich als Skalar); auch `XLOOKUP` mit ungleich großem Such-/Rückgabebereich. *Erweiterung über die Aufgabenstellung hinaus, dokumentiert.* |
|
||||
| `#N/A` | `XLOOKUP` ohne Treffer und ohne `WennNichtGefunden`. *Erweiterung über die Aufgabenstellung hinaus, dokumentiert.* |
|
||||
|
||||
Es gibt **keinen** Absturz und **keinen** Hang: Lexer/Parser werfen intern,
|
||||
`Workbook` fängt alles ab und schreibt einen Fehlerwert in die Zelle.
|
||||
|
||||
Reference in New Issue
Block a user