- init
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseFormula, collectRefs, ParseError, type Node } from '../parser';
|
||||
|
||||
/** Compact s-expression rendering of an AST for precedence assertions. */
|
||||
function sexpr(n: Node): string {
|
||||
switch (n.kind) {
|
||||
case 'number':
|
||||
return String(n.value);
|
||||
case 'string':
|
||||
return JSON.stringify(n.value);
|
||||
case 'unary':
|
||||
return `(${n.op} ${sexpr(n.operand)})`;
|
||||
case 'binary':
|
||||
return `(${n.op} ${sexpr(n.left)} ${sexpr(n.right)})`;
|
||||
case 'func':
|
||||
return `(${n.name} ${n.args.map(sexpr).join(' ')})`;
|
||||
case 'cell':
|
||||
return `${n.qualifier ? n.qualifier + '!' : ''}cell(${n.col},${n.row})`;
|
||||
case 'range':
|
||||
return `${n.qualifier ? n.qualifier + '!' : ''}range(${n.start.col},${n.start.row}:${n.end.col},${n.end.row})`;
|
||||
}
|
||||
}
|
||||
const parse = (s: string) => sexpr(parseFormula(s));
|
||||
|
||||
describe('parser precedence & associativity', () => {
|
||||
it('* / bind tighter than + -', () => {
|
||||
expect(parse('=1+2*3')).toBe('(+ 1 (* 2 3))');
|
||||
expect(parse('=1-2/2')).toBe('(- 1 (/ 2 2))');
|
||||
});
|
||||
|
||||
it('+ - are left-associative', () => {
|
||||
expect(parse('=1-2-3')).toBe('(- (- 1 2) 3)');
|
||||
});
|
||||
|
||||
it('^ binds tighter than * / and is right-associative', () => {
|
||||
expect(parse('=2*3^2')).toBe('(* 2 (^ 3 2))');
|
||||
expect(parse('=2^3^2')).toBe('(^ 2 (^ 3 2))');
|
||||
});
|
||||
|
||||
it('unary minus binds weaker than ^ (-2^2 = -(2^2))', () => {
|
||||
expect(parse('=-2^2')).toBe('(- (^ 2 2))');
|
||||
expect(parse('=2^-3')).toBe('(^ 2 (- 3))');
|
||||
});
|
||||
|
||||
it('parentheses override precedence', () => {
|
||||
expect(parse('=(1+2)*3')).toBe('(* (+ 1 2) 3)');
|
||||
expect(parse('=(B2+C2)*2-D2/4')).toBe(
|
||||
'(- (* (+ cell(1,1) cell(2,1)) 2) (/ cell(3,1) 4))',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser references', () => {
|
||||
it('parses local cells and ranges', () => {
|
||||
expect(parse('=A1')).toBe('cell(0,0)');
|
||||
expect(parse('=A1:C5')).toBe('range(0,0:2,4)');
|
||||
});
|
||||
|
||||
it('parses table-qualified refs, bare and quoted', () => {
|
||||
expect(parse('=Umsatz!B2')).toBe('Umsatz!cell(1,1)');
|
||||
expect(parse("='Kosten Q3'!B2:E2")).toBe('Kosten Q3!range(1,1:4,1)');
|
||||
});
|
||||
|
||||
it('accepts the Numbers-style "::" qualifier interchangeably with "!"', () => {
|
||||
expect(parse('=Umsatz::B2')).toBe('Umsatz!cell(1,1)');
|
||||
expect(parse("='Kosten Q3'::B2:E2")).toBe('Kosten Q3!range(1,1:4,1)');
|
||||
expect(parse('=Umsatz::B2')).toBe(parse('=Umsatz!B2'));
|
||||
});
|
||||
|
||||
it('parses function calls with ; separator and mixed args', () => {
|
||||
expect(parse('=SUM(Umsatz!B2:E2; 100; A1)')).toBe(
|
||||
'(SUM Umsatz!range(1,1:4,1) 100 cell(0,0))',
|
||||
);
|
||||
});
|
||||
|
||||
it('collectRefs finds every reference', () => {
|
||||
const refs = collectRefs(parseFormula('=SUM(A1:A3; Kosten!B2) + C4'));
|
||||
expect(refs).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parser errors', () => {
|
||||
it('throws ParseError on a dangling operator', () => {
|
||||
expect(() => parseFormula('=A1+')).toThrow(ParseError);
|
||||
});
|
||||
it('throws on an unbalanced paren', () => {
|
||||
expect(() => parseFormula('=SUM(A1')).toThrow(ParseError);
|
||||
});
|
||||
it('throws on a non-cell identifier used as a reference', () => {
|
||||
expect(() => parseFormula('=foo + 1')).toThrow(ParseError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user