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
+21 -1
View File
@@ -13,7 +13,7 @@
* - text -> ignored by the numeric aggregates
*/
import { FormulaError, type CellValue } from './types';
import { FormulaError, isError, type CellValue } from './types';
export type FunctionFn = (values: CellValue[]) => CellValue;
@@ -50,6 +50,26 @@ export const FUNCTIONS: Record<string, FunctionFn> = {
COUNT(values) {
return nums(values).length;
},
// IF(cond; then; else?) -- evaluator.ts special-cases this name *before*
// ever reaching FUNCTIONS, giving it real short-circuit evaluation (only
// the taken branch is computed, so e.g. IF(B1=0; 0; A1/B1) never runs the
// division when B1=0). This entry only exists so IF is a registered
// built-in for hasFunction()'s sake (the store's custom-function name
// check then correctly refuses to let a user shadow it) and as a
// defensive, best-effort fallback if this were ever reached directly
// (values are already-eagerly-computed here, so it can't short-circuit --
// see evaluator.ts's evalIf() for the real implementation).
IF(values) {
if (values.length !== 2 && values.length !== 3) {
return new FormulaError('#ERR', 'IF expects 2 or 3 arguments');
}
const [cond, thenValue, elseValue] = values;
if (isError(cond)) return cond;
const truthy = typeof cond === 'number' ? cond !== 0 : cond !== null && cond !== '';
if (truthy) return thenValue;
return values.length === 3 ? elseValue : 0;
},
};
// AVERAGE is an alias of AVG.