diff --git a/README.md b/README.md index 4ac3dc2..d71762e 100644 --- a/README.md +++ b/README.md @@ -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), 200 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 237 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -193,6 +193,48 @@ eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`. Aufruf), lösen bei Änderung korrekt eine Neuberechnung aus. - Vollständig **undo-/redo-fähig** und Teil des JSON-Exports/-Imports. +#### Mit Schleife: FOR-Code statt einer einzelnen Formel + +Der „Art"-Umschalter im Formular bietet neben **„Einfache Formel"** (s. o.) +auch **„Mit Schleife"** — echter Code statt eines einzelnen Ausdrucks, für +Fälle, die eine einzelne Formel nicht ausdrücken kann: über die Elemente +eines Bereichs selbst iterieren (z. B. ein Produkt aller Zellen, nicht nur +Summe/Mittel/Min/Max). Das Code-Feld akzeptiert eine Folge aus: + +- **Zuweisungen** — `name = ausdruck`. +- **FOR-Schleifen** — `FOR var IN bereich DO name = ausdruck`; `bereich` ist + ein Parameter, eine Zelle oder ein Bereich (kein berechneter Ausdruck). + +— beliebig oft, beliebig gemischt, zuletzt **`RETURN ausdruck`**. Jeder +eingeführte Name (Zuweisungsziel oder Schleifenvariable) bleibt für den +**restlichen** Code sichtbar, auch nach dem Ende „seiner" Schleife (mit dem +Wert der letzten Iteration) — flacher Namensraum, kein Block-Scoping. +`FOR`/`IN`/`DO`/`RETURN` sind reservierte Wörter, nicht als Parameter-/ +Variablenname erlaubt. + +Beispiel — Produkt aller Elemente eines Bereichs (`PRODUCT` gibt es nicht als +eingebaute Funktion, nur `SUM`/`AVG`/`MIN`/`MAX`/`COUNT`): + +``` +ergebnis = 1 +FOR x IN bereich DO ergebnis = ergebnis * x +RETURN ergebnis +``` + +aufgerufen als `=PRODUCT(Umsatz!B2:E6)`. + +**Keine echte Laufzeit-Schleife im Evaluator:** Da die Größe eines Bereichs +beim Aufruf bereits feststeht, wird eine `FOR`-Schleife beim Einsetzen des +Funktionsaufrufs vollständig **abgerollt** — jede Iteration wird zu einem +eigenen, ineinander verschachtelten Ausdrucks-Teilbaum (ein Links-Fold). Am +Ende steht wieder eine ganz normale, statische Formel-AST — Evaluator und +Abhängigkeitsgraph sehen keinen Unterschied zu einer von Hand ausgeschriebenen +Formel, Neuberechnung bei Änderung *jeder* im Bereich enthaltenen Zelle +funktioniert automatisch korrekt. Kein Endlos-Risiko: mehr als insgesamt +10.000 abgerollte Zellen (über alle Schleifen einer Funktion) ergeben `#ERR` +statt zu hängen. Details: [`src/engine/README.md`](src/engine/README.md)s +„Schleifen"-Abschnitt. + ### Zell-Formatierung Auswahl (Zelle, Bereich, Mehrfachauswahl, ganze Zeile/Spalte über den Kopf) → im @@ -445,7 +487,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 200 Tests: +`npm run test` — Vitest, 237 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -521,6 +563,30 @@ 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. +- **„Mit Schleife" (FOR-Code)** — Parser-Ebene (`parseFunctionBlock`): + eine Schleife (Akkumulator-Muster), mehrere unabhängige Schleifen + nacheinander, mehrere Zuweisungen vor/zwischen Schleifen, ein Name aus + einer früheren Anweisung in einer späteren Schleife nutzbar; Fehler bei + fehlendem `RETURN`, `FOR` ohne `DO`/`IN`, fehlendem `=`, Selbstbezug im + eigenen Initialisierer. Engine-Ebene (`compileBlock`): korrektes Abrollen + (ein Bereich → verschachtelte Ausdrücke in Zellreihenfolge), ein einzelner + Zellbezug als Quelle → genau eine Iteration, die Schleifenvariable/das + letzte Zuweisungsziel bleiben danach mit ihrem letzten Wert sichtbar, + verschachtelter Aufruf einer anderen Funktion im Schleifenkörper wird mit- + expandiert, Arity-Prüfung gilt auch für „Mit Schleife"-Funktionen, eine + FOR-Quelle, die nach Parametersubstitution kein Zell-/Bereichsbezug ist, + wirft, mehr als 10.000 abgerollte Zellen wirft. Workbook-Integration: + korrekter Wert und Neuberechnung bei Änderung einer Zelle *innerhalb* des + übergebenen Bereichs, Überschreitung des Abroll-Limits und eine ungültige + FOR-Quelle ergeben `#ERR` statt eines Absturzes. Store-Ebene: reservierte + Wörter (`FOR`/`IN`/`DO`/`RETURN`) als Parameter- oder lokaler Name + abgelehnt, zellartiger lokaler Name abgelehnt, Wiederverwendung desselben + lokalen Namens über mehrere Anweisungen (das Akkumulator-Muster) ist + **keine** Kollision, ein lokaler Name, der einen Parameter überschattet, + wird abgelehnt, eine berechnete FOR-Quelle wird abgelehnt, Funktions-Zyklus + durch einen Schleifenkörper wird erkannt, Undo/Redo, `kind` im JSON- + Round-Trip, ein älteres gespeichertes Dokument ohne `kind`-Feld defaultet + auf `'expression'`. - **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 @@ -564,6 +630,10 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, Ergebnisses im Bearbeiten-Dialog, variadische Parameter/Default-Werte, und automatisches Nachführen bestehender Aufrufe bei Umbenennung (Verhalten entspricht bewusst dem einer Tabellen-Umbenennung, s. o.). +- **„Mit Schleife"-Code** bewusst ohne: verschachtelte Schleifen (`FOR` + innerhalb einer `DO`-Klausel), bedingte Anweisungen (`IF`), echtes + Block-Scoping (Namen bleiben absichtlich bis zum Ende des Codes sichtbar), + frühzeitiges `RETURN` mitten in der Anweisungsfolge (nur am Ende erlaubt). ## Projektstruktur diff --git a/src/components/CustomFunctionsModal.vue b/src/components/CustomFunctionsModal.vue index 35b96e6..331e823 100644 --- a/src/components/CustomFunctionsModal.vue +++ b/src/components/CustomFunctionsModal.vue @@ -16,9 +16,10 @@ const emit = defineEmits<{ (e: 'close'): void }>(); const store = useDocument(); type Mode = 'list' | 'edit'; +type Kind = 'expression' | 'block'; const mode = ref('list'); const editingId = ref(null); -const form = reactive({ name: '', params: [''] as string[], body: '' }); +const form = reactive({ name: '', params: [''] as string[], body: '', kind: 'expression' as Kind }); const formError = ref(null); // reset to the list every time the modal is (re-)opened, so a stray edit-in- @@ -38,6 +39,7 @@ function openNew() { form.name = ''; form.params = ['']; form.body = ''; + form.kind = 'expression'; formError.value = null; mode.value = 'edit'; } @@ -49,6 +51,7 @@ function openEdit(id: string) { form.name = f.name; form.params = f.params.length ? [...f.params] : ['']; form.body = f.body; + form.kind = f.kind; formError.value = null; mode.value = 'edit'; } @@ -63,8 +66,8 @@ function removeParamField(i: number) { function save() { const params = form.params.map((p) => p.trim()).filter((p) => p !== ''); const result = editingId.value - ? store.updateCustomFunction(editingId.value, form.name, params, form.body) - : store.addCustomFunction(form.name, params, form.body); + ? store.updateCustomFunction(editingId.value, form.name, params, form.body, form.kind) + : store.addCustomFunction(form.name, params, form.body, form.kind); if (result && typeof result === 'object' && 'error' in result) { formError.value = result.error; return; @@ -103,7 +106,10 @@ function close() {

  • - {{ f.name }}({{ f.params.join('; ') }}) + + ↻ + {{ f.name }}({{ f.params.join('; ') }}) + @@ -119,6 +125,22 @@ function close() { +
    + Art +
    + + +
    +
    +
    Parameter
    @@ -138,15 +160,28 @@ function close() {