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:
+27
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user