@borgar/fx
Version:
Utilities for working with Excel formulas
1,197 lines (1,166 loc) • 39.6 kB
JavaScript
/* eslint-disable object-property-newline, object-curly-newline */
import { test, Test } from 'tape';
import { parse } from './parser.js';
Test.prototype.isParsed = function isParsed (expr, expect, opts) {
const result = parse(expr, { allowTernary: true, withLocation: false, ...opts });
const cleaned = JSON.parse(JSON.stringify(result));
this.deepEqual(cleaned, expect, `\x1b[32m${expr}\x1b[0m`);
};
Test.prototype.isInvalidExpr = function isInvalidExpr (expr, opts) {
this.throws(
() => parse(expr, { allowTernary: true, ...opts }),
`\x1b[36m${expr}\x1b[0m`
);
};
test('parse numbers', t => {
t.isParsed('1', { type: 'Literal', value: 1, raw: '1' });
t.isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
t.isParsed('2.4e+3', { type: 'Literal', value: 2400, raw: '2.4e+3' });
t.isParsed('-1e+10', { type: 'Literal', value: -10000000000, raw: '-1e+10' });
t.isParsed('1e-3', { type: 'Literal', value: 0.001, raw: '1e-3' });
t.end();
});
test('parse booleans', t => {
t.isParsed('TRUE', { type: 'Literal', value: true, raw: 'TRUE' });
t.isParsed('true', { type: 'Literal', value: true, raw: 'true' });
t.isParsed('trUe', { type: 'Literal', value: true, raw: 'trUe' });
t.isParsed('TRue', { type: 'Literal', value: true, raw: 'TRue' });
t.isParsed('FALSE', { type: 'Literal', value: false, raw: 'FALSE' });
t.isParsed('false', { type: 'Literal', value: false, raw: 'false' });
t.isParsed('False', { type: 'Literal', value: false, raw: 'False' });
t.isParsed('fAlSe', { type: 'Literal', value: false, raw: 'fAlSe' });
t.end();
});
test('parse strings', t => {
t.isParsed('""', { type: 'Literal', value: '', raw: '""' });
t.isParsed('""""', { type: 'Literal', value: '"', raw: '""""' });
t.isParsed('" "', { type: 'Literal', value: ' ', raw: '" "' });
t.isParsed('"foobar"', { type: 'Literal', value: 'foobar', raw: '"foobar"' });
t.isParsed('"foo bar"', { type: 'Literal', value: 'foo bar', raw: '"foo bar"' });
t.isParsed('"foo""bar"', { type: 'Literal', value: 'foo"bar', raw: '"foo""bar"' });
t.end();
});
test('parse errors', t => {
t.isParsed('#CALC!', { type: 'ErrorLiteral', value: '#CALC!', raw: '#CALC!' });
t.isParsed('#DIV/0!', { type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' });
t.isParsed('#FIELD!', { type: 'ErrorLiteral', value: '#FIELD!', raw: '#FIELD!' });
t.isParsed('#GETTING_DATA', { type: 'ErrorLiteral', value: '#GETTING_DATA', raw: '#GETTING_DATA' });
t.isParsed('#N/A', { type: 'ErrorLiteral', value: '#N/A', raw: '#N/A' });
t.isParsed('#NAME?', { type: 'ErrorLiteral', value: '#NAME?', raw: '#NAME?' });
t.isParsed('#NULL!', { type: 'ErrorLiteral', value: '#NULL!', raw: '#NULL!' });
t.isParsed('#NUM!', { type: 'ErrorLiteral', value: '#NUM!', raw: '#NUM!' });
t.isParsed('#REF!', { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' });
t.isParsed('#SPILL!', { type: 'ErrorLiteral', value: '#SPILL!', raw: '#SPILL!' });
t.isParsed('#SYNTAX?', { type: 'ErrorLiteral', value: '#SYNTAX?', raw: '#SYNTAX?' });
t.isParsed('#UNKNOWN!', { type: 'ErrorLiteral', value: '#UNKNOWN!', raw: '#UNKNOWN!' });
t.isParsed('#VALUE!', { type: 'ErrorLiteral', value: '#VALUE!', raw: '#VALUE!' });
t.end();
});
test('parse ranges', t => {
t.isParsed('A1', { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' });
t.isParsed('A1:B2', { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' });
t.isParsed('A:B', { type: 'ReferenceIdentifier', value: 'A:B', kind: 'beam' });
t.isParsed('1:2', { type: 'ReferenceIdentifier', value: '1:2', kind: 'beam' });
t.isParsed('A1:2', { type: 'ReferenceIdentifier', value: 'A1:2', kind: 'range' });
t.isParsed('1:A2', { type: 'ReferenceIdentifier', value: '1:A2', kind: 'range' });
t.isParsed('A1.:.B2', { type: 'ReferenceIdentifier', value: 'A1.:.B2', kind: 'range' });
t.isParsed('Sheet!A1', { type: 'ReferenceIdentifier', value: 'Sheet!A1', kind: 'range' });
t.isParsed('[Workbook]Sheet!A1', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!A1', kind: 'range' });
t.isParsed('\'Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'Sheet\'!A1', kind: 'range' });
t.isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
t.isParsed('foo', { type: 'ReferenceIdentifier', value: 'foo', kind: 'name' });
t.isParsed('Workbook!foo', { type: 'ReferenceIdentifier', value: 'Workbook!foo', kind: 'name' });
t.isParsed('[Workbook]Sheet!foo', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!foo', kind: 'name' });
t.isParsed('\'Workbook\'!A1', { type: 'ReferenceIdentifier', value: '\'Workbook\'!A1', kind: 'range' });
t.isParsed('\'[Workbook]Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'[Workbook]Sheet\'!A1', kind: 'range' });
t.end();
});
test('parse array literals', t => {
t.isParsed('{1}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' }
] ]
});
t.isParsed('{-1}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: -1, raw: '-1' }
] ]
});
t.isParsed('{#DIV/0!}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'ErrorLiteral', value: '#DIV/0!', raw: '#DIV/0!' }
] ]
});
t.isParsed('{TRUE}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: true, raw: 'TRUE' }
] ]
});
t.isParsed('{"foo"}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 'foo', raw: '"foo"' }
] ]
});
t.isParsed('{1,2}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
] ]
});
t.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' }
] ]
});
t.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' }
] ]
});
t.isParsed('{1;2}', {
type: 'ArrayExpression',
elements: [ [
{ type: 'Literal', value: 1, raw: '1' }
], [
{ type: 'Literal', value: 2, raw: '2' }
] ]
});
t.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' }
] ]
});
t.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' }
] ]
});
t.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 });
t.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' }
] ]
});
t.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' }
] ]
});
// TODO: consider supporting this?:
// t.isParsed('{-0.1}', {
// type: 'ArrayExpression',
// elements: [ [
// { type: 'Literal', value: -0.1, raw: '-0.1' }
// ] ]
// }, { negativeNumbers: false });
t.isInvalidExpr('{A1}', { permitArrayRanges: false });
t.isInvalidExpr('{--1}', { negativeNumbers: true });
t.isInvalidExpr('{--1}', { negativeNumbers: false });
t.isInvalidExpr('{---1}', { negativeNumbers: true });
t.isInvalidExpr('{---1}', { negativeNumbers: false });
t.isInvalidExpr('{+1}'); // Excel silently corrects this 🤔
t.isInvalidExpr('{(1)}');
t.isInvalidExpr('{SUM(1)}');
t.isInvalidExpr('{{}}');
t.isInvalidExpr('{{}');
t.isInvalidExpr('{}}');
t.isInvalidExpr('{2+2}');
t.isInvalidExpr('{}');
t.isInvalidExpr('{,}');
t.isInvalidExpr('{1,}');
t.isInvalidExpr('{,1}');
t.isInvalidExpr('{;}');
// permitArrayCalls
t.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 });
// permitArrayCalls can be nested
t.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 });
t.end();
});
test('parse function calls', t => {
t.isParsed('=foo()', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'foo' },
arguments: []
});
t.isParsed('=FOO()', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: []
});
t.isParsed('=FOO(1)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
t.isParsed('=FOO(1,2)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
]
});
const args = Array(300).fill('1');
t.isParsed(`=FOO(${args.join(',')})`, {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ ...args.map(() => ({ type: 'Literal', value: 1, raw: '1' })) ]
});
t.isParsed('=FOO(A1,B2)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
t.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' }
] }
]
});
t.isParsed('=FOO(BAR())', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [
{ type: 'CallExpression', callee: { type: 'Identifier', name: 'BAR' },
arguments: [] }
]
});
t.isParsed('=FOO(,)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, null ]
});
t.isParsed('=FOO(,,)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, null, null ]
});
t.isParsed('=FOO(1,)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ { type: 'Literal', value: 1, raw: '1' }, null ]
});
t.isParsed('=FOO(,1)', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FOO' },
arguments: [ null, { type: 'Literal', value: 1, raw: '1' } ]
});
t.isInvalidExpr('=FOO((1,2))');
t.isInvalidExpr('=FOO(');
t.isInvalidExpr('=FOO ()');
t.isParsed('=FALSE()', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'FALSE' },
arguments: []
});
t.isParsed('=TRUE()', {
type: 'CallExpression', callee: { type: 'Identifier', name: 'TRUE' },
arguments: []
});
t.end();
});
test('parse unary operator %', t => {
t.isParsed('A1%', {
type: 'UnaryExpression', operator: '%',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' } ]
});
t.isParsed('1%', {
type: 'UnaryExpression', operator: '%',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
t.isParsed('(1)%', {
type: 'UnaryExpression', operator: '%',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
t.isInvalidExpr('%');
t.end();
});
test('parse unary operator -', t => {
t.isParsed('-1', { type: 'Literal', value: -1, raw: '-1' });
t.isParsed('-1', {
type: 'UnaryExpression', operator: '-',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
}, { negativeNumbers: false });
t.isParsed('-"1"', {
type: 'UnaryExpression', operator: '-',
arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
});
t.isParsed('-A1:B2', {
type: 'UnaryExpression', operator: '-',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
t.isParsed('--1', {
type: 'UnaryExpression', operator: '-',
arguments: [ { type: 'Literal', value: -1, raw: '-1' } ]
});
t.isParsed('--1', {
type: 'UnaryExpression', operator: '-',
arguments: [ {
type: 'UnaryExpression', operator: '-',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
} ]
}, { negativeNumbers: false });
t.isInvalidExpr('--');
t.isInvalidExpr('-');
t.end();
});
test('parse unary operator +', t => {
t.isParsed('+1', {
type: 'UnaryExpression', operator: '+',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
t.isParsed('+(1)', {
type: 'UnaryExpression', operator: '+',
arguments: [ { type: 'Literal', value: 1, raw: '1' } ]
});
t.isParsed('+"1"', {
type: 'UnaryExpression', operator: '+',
arguments: [ { type: 'Literal', value: '1', raw: '"1"' } ]
});
t.isParsed('+A1:B2', {
type: 'UnaryExpression', operator: '+',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
t.isInvalidExpr('++');
t.isInvalidExpr('+');
t.end();
});
test('parse unary operator #', t => {
t.isParsed('D9#', {
type: 'UnaryExpression', operator: '#',
arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
});
t.isParsed('A1:B2#', { // this parses but is a runtime error in Excel
type: 'UnaryExpression', operator: '#',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
t.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' }
] } ]
});
t.isParsed('(A1,B2)#', {
type: 'UnaryExpression', operator: '#',
arguments: [ {
type: 'BinaryExpression', operator: ',',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] } ]
});
t.isParsed('(A1 B2)#', {
type: 'UnaryExpression', operator: '#',
arguments: [ {
type: 'BinaryExpression', operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] } ]
});
t.isParsed('#REF!#', {
type: 'UnaryExpression', operator: '#',
arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
});
t.isParsed('INDIRECT("d9")#', {
type: 'UnaryExpression', operator: '#',
arguments: [ {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'INDIRECT' },
arguments: [ { type: 'Literal', value: 'd9', raw: '"d9"' } ]
} ]
});
t.isInvalidExpr('1#');
t.isInvalidExpr('"foo"#');
t.isInvalidExpr('#A1');
t.isInvalidExpr('##');
t.isInvalidExpr('#VALUE!#');
t.isInvalidExpr('#');
t.isInvalidExpr('#A1');
t.end();
});
test('parse unary operator @', t => {
t.isParsed('@1', {
type: 'UnaryExpression', operator: '@',
arguments: [ { type: 'Literal', raw: '1', value: 1 } ]
});
t.isParsed('@"foo"', {
type: 'UnaryExpression', operator: '@',
arguments: [ { type: 'Literal', raw: '"foo"', value: 'foo' } ]
});
t.isParsed('@D9', {
type: 'UnaryExpression', operator: '@',
arguments: [ { type: 'ReferenceIdentifier', value: 'D9', kind: 'range' } ]
});
t.isParsed('@A1:B2', {
type: 'UnaryExpression', operator: '@',
arguments: [ { type: 'ReferenceIdentifier', value: 'A1:B2', kind: 'range' } ]
});
t.isParsed('@#REF!', {
type: 'UnaryExpression', operator: '@',
arguments: [ { type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' } ]
});
t.isParsed('@FOO()', {
type: 'UnaryExpression', operator: '@',
arguments: [ {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: []
} ]
});
t.isInvalidExpr('@');
t.isInvalidExpr('@@');
t.end();
});
// parse binary operators
// FIXME: add precedence & associativity tests (2+3*4)
[
'+',
'-',
'^',
'*',
'/',
'&',
'=',
'<',
'>',
'<=',
'>=',
'<>'
].forEach(op => {
test('parse binary operator ' + op, t => {
t.isParsed(`1${op}2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 2, raw: '2' }
]
});
t.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' }
]
});
t.isParsed(`"foo"${op}"bar"`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'Literal', value: 'foo', raw: '"foo"' },
{ type: 'Literal', value: 'bar', raw: '"bar"' }
]
});
t.isParsed(`"foo"${op}"bar"`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'Literal', value: 'foo', raw: '"foo"' },
{ type: 'Literal', value: 'bar', raw: '"bar"' }
]
});
t.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' }
] ] }
]
});
t.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: [] }
]
});
t.isInvalidExpr(op);
t.isInvalidExpr(op + op);
t.isInvalidExpr('1' + op);
if (op !== '+' && op !== '-') {
t.isInvalidExpr('=' + op + '1');
}
t.end();
});
});
// parse range operators
[
[ ':', 'range-join' ],
[ ',', 'union' ],
[ ' ', 'intersection' ]
].forEach(([ op, opName ]) => {
test(`parse ${opName} operator "${op}"`, t => {
t.isParsed(`named1${op}named2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
] });
t.isParsed(`A1${op}named2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'named2', kind: 'name' }
] });
t.isParsed(`named1${op}B2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'named1', kind: 'name' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
t.isParsed(`(A1)${op}(B2)`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
t.isInvalidExpr(`A1${op}0`);
t.isInvalidExpr(`0${op}A1`);
t.isInvalidExpr(`0${op}0`);
t.isInvalidExpr(`"foo"${op}"bar"`);
t.isInvalidExpr(`TRUE${op}FALSE`);
// REF! errors are a valid ref expression component
t.isParsed(`A1${op}#REF!`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' }
] });
t.isParsed(`#REF!${op}B2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ErrorLiteral', value: '#REF!', raw: '#REF!' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
t.isInvalidExpr(`A1${op}#NAME?`);
t.isInvalidExpr(`A1${op}#VALUE!`);
t.isInvalidExpr(`#NULL!${op}A1`);
// union ops
t.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' }
] });
t.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' }
] }
] });
// intersection ops
t.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' }
] });
t.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' }
] }
] });
// join ops
t.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' }
] });
t.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' }
] }
] });
t.isParsed(`A1 ${op} B2`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
] });
// ref calls
([
[ 'ANCHORARRAY', true ],
[ 'CHOOSE', true ],
[ 'DROP', true ],
[ 'IF', true ],
[ 'IFS', true ],
[ 'INDEX', true ],
[ 'INDIRECT', true ],
// [ 'LAMBDA', true ],
// [ 'LET', true ],
[ 'OFFSET', true ],
[ 'REDUCE', true ],
[ 'SINGLE', true ],
[ 'SWITCH', true ],
[ 'TAKE', true ],
[ 'XLOOKUP', true ],
// non-ref functions
[ 'CELL', false ],
[ 'COUNT', false ],
[ 'HSTACK', false ],
[ 'N', false ],
[ 'SUM', false ]
]).forEach(([ funcName, shouldWork ]) => {
if (shouldWork) {
t.isParsed(`${funcName}()${op}C3`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] },
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' }
] });
t.isParsed(`C3${op}${funcName}()`, {
type: 'BinaryExpression', operator: op,
arguments: [
{ type: 'ReferenceIdentifier', value: 'C3', kind: 'range' },
{ type: 'CallExpression', callee: { type: 'Identifier', name: funcName }, arguments: [] }
] });
}
else {
t.isInvalidExpr(`${funcName}()${op}C3`);
t.isInvalidExpr(`C3${op}${funcName}()`);
}
});
t.end();
});
});
test('union operators are normalized', t => {
t.isParsed('A1 B2', {
type: 'BinaryExpression', operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
t.isParsed('A1 B2', {
type: 'BinaryExpression', operator: ' ',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
t.end();
});
test('does not tolerate unterminated tokens', t => {
t.isInvalidExpr('="foo');
t.end();
});
test('position information is correct', t => {
t.isParsed(
'=123.45',
{ type: 'Literal', value: 123.45, loc: [ 1, 7 ], raw: '123.45' },
{ withLocation: true }
);
t.isParsed(
'="foo"',
{ type: 'Literal', value: 'foo', loc: [ 1, 6 ], raw: '"foo"' },
{ withLocation: true }
);
t.isParsed(
'=true',
{ type: 'Literal', value: true, loc: [ 1, 5 ], raw: 'true' },
{ withLocation: true }
);
t.isParsed(
'=Sheet1!A1:B2',
{ type: 'ReferenceIdentifier', value: 'Sheet1!A1:B2', kind: 'range', loc: [ 1, 13 ] },
{ withLocation: true }
);
t.isParsed(
'=(#VALUE!)',
{ type: 'ErrorLiteral', value: '#VALUE!', loc: [ 2, 9 ], raw: '#VALUE!' },
{ withLocation: true }
);
// UnaryExpression
t.isParsed(
'=-A1',
{ type: 'UnaryExpression', loc: [ 1, 4 ], operator: '-', arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range', loc: [ 2, 4 ] }
] },
{ withLocation: true }
);
t.isParsed(
'=10%',
{ type: 'UnaryExpression', loc: [ 1, 4 ], operator: '%', arguments: [
{ type: 'Literal', value: 10, loc: [ 1, 3 ], raw: '10' }
] },
{ withLocation: true }
);
t.isParsed(
'=-(123)',
{ type: 'UnaryExpression', loc: [ 1, 6 ], operator: '-', arguments: [
{ type: 'Literal', value: 123, loc: [ 3, 6 ], raw: '123' }
] },
{ withLocation: true }
);
t.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 }
);
t.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 }
);
t.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
t.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 }
);
t.end();
});
test('does not tolerate unterminated tokens', t => {
// whitespace in arrays
t.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
t.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
t.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
t.isParsed('=A:A= C1',
{ type: 'BinaryExpression', operator: '=', arguments: [
{ type: 'ReferenceIdentifier', value: 'A:A', kind: 'beam' },
{ type: 'ReferenceIdentifier', value: 'C1', kind: 'range' }
] },
{ permitArrayCalls: true });
t.end();
});
test('parser can permit xlsx mode references', t => {
t.isInvalidExpr('=SUM([Workbook.xlsx]!A1+[Workbook.xlsx]!Table1[#Data])');
t.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 });
t.end();
});
test('parser supports LAMBDA expressions', t => {
t.isInvalidExpr('LAMBDA(,)');
t.isInvalidExpr('LAMBDA(a,)');
t.isInvalidExpr('LAMBDA(a,,)');
t.isInvalidExpr('=LAMBDA(1,1)');
t.isInvalidExpr('=LAMBDA(a,1,a)');
t.isInvalidExpr('=LAMBDA(a,A,1)');
t.isInvalidExpr('=LAMBDA(a,a,1)');
t.isInvalidExpr('=LAMBDA(A1,B1,1)');
t.isParsed('=LAMBDA()', {
type: 'LambdaExpression',
params: [],
body: null
});
t.isParsed('=LAMBDA(1)', {
type: 'LambdaExpression',
params: [],
body: {
type: 'Literal',
value: 1,
raw: '1'
}
});
t.isParsed('=LAMBDA(1+1)', {
type: 'LambdaExpression',
params: [],
body: {
type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'Literal', value: 1, raw: '1' },
{ type: 'Literal', value: 1, raw: '1' }
]
}
});
t.isParsed('=LAMBDA(a,1)', {
type: 'LambdaExpression',
body: { type: 'Literal', value: 1, raw: '1' },
params: [
{ type: 'Identifier', name: 'a' }
]
});
t.isParsed('=LAMBDA(a,b,1)', {
type: 'LambdaExpression',
body: { type: 'Literal', value: 1, raw: '1' },
params: [
{ type: 'Identifier', name: 'a' },
{ type: 'Identifier', name: 'b' }
]
});
t.isParsed('=LAMBDA(a,b,1)', {
type: 'LambdaExpression',
body: { type: 'Literal', value: 1, raw: '1' },
params: [
{ type: 'Identifier', name: 'a' },
{ type: 'Identifier', name: 'b' }
]
});
t.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' }
]
});
t.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' }
]
});
t.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
t.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' }
]
});
t.end();
});
test('parser allows calling refs, lambda, let, and call expressions', t => {
t.isInvalidExpr('1()');
t.isInvalidExpr('"str"()');
t.isInvalidExpr('#VALUE!()');
t.isInvalidExpr('foo%()');
t.isInvalidExpr('foo ()');
t.isParsed('=lambda()()', {
type: 'CallExpression',
callee: {
type: 'LambdaExpression',
params: [],
body: null
},
arguments: []
});
t.isParsed('=lambda(1)(1)', {
type: 'CallExpression',
callee: {
type: 'LambdaExpression',
params: [],
body: { type: 'Literal', value: 1, raw: '1' }
},
arguments: [
{ type: 'Literal', value: 1, raw: '1' }
]
});
t.isParsed('=FOO()()', {
type: 'CallExpression',
callee: {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'FOO' },
arguments: []
},
arguments: []
});
t.isParsed('=(A1)()', {
type: 'CallExpression',
callee: { type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
arguments: []
});
t.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: []
});
t.isParsed('=#REF!()', {
type: 'CallExpression',
callee: {
type: 'ErrorLiteral',
value: '#REF!',
raw: '#REF!'
},
arguments: []
});
// this is allowed because in Excel: `foo#()` is really `ANCHORARRAY(foo)()`
t.isParsed('foo#()', {
type: 'CallExpression',
callee: {
type: 'UnaryExpression', operator: '#', arguments: [
{ type: 'ReferenceIdentifier', value: 'foo', kind: 'name' }
]
},
arguments: []
});
// `@1()` works in Excel because it is really `SINGLE(foo)()`
t.end();
});
test('parser supports LET expressions', t => {
// Argument is not a name
t.isInvalidExpr('LET(,)');
t.isInvalidExpr('LET(1,a,1)');
// Unexpected end of arguments
t.isInvalidExpr('LET()');
t.isInvalidExpr('LET(a,b)');
t.isInvalidExpr('LET(a,)');
// Unexpected argument following calculation
t.isInvalidExpr('LET(a,a,1,1)');
t.isInvalidExpr('LET(a,a,1,a)');
t.isInvalidExpr('LET(a,1,b,1,c,1,a1+b,1)');
// Duplicate name: a
t.isInvalidExpr('LET(a,1,a,1,1)');
t.isInvalidExpr('LET(a,1,A,1,1)');
t.isParsed('=LET(a,1,)', {
type: 'LetExpression',
declarations: [
{
type: 'LetDeclarator',
id: { type: 'Identifier', name: 'a' },
init: { type: 'Literal', value: 1, raw: '1' }
}
],
body: null
});
t.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' }
});
t.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
t.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' }
]
}
});
t.end();
});
test('parser whitespace handling', t => {
t.isParsed('\tA1\u00a0+\nB2\r', {
type: 'BinaryExpression',
operator: '+',
arguments: [
{ type: 'ReferenceIdentifier', value: 'A1', kind: 'range' },
{ type: 'ReferenceIdentifier', value: 'B2', kind: 'range' }
]
});
t.end();
});