UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

171 lines (164 loc) 6.85 kB
import { test, Test } from 'tape'; import { tokenize } from './lexer.js'; import { addTokenMeta } from './addTokenMeta.js'; import { fixRanges } from './fixRanges.js'; import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_STRUCT, REF_TERNARY } from './constants.js'; Test.prototype.isFixed = function (expr, expected, options = {}) { const result = fixRanges(expr, options); this.is(result, expected, expr + ' → ' + expected); }; test('fixRanges basics', t => { const fx = '=SUM([wb]Sheet1!B2:A1)'; t.throws(() => fixRanges(123), 'throws on non arrays (number)'); t.throws(() => fixRanges(null), 'throws on non arrays (null)'); const tokens = addTokenMeta(tokenize(fx, { mergeRefs: true })); tokens[3].foo = 'bar'; const fixedTokens = fixRanges(tokens, { debug: 0 }); t.ok(tokens !== fixedTokens, 'emits a new array instance'); t.ok(tokens[3] !== fixedTokens[3], 'does not mutate existing range tokens'); t.deepEqual(tokens[3], { type: REF_RANGE, value: '[wb]Sheet1!B2:A1', index: 3, depth: 1, groupId: 'fxg1', foo: 'bar' }, 'keeps meta (pre-fix range token)'); t.deepEqual(fixedTokens[3], { type: REF_RANGE, value: '[wb]Sheet1!A1:B2', index: 3, depth: 1, groupId: 'fxg1', foo: 'bar' }, 'keeps meta (post-fix range token)'); const tokensWithRanges = tokenize( '=SUM(B2:A,table[[#This Row],[Foo]])', { withLocation: true, mergeRefs: true, allowTernary: true } ); t.deepEqual(fixRanges(tokensWithRanges, { addBounds: true }), [ { type: FX_PREFIX, value: '=', loc: [ 0, 1 ] }, { type: FUNCTION, value: 'SUM', loc: [ 1, 4 ] }, { type: OPERATOR, value: '(', loc: [ 4, 5 ] }, { type: REF_TERNARY, value: 'A2:B1048576', loc: [ 5, 16 ] }, { type: OPERATOR, value: ',', loc: [ 16, 17 ] }, { type: REF_STRUCT, value: 'table[@Foo]', loc: [ 17, 28 ] }, { type: OPERATOR, value: ')', loc: [ 28, 29 ] } ], 'updates token source location information'); t.end(); }); test('fixRanges A1', t => { const opt = { allowTernary: true }; // doesn't mess with things that it doesn't have to t.isFixed('=A1', '=A1', opt); t.isFixed('=ZZ123', '=ZZ123', opt); t.isFixed('=A1:B2', '=A1:B2', opt); t.isFixed('=B3:OFFSET(A1,10,10)', '=B3:OFFSET(A1,10,10)', opt); t.isFixed('=A:B', '=A:B', opt); t.isFixed('=C:C', '=C:C', opt); t.isFixed('=3:6', '=3:6', opt); t.isFixed('=3:3', '=3:3', opt); // redundancy t.isFixed('=A1:$A$1', '=A1:$A$1', opt); t.isFixed('=A1:A1', '=A1', opt); // lowercase to uppercase t.isFixed('=a1', '=A1', opt); t.isFixed('=zz123', '=ZZ123', opt); t.isFixed('=a1:b2', '=A1:B2', opt); // flipped rects t.isFixed('=B2:A1', '=A1:B2', opt); t.isFixed('=$B$2:$A$1', '=$A$1:$B$2', opt); // flipped beams t.isFixed('=C:A', '=A:C', opt); t.isFixed('=$D:B', '=B:$D', opt); t.isFixed('=10:1', '=1:10', opt); t.isFixed('=$5:3', '=3:$5', opt); // flipped partials - bottom t.isFixed('=A:A1', '=A1:A', opt); t.isFixed('=A:A$1', '=A$1:A', opt); // flipped partials - right t.isFixed('=1:A1', '=A1:1', opt); // $1:$A1 is rather counter intuitive case: // This range is parsed as { left=null, top=$1, right=$A, bottom=1 } but, // because left is null, right and left are flipped around, making this // end up as { left=$A, top=$1, right=null, bottom=1 } which serializes // as $A$1:1 t.isFixed('=$1:$A1', '=$A$1:1', opt); t.end(); }); test('fixRanges A1 addBounds', t => { const opt = { allowTernary: true, addBounds: true }; t.isFixed('=B3:OFFSET(A1,10,10)', '=B3:OFFSET(A1,10,10)', opt); t.isFixed('=A:A', '=A:A', opt); t.isFixed('=A:A1', '=A:A', opt); t.isFixed('=A:A$1', '=A:A', opt); t.isFixed('=A:$A$1', '=A:$A', opt); t.isFixed('=A.:A', '=A.:A', opt); // partials - bottom t.isFixed('=A1:A', '=A:A', opt); t.isFixed('=A1:Z', '=A:Z', opt); t.isFixed('=A:A1', '=A:A', opt); t.isFixed('=$A1:A', '=$A:A', opt); t.isFixed('=A$1:A', '=A:A', opt); t.isFixed('=A1:$A', '=A:$A', opt); t.isFixed('=A2:A', '=A2:A1048576', opt); t.isFixed('=B2:B', '=B2:B1048576', opt); t.isFixed('=A:A2', '=A2:A1048576', opt); t.isFixed('=B:B2', '=B2:B1048576', opt); t.isFixed('=B.:.B2', '=B2.:.B1048576', opt); // flipped partials - bottom t.isFixed('=A1:1', '=1:1', opt); t.isFixed('=A1:4', '=1:4', opt); t.isFixed('=1:A1', '=1:1', opt); t.isFixed('=$A1:1', '=1:1', opt); t.isFixed('=A$1:1', '=$1:1', opt); t.isFixed('=A1:$1', '=1:$1', opt); t.isFixed('=B1:1', '=B1:XFD1', opt); t.isFixed('=1:B1', '=B1:XFD1', opt); t.isFixed('=B2:20', '=B2:XFD20', opt); t.isFixed('=2:B20', '=B2:XFD20', opt); t.isFixed('=2:.B20', '=B2:.XFD20', opt); t.end(); }); test('fixRanges structured references', t => { t.isFixed('=Table1[[#This Row],[Foo]]', '=Table1[@Foo]'); t.isFixed('=[[#This Row],[s:s]]', '=[@[s:s]]'); t.isFixed('=Table1[[#Totals],col name:Foo]', '=Table1[[#Totals],[col name]:[Foo]]'); t.isFixed('[[#data],[#headers]]', '[[#Headers],[#Data]]'); t.isFixed('[[#headers],[#data]]', '[[#Headers],[#Data]]'); t.isFixed('[[#totals],[#data]]', '[[#Data],[#Totals]]'); t.isFixed('[ [#totals], [#data] ]', '[[#Data],[#Totals]]'); t.isFixed('[[#data],[#totals]]', '[[#Data],[#Totals]]'); t.isFixed('[[#all],foo:bar]', '[[#All],[foo]:[bar]]'); t.isFixed('[[#all],[#all],[#all],[#all],[ColumnName]]', '[[#All],[ColumnName]]'); t.isFixed('[@[foo]:bar]', '[@[foo]:[bar]]'); t.isFixed('[@foo bar]', '[@[foo bar]]'); // Care must be taken with spaces in column headers. // Excel considers refs only valid if they match the column name // but the parser does not know the names, so it must preserve // leading/trailing whitespace. t.isFixed('[ @[foo bar] ]', '[@[foo bar]]'); t.isFixed('[ @[ foo bar ] ]', '[@[ foo bar ]]'); t.isFixed('[ @foo bar ]', '[@[foo bar ]]'); t.isFixed('[@ foo bar]', '[@[ foo bar]]'); t.isFixed('[ @ foo bar ]', '[@[ foo bar ]]'); t.end(); }); test('fixRanges works with xlsx mode', t => { // should not mess with invalid ranges in normal mode t.isFixed("='[Workbook]'!Table[Column]", "='[Workbook]'!Table[Column]"); t.isFixed('=[Workbook]!Table[Column]', '=[Workbook]!Table[Column]'); t.isFixed("='[Foo]'!A1", "='[Foo]'!A1"); t.isFixed('=[Foo]!A1', '=[Foo]!A1'); // should fix things in xlsx mode const opts = { xlsx: true }; t.isFixed("='[Workbook]'!Table[Column]", '=[Workbook]!Table[Column]', opts); t.isFixed('=[Workbook]!Table[Column]', '=[Workbook]!Table[Column]', opts); t.isFixed('=[Lorem Ipsum]!Table[Column]', "='[Lorem Ipsum]'!Table[Column]", opts); t.isFixed("='[Foo]'!A1", '=[Foo]!A1', opts); t.isFixed('=[Foo]Bar!A1', '=[Foo]Bar!A1', opts); t.isFixed('=[Foo Bar]Baz!A1', "='[Foo Bar]Baz'!A1", opts); t.isFixed('=[Foo]!A1', '=[Foo]!A1', opts); t.isFixed('=[Lorem Ipsum]!A1', "='[Lorem Ipsum]'!A1", opts); t.end(); });