Show a range-handling pattern in the function-editing UI
A user function parameter already worked as a range when passed straight into a range-capable call (SUM/AVG/MIN/MAX/another user function) -- expandUserFunctions splices the caller's RangeNode in unchanged, so it behaves exactly like a normal formula (and #VALUE! outside a function-argument position, also exactly like normal). Added 2 tests confirming this (incl. recalculation on a change inside the passed range) and a hint under the Formel field in CustomFunctionsModal.vue showing the pattern (SPANNE(bereich) = MAX(bereich) - MIN(bereich)), since nothing surfaced that this was possible. 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
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run dev # Vite-Devserver
|
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 build # Typecheck + Produktionsbuild
|
||||||
npm run lint # ESLint (flat config)
|
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 `=`.
|
Formel-Feld optisch identisch zur Formelleiste — **ohne** führendes `=`.
|
||||||
Ein Parametername verhält sich im Funktionskörper wie eine Zelle mit
|
Ein Parametername verhält sich im Funktionskörper wie eine Zelle mit
|
||||||
eigenem Wert; er darf selbst nicht wie ein Zellbezug aussehen (`A1`, `$B2`
|
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
|
- **Validierung beim Speichern**, mit **Inline-Fehlermeldung** direkt im
|
||||||
Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer
|
Formular (kein Toast, nichts geht verloren): Name-Kollision mit einer
|
||||||
eingebauten oder einer anderen benutzerdefinierten Funktion, ungültiger
|
eingebauten oder einer anderen benutzerdefinierten Funktion, ungültiger
|
||||||
@@ -406,7 +411,7 @@ Grundrechenarten: `Übersicht!B2 = =SUM(Umsatz!B2:E2)`, `D2 = =B2-C2`,
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`npm run test` — Vitest, 179 Tests:
|
`npm run test` — Vitest, 181 Tests:
|
||||||
|
|
||||||
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
- **lexer / parser** — Präzedenz & Assoziativität (`^` vor `*/` vor `+-`, `^`
|
||||||
rechts, `-2^2 = -(2^2)`), lokale/qualifizierte/quoted Bezüge, `!` **und** `::`,
|
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 →
|
auch bei kollidierenden Parameternamen zwischen den Scopes), Arity-Mismatch →
|
||||||
`#ERR`, unbekannter Name → `#NAME?`, Rekursionstiefen-Sicherheitsnetz,
|
`#ERR`, unbekannter Name → `#NAME?`, Rekursionstiefen-Sicherheitsnetz,
|
||||||
Abhängigkeits-Edges für Zellen, die nur im Funktionskörper vorkommen (nicht
|
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,
|
Parameter-Validierung, Kollision mit eingebauten/anderen Funktionen,
|
||||||
Zyklus-Ablehnung beim Speichern (direkt und über mehrere Stufen), Umbenennen/
|
Zyklus-Ablehnung beim Speichern (direkt und über mehrere Stufen), Umbenennen/
|
||||||
Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer
|
Löschen lässt bestehende Aufrufe zu `#NAME?` werden, Undo/Redo einer
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ function close() {
|
|||||||
placeholder="z. B. (revenue - cost) / revenue"
|
placeholder="z. B. (revenue - cost) / revenue"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<p class="hint">
|
||||||
|
Ein Parameter kann auch ein <strong>Bereich</strong> sein, solange er
|
||||||
|
direkt an eine Funktion wie <code>SUM</code>/<code>AVG</code>/
|
||||||
|
<code>MIN</code>/<code>MAX</code> übergeben wird — z. B.
|
||||||
|
<code>SPANNE(bereich) = MAX(bereich) - MIN(bereich)</code>, aufgerufen
|
||||||
|
als <code>=SPANNE(Umsatz!B2:E6)</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p v-if="formError" class="form-error">⚠ {{ formError }}</p>
|
<p v-if="formError" class="form-error">⚠ {{ formError }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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!');
|
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', () => {
|
it('debugOrder places a function-body-only precedent before its dependent', () => {
|
||||||
const umsatz = sheet('s-umsatz', 'Umsatz', {
|
const umsatz = sheet('s-umsatz', 'Umsatz', {
|
||||||
B2: '1200', E1: '3',
|
B2: '1200', E1: '3',
|
||||||
|
|||||||
Reference in New Issue
Block a user