@borgar/fx
Version:
Utilities for working with Excel formulas
1,411 lines (1,270 loc) • 46.2 kB
text/typescript
import { describe, test, expect } from 'vitest';
import { parse } from './parse.ts';
import { tokenize } from './tokenize.ts';
function isParsed (expr: string, expected: any, opts?: any) {
const result = parse(tokenize(expr, { ...opts, allowTernary: true }), opts);
const cleaned = JSON.parse(JSON.stringify(result));
expect(cleaned).toEqual(expected);
}
function isInvalidExpr (expr: string, opts?: any) {
expect(() => parse(tokenize(expr, { allowTernary: true }), { allowTernary: true, ...opts })).toThrow();
}
describe('parser', () => {
describe('parse numbers', () => {
test('basic number parsing', () => {
isParsed('1', { type: 'Literal', value: 1, raw: '1' });
isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
isParsed('2.4e+3', { type: 'Literal', value: 2400, raw: '2.4e+3' });
isParsed('-1e+10', { type: 'Literal', value: -10000000000, raw: '-1e+10' });
isParsed('1e-3', { type: 'Literal', value: 0.001, raw: '1e-3' });
});
});
describe('parse booleans', () => {
test('true values with different cases', () => {
isParsed('TRUE', { type: 'Literal', value: true, raw: 'TRUE' });
isParsed('true', { type: 'Literal', value: true, raw: 'true' });
isParsed('trUe', { type: 'Literal', value: true, raw: 'trUe' });
isParsed('TRue', { type: 'Literal', value: true, raw: 'TRue' });
});
test('false values with different cases', () => {
isParsed('FALSE', { type: 'Literal', value: false, raw: 'FALSE' });
isParsed('false', { type: 'Literal', value: false, raw: 'false' });
isParsed('False', { type: 'Literal', value: false, raw: 'False' });
isParsed('fAlSe', { type: 'Literal', value: false, raw: 'fAlSe' });
});
});
describe('parse strings', () => {
test('string literal parsing', () => {
isParsed('""', { type: 'Literal', value: '', raw: '""' });
isParsed('""""', { type: 'Literal', value: '"', raw: '""""' });
isParsed('" "', { type: 'Literal', value: ' ', raw: '" "' });
isParsed('"foobar"', { type: 'Literal', value: 'foobar', raw: '"foobar"' });
isParsed('"foo bar"', { type: 'Literal', value: 'foo bar', raw: '"foo bar"' });
isParsed('"foo""bar"', { type: 'Literal', value: 'foo"bar', raw: '"foo""bar"' });
});
});
describe('parse errors', () => {
test('error literal parsing', () => {
isParsed('#CALC!', { type: 'ErrorLiteral', value: '#CALC!', raw: '#CALC!' });
isParsed('#DIV/0!', { type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' });
isParsed('#FIELD!', { type: 'ErrorLiteral', value: '#FIELD!', raw: '#FIELD!' });
isParsed('#GETTING_DATA', { type: 'ErrorLiteral', value: '#GETTING_DATA', raw: '#GETTING_DATA' });
isParsed('#N/A', { type: 'ErrorLiteral', value: '#N/A', raw: '#N/A' });
isParsed('#NAME?', { type: 'ErrorLiteral', value: '#NAME?', raw: '#NAME?' });
isParsed('#NULL!', { type: 'ErrorLiteral', value: '#NULL!', raw: '#NULL!' });
isParsed('#NUM!', { type: 'ErrorLiteral', value: '#NUM!', raw: '#NUM!' });
isParsed('#REF!', { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' });
isParsed('#SPILL!', { type: 'ErrorLiteral', value: '#SPILL!', raw: '#SPILL!' });
isParsed('#SYNTAX?', { type: 'ErrorLiteral', value: '#SYNTAX?', raw: '#SYNTAX?' });
isParsed('#UNKNOWN!', { type: 'ErrorLiteral', value: '#UNKNOWN!', raw: '#UNKNOWN!' });
isParsed('#VALUE!', { type: 'ErrorLiteral', value: '#VALUE!', raw: '#VALUE!' });
});
});
describe('parse ranges', () => {
test('basic range references', () => {
isParsed('A1', { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' });
isParsed('A1:B2', { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' });
isParsed('A:B', { type: 'ReferenceIdentifier', value: 'A:B', kind: 'beam' });
isParsed('1:2', { type: 'ReferenceIdentifier', value: '1:2', kind: 'beam' });
isParsed('A1:2', { type: 'ReferenceIdentifier', value: 'A1:2', kind: 'range' });
isParsed('1:A2', { type: 'ReferenceIdentifier', value: '1:A2', kind: 'range' });
isParsed('A1.:.B2', { type: 'ReferenceIdentifier', value: 'A1.:.B2', kind: 'range' });
});
test('sheet qualified references', () => {
isParsed('Sheet!A1', { type: 'ReferenceIdentifier', value: 'Sheet!A1', kind: 'range' });
isParsed('[Workbook]Sheet!A1', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!A1', kind: 'range' });
isParsed('\'Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'Sheet\'!A1', kind: 'range' });
isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
isParsed('\'Workbook\'!A1', { type: 'ReferenceIdentifier', value: '\'Workbook\'!A1', kind: 'range' });
isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
});
test('named references', () => {
isParsed('foo', { type: 'ReferenceIdentifier', value: 'foo', kind: 'name' });
isParsed('Workbook!foo', { type: 'ReferenceIdentifier', value: 'Workbook!foo', kind: 'name' });
isParsed('[Workbook]Sheet!foo', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!foo', kind: 'name' });
});
});
describe('parse array literals', () => {
test('single element arrays', () => {
isParsed('{1}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' }
] ]
});
isParsed('{-1}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: -1, raw: '-1' }
] ]
});
isParsed('{#DIV/0!}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' }
] ]
});
isParsed('{TRUE}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: true, raw: 'TRUE' }
] ]
});
isParsed('{"foo"}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 'foo', raw: '"foo"' }
] ]
});
});
test('multi-element arrays', () => {
isParsed('{1,2}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
] ]
});
isParsed('{1,2;3}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
], [
{ type: 'Literal', value: 3, raw: '3' }
] ]
});
isParsed('{1,2;3,4}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
], [
{ type: 'Literal', value: 3, raw: '3' },
{ type: 'Literal', value: 4, raw: '4' }
] ]
});
isParsed('{1;2}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' }
], [
{ type: 'Literal', value: 2, raw: '2' }
] ]
});
isParsed('{1;2,3}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' }
], [
{ type: 'Literal', value: 2, raw: '2' },
{ type: 'Literal', value: 3, raw: '3' }
] ]
});
});
test('arrays with ranges', () => {
isParsed('{A1,A1:B2;A:A}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' }
], [
{ type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' }
] ]
}, { permitArrayRanges: true });
});
test('mixed type arrays', () => {
isParsed('{-0.1,"foo";#NAME?,false}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: -0.1, raw: '-0.1' },
{ type: 'Literal', value: 'foo', raw: '"foo"' }
], [
{ type: 'ErrorLiteral', value: '#NAME?', raw: '#NAME?' },
{ type: 'Literal', value: false, raw: 'false' }
] ]
});
});
test('invalid array expressions', () => {
isInvalidExpr('{A1}', { permitArrayRanges: false });
isInvalidExpr('{--1}', { negativeNumbers: true });
isInvalidExpr('{--1}', { negativeNumbers: false });
isInvalidExpr('{---1}', { negativeNumbers: true });
isInvalidExpr('{---1}', { negativeNumbers: false });
isInvalidExpr('{+1}'); // Excel silently corrects this 🤔
isInvalidExpr('{(1)}');
isInvalidExpr('{SUM(1)}');
isInvalidExpr('{{}}');
isInvalidExpr('{{}');
isInvalidExpr('{}}');
isInvalidExpr('{2+2}');
isInvalidExpr('{}');
isInvalidExpr('{,}');
isInvalidExpr('{1,}');
isInvalidExpr('{,1}');
isInvalidExpr('{;}');
});
test('array expressions with function calls', () => {
isParsed('={1234; UNIQUE(A:A)}',
{ type: 'ArrayExpression',
elements: [
[ { type: 'Literal', value: 1234, raw: '1234' } ],
[ { type: 'CallExpression',
callee: { type: 'Identifier', name: 'UNIQUE' },
arguments: [
{ type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' }
] } ]
] },
{ permitArrayCalls: true });
isParsed('={SUM({1,2}),3}',
{ type: 'ArrayExpression',
elements: [
[ { type: 'CallExpression',
callee: { type: 'Identifier', name: 'SUM' },
arguments: [
{ type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
] ] }
] },
{ type: 'Literal', value: 3, raw: '3' } ]
] },
{ permitArrayCalls: true });
});
});
describe('parse function calls', () => {
test('basic function calls', () => {
isParsed('=foo()', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'foo' },
arguments: []
});
isParsed('=FOO()', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: []
});
isParsed('=FOO(1)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
isParsed('=FOO(1,2)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
]
});
});
test('function calls with many arguments', () => {
const args = Array(300).fill('1');
isParsed(`=FOO(${args.join(',')})`, {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ ...args.map(() => ({ type: 'Literal', value: 1, raw: '1' })) ]
});
});
test('function calls with ranges', () => {
isParsed('=FOO(A1,B2)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
isParsed('=FOO((A1,B2))', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'BinaryExpression',
operator: ',',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] }
]
});
});
test('nested function calls', () => {
isParsed('=FOO(BAR())', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'CallExpression',
callee: { type: 'Identifier', name: 'BAR' },
arguments: [] }
]
});
});
test('function calls with null arguments', () => {
isParsed('=FOO(,)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, null ]
});
isParsed('=FOO(,,)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, null, null ]
});
isParsed('=FOO(1,)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ { type: 'Literal', value: 1, raw: '1' }, null ]
});
isParsed('=FOO(,1)', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, { type: 'Literal', value: 1, raw: '1' } ]
});
});
test('boolean function names', () => {
isParsed('=FALSE()', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FALSE' },
arguments: []
});
isParsed('=TRUE()', {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'TRUE' },
arguments: []
});
});
test('invalid function calls', () => {
isInvalidExpr('=FOO((1,2))');
isInvalidExpr('=FOO(');
isInvalidExpr('=FOO ()');
});
});
describe('parse unary operators', () => {
describe('unary operator %', () => {
test('percentage operator', () => {
isParsed('A1%', {
type: 'UnaryExpression',
operator: '%',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' } ]
});
isParsed('1%', {
type: 'UnaryExpression',
operator: '%',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
isParsed('(1)%', {
type: 'UnaryExpression',
operator: '%',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
});
test('invalid percentage usage', () => {
isInvalidExpr('%');
});
});
describe('unary operator -', () => {
test('negative numbers', () => {
isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
isParsed('-1', {
type: 'UnaryExpression',
operator: '-',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
}, { negativeNumbers: false });
isParsed('-"1"', {
type: 'UnaryExpression',
operator: '-',
arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
});
isParsed('-A1:B2', {
type: 'UnaryExpression',
operator: '-',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
});
test('double negative', () => {
isParsed('--1', {
type: 'UnaryExpression',
operator: '-',
arguments: [ { type: 'Literal', value: -1, raw: '-1' } ]
});
isParsed('--1', {
type: 'UnaryExpression',
operator: '-',
arguments: [ {
type: 'UnaryExpression',
operator: '-',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
} ]
}, { negativeNumbers: false });
});
test('invalid negative usage', () => {
isInvalidExpr('--');
isInvalidExpr('-');
});
});
describe('unary operator +', () => {
test('positive operator', () => {
isParsed('+1', {
type: 'UnaryExpression',
operator: '+',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
isParsed('+(1)', {
type: 'UnaryExpression',
operator: '+',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
isParsed('+"1"', {
type: 'UnaryExpression',
operator: '+',
arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
});
isParsed('+A1:B2', {
type: 'UnaryExpression',
operator: '+',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
});
test('invalid positive usage', () => {
isInvalidExpr('++');
isInvalidExpr('+');
});
});
describe('unary operator #', () => {
test('spill operator', () => {
isParsed('D9#', {
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
});
isParsed('A1:B2#', { // this parses but is a runtime error in Excel
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
isParsed('(A1):(B2)#', { // this parses but is a runtime error in Excel
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'BinaryExpression',
operator: ':',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] } ]
});
isParsed('(A1,B2)#', {
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'BinaryExpression',
operator: ',',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] } ]
});
isParsed('(A1 B2)#', {
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] } ]
});
isParsed('#REF!#', {
type: 'UnaryExpression',
operator: '#',
arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
});
isParsed('INDIRECT("d9")#', {
type: 'UnaryExpression',
operator: '#',
arguments: [ {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'INDIRECT' },
arguments: [ { type: 'Literal', value: 'd9', raw: '"d9"' } ]
} ]
});
});
test('invalid spill operator usage', () => {
isInvalidExpr('1#');
isInvalidExpr('"foo"#');
isInvalidExpr('#A1');
isInvalidExpr('##');
isInvalidExpr('#VALUE!#');
isInvalidExpr('#');
isInvalidExpr('#A1');
});
});
describe('unary operator @', () => {
test('implicit intersection operator', () => {
isParsed('@1', {
type: 'UnaryExpression',
operator: '@',
arguments: [ { type: 'Literal', raw: '1', value: 1 } ]
});
isParsed('@"foo"', {
type: 'UnaryExpression',
operator: '@',
arguments: [ { type: 'Literal', raw: '"foo"', value: 'foo' } ]
});
isParsed('@D9', {
type: 'UnaryExpression',
operator: '@',
arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
});
isParsed('@A1:B2', {
type: 'UnaryExpression',
operator: '@',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
isParsed('@#REF!', {
type: 'UnaryExpression',
operator: '@',
arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
});
isParsed('@FOO()', {
type: 'UnaryExpression',
operator: '@',
arguments: [ {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: []
} ]
});
});
test('invalid implicit intersection usage', () => {
isInvalidExpr('@');
isInvalidExpr('@@');
});
});
});
describe('parse binary operators', () => {
const operators = [ '+', '-', '^', '*', '/', '&', '=', '<', '>', '<=', '>=', '<>' ];
operators.forEach(op => {
describe(`binary operator ${op}`, () => {
test(`basic ${op} operations`, () => {
isParsed(`1${op}2`, {
type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
]
});
isParsed(`1${op}2${op}3`, {
type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
] },
{ type: 'Literal', value: 3, raw: '3' }
]
});
});
test(`${op} with strings`, () => {
isParsed(`"foo"${op}"bar"`, {
type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'Literal', value: 'foo', raw: '"foo"' },
{ type: 'Literal', value: 'bar', raw: '"bar"' }
]
});
});
test(`${op} with arrays`, () => {
isParsed(`{1,2}${op}{3,4}`, {
type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
] ] },
{ type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 3, raw: '3' },
{ type: 'Literal', value: 4, raw: '4' }
] ] }
]
});
});
test(`${op} with function calls`, () => {
isParsed(`FOO()${op}BAR()`, {
type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' }, arguments: [] },
{ type: 'CallExpression', callee: { type: 'Identifier', name: 'BAR' }, arguments: [] }
]
});
});
test(`invalid ${op} usage`, () => {
isInvalidExpr(op);
isInvalidExpr(op + op);
isInvalidExpr('1' + op);
if (op !== '+' && op !== '-') {
isInvalidExpr('=' + op + '1');
}
});
});
});
});
describe('parse range operators', () => {
const rangeOps = [
[ ':', 'range-join' ],
[ ',', 'union' ],
[ ' ', 'intersection' ]
];
rangeOps.forEach(([ op, opName ]) => {
describe(`${opName} operator "${op}"`, () => {
test('basic range operations', () => {
isParsed(`named1${op}named2`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
] });
isParsed(`A1${op}named2`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
] });
isParsed(`named1${op}B2`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
isParsed(`(A1)${op}(B2)`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
});
test('range operator with whitespace', () => {
isParsed(`A1 ${op} B2`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
});
test('invalid range operations', () => {
isInvalidExpr(`A1${op}0`);
isInvalidExpr(`0${op}A1`);
isInvalidExpr(`0${op}0`);
isInvalidExpr(`"foo"${op}"bar"`);
isInvalidExpr(`TRUE${op}FALSE`);
isInvalidExpr(`A1${op}#NAME?`);
isInvalidExpr(`A1${op}#VALUE!`);
isInvalidExpr(`#NULL!${op}A1`);
});
test('range operations with REF errors', () => {
isParsed(`A1${op}#REF!`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' }
] });
isParsed(`#REF!${op}B2`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
});
test('range operations with complex expressions', () => {
isParsed(`(A1,B2)${op}C3`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'BinaryExpression',
operator: ',',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] },
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
] });
isParsed(`C3${op}(A1,B2)`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
{ type: 'BinaryExpression',
operator: ',',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] }
] });
isParsed(`(A1 B2)${op}C3`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] },
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
] });
isParsed(`C3${op}(A1 B2)`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
{ type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] }
] });
isParsed(`(A1:(B2))${op}C3`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'BinaryExpression',
operator: ':',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] },
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
] });
isParsed(`C3${op}(A1:(B2))`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
{ type: 'BinaryExpression',
operator: ':',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] }
] });
});
test('range operations with ref functions', () => {
const refFunctions = [
[ 'ANCHORARRAY', true ],
[ 'CHOOSE', true ],
[ 'DROP', true ],
[ 'IF', true ],
[ 'IFS', true ],
[ 'INDEX', true ],
[ 'INDIRECT', true ],
[ 'OFFSET', true ],
[ 'REDUCE', true ],
[ 'SINGLE', true ],
[ 'SWITCH', true ],
[ 'TAKE', true ],
[ 'XLOOKUP', true ],
[ 'CELL', false ],
[ 'COUNT', false ],
[ 'HSTACK', false ],
[ 'N', false ],
[ 'SUM', false ]
];
refFunctions.forEach(([ funcName, shouldWork ]) => {
if (shouldWork) {
isParsed(`${funcName}()${op}C3`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] },
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
] });
isParsed(`C3${op}${funcName}()`, { type: 'BinaryExpression',
operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
{ type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] }
] });
}
else {
isInvalidExpr(`${funcName}()${op}C3`);
isInvalidExpr(`C3${op}${funcName}()`);
}
});
});
});
});
});
describe('advanced parsing features', () => {
test('union operators are normalized', () => {
isParsed('A1 B2', {
type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
isParsed('A1 B2', {
type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
});
test('does not tolerate unterminated tokens', () => {
isInvalidExpr('="foo');
});
test('position information is correct', () => {
isParsed(
'=123.45',
{ type: 'Literal', value: 123.45, loc: [ 1, 7 ], raw: '123.45' },
{ withLocation: true }
);
isParsed(
'="foo"',
{ type: 'Literal', value: 'foo', loc: [ 1, 6 ], raw: '"foo"' },
{ withLocation: true }
);
isParsed(
'=true',
{ type: 'Literal', value: true, loc: [ 1, 5 ], raw: 'true' },
{ withLocation: true }
);
isParsed(
'=Sheet1!A1:B2',
{ type: 'ReferenceIdentifier', value: 'Sheet1!A1:B2', kind: 'range', loc: [ 1, 13 ] },
{ withLocation: true }
);
isParsed(
'=(#VALUE!)',
{ type: 'ErrorLiteral', value: '#VALUE!', loc: [ 2, 9 ], raw: '#VALUE!' },
{ withLocation: true }
);
// UnaryExpression
isParsed(
'=-A1',
{ type: 'UnaryExpression',
loc: [ 1, 4 ],
operator: '-',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range', loc: [ 2, 4 ] }
] },
{ withLocation: true }
);
isParsed(
'=10%',
{ type: 'UnaryExpression',
loc: [ 1, 4 ],
operator: '%',
arguments: [
{ type: 'Literal', value: 10, loc: [ 1, 3 ], raw: '10' }
] },
{ withLocation: true }
);
isParsed(
'=-(123)',
{ type: 'UnaryExpression',
loc: [ 1, 6 ],
operator: '-',
arguments: [
{ type: 'Literal', value: 123, loc: [ 3, 6 ], raw: '123' }
] },
{ withLocation: true }
);
isParsed(
'(123+(234))',
{ type: 'BinaryExpression',
loc: [ 1, 9 ],
operator: '+',
arguments: [
{ type: 'Literal', value: 123, loc: [ 1, 4 ], raw: '123' },
{ type: 'Literal', value: 234, loc: [ 6, 9 ], raw: '234' }
] },
{ withLocation: true }
);
isParsed(
'=(A1 B2)',
{ type: 'BinaryExpression',
loc: [ 2, 7 ],
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range', loc: [ 2, 4 ] },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range', loc: [ 5, 7 ] }
] },
{ withLocation: true }
);
isParsed(
'=SUM(4,5)',
{ type: 'CallExpression',
loc: [ 1, 9 ],
callee: { type: 'Identifier', name: 'SUM', loc: [ 1, 4 ] },
arguments: [
{ type: 'Literal', value: 4, loc: [ 5, 6 ], raw: '4' },
{ type: 'Literal', value: 5, loc: [ 7, 8 ], raw: '5' }
] },
{ withLocation: true }
);
// ArrayExpression
isParsed(
'={ 1, 2; 3, 4 }',
{ type: 'ArrayExpression',
loc: [ 1, 15 ],
elements: [
[ { type: 'Literal', value: 1, loc: [ 3, 4 ], raw: '1' },
{ type: 'Literal', value: 2, loc: [ 6, 7 ], raw: '2' } ],
[ { type: 'Literal', value: 3, loc: [ 9, 10 ], raw: '3' },
{ type: 'Literal', value: 4, loc: [ 12, 13 ], raw: '4' } ]
] },
{ withLocation: true }
);
});
test('whitespace handling in various contexts', () => {
// whitespace in arrays
isParsed('=SORT({ A:A, B:B })',
{ type: 'CallExpression',
callee: { type: 'Identifier', name: 'SORT' },
arguments: [
{ type: 'ArrayExpression',
elements: [
[ { type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' },
{ type: 'ReferenceIdentifier', value: 'B:B', kind: 'beam' } ]
] }
] },
{ permitArrayRanges: true });
// whitespace in arguments
isParsed('=A2:A5=XLOOKUP(B1,C:C, D:D)',
{ type: 'BinaryExpression',
operator: '=',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A2:A5', kind: 'range' },
{ type: 'CallExpression',
callee: { type: 'Identifier', name: 'XLOOKUP' },
arguments: [
{ type: 'ReferenceIdentifier', value: 'B1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'C:C', kind: 'beam' },
{ type: 'ReferenceIdentifier', value: 'D:D', kind: 'beam' }
] }
] },
{ permitArrayRanges: true });
// whitespace surrounding comma
isParsed('=SUM(12 , B:B)',
{ type: 'CallExpression',
callee: { type: 'Identifier', name: 'SUM' },
arguments: [
{ type: 'Literal', value: 12, raw: '12' },
{ type: 'ReferenceIdentifier', value: 'B:B', kind: 'beam' }
] },
{ permitArrayCalls: true });
// whitespace tailing operator
isParsed('=A:A= C1',
{ type: 'BinaryExpression',
operator: '=',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' },
{ type: 'ReferenceIdentifier', value: 'C1', kind: 'range' }
] },
{ permitArrayCalls: true });
});
test('parser can permit xlsx mode references', () => {
isInvalidExpr('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])');
isParsed('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])',
{ type: 'CallExpression',
callee: { type: 'Identifier', name: 'SUM' },
arguments: [
{ type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: '[Workbook.xlsx]!Table1[#Data]', kind: 'table' }
] }
] },
{ xlsx: true });
});
test('parser supports LAMBDA expressions', () => {
// Invalid LAMBDA expressions
isInvalidExpr('LAMBDA(,)');
isInvalidExpr('LAMBDA(a,)');
isInvalidExpr('LAMBDA(a,,)');
isInvalidExpr('=LAMBDA(1,1)');
isInvalidExpr('=LAMBDA(a,1,a)');
isInvalidExpr('=LAMBDA(a,A,1)');
isInvalidExpr('=LAMBDA(a,a,1)');
isInvalidExpr('=LAMBDA(A1,B1,1)');
// Valid LAMBDA expressions
isParsed('=LAMBDA()', {
type: 'LambdaExpression',
params: [],
body: null
});
isParsed('=LAMBDA(1)', {
type: 'LambdaExpression',
params: [],
body: {
type: 'Literal',
value: 1,
raw: '1'
}
});
isParsed('=LAMBDA(1+1)', {
type: 'LambdaExpression',
params: [],
body: {
type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 1, raw: '1' }
]
}
});
isParsed('=LAMBDA(a,1)', {
type: 'LambdaExpression',
body: { type: 'Literal', value: 1, raw: '1' },
params: [
{ type: 'Identifier', name: 'a' }
]
});
isParsed('=LAMBDA(a,b,1)', {
type: 'LambdaExpression',
body: { type: 'Literal', value: 1, raw: '1' },
params: [
{ type: 'Identifier', name: 'a' },
{ type: 'Identifier', name: 'b' }
]
});
isParsed('=lambda(a,b,a*b)', {
type: 'LambdaExpression',
body: {
type: 'BinaryExpression',
operator: '*',
arguments: [
{ type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'b', kind: 'name' }
]
},
params: [
{ type: 'Identifier', name: 'a' },
{ type: 'Identifier', name: 'b' }
]
});
isParsed('=lambda( a , b , a b )', {
type: 'LambdaExpression',
body: {
type: 'BinaryExpression',
operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'b', kind: 'name' }
]
},
params: [
{ type: 'Identifier', name: 'a' },
{ type: 'Identifier', name: 'b' }
]
});
// r and c are forbidden as names, but should work here
isParsed('=LAMBDA(r,c,r*c)', {
type: 'LambdaExpression',
body: {
type: 'BinaryExpression',
operator: '*',
arguments: [
{ type: 'ReferenceIdentifier', value: 'r', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
]
},
params: [
{ type: 'Identifier', name: 'r' },
{ type: 'Identifier', name: 'c' }
]
});
});
test('parser allows calling refs, lambda, let, and call expressions', () => {
// Invalid function calls
isInvalidExpr('1()');
isInvalidExpr('"str"()');
isInvalidExpr('#VALUE!()');
isInvalidExpr('foo%()');
isInvalidExpr('foo ()');
// Valid callable expressions
isParsed('=lambda()()', {
type: 'CallExpression',
callee: {
type: 'LambdaExpression',
params: [],
body: null
},
arguments: []
});
isParsed('=lambda(1)(1)', {
type: 'CallExpression',
callee: {
type: 'LambdaExpression',
params: [],
body: { type: 'Literal', value: 1, raw: '1' }
},
arguments: [
{ type: 'Literal', value: 1, raw: '1' }
]
});
isParsed('=FOO()()', {
type: 'CallExpression',
callee: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: []
},
arguments: []
});
isParsed('=(A1)()', {
type: 'CallExpression',
callee: { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
arguments: []
});
isParsed('=LET(a,1,a)()', {
type: 'CallExpression',
callee: {
type: 'LetExpression',
declarations: [
{ type: 'LetDeclarator',
id: { type: 'Identifier', name: 'a' },
init: { type: 'Literal', value: 1, raw: '1' } }
],
body: { type: 'ReferenceIdentifier', value: 'a', kind: 'name' }
},
arguments: []
});
isParsed('=#REF!()', {
type: 'CallExpression',
callee: {
type: 'ErrorLiteral',
value: '#REF!',
raw: '#REF!'
},
arguments: []
});
// this is allowed because in Excel: `foo#()` is really `ANCHORARRAY(foo)()`
isParsed('foo#()', {
type: 'CallExpression',
callee: {
type: 'UnaryExpression',
operator: '#',
arguments: [
{ type: 'ReferenceIdentifier', value: 'foo', kind: 'name' }
]
},
arguments: []
});
});
test('parser supports LET expressions', () => {
// Invalid LET expressions
isInvalidExpr('LET(,)');
isInvalidExpr('LET(1,a,1)');
isInvalidExpr('LET()');
isInvalidExpr('LET(a,b)');
isInvalidExpr('LET(a,)');
isInvalidExpr('LET(a,a,1,1)');
isInvalidExpr('LET(a,a,1,a)');
isInvalidExpr('LET(a,1,b,1,c,1,a1+b,1)');
isInvalidExpr('LET(a,1,a,1,1)');
isInvalidExpr('LET(a,1,A,1,1)');
// Valid LET expressions
isParsed('=LET(a,1,)', {
type: 'LetExpression',
declarations: [
{
type: 'LetDeclarator',
id: { type: 'Identifier', name: 'a' },
init: { type: 'Literal', value: 1, raw: '1' }
}
],
body: null
});
isParsed('=LET(a,1,a)', {
type: 'LetExpression',
declarations: [
{
type: 'LetDeclarator',
id: { type: 'Identifier', name: 'a' },
init: { type: 'Literal', value: 1, raw: '1' }
}
],
body: { type: 'ReferenceIdentifier', value: 'a', kind: 'name' }
});
isParsed('=LET(a,1,b,1,c,1,a+b*c)', {
type: 'LetExpression',
declarations: [
{ type: 'LetDeclarator',
id: { type: 'Identifier', name: 'a' },
init: { type: 'Literal', value: 1, raw: '1' } },
{ type: 'LetDeclarator',
id: { type: 'Identifier', name: 'b' },
init: { type: 'Literal', value: 1, raw: '1' } },
{ type: 'LetDeclarator',
id: { type: 'Identifier', name: 'c' },
init: { type: 'Literal', value: 1, raw: '1' } }
],
body: {
type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'ReferenceIdentifier', value: 'a', kind: 'name' },
{ type: 'BinaryExpression',
operator: '*',
arguments: [
{ type: 'ReferenceIdentifier', value: 'b', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
] }
]
}
});
// r and c are forbidden as names, but should work here
isParsed('LET(r,1,c,1,r*c)', {
type: 'LetExpression',
declarations: [
{ type: 'LetDeclarator',
id: { type: 'Identifier', name: 'r' },
init: { type: 'Literal', value: 1, raw: '1' } },
{
type: 'LetDeclarator',
id: { type: 'Identifier', name: 'c' },
init: { type: 'Literal', value: 1, raw: '1' }
}
],
body: {
type: 'BinaryExpression',
operator: '*',
arguments: [
{ type: 'ReferenceIdentifier', value: 'r', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'c', kind: 'name' }
]
}
});
});
test('parser whitespace handling', () => {
isParsed('\tA1\u00a0+\nB2\r', {
type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
});
test('looseRefCalls: true relaxes ref function restrictions', () => {
isInvalidExpr('A1:TESTFN()');
isParsed('A1:TESTFN()', {
type: 'BinaryExpression',
operator: ':',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'CallExpression', callee: { type: 'Identifier', name: 'TESTFN' }, arguments: [] }
]
}, { looseRefCalls: true });
});
});
});