- init
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
# Formel-Engine
|
||||
|
||||
Selbst geschriebene, framework-agnostische Formel-Engine. Kein HyperFormula, kein
|
||||
`formula-parser`, keine Spreadsheet-Library. Reines ES-Modul, in Vitest getestet.
|
||||
|
||||
Die adressierbare Einheit ist eine **Tabelle** – ein Tabellen-Objekt auf einer
|
||||
Numbers-artigen Blattfläche. Tabellennamen sind dokumentweit eindeutig, deshalb
|
||||
löst `Umsatz!B2` (bzw. `Umsatz::B2`) von jedem Blatt aus identisch auf.
|
||||
|
||||
## Module
|
||||
|
||||
| Datei | Aufgabe |
|
||||
|----------------|---------|
|
||||
| `lexer.ts` | Tokenizer. Flacher Token-Strom, keine strukturellen Entscheidungen. Kennt `!` und `::`. |
|
||||
| `parser.ts` | Recursive-Descent → AST. Grammatik als EBNF-Kommentarblock im Kopf der Datei. |
|
||||
| `evaluator.ts` | AST + `EngineHost` → `CellValue`. Numerisches Auswertungsmodell, Fehler-Propagierung. |
|
||||
| `graph.ts` | Dokumentweiter Abhängigkeitsgraph: `affected()`, `topoOrder()`, Zirkel-Erkennung (iterative DFS, white/grey/black). |
|
||||
| `refs.ts` | A1 ↔ `(col,row)`, Spaltenbuchstaben, Bereichs-Expansion. |
|
||||
| `functions.ts` | `SUM/AVG/MIN/MAX/COUNT` (+ Alias `AVERAGE`) als Tabelle `{ NAME → fn(values) }`. |
|
||||
| `workbook.ts` | Bindeglied: flache Liste von **Tabellen** + rohe Eingaben + ASTs + Werte + Graph. Inkrementelle Neuberechnung. Implementiert `EngineHost`. |
|
||||
| `types.ts` | Gemeinsame Typen, `FormulaError`, `keyOf()`. |
|
||||
|
||||
Die Engine kennt **kein** Vue. Der Vue-Store (`src/store/`) legt eine `Workbook`-
|
||||
Instanz in ein `reactive()` und spiegelt berechnete Werte über einen
|
||||
`recalcTick`-Ref – dadurch verhält sich jede Formelzelle im UI wie ein `computed`.
|
||||
|
||||
## Grammatik (EBNF)
|
||||
|
||||
```ebnf
|
||||
formula = "=" expression ;
|
||||
|
||||
expression = term { ("+" | "-") term } ; (* links-assoziativ *)
|
||||
term = unary { ("*" | "/") unary } ; (* links-assoziativ *)
|
||||
unary = ("+" | "-") unary
|
||||
| power ;
|
||||
power = primary [ "^" unary ] ; (* rechts-assoziativ; bindet
|
||||
stärker als unäres Minus:
|
||||
-2^2 = -(2^2) = -4 *)
|
||||
primary = NUMBER
|
||||
| STRING
|
||||
| "(" expression ")"
|
||||
| funcall
|
||||
| reference ;
|
||||
|
||||
funcall = IDENT "(" [ expression { ";" expression } ] ")" ;
|
||||
|
||||
reference = [ qualifier ] CELL [ ":" CELL ] ;
|
||||
qualifier = (IDENT | QUOTED) ("!" | "::") ; (* Tabellenname; "!" und der
|
||||
Numbers-Stil "::" sind
|
||||
austauschbar *)
|
||||
|
||||
NUMBER = /[0-9]+ ("." [0-9]+)? ([eE] [+-]? [0-9]+)?/ | /"." [0-9]+/ ;
|
||||
STRING = /" ... "/ (Backslash-Escapes) ;
|
||||
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
|
||||
QUOTED = /' ( [^'] | '' )* '/ (* Tabellenname mit Leer-/Sonderzeichen *)
|
||||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
```
|
||||
|
||||
Präzedenz, schwach → stark: `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
|
||||
`$` in Referenzen wird toleriert, aber ignoriert (es gibt kein Kopieren/Füllen,
|
||||
alle Bezüge sind relativ). Tabellennamen werden **case-insensitiv** und
|
||||
whitespace-tolerant aufgelöst: `Kosten!A2`, `'Kosten'!A2`, `kosten!a2`,
|
||||
`Kosten::A2` sind identisch.
|
||||
|
||||
## Auswertungsmodell
|
||||
|
||||
- **Arithmetik ist numerisch.** Leere Zelle (`null`) zählt als `0`. Eine
|
||||
zahlenförmige Zeichenkette wird gecastet, jede andere ergibt `#VALUE!`.
|
||||
- **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!`.
|
||||
- `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`.
|
||||
|
||||
## Neuberechnung
|
||||
|
||||
1. `setCell` reparst **eine** Zelle, aktualisiert ihre Graph-Kanten.
|
||||
2. `graph.affected(dirty)` = geänderte Zellen + alle transitiven Abhängigen.
|
||||
3. `graph.topoOrder(affected)` liefert eine sichere Auswertungsreihenfolge
|
||||
(Präzedenzen zuerst) **und** die Menge der Zellen auf einem Zyklus.
|
||||
4. Zyklus-Zellen bekommen direkt `#CIRC`; der Rest wird in topologischer
|
||||
Reihenfolge ausgewertet. Nicht betroffene Zellen behalten ihren Wert
|
||||
(„nur betroffene Zellen neu rechnen“).
|
||||
|
||||
Strukturänderungen (Tabelle umbenennen/löschen, Zeilen/Spalten, Blatt löschen)
|
||||
rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
|
||||
## Fehlermodell
|
||||
|
||||
| Wert | Bedeutung |
|
||||
|------------|-----------|
|
||||
| `#REF` | ungültiger/gelöschter Bezug **oder** unbekannter Tabellenname |
|
||||
| `#DIV/0!` | Division durch 0 (auch `AVG` ohne Zahlen) |
|
||||
| `#NAME?` | unbekannte Funktion |
|
||||
| `#ERR` | Parse-Fehler (`=A1+`, `=SUM(`) |
|
||||
| `#CIRC` | Zirkelbezug – auch tabellen-/blattübergreifend |
|
||||
| `#VALUE!` | Typfehler in Arithmetik (Text × Zahl, Bereich als Skalar). *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