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:
@@ -258,6 +258,115 @@ ebenso — das Formular warnt davor zusätzlich schon **beim Speichern**, wenn
|
||||
nicht erkennbar jeder Codepfad ein `RETURN` erreicht. Details:
|
||||
[`src/engine/README.md`](src/engine/README.md)s „Schleifen"-Abschnitt.
|
||||
|
||||
#### SUMIF/COUNTIF/AVERAGEIF nachbauen
|
||||
|
||||
Es gibt keine eingebauten `SUMIF`/`COUNTIF`/`AVERAGEIF` — aber die **„Mit
|
||||
Schleife"**-Funktionen reichen aus, um die **selbstbezügliche** Form
|
||||
nachzubauen: Kriterium und Aggregation laufen über **denselben** Bereich
|
||||
(„Summe/Anzahl/Mittelwert aller Werte in `bereich`, die eine Bedingung
|
||||
erfüllen"). Das deckt den häufigsten Alltagsfall ab — z. B. „Summe aller
|
||||
Umsätze über 100" oder „Anzahl der Zeilen mit Region = Nord", solange
|
||||
Kriterium und zu summierender Wert in derselben Zelle stehen.
|
||||
|
||||
```
|
||||
summe = 0
|
||||
FOR x IN bereich DO
|
||||
IF x > schwelle THEN
|
||||
summe = summe + x
|
||||
END
|
||||
END
|
||||
RETURN summe
|
||||
```
|
||||
|
||||
aufgerufen als `=SUMME_WENN(Umsatz!B2:B13; 100)` — das Excel-Äquivalent zu
|
||||
`SUMIF(Umsatz!B2:B13; ">100")` mit gleichem Kriterien- und Summenbereich.
|
||||
`COUNTIF`/`AVERAGEIF` folgen demselben Muster, nur mit `zaehler = zaehler + 1`
|
||||
statt `summe = summe + x`, bzw. am Ende `RETURN summe / zaehler` (mit
|
||||
`IF zaehler = 0 THEN RETURN 0 END`-Guard gegen `#DIV/0!`).
|
||||
|
||||
Seit dem Fix der Vergleichsoperatoren (s. u. „Formelgrammatik") funktioniert
|
||||
das Kriterium auch mit **Text** — case-insensitiv und getrimmt, wie bei
|
||||
`XLOOKUP`:
|
||||
|
||||
```
|
||||
anzahl = 0
|
||||
FOR zeile IN bereich DO
|
||||
IF zeile = kriterium THEN
|
||||
anzahl = anzahl + 1
|
||||
END
|
||||
END
|
||||
RETURN anzahl
|
||||
```
|
||||
|
||||
aufgerufen als `=ANZAHL_WENN(Umsatz!A2:A13; "Nord")` — zählt alle Zellen in
|
||||
`Umsatz!A2:A13`, deren Text „Nord" ist (unabhängig von Groß-/Kleinschreibung
|
||||
und Leerzeichen).
|
||||
|
||||
**Grenze, die (noch) bleibt:** ein Excel-`SUMIF(Kriterienbereich; Kriterium;
|
||||
Summenbereich)` mit **getrennten** Bereichen (Kriterium in Spalte A, zu
|
||||
summierender Wert in Spalte B) ist damit **nicht** nachbaubar — die Sprache
|
||||
hat kein `OFFSET`/`INDEX`-Äquivalent, um „die Zelle an derselben Position im
|
||||
anderen Bereich" zu adressieren; eine `FOR`-Schleife erreicht nur den
|
||||
Bereich, über den sie selbst iteriert. Siehe „Bekannte Grenzen" unten.
|
||||
|
||||
### Benannte Bereiche
|
||||
|
||||
Der Button **„🏷" (Benannte Bereiche)** in der Symbolleiste, direkt neben
|
||||
**„𝑓 Funktionen"**, öffnet die einzige Stelle, an der dokumentweite Namen für
|
||||
eine feste Zelle/einen festen Bereich angelegt, bearbeitet und gelöscht
|
||||
werden — z. B. `Umsatz_Q1` statt `Umsatz!B2:B6`. Danach in **jeder**
|
||||
Top-Level-Formel unqualifiziert nutzbar, ohne Tabellen-Präfix:
|
||||
`=SUM(Umsatz_Q1)` oder `=Umsatz_Q1 + 1` für eine einzelne Zelle. Das
|
||||
vorbefüllte Dokument bringt `Umsatz_Q1` (→ `Umsatz!B2:B6`) bereits als
|
||||
Beispiel mit.
|
||||
|
||||
- **Liste** zeigt jeden Namen als `Name → Tabelle (Blatt)!Bereich` — mit
|
||||
**Bearb.** und **🗑** wie bei den Funktionen.
|
||||
- **Formular**: Name, eine Tabelle (Auswahl über alle Blätter hinweg) und
|
||||
zwei Zellfelder (Start/Ende als A1-Notation, z. B. `B2` und `B6`); ein
|
||||
leeres zweites Feld bedeutet „nur die erste Zelle". **„+ Neuer Name"**
|
||||
übernimmt automatisch die aktuelle Zellauswahl (Tabelle + Bereich) als
|
||||
Vorbelegung, wenn eine Zelle/ein Bereich gerade markiert ist — analog zu
|
||||
Excels „Namen aus Auswahl erstellen".
|
||||
- **Validierung beim Speichern**: gültiger Name (keine Zahl/Zellform wie
|
||||
`A1`), kein reserviertes Schlüsselwort (`FOR`/`IN`/`DO`/`RETURN`), keine
|
||||
Kollision mit einem eingebauten Namen, einer benutzerdefinierten Funktion
|
||||
oder einem anderen benannten Bereich (case-insensitiv). **Kein
|
||||
Zyklus-Check nötig** — anders als eine benutzerdefinierte Funktion hat ein
|
||||
benannter Bereich keinen eigenen Ausdruck, nur feste Koordinaten, kann sich
|
||||
also nie selbst referenzieren.
|
||||
- **Überlebt eine Tabellen-Umbenennung.** Intern wird die Tabellen-*ID*
|
||||
gespeichert, nicht ihr Name — anders als ein normaler `Tabelle!Zelle`-Bezug
|
||||
im Formeltext (der bricht bei Umbenennung, wie eh und je) wird ein
|
||||
benannter Bereich beim Nachschlagen live auf den *aktuellen* Tabellennamen
|
||||
aufgelöst. Wird die Tabelle dagegen **gelöscht**, bleibt der Name bestehen
|
||||
und zeigt in Formeln danach `#NAME?` — „nichts wird automatisch
|
||||
umgeschrieben", dieselbe Konvention wie beim Löschen einer benutzerdefinierten
|
||||
Funktion oder Tabelle.
|
||||
- Vollständig **undo-/redo-fähig** und Teil des JSON-Exports/-Imports.
|
||||
|
||||
**Bewusst ohne** (Stand jetzt):
|
||||
- **Nicht innerhalb einer benutzerdefinierten Funktion nutzbar** — im
|
||||
Funktionskörper selbst bleibt ein unqualifizierter, nicht-zellförmiger
|
||||
Name ein harter Parse-Fehler, genau wie vor diesem Feature. Grund: sonst
|
||||
würde z. B. eine lokale Variable, die versehentlich vor ihrer eigenen
|
||||
Zuweisung verwendet wird, nicht mehr sofort beim Speichern auffallen,
|
||||
sondern erst zur Laufzeit als `#NAME?` — oder, schlimmer, sie würde
|
||||
still passend zu einem gleichnamigen benannten Bereich irgendwo im
|
||||
Dokument auflösen. Ein **Aufrufer** kann einen benannten Bereich aber
|
||||
ganz normal als Argument übergeben (`=MEINEFUNKTION(Umsatz_Q1)`) — die
|
||||
Einschränkung gilt nur fürs *Eintippen des bloßen Namens* im Funktionskörper
|
||||
selbst.
|
||||
- **Keine eigene Formel/kein Ausdruck** — nur eine feste Zelle/ein fester
|
||||
Bereich, kein benannter *berechneter* Wert (dafür gibt es weiterhin
|
||||
benutzerdefinierte Funktionen ohne Parameter, s. o.).
|
||||
- **Nicht in der Funktions-Autovervollständigung** — das Dropdown beim
|
||||
Tippen nach `=` schlägt bisher nur Funktionsnamen vor, keine benannten
|
||||
Bereiche.
|
||||
- **Kein „Bereich" als `Name:B2`** — ein benannter Bereich ist immer die
|
||||
ganze, vordefinierte Adresse; er kann nicht als Startpunkt einer neu
|
||||
getippten `:`-Bereichsangabe kombiniert werden.
|
||||
|
||||
### Zell-Formatierung
|
||||
|
||||
Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im
|
||||
@@ -327,7 +436,7 @@ fixierter Kopfzeile und fixierter Spalte A** angelegt, um das Feature zu zeigen.
|
||||
formula = "=" expression ;
|
||||
|
||||
expression = comparison ;
|
||||
comparison = additive [ ("="|"<>"|">"|"<"|">="|"<=") additive ] ; (* höchstens
|
||||
comparison = concat [ ("="|"<>"|">"|"<"|">="|"<=") concat ] ; (* höchstens
|
||||
ein Vergleich, nicht
|
||||
verkettbar wie
|
||||
"a < b < c"; vor allem
|
||||
@@ -335,6 +444,12 @@ comparison = additive [ ("="|"<>"|">"|"<"|">="|"<=") additive ] ; (* höchsten
|
||||
„Mit Schleife"-Code
|
||||
(s. o.), aber in jeder
|
||||
Formel nutzbar *)
|
||||
concat = additive { "&" additive } ; (* links-assoziativ; Text-
|
||||
Verkettung, bindet
|
||||
schwächer als "+ -",
|
||||
stärker als ein
|
||||
Vergleich -- derselbe
|
||||
Platz wie bei Excel *)
|
||||
additive = term { ("+" | "-") term } ; (* links-assoziativ *)
|
||||
term = unary { ("*" | "/") unary } ; (* links-assoziativ *)
|
||||
unary = ("+" | "-") unary
|
||||
@@ -342,10 +457,19 @@ 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 ;
|
||||
primary = NUMBER | STRING | "(" expression ")" | funcall | reference | namedref ;
|
||||
|
||||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||||
|
||||
namedref = IDENT ; (* ein unqualifizierter,
|
||||
nicht zellförmiger,
|
||||
nicht in `(` mündender
|
||||
Bezeichner -- ein
|
||||
benannter-Bereich-
|
||||
Kandidat, s. u.; nur
|
||||
außerhalb eines
|
||||
Funktionskörpers *)
|
||||
|
||||
reference = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
|
||||
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname *)
|
||||
sheetQualifier = (IDENT | QUOTED) "!" ; (* Blattname -- nur erkannt,
|
||||
@@ -362,9 +486,38 @@ QUOTED = /' ( [^'] | '' )* '/ ; (* Name mit Leerzeichen *)
|
||||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
```
|
||||
|
||||
Präzedenz schwach → stark: Vergleich (`= <> > < >= <=`) < `+ -` < `* /` <
|
||||
Präzedenz schwach → stark: Vergleich (`= <> > < >= <=`) < `&` < `+ -` < `* /` <
|
||||
unäres `+ -` < `^`.
|
||||
|
||||
**Vergleiche** sind numerisch, wenn beide Seiten Zahlen sind (leere Zelle
|
||||
zählt als `0`), und **text**uell — case-insensitiv, getrimmt, wie bei
|
||||
`XLOOKUP` (s. u.) —, wenn beide Seiten Text sind (leere Zelle zählt dann als
|
||||
`''`). Eine echte Zahl gegen echten Text: `=`/`<>` antworten eindeutig
|
||||
falsch/wahr, `> < >= <=` ergeben `#VALUE!` (kein sinnvoller Vergleich über
|
||||
Typgrenzen). Nützlich z. B. für `=A1="Nord"` oder als Kriterium einer
|
||||
selbstgebauten `COUNTIF`/`SUMIF`-Funktion (s. „SUMIF/COUNTIF/AVERAGEIF
|
||||
nachbauen" oben).
|
||||
|
||||
**`&` verkettet als Text**, z. B. `=A1&" "&B1` oder `="Herr "&Name`. Eine Zahl
|
||||
wird dabei ohne Locale-Formatierung dargestellt (`5`, nicht `5,00`), eine
|
||||
leere Zelle trägt `''` bei (nicht `'0'`). Bindet schwächer als `+`/`-`
|
||||
(`="a"&1+2` = `"a3"`, nicht `"a1"+2`), aber stärker als ein Vergleich
|
||||
(`=A1&B1="ab"` prüft das *Ergebnis* der Verkettung, nicht `B1="ab"` allein).
|
||||
`CONCAT(Text1; Text2; …)`/`CONCATENATE(...)` machen dasselbe als Funktion,
|
||||
ohne Operator-Präzedenz zu beachten — nützlich mit vielen Teilen oder
|
||||
innerhalb eines „Mit Schleife"-Codes.
|
||||
|
||||
Ein unqualifizierter Bezeichner, der weder eine Zelle noch (mit
|
||||
`paramNames`) ein Parameter ist, parst als **`namedref`** — ein Kandidat für
|
||||
einen **benannten Bereich** (s. „Benannte Bereiche" oben), erst zur
|
||||
Auswertung/Abhängigkeits-Ermittlung gegen das Dokument aufgelöst (genau wie
|
||||
ein Tabellen-Qualifier nie beim Parsen selbst geprüft wird). Ein unbekannter
|
||||
Name ergibt `#NAME?`. **Nur außerhalb eines Funktionskörpers** — mit
|
||||
`paramNames` bleibt ein unqualifizierter, nicht-zellförmiger Bezeichner ein
|
||||
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.
|
||||
|
||||
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`.
|
||||
@@ -411,10 +564,10 @@ Vorschlag (fügt `NAME(` ein, bereit für die Argumente), `Esc` schließt nur
|
||||
das Dropdown, ohne die Bearbeitung selbst abzubrechen.
|
||||
|
||||
**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()`, `XLOOKUP(Suchwert;
|
||||
Suchbereich; Rückgabebereich; [WennNichtGefunden]; [Vergleichsmodus];
|
||||
[Suchmodus])` — plus alle über **„𝑓 Funktionen"** angelegten
|
||||
`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];
|
||||
[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.
|
||||
@@ -712,7 +865,23 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
Funktionsänderung, voller JSON-Round-Trip, `customFunctions: []` bei
|
||||
v1→v2-Migration.
|
||||
- **Vergleichsoperatoren** — alle sechs (`= <> > < >= <=`), Präzedenz
|
||||
unterhalb `+ -`, nicht verkettbar (`a < b < c` ist ein Parse-Fehler).
|
||||
unterhalb `+ -`, nicht verkettbar (`a < b < c` ist ein Parse-Fehler);
|
||||
numerischer Vergleich, Text-Vergleich (case-insensitiv/getrimmt), leere
|
||||
Zelle nimmt den Typ der Gegenseite an, Zahl-gegen-Text-Mismatch
|
||||
(`=`/`<>` eindeutig, `> < >= <=` → `#VALUE!`).
|
||||
- **`&`-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.
|
||||
- **Benannte Bereiche** — Auflösung als Einzelzelle/Bereich (Skalar- und
|
||||
Funktionsargument-Position), case-insensitives/getrimmtes Namens-Matching,
|
||||
Abhängigkeitsgraph-Kante (Neuberechnung bei Änderung der Zielzelle),
|
||||
Überleben einer Tabellen-Umbenennung, `#NAME?` bei unbekanntem Namen und
|
||||
bei gelöschter Zieltabelle, `addNamedRange`/`updateNamedRange`/
|
||||
`deleteNamedRange`-Validierung (Kollision mit Built-in/eigener
|
||||
Funktion/anderem Namen, Zellform), Undo/Redo, JSON-Round-Trip, Nutzung als
|
||||
`FOR`-Quelle in einer benutzerdefinierten Funktion (als übergebenes
|
||||
Argument), harter Parse-Fehler statt Fallback innerhalb eines
|
||||
Funktionskörpers selbst.
|
||||
- **„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`
|
||||
@@ -784,9 +953,12 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
reaktiv), **kein** echter Web Worker.
|
||||
- **xlsx-Formel-Übernahme** ist heuristisch (Separator-Swap unter Beachtung von
|
||||
Quotes); exotische Excel-Formeln fallen auf den Wert zurück.
|
||||
- Kein Text-Verkettungsoperator, keine benannten Bereiche (nur
|
||||
benutzerdefinierte *Funktionen*, keine benannten *Zellen/Bereiche*), kein
|
||||
Snapping der Canvas-Objekte.
|
||||
- Kein Snapping der Canvas-Objekte.
|
||||
- **Benannte Bereiche** bewusst ohne: Nutzung innerhalb einer
|
||||
benutzerdefinierten Funktion (ein Aufrufer kann aber einen benannten
|
||||
Bereich als Argument übergeben), einen eigenen Ausdruck/berechneten Wert
|
||||
(nur feste Koordinaten), Funktions-Autovervollständigungs-Vorschläge, und
|
||||
`Name:B2` als kombinierten Bereichsstart — s. „Benannte Bereiche" oben.
|
||||
- **Datum & Zeit**: die automatische Erkennung beim Eintippen erfasst nur ISO-
|
||||
und Punkt-Daten, absichtlich **nicht** Schrägstrich-Daten (Tag-zuerst vs.
|
||||
Monat-zuerst ist über die unterstützten Locales nicht eindeutig auflösbar,
|
||||
@@ -813,12 +985,19 @@ 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), Text-Vergleich (Vergleichsoperatoren
|
||||
bleiben numerisch), `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.).
|
||||
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).
|
||||
- **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
|
||||
Position in einem anderen Bereich". Ein klassisches dreiargumentiges
|
||||
`SUMIF(Kriterienbereich; Kriterium; Summenbereich)` mit **getrennten**
|
||||
Bereichen ist deshalb nicht nachbaubar; nur die selbstbezügliche Form
|
||||
(Kriterium und Aggregation auf demselben Bereich) geht (s. o.).
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
|
||||
Reference in New Issue
Block a user