IF-Funktion als Formel: echte Kurzschluss-Auswertung

Neue eingebaute Funktion IF(Bedingung; Dann; Sonst?), nutzbar in jeder
gewöhnlichen Zellformel (nicht nur in "Mit Schleife"-Funktionskörpern).

- functions.ts: IF als Platzhalter-Eintrag registriert, nur damit
  hasFunction('IF') greift (Namensreservierung -- ein Benutzer kann
  keine gleichnamige eigene Funktion mehr anlegen) und als defensiver
  Fallback. Trägt einen Kommentar, der auf die echte Implementierung
  verweist.
- evaluator.ts: die echte, kurzschließende Auswertung sitzt in
  evalScalar()s case 'func', VOR dem generischen eager-args-Pfad.
  evalIf() wertet nur die Bedingung immer aus, danach ausschließlich
  den genommenen Zweig -- der andere Zweig wird nie evaluiert, ein
  Fehler darin (z. B. =IF(B1=0; 0; A1/B1)) schlägt also nie durch.
  Gleiche Wahrheits-Konvention wie überall sonst im Engine (Zahl != 0
  bzw. nicht-leerer Text = wahr).
- collectRefs() bleibt unverändert: sein generischer case 'func' läuft
  bereits konservativ durch alle Argumente, unabhängig vom zur
  Laufzeit genommenen Zweig -- exakt wie Excels IF().

Tests: 8 neue (evaluator.spec.ts: beide Zweige, 2-/3-Argument-Form,
Kurzschluss-Beweis, Fehler in der Bedingung, Arity-Fehler, Vergleiche
als Bedingung; document.spec.ts: Namenskollision mit eingebautem IF).
257 -> 265 Tests, alle grün. vue-tsc/eslint/build sauber. Live per
Playwright verifiziert (inkl. Kurzschluss- und Neuberechnungsverhalten).

READMEs aktualisiert (Funktionen-Liste, Auswertungsmodell, Testzahl).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-02 23:00:45 +02:00
co-authored by Claude Sonnet 5
parent 3fd5a84701
commit ec85c371e8
6 changed files with 129 additions and 11 deletions
+27 -1
View File
@@ -27,7 +27,7 @@
* ordinary function call this can't be resolved any earlier.
*/
import type { BlockCallNode, BlockNode, CellNode, Node, Statement } from './parser';
import type { BlockCallNode, BlockNode, CellNode, FuncNode, Node, Statement } from './parser';
import { getFunction } from './functions';
import { expandRange } from './refs';
import { substitute } from './userFunctions';
@@ -101,6 +101,12 @@ function evalScalar(node: Node, host: EngineHost): CellValue {
return new FormulaError('#VALUE!', 'range used where a value is expected');
case 'func': {
// IF is the one built-in that must NOT evaluate its arguments eagerly
// (see functions.ts's FUNCTIONS.IF comment): only the taken branch may
// be computed, so e.g. IF(B1=0; 0; A1/B1) never divides by zero when
// B1=0. Special-cased here, before the generic eager-args dispatch.
if (node.name.toUpperCase() === 'IF') return evalIf(node, host);
const fn = getFunction(node.name);
if (!fn) return new FormulaError('#NAME?', `unknown function ${node.name}`);
const values: CellValue[] = [];
@@ -127,6 +133,26 @@ function evalScalar(node: Node, host: EngineHost): CellValue {
}
}
/** IF(cond; then; else?) -- the real, short-circuiting implementation (see
* the `case 'func'` special-case above and functions.ts's FUNCTIONS.IF
* comment). Only `node.args[0]` is always evaluated; whichever of the
* other two branches is taken is evaluated next, and the other is never
* touched at all -- so an error (or, in a block-mode call, any other side
* effect) in the untaken branch never surfaces. Same truthy convention as
* everywhere else in this engine (`execStatement`'s `ifstmt` case, above):
* a number is truthy unless it's exactly 0; text/refs are truthy unless
* empty. */
function evalIf(node: FuncNode, host: EngineHost): CellValue {
if (node.args.length !== 2 && node.args.length !== 3) {
return new FormulaError('#ERR', 'IF expects 2 or 3 arguments');
}
const cond = evalScalar(node.args[0], host);
if (isError(cond)) return cond;
const truthy = typeof cond === 'number' ? cond !== 0 : cond !== null && cond !== '';
if (truthy) return evalScalar(node.args[1], host);
return node.args.length === 3 ? evalScalar(node.args[2], host) : 0;
}
// ---------------------------------------------------------------------------
// `kind: 'block'` custom functions: a small tree-walking interpreter for a
// BlockCallNode's `block` (see parser.ts's grammar comment and