AND/OR/NOT, Fehler-Literale und Zeilen/Spalten einfügen/löschen
- AND(...)/OR(...)/NOT(...) als gewöhnliche Funktionen (kein Kurzschluss- Sonderfall wie IF/XLOOKUP nötig, werten wie in echtem Excel alle Argumente eager aus). Dabei die bisher dreifach duplizierte Wahrheits-Prüfung zu functions.ts#isTruthy() zusammengezogen, jetzt von FUNCTIONS.IF, evalIf() und dem Block-IF-Statement gemeinsam genutzt. Schließt "kein AND/OR für zusammengesetzte Bedingungen" aus den Bekannten Grenzen, auch nutzbar in "Mit Schleife"-IF-Bedingungen. - Neue Grammatik-Ergänzung: ErrorLiteralNode. Jeder der sieben Fehlercodes (#REF, #DIV/0!, #NAME?, #ERR, #CIRC, #VALUE!, #N/A) lässt sich jetzt direkt in eine Formel tippen (=IF(A1=0;#N/A;A1)) und wertet unbedingt zu genau diesem Fehler aus. - Zeilen/Spalten einfügen/löschen mitten im Gitter (Rechtsklick auf einen Zeilen-/Spaltenkopf), nicht nur am Rand wie bisher: Zellinhalte, Formate und Zeilenhöhen/Spaltenbreiten verschieben sich innerhalb der Tabelle, und jede Formel im gesamten Dokument, die die Tabelle referenziert (auch über Tabelle!Zelle/Blatt!Tabelle::Zelle-Qualifier aus anderen Tabellen), wird automatisch mitgezogen. Ein Bereich, der die Stelle überspannt, wächst/schrumpft entsprechend; eine Referenz, die exakt die gelöschte Zeile/Spalte trifft, wird zu einem echten #REF-Literal im Formeltext (nutzt die neue ErrorLiteralNode-Grammatik). $-Sperren bleiben dabei bewusst wirkungslos (anders als beim Kopieren), unveränderte Referenzen bleiben byte-identisch. Token-Level-Splice wie shiftFormula (Kopieren/ Ausschneiden), keine AST-Neuserialisierung. 406 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,74 @@ describe('evaluator IF', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluator AND/OR/NOT', () => {
|
||||
const host = makeHost({ S1: { A1: 8, B1: 0, C1: 'x' } });
|
||||
|
||||
it('AND is true only when every argument is truthy', () => {
|
||||
expect(ev('=AND(1;1;1)', host)).toBe(1);
|
||||
expect(ev('=AND(1;0;1)', host)).toBe(0);
|
||||
expect(ev('=AND(A1>0;A1<10)', host)).toBe(1);
|
||||
expect(ev('=AND(A1>0;A1<5)', host)).toBe(0);
|
||||
});
|
||||
|
||||
it('OR is true when at least one argument is truthy', () => {
|
||||
expect(ev('=OR(0;0;1)', host)).toBe(1);
|
||||
expect(ev('=OR(0;0;0)', host)).toBe(0);
|
||||
});
|
||||
|
||||
it('NOT inverts a single condition', () => {
|
||||
expect(ev('=NOT(1)', host)).toBe(0);
|
||||
expect(ev('=NOT(0)', host)).toBe(1);
|
||||
expect(ev('=NOT(B1=0)', host)).toBe(0);
|
||||
});
|
||||
|
||||
it('a range argument checks every cell (Excel behaviour)', () => {
|
||||
const h = makeHost({ S1: { A1: 1, A2: 1, A3: 0 } });
|
||||
expect(ev('=AND(A1:A3)', h)).toBe(0);
|
||||
expect(ev('=OR(A1:A3)', h)).toBe(1);
|
||||
});
|
||||
|
||||
it('empty cell and non-empty text follow the shared truthy convention', () => {
|
||||
expect(ev('=AND(Z9)', host)).toBe(0); // empty cell -> falsy
|
||||
expect(ev('=AND(C1)', host)).toBe(1); // non-empty text -> truthy
|
||||
});
|
||||
|
||||
it('composes inside IF, unlike a bare chained comparison', () => {
|
||||
expect(ev('=IF(AND(A1>0;A1<10);"ok";"nein")', host)).toBe('ok');
|
||||
});
|
||||
|
||||
it('does NOT short-circuit -- every argument is evaluated, same as real Excel', () => {
|
||||
expect(isError(ev('=AND(1=2;1/0)', host))).toBe(true);
|
||||
expect(isError(ev('=OR(1=1;1/0)', host))).toBe(true);
|
||||
});
|
||||
|
||||
it('wrong arity -> #ERR', () => {
|
||||
expect(isError(ev('=AND()', host))).toBe(true);
|
||||
expect(isError(ev('=NOT(1;2)', host))).toBe(true);
|
||||
expect(isError(ev('=NOT()', host))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluator error literals (ErrorLiteralNode)', () => {
|
||||
const host = makeHost({ S1: {} });
|
||||
|
||||
it('evaluates directly to that FormulaError', () => {
|
||||
const r = ev('=#REF', host);
|
||||
expect(isError(r) && r.code).toBe('#REF');
|
||||
});
|
||||
|
||||
it('propagates through arithmetic like any other error', () => {
|
||||
const r = ev('=1+#DIV/0!', host);
|
||||
expect(isError(r) && r.code).toBe('#DIV/0!');
|
||||
});
|
||||
|
||||
it('usable as an IF branch, e.g. a deliberate #N/A', () => {
|
||||
expect(ev('=IF(1=0;#N/A;5)', host)).toBe(5);
|
||||
const r = ev('=IF(1=1;#N/A;5)', host);
|
||||
expect(isError(r) && r.code).toBe('#N/A');
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluator comparisons', () => {
|
||||
const host = makeHost({ S1: { A1: 'Nord', B1: 5 } });
|
||||
|
||||
|
||||
@@ -89,4 +89,27 @@ describe('lexer', () => {
|
||||
expect(types('A1<B2')).toEqual(['IDENT', 'LT', 'IDENT', 'EOF']);
|
||||
expect(types('A1>B2')).toEqual(['IDENT', 'GT', 'IDENT', 'EOF']);
|
||||
});
|
||||
|
||||
it('tokenizes every error code as one ERRORLIT token', () => {
|
||||
expect(types('#REF')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(values('#REF')).toEqual(['#REF', '']);
|
||||
expect(types('#DIV/0!')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(types('#NAME?')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(types('#ERR')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(types('#CIRC')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(types('#VALUE!')).toEqual(['ERRORLIT', 'EOF']);
|
||||
expect(types('#N/A')).toEqual(['ERRORLIT', 'EOF']);
|
||||
});
|
||||
|
||||
it('an ERRORLIT is followed by whatever comes next, not swallowed into it', () => {
|
||||
expect(types('1+#REF')).toEqual(['NUMBER', 'PLUS', 'ERRORLIT', 'EOF']);
|
||||
// "#REF" matches greedily -- a trailing "!" some might expect (Excel
|
||||
// spells it "#REF!") is its own BANG token, not part of the literal;
|
||||
// this engine's own error code is "#REF" without "!" (see types.ts).
|
||||
expect(types('#REF!')).toEqual(['ERRORLIT', 'BANG', 'EOF']);
|
||||
});
|
||||
|
||||
it('throws on an unrecognized "#..." sequence', () => {
|
||||
expect(() => tokenize('#FOO')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ function sexpr(n: Node): string {
|
||||
return String(n.value);
|
||||
case 'string':
|
||||
return JSON.stringify(n.value);
|
||||
case 'errorliteral':
|
||||
return n.code;
|
||||
case 'unary':
|
||||
return `(${n.op} ${sexpr(n.operand)})`;
|
||||
case 'binary':
|
||||
@@ -242,6 +244,23 @@ describe('parser named-range references (NamedRefNode)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser error literals (ErrorLiteralNode)', () => {
|
||||
it('every error code parses as a leaf', () => {
|
||||
expect(parse('=#REF')).toBe('#REF');
|
||||
expect(parse('=#DIV/0!')).toBe('#DIV/0!');
|
||||
expect(parse('=#NAME?')).toBe('#NAME?');
|
||||
expect(parse('=#ERR')).toBe('#ERR');
|
||||
expect(parse('=#CIRC')).toBe('#CIRC');
|
||||
expect(parse('=#VALUE!')).toBe('#VALUE!');
|
||||
expect(parse('=#N/A')).toBe('#N/A');
|
||||
});
|
||||
|
||||
it('composes with operators/functions like any other literal', () => {
|
||||
expect(parse('=1+#REF')).toBe('(+ 1 #REF)');
|
||||
expect(parse('=IF(1=0;#N/A;5)')).toBe('(IF (= 1 0) #N/A 5)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser comparison operators (available in every formula, not just block IF conditions)', () => {
|
||||
it('parses all six comparison operators', () => {
|
||||
expect(parse('=A1=B1')).toBe('(= cell(0,0) cell(1,0))');
|
||||
|
||||
@@ -395,6 +395,21 @@ describe('Workbook integration: block-kind (FOR/IF/RETURN "code") user-defined f
|
||||
expect(val(wb, 's-umsatz', 'B9')).toBe(0);
|
||||
});
|
||||
|
||||
it('AND/OR compose a compound condition inside an IF, same as any other function call', () => {
|
||||
const umsatz = sheet('s-umsatz', 'Umsatz', { A1: '5', B9: '=INRANGE(A1)' });
|
||||
const inRange = def({
|
||||
id: 'f1',
|
||||
name: 'INRANGE',
|
||||
params: ['x'],
|
||||
kind: 'block',
|
||||
body: 'IF AND(x > 0; x < 10) THEN RETURN 1 ELSE RETURN 0 END',
|
||||
});
|
||||
const wb = new Workbook([umsatz], [inRange]);
|
||||
expect(val(wb, 's-umsatz', 'B9')).toBe(1);
|
||||
wb.setCell('s-umsatz', 0, 0, '20'); // A1: 5 -> 20, out of range
|
||||
expect(val(wb, 's-umsatz', 'B9')).toBe(0);
|
||||
});
|
||||
|
||||
it('an early RETURN nested inside an IF inside a FOR stops the loop -- finds the first match, never touches later cells', () => {
|
||||
// A3 is the first cell > 100; A4 must never affect the result
|
||||
const umsatz = sheet('s-umsatz', 'Umsatz', {
|
||||
|
||||
Reference in New Issue
Block a user