Text-Vergleich, &-Verkettung und benannte Bereiche
- Vergleichsoperatoren (= <> > < >= <=) unterscheiden jetzt Text- von Zahlenvergleich statt jede Seite zwangsweise durch toNumber() zu schicken (=A1="Nord" gab vorher #VALUE! statt 1); ermöglicht Text-Kriterien in selbstgebauten COUNTIF/SUMIF/AVERAGEIF-Funktionen (neue README-Sektion "SUMIF/COUNTIF/AVERAGEIF nachbauen"). - Neuer &-Operator (Excel-Präzedenz zwischen Vergleich und +/-) plus CONCAT/CONCATENATE als Funktions-Äquivalent. - Benannte Bereiche: neuer Toolbar-Button "🏷" verwaltet dokumentweite Namen für eine feste Zelle/einen festen Bereich (z. B. Umsatz_Q1statt Umsatz!B2:B6), unqualifiziert in jeder Top-Level-Formel nutzbar. Live gegen die Tabellen-ID aufgelöst (übersteht Tabellen-Umbenennung), bewusst nicht innerhalb einer benutzerdefinierten Funktion selbst nutzbar (Aufrufer können sie aber als Argument übergeben). - Nebenbei: COUNT2s Autovervollständigungs-Hint korrigiert (zeigte "COUNT(...)" statt "COUNT2(...)"). 374 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+99
-9
@@ -35,11 +35,14 @@ Instanz in ein `reactive()` und spiegelt berechnete Werte über einen
|
||||
formula = "=" expression ;
|
||||
|
||||
expression = comparison ;
|
||||
comparison = additive [ ("="|"<>"|">"|"<"|">="|"<=") additive ] ; (* höchstens
|
||||
comparison = concat [ ("="|"<>"|">"|"<"|">="|"<=") concat ] ; (* höchstens
|
||||
ein Vergleich, nicht
|
||||
verkettbar; Ergebnis
|
||||
1/0 (siehe Auswertungs-
|
||||
modell unten) *)
|
||||
concat = additive { "&" additive } ; (* links-assoziativ; Text-
|
||||
Verkettung, zwischen
|
||||
Vergleich und "+ -" *)
|
||||
additive = term { ("+" | "-") term } ; (* links-assoziativ; die
|
||||
alte "expression"-Regel,
|
||||
bevor Vergleiche
|
||||
@@ -54,10 +57,16 @@ primary = NUMBER
|
||||
| STRING
|
||||
| "(" expression ")"
|
||||
| funcall
|
||||
| reference ;
|
||||
| reference
|
||||
| namedref ;
|
||||
|
||||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||||
|
||||
namedref = IDENT ; (* unqualifizierter, nicht zellförmiger
|
||||
Bezeichner, kein Parameter -- ein
|
||||
benannter-Bereich-Kandidat (s. u.),
|
||||
nur außerhalb eines Funktionskörpers *)
|
||||
|
||||
reference = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
|
||||
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname; "!" und der
|
||||
Numbers-Stil "::" sind
|
||||
@@ -78,7 +87,7 @@ QUOTED = /' ( [^'] | '' )* '/ (* Tabellenname mit Leer-/Sonderzeichen *
|
||||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
```
|
||||
|
||||
Präzedenz, schwach → stark: Vergleich (`= <> > < >= <=`) < `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
Präzedenz, schwach → stark: Vergleich (`= <> > < >= <=`) < `&` < `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
|
||||
`$` sperrt eine Referenz-Achse: `$A1` (Spalte fix), `A$1` (Zeile fix), `$A$1`
|
||||
(beide fix) vs. `A1` (relativ). Für die **Auswertung** ist das irrelevant – sie
|
||||
@@ -111,8 +120,20 @@ wirft bei einem kaputten Präfix) liefert `null` — kein Dropdown.
|
||||
Parameter: eine Liste von Namen, gegen die ein bare `IDENT` (das nicht wie
|
||||
eine Zelle aussieht) zusätzlich geprüft wird — ein Treffer erzeugt statt eines
|
||||
`ParseError` einen `ParamNode`. Nur beim Parsen eines **Funktionskörpers**
|
||||
verwendet (`userFunctions.ts`); ohne das Argument ist das Verhalten exakt wie
|
||||
zuvor, jeder bare Bezeichner ohne Zellform bleibt ein `ParseError`.
|
||||
verwendet (`userFunctions.ts`).
|
||||
|
||||
Ohne das Argument (`paramNames === undefined`, jede gewöhnliche
|
||||
Top-Level-Zellformel) hat sich das Verhalten für einen bare Bezeichner ohne
|
||||
Zellform seit **benannten Bereichen** geändert: er parst jetzt als
|
||||
`NamedRefNode` (Feld `namedRefFallback` am `Parser`, s. u. „Benannte
|
||||
Bereiche") statt als `ParseError`. **Mit** `paramNames` — auch als leeres
|
||||
Array, d. h. innerhalb eines Funktionskörpers — bleibt das alte, strikte
|
||||
Verhalten exakt erhalten: ein bare, nicht-zellförmiger, nicht-Parameter-Name
|
||||
ist weiterhin ein harter `ParseError`. Grund: eine lokale/Schleifen-Variable,
|
||||
die vor ihrer eigenen deklarierenden Anweisung verwendet wird (`x = x + 1`),
|
||||
soll sofort beim Speichern scheitern, statt sich still zu einem
|
||||
gleichnamigen benannten Bereich im Dokument aufzulösen (oder erst zur
|
||||
Laufzeit als `#NAME?` aufzufallen).
|
||||
|
||||
## Blatt-Qualifier ("Blatt!Tabelle::Zelle")
|
||||
|
||||
@@ -169,6 +190,54 @@ unbekannter Funktionsname – eingebaut oder benutzerdefiniert – bleibt exakt
|
||||
derselbe Pfad wie zuvor: der `FuncNode` bleibt unverändert stehen,
|
||||
`evaluate()`s `getFunction()` findet ihn nicht → `#NAME?`.
|
||||
|
||||
## Benannte Bereiche
|
||||
|
||||
Anders als eine benutzerdefinierte Funktion wird ein `NamedRefNode`
|
||||
**nicht** beim Parsen aufgelöst/inlined, sondern **live, bei jeder
|
||||
Auswertung**, gegen `EngineHost.resolveNamedRange(name)` – dieselbe
|
||||
"Name wird nicht in die AST gebacken"-Philosophie wie ein Tabellen-Qualifier
|
||||
(auch der wird nie zur Parse-Zeit zu einer Tabellen-*ID* aufgelöst). Grund:
|
||||
so übersteht ein benannter Bereich eine spätere Umbenennung *seiner*
|
||||
Zieltabelle, ohne dass irgendeine gespeicherte Formel neu geparst werden
|
||||
müsste.
|
||||
|
||||
- `types.ts#NamedRangeDef` (`{id, name, tableId, start, end}`) wird dem
|
||||
`Workbook`-Konstruktor als dritter Parameter mitgegeben (analog zu den
|
||||
`FunctionDef`s); `store/document.ts#toNamedRangeDefs` mappt
|
||||
`DocumentState.namedRanges` dorthin.
|
||||
- `workbook.ts#Workbook.resolveNamedRange(name)` sucht case-insensitiv/
|
||||
getrimmt (dieselbe Konvention wie `resolveTableName`), löst `tableId` über
|
||||
`tableById()` in den **aktuellen** Tabellennamen auf und gibt
|
||||
`{qualifier, sheetQualifier, start, end}` zurück – exakt die Form, die ein
|
||||
`CellNode`/`RangeNode` sonst auch trägt. Zieltabelle nicht mehr vorhanden
|
||||
→ `null` → `#NAME?` (dieselbe Fehlerklasse wie ein unbekannter Name).
|
||||
- `evaluator.ts#resolveNamedRef()` baut aus diesem Ergebnis einen
|
||||
synthetischen `CellNode` (1×1-Bereich) oder `RangeNode` und lässt jeden
|
||||
Aufrufer (`evalScalar`s `case 'namedref'`, `evalArg`,
|
||||
`rangeOrCellToCellNodes` für eine `FOR`-Quelle) in die **bereits
|
||||
vorhandene** Zell-/Bereichs-Logik durchfallen – keine Duplikation.
|
||||
- `parser.ts#collectRefs` sammelt einen `NamedRefNode` unaufgelöst (der
|
||||
Parser kennt das Dokument nicht); `workbook.ts#precedentsOf` löst ihn
|
||||
dann über `resolveNamedRange` in konkrete Zell-Keys für den
|
||||
Abhängigkeitsgraphen auf – unbekannter Name → keine Kante (keine
|
||||
Neuberechnung nötig, `evaluate()` liefert ohnehin `#NAME?`).
|
||||
- **Grammatik-Änderung, die das erst ermöglicht:** ein unqualifizierter,
|
||||
nicht zellförmiger, nicht Parameter-Bezeichner ist jetzt ein `NamedRefNode`
|
||||
statt eines `ParseError` – aber **nur außerhalb eines Funktionskörpers**
|
||||
(`Parser`s `namedRefFallback`-Flag, `true` nur wenn `parseFormula()` ganz
|
||||
ohne `paramNames`-Argument aufgerufen wird). Ein Aufrufer kann trotzdem
|
||||
einen benannten Bereich als Argument an eine Funktion übergeben – der
|
||||
`NamedRefNode` steht dann in `node.args`, wird über
|
||||
`expand()`/`substitute()` (beide bekamen dafür einen `case 'namedref':
|
||||
return node;`-Leaf, wie `cell`/`range`) an die passende Parameterstelle im
|
||||
Funktionskörper substituiert, und trifft dort z. B. als `FOR`-Quelle auf
|
||||
`rangeOrCellToCellNodes`, das ihn ganz normal auflöst.
|
||||
`userFunctions.ts#findInvalidForSource` erlaubt `namedref` deshalb
|
||||
ausdrücklich als gültige `FOR`-Quellenform (neben `param`/`cell`/`range`).
|
||||
- **Kein Zyklus-Check.** Ein benannter Bereich hat keinen eigenen Ausdruck,
|
||||
nur feste Koordinaten – er kann sich nicht selbst referenzieren, anders
|
||||
als eine benutzerdefinierte Funktion.
|
||||
|
||||
## Schleifen ("Mit Schleife"-Funktionen, `kind: 'block'`)
|
||||
|
||||
Eine zweite, eigene Grammatik nur für Funktionskörper mit `kind: 'block'`
|
||||
@@ -302,14 +371,35 @@ dagegen erlaubt), `blockAlwaysReturns` sowie die Zyklus-Prüfung.
|
||||
|
||||
- **Arithmetik ist numerisch.** Leere Zelle (`null`) zählt als `0`. Eine
|
||||
zahlenförmige Zeichenkette wird gecastet, jede andere ergibt `#VALUE!`.
|
||||
- **Vergleiche sind ebenfalls numerisch** (`toNumber()`-Koerzierung wie bei
|
||||
`+ - * /`), Ergebnis `1` (wahr) oder `0` (falsch); genutzt vor allem in
|
||||
einer `IF`-Bedingung eines „Mit Schleife"-Funktionskörpers (s. o.), aber in
|
||||
jeder Formel gültig.
|
||||
- **Vergleiche** (`= <> > < >= <=`), Ergebnis `1` (wahr) oder `0` (falsch);
|
||||
genutzt vor allem in einer `IF`-Bedingung eines „Mit Schleife"-
|
||||
Funktionskörpers (s. o.), aber in jeder Formel gültig. Zwei Zahlen (oder
|
||||
eine leere Zelle, die als `0` zählt) vergleichen **numerisch**
|
||||
(`toNumber()`-Koerzierung wie bei `+ - * /`); zwei Texte (oder eine leere
|
||||
Zelle, die dann als `''` zählt) vergleichen **text**uell –
|
||||
case-insensitiv und getrimmt, dieselbe Konvention wie `XLOOKUP`s
|
||||
Text-Matching (s. u.), bewusst als eine einzige Regel im Motor statt
|
||||
zweier verschiedener. Eine leere Zelle nimmt dabei immer den Typ der
|
||||
**anderen** Seite an, sodass sowohl `=A1=0` als auch `=A1=""` auf einer
|
||||
echt leeren `A1` funktionieren. Eine echte (nicht-leere) Zahl gegen
|
||||
echten Text ist ein **Typ-Mismatch**, kein Crossmatch: `=`/`<>`
|
||||
antworten eindeutig falsch/wahr, `> < >= <=` haben keine sinnvolle
|
||||
Antwort und ergeben `#VALUE!` (siehe `evaluator.ts`s `compareValues()`).
|
||||
- **`&` verkettet als Text** (`toText()`): eine Zahl rendert `String(n)`
|
||||
ohne Locale-Formatierung (kein `Intl`, dieselbe „locale-neutral"-
|
||||
Konvention wie der CSV-/XLSX-Export), eine leere Zelle trägt `''` bei
|
||||
(nicht `'0'`). Präzedenz zwischen `+ -` und einem Vergleich (s. o. EBNF),
|
||||
links-assoziativ. `CONCAT`/`CONCATENATE(Text1; Text2; …)` sind das
|
||||
Funktions-Äquivalent (`functions.ts`), ohne Operator-Präzedenz.
|
||||
- **Fehler propagieren unverändert** durch Operatoren und Funktionen (der erste
|
||||
gefundene Fehler gewinnt).
|
||||
- Ein **Bereich** darf nur als Funktionsargument stehen. In Skalar-Position
|
||||
ergibt er `#VALUE!`.
|
||||
- Ein **`NamedRefNode`** (benannter Bereich) löst live gegen
|
||||
`host.resolveNamedRange()` auf und verhält sich danach exakt wie die
|
||||
Zelle/der Bereich, die er dort ergibt (inkl. der Skalar-Regel oben für
|
||||
einen mehrzelligen Treffer). Unbekannter Name → `#NAME?`. Details:
|
||||
„Benannte Bereiche" oben.
|
||||
- `SUM/MIN/MAX` über „keine Zahlen“ → `0`; `AVG` über „keine Zahlen“ → `#DIV/0!`.
|
||||
- `COUNT` zählt nur echte Zahlen – leere Zellen und Text werden nicht gezählt.
|
||||
- `^` = `Math.pow`, rechts-assoziativ: `=2^3^2` → `2^(3^2)` = `512`.
|
||||
|
||||
Reference in New Issue
Block a user