Add a sheet qualifier: "Blatt!Tabelle::Zelle", table names unique per sheet
Table names were document-unique; now they're unique per sheet only -- two different sheets may each hold a table called "Umsatz". A plain, unqualified reference (Umsatz!B2 / Umsatz::B2, unchanged from before) resolves to the table on the SAME sheet as the formula referencing it when the name is otherwise ambiguous, falling back to the single document-wide match when it isn't ambiguous at all -- exactly as before for every existing formula. The new explicit 3-part form (Analyse!Umsatz::B2, quoted where a name has spaces) always targets the named sheet's table regardless of where the formula itself lives, for the case that's still genuinely ambiguous. Engine: parser.ts's afterFirstQualifierToken() disambiguates via lookahead -- "X!Y::" (a name, then "::") is the new 3-part sheet+table form; a bare "X!Y" keeps its original single-qualifier meaning, so every formula written before this exists still parses identically. CellNode/RangeNode gain an optional sheetQualifier field. Workbook.resolveTableName(name, sheetName?) implements the resolution policy; TableModel gains sheetName. precedentsOf() now also sets homeTableId (previously only evaluateCell did), so graph edges use the same same-sheet-preference resolution an evaluation would. Store: table name uniqueness checks (addTable, renameObject) are now scoped to the target sheet instead of the whole document. Point-mode's qualifyRef() inserts the full "Blatt!Tabelle::" form only when the clicked table's name is genuinely ambiguous and lives on a different sheet than the editor; otherwise the simple form, unchanged. 200 tests passing (19 new), vue-tsc and eslint clean, verified live in the running app: renamed a Blatt-2 table to the same name as Analyse's seed "Umsatz", confirmed an unqualified reference resolves per-sheet, an explicit sheet-qualified one always targets the named sheet, and the original sheet's own formulas are unaffected. 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), 181 Tests
|
||||
npm run test # Vitest-Suite (Engine + Store + IO), 200 Tests
|
||||
npm run build # Typecheck + Produktionsbuild
|
||||
npm run lint # ESLint (flat config)
|
||||
```
|
||||
@@ -28,8 +28,10 @@ I/O über File API und Blob-URLs.
|
||||
Wie in Numbers ist ein *Blatt* kein einzelnes Endlos-Raster, sondern eine
|
||||
scroll- und zoombare Fläche, die **Objekte** trägt:
|
||||
|
||||
- **Tabellen-Objekt** — ein eigenes Gitter mit dokumentweit eindeutigem Namen.
|
||||
Frei positionierbar (Ziehen an der Kopfleiste). Zwei Darstellungen:
|
||||
- **Tabellen-Objekt** — ein eigenes Gitter mit einem Namen, der **pro Blatt**
|
||||
eindeutig ist (siehe „Formelgrammatik" unten für Bezüge über mehrere Blätter
|
||||
mit demselben Tabellennamen). Frei positionierbar (Ziehen an der
|
||||
Kopfleiste). Zwei Darstellungen:
|
||||
- **Automatisch** — so groß wie die Zellen, alle Zeilen sichtbar; der SE-Griff
|
||||
skaliert die Spaltenbreiten proportional.
|
||||
- **Feste Größe** — fester Viewport, **intern scrollbar**, mit frei wählbarer
|
||||
@@ -268,8 +270,16 @@ primary = NUMBER | STRING | "(" expression ")" | funcall | reference ;
|
||||
|
||||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||||
|
||||
reference = [ qualifier ] CELL [ ":" CELL ] ;
|
||||
reference = [ sheetQualifier ] [ qualifier ] CELL [ ":" CELL ] ;
|
||||
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname *)
|
||||
sheetQualifier = (IDENT | QUOTED) "!" ; (* Blattname -- nur erkannt,
|
||||
wenn direkt gefolgt von
|
||||
einem vollen "Tabelle::"
|
||||
(3-teilige Form); ein
|
||||
einzelnes "X!Y" ohne
|
||||
folgendes "::" bleibt
|
||||
die alte 2-teilige Form
|
||||
(X = Tabelle) *)
|
||||
|
||||
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
|
||||
QUOTED = /' ( [^'] | '' )* '/ ; (* Name mit Leerzeichen *)
|
||||
@@ -278,11 +288,32 @@ CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
|
||||
Präzedenz schwach → stark: `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
|
||||
Der Qualifier ist der **Tabellenname** (nicht der Blattname). `!` und der
|
||||
Numbers-Stil `::` sind austauschbar. Tabellennamen werden dokumentweit eindeutig
|
||||
gehalten und **case-/whitespace-tolerant** aufgelöst:
|
||||
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`.
|
||||
|
||||
**Tabellennamen sind nur pro Blatt eindeutig**, nicht dokumentweit — zwei
|
||||
verschiedene Blätter dürfen je eine Tabelle „Umsatz" haben (beim Anlegen/
|
||||
Umbenennen wird nur gegen das **eigene** Blatt geprüft). Ein unqualifizierter
|
||||
Bezug wie `Umsatz!B2` (ohne Blattangabe) löst sich dann wie folgt auf:
|
||||
|
||||
1. Gibt es die Tabelle nur auf **einem** Blatt (der Normalfall), wird sie
|
||||
genommen — unverändert wie zuvor.
|
||||
2. Gibt es sie auf **mehreren** Blättern, gewinnt die Tabelle auf **demselben
|
||||
Blatt wie die Formel selbst** (nicht: dem gerade angezeigten Blatt in der
|
||||
UI — das spielt für die Formel keine Rolle).
|
||||
3. Bleibt es danach noch mehrdeutig, ergibt sich `#REF` — hier hilft die
|
||||
explizite **3-teilige Form** `Blatt!Tabelle::Zelle`, z. B.
|
||||
`=Analyse!Umsatz::B2` oder mit Leerzeichen `='Blatt 2'!Umsatz::B2`, die
|
||||
**immer** genau die Tabelle auf dem genannten Blatt trifft, unabhängig
|
||||
davon, wo die Formel selbst steht. Die 2-teilige Form (`Umsatz!B2` /
|
||||
`Umsatz::B2`) bleibt für jede bestehende Formel exakt gültig — die
|
||||
3-teilige Form ist rein additiv.
|
||||
|
||||
Die Formel-**Point-Mode**-Einfügung (s. o.) erkennt eine mehrdeutige
|
||||
Zieltabelle selbst und fügt automatisch die volle 3-teilige Form ein, statt
|
||||
einer Formel, die versehentlich die falsche Tabelle träfe.
|
||||
|
||||
**`$`-Bezüge sperren** (relativ/gemischt/absolut): `A1` (relativ) vs. `$A1` /
|
||||
`A$1` (Spalte bzw. Zeile gesperrt) vs. `$A$1` (beide gesperrt). Für die
|
||||
Auswertung selbst ist `$` irrelevant — sie ist rein positionsbasiert. Es zählt
|
||||
@@ -301,7 +332,10 @@ Bereiche (lokal oder `Name!…`), Zahl-Literale — z. B.
|
||||
`=SUM(Umsatz!B2:E2; 100; A1)`.
|
||||
|
||||
**Fehlermodell** (propagiert an abhängige Zellen):
|
||||
`#REF` (ungültiger/gelöschter Bezug oder unbekannter Tabellenname), `#DIV/0!`,
|
||||
`#REF` (ungültiger/gelöschter Bezug, unbekannter Tabellen-/Blattname, oder ein
|
||||
unqualifizierter Tabellenname, der auf mehreren Blättern existiert und auch
|
||||
über die „gleiches Blatt wie die Formel"-Regel nicht eindeutig wird — siehe
|
||||
oben), `#DIV/0!`,
|
||||
`#NAME?` (unbekannte Funktion, eingebaut oder benutzerdefiniert), `#ERR`
|
||||
(Parse-Fehler; auch eine falsche Argumentanzahl bei einer benutzerdefinierten
|
||||
Funktion), `#CIRC` (Zirkelbezug, auch tabellenübergreifend), `#VALUE!`
|
||||
@@ -411,7 +445,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
|
||||
## Tests
|
||||
|
||||
`npm run test` — Vitest, 181 Tests:
|
||||
`npm run test` — Vitest, 200 Tests:
|
||||
|
||||
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
||||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
|
||||
@@ -487,6 +521,19 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
||||
Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer
|
||||
Funktionsänderung, voller JSON-Round-Trip, `customFunctions: []` bei
|
||||
v1→v2-Migration.
|
||||
- **Blatt-Qualifier „Blatt!Tabelle::Zelle"** — Parser-Ebene: die explizite
|
||||
3-teilige Form (bar und quoted, in Bereichen/Funktionsargumenten), volle
|
||||
Rückwärtskompatibilität der 2-teiligen Form (`X!Y`, `X::Y`, auch ein
|
||||
zweites `!` statt `::`), Fehler bei fehlender Zelle nach `::`. Workbook-
|
||||
Ebene: zwei gleichnamige Tabellen auf verschiedenen Blättern lösen ein
|
||||
unqualifiziertes `Tabelle!Zelle` zum **gleichen Blatt wie die Formel**
|
||||
auf, die explizite Form trifft immer das genannte Blatt unabhängig vom
|
||||
Formel-Standort, ein unbekanntes Blatt/eine weiterhin mehrdeutige
|
||||
Tabelle ergibt `#REF`, Neuberechnung folgt korrekt der Auflösung. Store-
|
||||
Ebene: Tabellennamen sind nur noch pro Blatt eindeutig (Anlegen/
|
||||
Umbenennen prüft nur gegen das eigene Blatt), Point-Mode fügt die volle
|
||||
3-teilige Form ein, sobald ein Klick auf eine mehrdeutig benannte Tabelle
|
||||
auf einem *anderen* Blatt trifft.
|
||||
- **IO** — JSON verlustfreier Round-Trip (identisch), defekte/leere Datei →
|
||||
Fehler ohne Nebenwirkung, unbekannter Tabellenname → `#REF`, v1→v2 beim
|
||||
Import; CSV Übersicht-Export→Re-Import gleiche Zahlen, RFC-4180-Quoting,
|
||||
|
||||
Reference in New Issue
Block a user