diff --git a/README.md b/README.md index fba370c..ab488d5 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), 179 Tests +npm run test # Vitest-Suite (Engine + Store + IO), 181 Tests npm run build # Typecheck + Produktionsbuild npm run lint # ESLint (flat config) ``` @@ -168,7 +168,12 @@ eingebauter Funktionsname: `=MARGE(Umsatz!B2; Umsatz!C2)`. Formel-Feld optisch identisch zur Formelleiste — **ohne** führendes `=`. Ein Parametername verhält sich im Funktionskörper wie eine Zelle mit eigenem Wert; er darf selbst nicht wie ein Zellbezug aussehen (`A1`, `$B2` - o. ä. sind ungültig). + o. ä. sind ungültig). Ein Parameter kann auch ein **Bereich** sein, solange + er im Funktionskörper direkt an eine bereichsfähige Funktion übergeben wird + — z. B. `SPANNE(bereich) = MAX(bereich) - MIN(bereich)`, aufgerufen als + `=SPANNE(Umsatz!B2:E6)`; das Formular zeigt dieses Muster als Hinweis unter + dem Formel-Feld. Wie bei jeder Formel ergibt ein Bereich außerhalb einer + Funktionsargument-Position `#VALUE!`. - **Validierung beim Speichern**, mit **Inline-Fehlermeldung** direkt im Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer eingebauten oder einer anderen benutzerdefinierten Funktion, ungültiger @@ -406,7 +411,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, ## Tests -`npm run test` — Vitest, 179 Tests: +`npm run test` — Vitest, 181 Tests: - **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^` rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`, @@ -473,7 +478,10 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`, auch bei kollidierenden Parameternamen zwischen den Scopes), Arity-Mismatch → `#ERR`, unbekannter Name → `#NAME?`, Rekursionstiefen-Sicherheitsnetz, Abhängigkeits-Edges für Zellen, die nur im Funktionskörper vorkommen (nicht - im Aufruf) — inkl. korrekter Neuberechnungs-Reihenfolge. Store-Ebene: Name-/ + im Aufruf) — inkl. korrekter Neuberechnungs-Reihenfolge; ein Bereichs- + Parameter funktioniert in Funktionsargument-Position (inkl. Neuberechnung + bei Änderung einer Zelle *innerhalb* des übergebenen Bereichs) und ergibt + `#VALUE!` außerhalb davon. Store-Ebene: Name-/ Parameter-Validierung, Kollision mit eingebauten/anderen Funktionen, Zyklus-Ablehnung beim Speichern (direkt und über mehrere Stufen), Umbenennen/ Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer diff --git a/src/components/CustomFunctionsModal.vue b/src/components/CustomFunctionsModal.vue index cd5bd75..35b96e6 100644 --- a/src/components/CustomFunctionsModal.vue +++ b/src/components/CustomFunctionsModal.vue @@ -146,6 +146,13 @@ function close() { placeholder="z. B. (revenue - cost) / revenue" /> +

+ Ein Parameter kann auch ein Bereich sein, solange er + direkt an eine Funktion wie SUM/AVG/ + MIN/MAX übergeben wird — z. B. + SPANNE(bereich) = MAX(bereich) - MIN(bereich), aufgerufen + als =SPANNE(Umsatz!B2:E6). +

⚠ {{ formError }}

diff --git a/src/engine/__tests__/userFunctions.spec.ts b/src/engine/__tests__/userFunctions.spec.ts index 4e45b1c..f2f0ef9 100644 --- a/src/engine/__tests__/userFunctions.spec.ts +++ b/src/engine/__tests__/userFunctions.spec.ts @@ -199,6 +199,34 @@ describe('Workbook integration: user-defined functions end-to-end', () => { expect((val(wb, 's-umsatz', 'B9') as FormulaError).code).toBe('#DIV/0!'); }); + it('a parameter can be a range, as long as it lands in a function-call argument position', () => { + // SPANNE(bereich) = MAX(bereich) - MIN(bereich) -- "bereich" (ParamNode) + // gets substituted with the caller's RangeNode, splicing a normal + // MAX(Umsatz!B2:E2) / MIN(Umsatz!B2:E2) call into the expanded AST. + const umsatz = sheet('s-umsatz', 'Umsatz', { + B2: '1200', C2: '1450', D2: '1600', E2: '1720', + B9: '=SPANNE(B2:E2)', + }); + const wb = new Workbook( + [umsatz], + [def({ id: 'f1', name: 'SPANNE', params: ['bereich'], body: 'MAX(bereich) - MIN(bereich)' })], + ); + expect(val(wb, 's-umsatz', 'B9')).toBe(1720 - 1200); + + // recalculates when a cell *inside* the range argument changes + wb.setCell('s-umsatz', 1, 1, '1000'); // B2: 1200 -> 1000 + expect(val(wb, 's-umsatz', 'B9')).toBe(1720 - 1000); + }); + + it('a range parameter used outside a function-call argument position is #VALUE!, same as a bare range in an ordinary formula', () => { + const umsatz = sheet('s-umsatz', 'Umsatz', { + B2: '1200', C2: '1450', + B9: '=BAD(B2:C2)', + }); + const wb = new Workbook([umsatz], [def({ id: 'f1', name: 'BAD', params: ['bereich'], body: 'bereich + 1' })]); + expect((val(wb, 's-umsatz', 'B9') as FormulaError).code).toBe('#VALUE!'); + }); + it('debugOrder places a function-body-only precedent before its dependent', () => { const umsatz = sheet('s-umsatz', 'Umsatz', { B2: '1200', E1: '3',