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:
@@ -55,6 +55,7 @@ power = primary [ "^" unary ] ; (* rechts-assoziativ; bindet
|
||||
-2^2 = -(2^2) = -4 *)
|
||||
primary = NUMBER
|
||||
| STRING
|
||||
| ERRORLIT
|
||||
| "(" expression ")"
|
||||
| funcall
|
||||
| reference
|
||||
@@ -85,6 +86,9 @@ STRING = /" ... "/ (Backslash-Escapes) ;
|
||||
IDENT = /[A-Za-z_][A-Za-z0-9_]*/ ;
|
||||
QUOTED = /' ( [^'] | '' )* '/ (* Tabellenname mit Leer-/Sonderzeichen *)
|
||||
CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
ERRORLIT = "#REF" | "#DIV/0!" | "#NAME?" | "#ERR" | "#CIRC" |
|
||||
"#VALUE!" | "#N/A" ; (* longest-match in the lexer -- see
|
||||
lexer.ts's ERROR_CODES_BY_LENGTH *)
|
||||
```
|
||||
|
||||
Präzedenz, schwach → stark: Vergleich (`= <> > < >= <=`) < `&` < `+ -` < `* /` < unäres `+ -` < `^`.
|
||||
@@ -417,6 +421,16 @@ dagegen erlaubt), `blockAlwaysReturns` sowie die Zyklus-Prüfung.
|
||||
Konvention wie bei `kind: 'block'`s `IF`-Abhängigkeiten (s. u.). Ein
|
||||
benutzerdefiniertes „IF" ist nicht anlegbar (Namenskollision mit dem
|
||||
eingebauten `FUNCTIONS.IF`-Eintrag).
|
||||
- **`AND`/`OR`/`NOT` brauchen dagegen keine Sonderbehandlung** — anders als
|
||||
`IF`/`XLOOKUP` sind es ganz normale `functions.ts`-Einträge, ohne
|
||||
`evaluator.ts`-Kurzschluss: `AND`/`OR` werten (wie in echtem Excel) **alle**
|
||||
Argumente eager aus, ein Fehler in irgendeinem Argument propagiert also
|
||||
immer, egal ob das logische Ergebnis schon feststeht (`=AND(1=2; 1/0)` →
|
||||
`#DIV/0!`, nicht `0`). `functions.ts#isTruthy()` ist die eine
|
||||
Wahrheits-Konvention, die überall im Motor geteilt wird, wo eine Bedingung
|
||||
gebraucht wird – `evalIf()` und `execStatement`s `ifstmt`-Fall importieren
|
||||
sie jetzt aus `functions.ts`, statt sie (wie vor diesem Feature) je einmal
|
||||
inline zu duplizieren.
|
||||
|
||||
## Datum & Zeit
|
||||
|
||||
@@ -558,3 +572,13 @@ rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
|
||||
Es gibt **keinen** Absturz und **keinen** Hang: Lexer/Parser werfen intern,
|
||||
`Workbook` fängt alles ab und schreibt einen Fehlerwert in die Zelle.
|
||||
|
||||
Jeder dieser sieben Codes ist außerdem als **`ErrorLiteralNode`** direkt in
|
||||
eine Formel tippbar (`=#REF`, `=IF(A1=0;#N/A;A1)`, …) – der Lexer erkennt
|
||||
ihn als eigenes `ERRORLIT`-Token (längster Treffer zuerst, s. o. EBNF),
|
||||
`evalScalar`s `case 'errorliteral':` gibt unbedingt `new FormulaError(code)`
|
||||
zurück. `store/document.ts`s Zeilen-/Spalten-Löschen ist der eine Ort, der
|
||||
selbst *Formeltext* mit `#REF`-Literalen erzeugt (für eine dadurch
|
||||
kollabierte Referenz) – ansonsten ist es reine Nutzer-Eingabe, wie eine
|
||||
Zahl- oder Text-Konstante. Kein `!` am Ende (`#REF`, nicht Excels `#REF!`)
|
||||
– derselbe Code, den diese Engine selbst überall sonst produziert.
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
*/
|
||||
|
||||
import type { BlockCallNode, BlockNode, CellNode, FuncNode, NamedRefNode, Node, RangeNode, Statement } from './parser';
|
||||
import { getFunction } from './functions';
|
||||
import { getFunction, isTruthy } from './functions';
|
||||
import { expandRange } from './refs';
|
||||
import { substitute } from './userFunctions';
|
||||
import { FormulaError, isError, type CellValue, type EngineHost } from './types';
|
||||
@@ -55,6 +55,8 @@ function evalScalar(node: Node, host: EngineHost): CellValue {
|
||||
return node.value;
|
||||
case 'string':
|
||||
return node.value;
|
||||
case 'errorliteral':
|
||||
return new FormulaError(node.code);
|
||||
|
||||
case 'unary': {
|
||||
const v = toNumber(evalScalar(node.operand, host));
|
||||
@@ -173,18 +175,17 @@ function evalScalar(node: Node, host: EngineHost): CellValue {
|
||||
* 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. */
|
||||
* effect) in the untaken branch never surfaces. Truthy via functions.ts's
|
||||
* `isTruthy()` -- the one convention shared everywhere in this engine a
|
||||
* condition is needed (also `execStatement`'s `ifstmt` case, below, and
|
||||
* `AND`/`OR`/`NOT`). */
|
||||
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);
|
||||
if (isTruthy(cond)) return evalScalar(node.args[1], host);
|
||||
return node.args.length === 3 ? evalScalar(node.args[2], host) : 0;
|
||||
}
|
||||
|
||||
@@ -403,8 +404,7 @@ function execStatement(
|
||||
case 'ifstmt': {
|
||||
const c = evalInEnv(stmt.cond, env, host);
|
||||
if (isError(c)) throw new ReturnSignal(c);
|
||||
const truthy = typeof c === 'number' ? c !== 0 : c !== null && c !== '';
|
||||
if (truthy) execBlock(stmt.then, env, host, budget);
|
||||
if (isTruthy(c)) execBlock(stmt.then, env, host, budget);
|
||||
else if (stmt.else) execBlock(stmt.else, env, host, budget);
|
||||
return;
|
||||
}
|
||||
|
||||
+35
-2
@@ -27,6 +27,17 @@ function nums(values: CellValue[]): number[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The one truthy convention used everywhere in this engine a condition is
|
||||
* needed (IF's condition -- both the formula function and its real
|
||||
* short-circuit implementation in evaluator.ts's evalIf() --, a "Mit
|
||||
* Schleife" block's IF statement, and AND/OR/NOT below): a number is
|
||||
* truthy unless it's exactly `0`; text is truthy unless empty; an empty
|
||||
* cell (`null`) is never truthy. Exported so evaluator.ts can reuse the
|
||||
* exact same rule instead of duplicating it. */
|
||||
export function isTruthy(v: CellValue): boolean {
|
||||
return typeof v === 'number' ? v !== 0 : v !== null && v !== '';
|
||||
}
|
||||
|
||||
export const FUNCTIONS: Record<string, FunctionFn> = {
|
||||
SUM(values) {
|
||||
return nums(values).reduce((a, b) => a + b, 0);
|
||||
@@ -78,11 +89,30 @@ export const FUNCTIONS: Record<string, FunctionFn> = {
|
||||
}
|
||||
const [cond, thenValue, elseValue] = values;
|
||||
if (isError(cond)) return cond;
|
||||
const truthy = typeof cond === 'number' ? cond !== 0 : cond !== null && cond !== '';
|
||||
if (truthy) return thenValue;
|
||||
if (isTruthy(cond)) return thenValue;
|
||||
return values.length === 3 ? elseValue : 0;
|
||||
},
|
||||
|
||||
// Ordinary (non-short-circuit) functions -- like real Excel, EVERY
|
||||
// argument is evaluated regardless of the others (so e.g.
|
||||
// =AND(1=2; 1/0) is still #DIV/0!, not FALSE, exactly as in Excel) --
|
||||
// no special-casing in evaluator.ts needed, unlike IF/XLOOKUP. A range
|
||||
// argument flattens into one truthy check per cell, same as Excel.
|
||||
AND(values) {
|
||||
if (values.length === 0) return new FormulaError('#ERR', 'AND expects at least 1 argument');
|
||||
return values.every(isTruthy) ? 1 : 0;
|
||||
},
|
||||
|
||||
OR(values) {
|
||||
if (values.length === 0) return new FormulaError('#ERR', 'OR expects at least 1 argument');
|
||||
return values.some(isTruthy) ? 1 : 0;
|
||||
},
|
||||
|
||||
NOT(values) {
|
||||
if (values.length !== 1) return new FormulaError('#ERR', 'NOT expects exactly 1 argument');
|
||||
return isTruthy(values[0]) ? 0 : 1;
|
||||
},
|
||||
|
||||
// Dates/times are plain numbers here (see dates.ts) -- DATE/TIME build one,
|
||||
// TODAY/NOW read the current one. `null` (empty cell) arguments count as 0,
|
||||
// same as everywhere else in this engine; a non-numeric text argument is
|
||||
@@ -169,6 +199,9 @@ export const BUILTIN_SIGNATURES: ReadonlyArray<{ name: string; hint: string }> =
|
||||
{ name: 'CONCAT', hint: 'CONCAT(Text1; Text2; …)' },
|
||||
{ name: 'CONCATENATE', hint: 'CONCATENATE(Text1; Text2; …)' },
|
||||
{ name: 'IF', hint: 'IF(Bedingung; Dann; Sonst?)' },
|
||||
{ name: 'AND', hint: 'AND(Bedingung1; Bedingung2; …)' },
|
||||
{ name: 'OR', hint: 'OR(Bedingung1; Bedingung2; …)' },
|
||||
{ name: 'NOT', hint: 'NOT(Bedingung)' },
|
||||
{ name: 'DATE', hint: 'DATE(Jahr; Monat; Tag)' },
|
||||
{ name: 'TIME', hint: 'TIME(Std; Min; Sek)' },
|
||||
{ name: 'TODAY', hint: 'TODAY()' },
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
* them by look-ahead (`(` -> function, `!` -> sheet qualifier, digits -> cell).
|
||||
*/
|
||||
|
||||
import { ERROR_CODES } from './types';
|
||||
|
||||
export type TokenType =
|
||||
| 'NUMBER'
|
||||
| 'STRING'
|
||||
| 'IDENT' // bare word: A1, SUM, Region, Q1 ...
|
||||
| 'QUOTED' // 'Kosten Q3' -- a sheet name in single quotes
|
||||
| 'ERRORLIT' // a literal error code, e.g. "#REF" or "#DIV/0!" -- see ERROR_CODES
|
||||
| 'PLUS'
|
||||
| 'MINUS'
|
||||
| 'STAR'
|
||||
@@ -74,6 +77,10 @@ function isWordPart(ch: string): boolean {
|
||||
return isWordStart(ch) || isDigit(ch);
|
||||
}
|
||||
|
||||
/** Longest-first, so a prefix of another code (none collide today, but this
|
||||
* stays correct if one ever did) never wins the match by accident. */
|
||||
const ERROR_CODES_BY_LENGTH = [...ERROR_CODES].sort((a, b) => b.length - a.length);
|
||||
|
||||
export function tokenize(src: string): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
let i = 0;
|
||||
@@ -182,6 +189,21 @@ export function tokenize(src: string): Token[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
// an error-code literal, e.g. "#REF" or "#DIV/0!" -- lets a formula
|
||||
// literally contain/produce one, same as typing it in Excel; also how a
|
||||
// structural row/col delete marks a now-broken reference in formula text
|
||||
// (see store/document.ts's row/col delete). Recognized as one token
|
||||
// regardless of what follows (a subsequent "!"/"?" is already consumed
|
||||
// as part of the matched code, never re-tokenized as BANG etc.).
|
||||
if (ch === '#') {
|
||||
const code = ERROR_CODES_BY_LENGTH.find((c) => src.startsWith(c, i));
|
||||
if (code) {
|
||||
tokens.push({ type: 'ERRORLIT', value: code, pos: i });
|
||||
i += code.length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// single-character punctuation / operators
|
||||
const t = SINGLE[ch];
|
||||
if (t) {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
* -2^2 = -(2^2) = -4 *)
|
||||
* primary = NUMBER
|
||||
* | STRING
|
||||
* | ERRORLIT
|
||||
* | "(" expression ")"
|
||||
* | funcall
|
||||
* | reference ;
|
||||
@@ -63,6 +64,12 @@
|
||||
* (backward-compatible --
|
||||
* see reference() below). *)
|
||||
* CELL = /\$?[A-Za-z]{1,3}\$?[0-9]{1,7}/ ;
|
||||
* ERRORLIT = "#REF" | "#DIV/0!" | "#NAME?" | "#ERR" | "#CIRC" |
|
||||
* "#VALUE!" | "#N/A" ; (* a literal error code,
|
||||
* typed directly or
|
||||
* produced by a
|
||||
* structural row/col
|
||||
* delete *)
|
||||
*
|
||||
* Precedence, lowest to highest: comparison < & < + - < * / < unary +/- < ^
|
||||
* ---------------------------------------------------------------------------
|
||||
@@ -84,10 +91,12 @@
|
||||
|
||||
import { tokenize, type Token, type TokenType, LexError } from './lexer';
|
||||
import { looksLikeCell, parseA1 } from './refs';
|
||||
import type { ErrorCode } from './types';
|
||||
|
||||
export type Node =
|
||||
| NumberNode
|
||||
| StringNode
|
||||
| ErrorLiteralNode
|
||||
| BinaryNode
|
||||
| UnaryNode
|
||||
| FuncNode
|
||||
@@ -106,6 +115,16 @@ export interface StringNode {
|
||||
kind: 'string';
|
||||
value: string;
|
||||
}
|
||||
/** A literal error code typed directly into a formula, e.g. `=#REF!+1` or
|
||||
* `=IF(A1=0;#N/A;A1)` -- same as typing one in Excel. Evaluates directly to
|
||||
* that `FormulaError`, unconditionally (it's not the *result* of anything,
|
||||
* just a value like a number or string literal). Also how a structural
|
||||
* row/col delete represents a now-broken reference in stored formula text
|
||||
* (see store/document.ts). */
|
||||
export interface ErrorLiteralNode {
|
||||
kind: 'errorliteral';
|
||||
code: ErrorCode;
|
||||
}
|
||||
export interface BinaryNode {
|
||||
kind: 'binary';
|
||||
op: '+' | '-' | '*' | '/' | '^' | '&' | '=' | '<>' | '>' | '<' | '>=' | '<=';
|
||||
@@ -487,6 +506,10 @@ class Parser {
|
||||
this.next();
|
||||
return { kind: 'string', value: t.value };
|
||||
}
|
||||
if (t.type === 'ERRORLIT') {
|
||||
this.next();
|
||||
return { kind: 'errorliteral', code: t.value as ErrorCode };
|
||||
}
|
||||
if (t.type === 'LPAREN') {
|
||||
this.next();
|
||||
const inner = this.expression();
|
||||
|
||||
@@ -91,6 +91,7 @@ function expand(
|
||||
switch (node.kind) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'errorliteral':
|
||||
case 'cell':
|
||||
case 'range':
|
||||
case 'namedref':
|
||||
@@ -179,6 +180,7 @@ export function substitute(node: Node, bound: ReadonlyMap<string, Node>): Node {
|
||||
return bound.get(node.name.toUpperCase()) ?? node;
|
||||
case 'number':
|
||||
case 'string':
|
||||
case 'errorliteral':
|
||||
case 'cell':
|
||||
case 'range':
|
||||
case 'namedref':
|
||||
|
||||
Reference in New Issue
Block a user