2c54537427edf10acbbd13018b975ca32d5eeb55
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c54537427 |
Text-Vergleich, &-Verkettung und benannte Bereiche
- Vergleichsoperatoren (= <> > < >= <=) unterscheiden jetzt Text- von Zahlenvergleich statt jede Seite zwangsweise durch toNumber() zu schicken (=A1="Nord" gab vorher #VALUE! statt 1); ermöglicht Text-Kriterien in selbstgebauten COUNTIF/SUMIF/AVERAGEIF-Funktionen (neue README-Sektion "SUMIF/COUNTIF/AVERAGEIF nachbauen"). - Neuer &-Operator (Excel-Präzedenz zwischen Vergleich und +/-) plus CONCAT/CONCATENATE als Funktions-Äquivalent. - Benannte Bereiche: neuer Toolbar-Button "🏷" verwaltet dokumentweite Namen für eine feste Zelle/einen festen Bereich (z. B. Umsatz_Q1statt Umsatz!B2:B6), unqualifiziert in jeder Top-Level-Formel nutzbar. Live gegen die Tabellen-ID aufgelöst (übersteht Tabellen-Umbenennung), bewusst nicht innerhalb einer benutzerdefinierten Funktion selbst nutzbar (Aufrufer können sie aber als Argument übergeben). - Nebenbei: COUNT2s Autovervollständigungs-Hint korrigiert (zeigte "COUNT(...)" statt "COUNT2(...)"). 374 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
01ea18106c |
Real control flow for "Mit Schleife" functions: DO/END, IF/ELSE/END, RETURN anywhere
Replaces the previous FOR-only, single-statement-body, static-unrolling design with genuine control flow: FOR var IN source DO <block> END loop bodies with any number of statements (including nested loops/IFs), IF cond THEN <block> [ELSE <block>] END branching (nestable in any block), and RETURN as an ordinary statement usable anywhere -- it aborts execution immediately with its value, including breaking out of a loop early. This required a real, if small, architecture change: an IF branch or an early RETURN depends on actual cell values, which the previous "unroll everything into one static formula at parse time" approach can't resolve (loop *counts* are knowable up front from a range's size; branch *outcomes* aren't). A kind: 'block' call is now inlined only as far as resolving nested function calls (expandBlock, parse-time, same inside-out principle as before) and wrapped in a new BlockCallNode; its own control flow is interpreted at evaluation time by a small tree-walking interpreter added to evaluator.ts (execBlock/execStatement, a thrown ReturnSignal for early exit), reusing userFunctions.ts's substitute() (now exported) to resolve parameter/local-variable names against the current environment. Dependency tracking stays fully correct and conservative: collectRefs() walks into a BlockCallNode's block (new collectRefsInBlock), so a cell referenced only in a branch that doesn't happen to run is still a tracked precedent -- same principle as an ordinary IF() in any spreadsheet. Also added: comparison operators (= <> > < >= <=, result 1/0) as a new, loosest-binding precedence level in the *general* expression grammar (not just inside a block's IF) -- needed for IF conditions to be useful at all. A definition-time static check (blockAlwaysReturns) rejects a function at save time when it's not obviously guaranteed to reach RETURN on every path (an IF without an ELSE, or one where only one branch returns); the interpreter's own runtime "ended without RETURN" -> #ERR remains the actual enforcement for cases the static check can't see (e.g. a loop over an empty range). Nested loops (FOR inside FOR) and nested IFs work as an emergent property of the recursive statement grammar -- verified with dedicated tests, not just assumed. 257 tests passing (30 net new/rewritten for this change), vue-tsc/ eslint/build clean. Verified live in the running app: a FIRSTOVER(range; threshold) function using a FOR loop with a nested IF and an early RETURN, confirmed it returns the first match, confirmed changing a cell *past* the match point doesn't affect the result while changing the match point itself does, and confirmed a function without a guaranteed RETURN is rejected inline at save time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
6daea9f956 |
Add FOR-loop "code" bodies to user-defined functions (kind: 'block')
A custom function body could previously only be a single expression -- no way to iterate over a range's elements to build something like PRODUCT (multiply every cell together), since none of the built-ins (SUM/AVG/MIN/MAX/COUNT) do that and a body can only compose existing functions, not define a new reduction. This adds a second body kind, 'block': a sequence of `name = expression` assignments and `FOR var IN source DO name = expression` loops, ending in `RETURN expression`. Flat scope -- every introduced name stays visible for the rest of the body, including after its own loop, holding its last iteration's value. No runtime loop in the evaluator: a loop's source size is already known once the caller's real argument is substituted in, so userFunctions.ts's new compileBlock() unrolls every FOR loop into a single nested expression (a left fold) at the same parse-time inlining point Phase 1 already used -- the result is exactly the same shape of static AST as any other inlined function call, so evaluator.ts and graph.ts need no changes, dependency edges on every unrolled cell are picked up automatically, and a bounded MAX_UNROLL_STEPS (10,000) keeps it crash/hang-free. parser.ts's parseFunctionBlock() parses the new grammar (a purely additive EQUALS lexer token; FOR/IN/DO/RETURN are plain case-matched IDENTs, not new reserved tokens) into a BlockNode kept outside the existing Node union, so no existing switch needs a new case. Store: CustomFunction/FunctionDef gain `kind: 'expression' | 'block'` (defaulted for every function saved before this existed, including via JSON import). validateCustomFunction validates the new grammar's local/ loop-variable names the same way as parameters (not cell-shaped, no reserved words, no shadowing a parameter -- but reusing the same local across statements, the accumulator pattern, is correctly not a collision) and that every FOR source is structurally a parameter, cell, or range. UI: a "𝑓 Funktionen" form toggle between "Einfache Formel" and "Mit Schleife", with its own placeholder/hint (a PRODUCT example) and a small badge on block-kind rows in the list. 237 tests passing (34 new), vue-tsc/eslint/build clean. Verified live in the running app: defined PRODUCT via the loop UI, called =PRODUCT(Umsatz!B2:E6), confirmed the correct value and recalculation on a change to a cell inside the range, and an inline rejection for an invalid FOR source at save time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7340307a70 |
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> |
||
|
|
fdde4ed5a7 |
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> |
||
|
|
c3fde283c8 |
Add user-defined functions, editable exclusively via a new UI
Document-wide, named formula functions (e.g. MARGE(revenue; cost) = (revenue - cost) / revenue) callable from any cell like a built-in (=MARGE(Umsatz!B2; Umsatz!C2)). Created, edited and deleted only through a new "𝑓 Funktionen" toolbar button -> modal (list + shared edit form), never via JSON handedit. Engine: a call is inlined into the caller's AST at parse time (src/engine/userFunctions.ts's expandUserFunctions), so evaluator.ts and graph.ts need no changes -- dependencies on cells referenced only inside a function body (not in the call) are picked up for free. parser.ts gains an optional paramNames argument to parseFormula() producing a new ParamNode leaf, used only when parsing a function body. Store: addCustomFunction/updateCustomFunction/deleteCustomFunction with inline (not toast) validation -- name/parameter shape, no collision with a built-in or another function, the body parses, and a definition-time cycle check across the whole call graph. Renaming/deleting a function mirrors table rename/delete: existing calls are not rewritten, they become #NAME? on the next rebuild. Fully undo/redo-able and part of the JSON document (customFunctions: [], migrated for v1 and older v2 docs). 178 tests passing (32 new), vue-tsc and eslint clean. Manually verified end-to-end in the running app via Playwright: define/call/recalculate, rename -> #NAME?, a closing cycle rejected inline at save, undo restores the function library. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |