UNPKG

@borgar/fx

Version:

Utilities for working with Excel formulas

345 lines (292 loc) 13.1 kB
/* eslint-disable @stylistic/object-property-newline */ import { describe, test, expect } from 'vitest'; import { parseA1Ref, parseA1RefXlsx, type OptsParseA1Ref } from './parseA1Ref.ts'; type IsA1EqualOptions = OptsParseA1Ref & { xlsx?: boolean, }; function isA1Equal (expr: string, expected: any, opts?: IsA1EqualOptions) { const xlsx = !!(opts?.xlsx); if (expected) { expected = xlsx ? { workbookName: '', sheetName: '', ...expected } : { context: [], ...expected }; Object.assign(expected, expected); if (expected.range && typeof expected.range === 'object') { // mix in some defaults so we don't have to write things out in full expected.range = { top: null, left: null, bottom: null, right: null, $top: false, $left: false, $bottom: false, $right: false, ...expected.range }; } } expect(xlsx ? parseA1RefXlsx(expr, opts) : parseA1Ref(expr, opts)).toEqual(expected); } describe('parse A1 references', () => { test('basic A1 references', () => { isA1Equal('A1', { range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } }); isA1Equal('$A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $left: true } }); isA1Equal('A$1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $top: true } }); isA1Equal('A1:$B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $right: true } }); isA1Equal('A1:B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, $bottom: true } }); }); test('column and row ranges', () => { isA1Equal('A:A', { range: { left: 0, right: 0 } }); isA1Equal('C:C', { range: { left: 2, right: 2 } }); isA1Equal('C:$C', { range: { left: 2, right: 2, $right: true } }); isA1Equal('$C:C', { range: { left: 2, right: 2, $left: true } }); isA1Equal('$C:$C', { range: { left: 2, right: 2, $left: true, $right: true } }); isA1Equal('1:1', { range: { top: 0, bottom: 0 } }); isA1Equal('10:10', { range: { top: 9, bottom: 9 } }); isA1Equal('10:$10', { range: { top: 9, bottom: 9, $bottom: true } }); isA1Equal('$10:10', { range: { top: 9, bottom: 9, $top: true } }); isA1Equal('$10:$10', { range: { top: 9, bottom: 9, $top: true, $bottom: true } }); }); test('maximum ranges', () => { isA1Equal('XFD1048576', { range: { top: 1048575, left: 16383, bottom: 1048575, right: 16383 } }); }); test('sheet references', () => { isA1Equal('Sheet1!A1', { context: [ 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal('\'Sheet1\'!A1', { context: [ 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal('\'Sheet1\'\'s\'!A1', { context: [ 'Sheet1\'s' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); }); test('workbook references', () => { isA1Equal('[Workbook.xlsx]Sheet1!A1', { context: [ 'Workbook.xlsx', 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal("'[Workbook.xlsx]Sheet1'!A1", { context: [ 'Workbook.xlsx', 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal("='[Workbook.xlsx]Sheet1'!A1", { context: [ 'Workbook.xlsx', 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal('[foo bar]Sheet1!A1', { context: [ 'foo bar', 'Sheet1' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); isA1Equal('[a "b" c]d!A1', { context: [ 'a "b" c', 'd' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); }); test('long context names', () => { // unless we know the contexts available, we don't know that this is a sheet // or a filename, so we can't reject it: isA1Equal('0123456789abcdefghijklmnopqrstuvwxyz!A1', { context: [ '0123456789abcdefghijklmnopqrstuvwxyz' ], range: { top: 0, left: 0, bottom: 0, right: 0 } }); }); test('invalid references', () => { isA1Equal('[Workbook.xlsx]!A1', undefined); isA1Equal('[Workbook.xlsx]!A1:B2', undefined); isA1Equal('[Workbook.xlsx]!A:A', undefined); isA1Equal('[Workbook.xlsx]!1:1', undefined); isA1Equal('[]Sheet1!A1', undefined); }); test('named ranges', () => { isA1Equal('namedrange', { name: 'namedrange' }); isA1Equal('Workbook.xlsx!namedrange', { context: [ 'Workbook.xlsx' ], name: 'namedrange' }); isA1Equal("'Workbook.xlsx'!namedrange", { context: [ 'Workbook.xlsx' ], name: 'namedrange' }); isA1Equal('[Workbook.xlsx]!namedrange', undefined); isA1Equal('pensioneligibilitypartner1', { name: 'pensioneligibilitypartner1' }); isA1Equal('XFE1048577', { name: 'XFE1048577' }); }); test('named ranges with allowNamed: false', () => { isA1Equal('namedrange', undefined, { allowNamed: false }); isA1Equal('Workbook.xlsx!namedrange', undefined, { allowNamed: false }); isA1Equal('pensioneligibilitypartner1', undefined, { allowNamed: false }); isA1Equal('XFE1048577', undefined, { allowNamed: false }); }); }); describe('parse A1 ranges in XLSX mode', () => { const opts = { xlsx: true }; test('workbook references', () => { isA1Equal('[1]!A1', { workbookName: '1', sheetName: '', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal('[Workbook.xlsx]!A1', { workbookName: 'Workbook.xlsx', sheetName: '', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal('[1]Sheet1!A1', { workbookName: '1', sheetName: 'Sheet1', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal('[Workbook.xlsx]Sheet1!A1', { workbookName: 'Workbook.xlsx', sheetName: 'Sheet1', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); }); test('named ranges in workbooks', () => { isA1Equal('[4]!name', { workbookName: '4', sheetName: '', name: 'name' }, opts); isA1Equal('[Workbook.xlsx]!name', { workbookName: 'Workbook.xlsx', sheetName: '', name: 'name' }, opts); isA1Equal('[16]Sheet1!name', { workbookName: '16', sheetName: 'Sheet1', name: 'name' }, opts); isA1Equal('[Workbook.xlsx]Sheet1!name', { workbookName: 'Workbook.xlsx', sheetName: 'Sheet1', name: 'name' }, opts); }); test('quoted references', () => { isA1Equal("='[1]'!A1", { workbookName: '1', sheetName: '', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal("='[Workbook.xlsx]'!A1", { workbookName: 'Workbook.xlsx', sheetName: '', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal("'[1]Sheet1'!A1", { workbookName: '1', sheetName: 'Sheet1', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal("'[Workbook.xlsx]Sheet1'!A1", { workbookName: 'Workbook.xlsx', sheetName: 'Sheet1', range: { top: 0, left: 0, bottom: 0, right: 0 } }, opts); isA1Equal("'[4]'!name", { workbookName: '4', sheetName: '', name: 'name' }, opts); isA1Equal("'[Workbook.xlsx]'!name", { workbookName: 'Workbook.xlsx', sheetName: '', name: 'name' }, opts); isA1Equal("'[16]Sheet1'!name", { workbookName: '16', sheetName: 'Sheet1', name: 'name' }, opts); isA1Equal("'[Workbook.xlsx]Sheet1'!name", { workbookName: 'Workbook.xlsx', sheetName: 'Sheet1', name: 'name' }, opts); }); }); describe('A1 partial ranges', () => { const opt = { allowTernary: true }; test('partial ranges not allowed by default', () => { isA1Equal('A10:A', undefined); isA1Equal('B3:2', undefined); }); test('unbounded bottom ranges', () => { isA1Equal('A10:A', { range: { top: 9, left: 0, right: 0 } }, opt); isA1Equal('A:A10', { range: { top: 9, left: 0, right: 0 } }, opt); isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt); isA1Equal('A:A$5', { range: { top: 4, left: 0, right: 0, $top: true } }, opt); isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt); isA1Equal('A:$B5', { range: { top: 4, left: 0, right: 1, $right: true } }, opt); isA1Equal('$B:B3', { range: { top: 2, left: 1, right: 1, $left: true } }, opt); isA1Equal('$B:C5', { range: { top: 4, left: 1, right: 2, $left: true } }, opt); isA1Equal('C2:B', { range: { top: 1, left: 1, right: 2 } }, opt); isA1Equal('C:B2', { range: { top: 1, left: 1, right: 2 } }, opt); }); test('unbounded right ranges', () => { isA1Equal('D1:1', { range: { top: 0, left: 3, bottom: 0 } }, opt); isA1Equal('1:D2', { range: { top: 0, left: 3, bottom: 1 } }, opt); isA1Equal('2:$D3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt); isA1Equal('$D2:3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt); isA1Equal('1:D$1', { range: { top: 0, left: 3, bottom: 0, $bottom: true } }, opt); isA1Equal('$1:D1', { range: { top: 0, left: 3, bottom: 0, $top: true } }, opt); isA1Equal('AA$3:4', { range: { top: 2, left: 26, bottom: 3, $top: true } }, opt); isA1Equal('B3:2', { range: { top: 1, bottom: 2, left: 1 } }, opt); isA1Equal('3:B2', { range: { top: 1, bottom: 2, left: 1 } }, opt); }); }); describe('A1 trimmed ranges', () => { const locks = { $top: true, $left: true, $bottom: true, $right: true }; const opts = [ {}, { xlsx: true } ]; for (const opt of opts) { test(`trimmed ranges with ${opt.xlsx ? 'XLSX' : 'default'} options`, () => { isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } }, opt); isA1Equal('A1.:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head' } }, opt); isA1Equal('A1:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail' } }, opt); isA1Equal('A1.:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both' } }, opt); isA1Equal('$A$1:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, ...locks } }, opt); isA1Equal('$A$1.:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head', ...locks } }, opt); isA1Equal('$A$1:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail', ...locks } }, opt); isA1Equal('$A$1.:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both', ...locks } }, opt); isA1Equal('J:J', { range: { top: null, left: 9, bottom: null, right: 9 } }, opt); isA1Equal('J.:J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'head' } }, opt); isA1Equal('J:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'tail' } }, opt); isA1Equal('J.:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'both' } }, opt); isA1Equal('10:10', { range: { top: 9, left: null, bottom: 9, right: null } }, opt); isA1Equal('10.:10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'head' } }, opt); isA1Equal('10:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'tail' } }, opt); isA1Equal('10.:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'both' } }, opt); isA1Equal('J10:J', undefined, { ...opt }); isA1Equal('J10:10', undefined, { ...opt }); isA1Equal('J10.:.J', undefined, { ...opt }); isA1Equal('J10.:.10', undefined, { ...opt }); isA1Equal('J10:J', { range: { top: 9, left: 9, bottom: null, right: 9 } }, { allowTernary: true, ...opt }); isA1Equal('J10:10', { range: { top: 9, left: 9, bottom: 9, right: null } }, { allowTernary: true, ...opt }); isA1Equal('J10.:.J', { range: { top: 9, left: 9, bottom: null, right: 9, trim: 'both' } }, { allowTernary: true, ...opt }); isA1Equal('J10.:.10', { range: { top: 9, left: 9, bottom: 9, right: null, trim: 'both' } }, { allowTernary: true, ...opt }); }); } }); describe('A1 trimmed ranges vs named ranges', () => { test('named ranges cannot be trimmed', () => { isA1Equal('name1.:.name1', undefined); isA1Equal('name1.:.foo', undefined); isA1Equal('foo.:.name1', undefined); }); test('trimmed column references', () => { // prior to the intruduction of trimed ranges, the following would have // been an expression: NAME:`foo.` OP:`:`, COLUMN:`bar` isA1Equal('foo.:bar', { range: { left: 1395, right: 4460, trim: 'head' } }); // prior to the intruduction of trimed ranges, the following would have // been an expression: NAME:`foo.` OP:`:`, CELL:`B2` isA1Equal('foo.:B2', { range: { top: 1, left: 1, right: 4460, trim: 'head' } }, { allowTernary: true }); }); }); describe('Sheet name that looks like an A1 ref', () => { test('parse correctly', () => { isA1Equal("'Sch1'!B2", { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } }); isA1Equal('Sch1!B2', { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } }); }); });