@borgar/fx 3.0.0 → 4.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/fixRanges.js CHANGED
@@ -1,12 +1,45 @@
1
1
  import { isRange } from './isType.js';
2
- import { parseA1Ref, stringifyA1Ref, addRangeBounds } from './a1.js';
2
+ import { parseA1Ref, stringifyA1Ref, addA1RangeBounds } from './a1.js';
3
+ import { parseStructRef, stringifyStructRef } from './sr.js';
3
4
  import { tokenize } from './lexer.js';
5
+ import { REF_STRUCT } from './constants.js';
4
6
 
5
7
  // There is no R1C1 counerpart to this. This is because without an anchor cell
6
8
  // it is impossible to determine if a relative+absolute range (R[1]C[1]:R5C5)
7
9
  // needs to be flipped or not. The solution is to convert to A1 first:
8
10
  // translateToRC(fixRanges(translateToA1(...)))
9
11
 
12
+ /**
13
+ * Normalizes A1 style ranges in a formula or list of tokens so that the top and
14
+ * left coordinates of the range are on the left-hand side of a colon operator:
15
+ *
16
+ * `B2:A1` → `A1:B2`
17
+ * `1:A1` → `A1:1`
18
+ * `A:A1` → `A1:A`
19
+ * `B:A` → `A:B`
20
+ * `2:1` → `1:2`
21
+ * `A1:A1` → `A1`
22
+ *
23
+ * When `{ addBounds: true }` is passed as an option, the missing bounds are
24
+ * also added. This can be done to ensure Excel compatible ranges. The fixes
25
+ * then additionally include:
26
+ *
27
+ * `1:A1` → `A1:1` → `1:1`
28
+ * `A:A1` → `A1:A` → `A:A`
29
+ * `A1:A` → `A:A`
30
+ * `A1:1` → `A:1`
31
+ * `B2:B` → `B2:1048576`
32
+ * `B2:2` → `B2:XFD2`
33
+ *
34
+ * Returns the same formula with the ranges updated. If an array of tokens was
35
+ * supplied, then a new array is returned.
36
+ *
37
+ * @param {(string | Array<Object>)} formula A string (an Excel formula) or a token list that should be adjusted.
38
+ * @param {Object} [options={}] Options
39
+ * @param {boolean} [options.addBounds=false] Fill in any undefined bounds of range objects. Top to 0, bottom to 1048575, left to 0, and right to 16383.
40
+ * @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
41
+ * @return {(string | Array<Object>)} A formula string or token list (depending on which was input)
42
+ */
10
43
  export function fixRanges (tokens, options = { addBounds: false }) {
11
44
  if (typeof tokens === 'string') {
12
45
  return fixRanges(tokenize(tokens, options), options)
@@ -20,20 +53,41 @@ export function fixRanges (tokens, options = { addBounds: false }) {
20
53
  if (r1c1) {
21
54
  throw new Error('fixRanges does not have an R1C1 mode');
22
55
  }
23
- return tokens.map(token => {
24
- if (isRange(token)) {
25
- const ref = parseA1Ref(token.value, options);
56
+ let offsetSkew = 0;
57
+ return tokens.map(t => {
58
+ const token = { ...t };
59
+ if (t.loc) {
60
+ token.loc = [ ...t.loc ];
61
+ }
62
+ let offsetDelta = 0;
63
+ if (token.type === REF_STRUCT) {
64
+ const newValue = stringifyStructRef(parseStructRef(token.value));
65
+ offsetDelta = newValue.length - token.value.length;
66
+ token.value = newValue;
67
+ }
68
+ else if (isRange(token)) {
69
+ const ref = parseA1Ref(token.value, { allowTernary: true });
26
70
  const range = ref.range;
27
71
  // fill missing dimensions?
28
72
  if (addBounds) {
29
- addRangeBounds(range);
73
+ addA1RangeBounds(range);
30
74
  }
31
- const ret = { ...token };
32
- ret.value = stringifyA1Ref(ref);
33
- if (ret.range) {
34
- ret.range = range;
75
+ const newValue = stringifyA1Ref(ref);
76
+ offsetDelta = newValue.length - token.value.length;
77
+ token.value = newValue;
78
+ }
79
+ // ensure that positioning is still correct
80
+ if (offsetSkew || offsetDelta) {
81
+ if (token.loc) {
82
+ token.loc[0] += offsetSkew;
35
83
  }
36
- return ret;
84
+ offsetSkew += offsetDelta;
85
+ if (token.loc) {
86
+ token.loc[1] += offsetSkew;
87
+ }
88
+ }
89
+ else {
90
+ offsetSkew += offsetDelta;
37
91
  }
38
92
  return token;
39
93
  });
@@ -1,8 +1,8 @@
1
1
  import { test, Test } from 'tape';
2
2
  import { tokenize } from './lexer.js';
3
- import { addMeta } from './addMeta.js';
3
+ import { addTokenMeta } from './addTokenMeta.js';
4
4
  import { fixRanges } from './fixRanges.js';
5
- import { RANGE } from './constants.js';
5
+ import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_STRUCT, REF_TERNARY } from './constants.js';
6
6
 
7
7
  Test.prototype.isFixed = function (expr, expected, options = {}) {
8
8
  const result = fixRanges(expr, options);
@@ -13,13 +13,13 @@ test('fixRanges basics', t => {
13
13
  const fx = '=SUM([wb]Sheet1!B2:A1)';
14
14
  t.throws(() => fixRanges(123), 'throws on non arrays (number)');
15
15
  t.throws(() => fixRanges(null), 'throws on non arrays (null)');
16
- const tokens = addMeta(tokenize(fx, { mergeRanges: true }));
16
+ const tokens = addTokenMeta(tokenize(fx, { mergeRefs: true }));
17
17
  tokens[3].foo = 'bar';
18
18
  const fixedTokens = fixRanges(tokens, { debug: 0 });
19
19
  t.ok(tokens !== fixedTokens, 'emits a new array instance');
20
20
  t.ok(tokens[3] !== fixedTokens[3], 'does not mutate existing range tokens');
21
21
  t.deepEqual(tokens[3], {
22
- type: RANGE,
22
+ type: REF_RANGE,
23
23
  value: '[wb]Sheet1!B2:A1',
24
24
  index: 3,
25
25
  depth: 1,
@@ -27,14 +27,26 @@ test('fixRanges basics', t => {
27
27
  foo: 'bar'
28
28
  }, 'keeps meta (pre-fix range token)');
29
29
  t.deepEqual(fixedTokens[3], {
30
- type: RANGE,
30
+ type: REF_RANGE,
31
31
  value: '[wb]Sheet1!A1:B2',
32
32
  index: 3,
33
33
  depth: 1,
34
34
  groupId: 'fxg1',
35
35
  foo: 'bar'
36
36
  }, 'keeps meta (post-fix range token)');
37
- // fixes all range meta
37
+ const tokensWithRanges = tokenize(
38
+ '=SUM(B2:A,table[[#This Row],[Foo]])',
39
+ { withLocation: true, mergeRefs: true, allowTernary: true }
40
+ );
41
+ t.deepEqual(fixRanges(tokensWithRanges, { addBounds: true }), [
42
+ { type: FX_PREFIX, value: '=', loc: [ 0, 1 ] },
43
+ { type: FUNCTION, value: 'SUM', loc: [ 1, 4 ] },
44
+ { type: OPERATOR, value: '(', loc: [ 4, 5 ] },
45
+ { type: REF_TERNARY, value: 'A2:B1048576', loc: [ 5, 16 ] },
46
+ { type: OPERATOR, value: ',', loc: [ 16, 17 ] },
47
+ { type: REF_STRUCT, value: 'table[@Foo]', loc: [ 17, 28 ] },
48
+ { type: OPERATOR, value: ')', loc: [ 28, 29 ] }
49
+ ], 'updates token source location information');
38
50
  t.end();
39
51
  });
40
52
 
@@ -109,3 +121,20 @@ test('fixRanges A1 addBounds', t => {
109
121
  t.isFixed('=2:B20', '=B2:XFD20', opt);
110
122
  t.end();
111
123
  });
124
+
125
+ test('fixRanges structured references', t => {
126
+ t.isFixed('=Table1[[#This Row],[Foo]]', '=Table1[@Foo]');
127
+ t.isFixed('=[[#This Row],[s:s]]', '=[@[s:s]]');
128
+ t.isFixed('=Table1[[#Totals],col name:Foo]', '=Table1[[#Totals],[col name]:[Foo]]');
129
+ t.isFixed('[[#data],[#headers]]', '[[#Headers],[#Data]]');
130
+ t.isFixed('[[#headers],[#data]]', '[[#Headers],[#Data]]');
131
+ t.isFixed('[[#totals],[#data]]', '[[#Data],[#Totals]]');
132
+ t.isFixed('[ [#totals], [#data] ]', '[[#Data],[#Totals]]');
133
+ t.isFixed('[[#data],[#totals]]', '[[#Data],[#Totals]]');
134
+ t.isFixed('[[#all],foo:bar]', '[[#All],[foo]:[bar]]');
135
+ t.isFixed('[[#all],[#all],[#all],[#all],[ColumnName]]', '[[#All],[ColumnName]]');
136
+ t.isFixed('[@[foo]:bar]', '[@[foo]:[bar]]');
137
+ t.isFixed('[@foo bar]', '[@[foo bar]]');
138
+ t.isFixed('[ @foo bar ]', '[@[foo bar]]');
139
+ t.end();
140
+ });
package/lib/index.js CHANGED
@@ -1,14 +1,32 @@
1
1
  export { tokenize } from './lexer.js';
2
- export { addMeta } from './addMeta.js';
3
- export { translateToRC, translateToA1 } from './translate.js';
4
- export { default as a1 } from './a1.js';
5
- export { default as rc } from './rc.js';
2
+ export { parse } from './parser.js';
3
+ export { addTokenMeta } from './addTokenMeta.js';
4
+ export { translateToR1C1, translateToA1 } from './translate.js';
6
5
  export { MAX_COLS, MAX_ROWS } from './constants.js';
7
6
  export { isReference, isRange } from './isType.js';
8
- export { mergeRefTokens as mergeRanges } from './mergeRefTokens.js';
7
+ export { mergeRefTokens } from './mergeRefTokens.js';
9
8
  export { fixRanges } from './fixRanges.js';
10
9
 
10
+ export {
11
+ fromCol,
12
+ toCol,
13
+ parseA1Ref,
14
+ stringifyA1Ref,
15
+ addA1RangeBounds
16
+ } from './a1.js';
17
+
18
+ export {
19
+ parseR1C1Ref,
20
+ stringifyR1C1Ref
21
+ } from './rc.js';
22
+
23
+ export {
24
+ parseStructRef,
25
+ stringifyStructRef
26
+ } from './sr.js';
27
+
11
28
  import {
29
+ // token types
12
30
  OPERATOR,
13
31
  BOOLEAN,
14
32
  ERROR,
@@ -19,15 +37,49 @@ import {
19
37
  STRING,
20
38
  CONTEXT,
21
39
  CONTEXT_QUOTE,
22
- RANGE,
23
- RANGE_BEAM,
24
- RANGE_TERNARY,
25
- RANGE_NAMED,
40
+ REF_RANGE,
41
+ REF_BEAM,
42
+ REF_TERNARY,
43
+ REF_NAMED,
44
+ REF_STRUCT,
26
45
  FX_PREFIX,
27
- UNKNOWN
46
+ UNKNOWN,
47
+ // AST types
48
+ UNARY,
49
+ BINARY,
50
+ REFERENCE,
51
+ LITERAL,
52
+ ERROR_LITERAL,
53
+ CALL,
54
+ ARRAY,
55
+ IDENTIFIER
28
56
  } from './constants.js';
29
57
 
30
- export const tokenTypes = {
58
+ /**
59
+ * A dictionary of the types used to identify token variants.
60
+ *
61
+ * @readonly
62
+ * @constant {Object<string>} tokenTypes
63
+ * @property {string} OPERATOR - Newline (`\n`)
64
+ * @property {string} BOOLEAN - Boolean literal (`TRUE`)
65
+ * @property {string} ERROR - Error literal (`#VALUE!`)
66
+ * @property {string} NUMBER - Number literal (`123.4`, `-1.5e+2`)
67
+ * @property {string} FUNCTION - Function name (`SUM`)
68
+ * @property {string} NEWLINE - Newline character (`\n`)
69
+ * @property {string} WHITESPACE - Whitespace character sequence (` `)
70
+ * @property {string} STRING - String literal (`"Lorem ipsum"`)
71
+ * @property {string} CONTEXT - Reference context ([Workbook.xlsx]Sheet1)
72
+ * @property {string} CONTEXT_QUOTE - Quoted reference context (`'[My workbook.xlsx]Sheet1'`)
73
+ * @property {string} REF_RANGE - A range identifier (`A1`)
74
+ * @property {string} REF_BEAM - A range "beam" identifier (`A:A` or `1:1`)
75
+ * @property {string} REF_TERNARY - A ternary range identifier (`B2:B`)
76
+ * @property {string} REF_NAMED - A name / named range identifier (`income`)
77
+ * @property {string} REF_STRUCT - A structured reference identifier (`table[[Column1]:[Column2]]`)
78
+ * @property {string} FX_PREFIX - A leading equals sign at the start of a formula (`=`)
79
+ * @property {string} UNKNOWN - Any unidentifiable range of characters.
80
+ * @see tokenize
81
+ */
82
+ export const tokenTypes = Object.freeze({
31
83
  OPERATOR,
32
84
  BOOLEAN,
33
85
  ERROR,
@@ -38,10 +90,37 @@ export const tokenTypes = {
38
90
  STRING,
39
91
  CONTEXT,
40
92
  CONTEXT_QUOTE,
41
- RANGE,
42
- RANGE_BEAM,
43
- RANGE_TERNARY,
44
- RANGE_NAMED,
93
+ REF_RANGE,
94
+ REF_BEAM,
95
+ REF_TERNARY,
96
+ REF_NAMED,
97
+ REF_STRUCT,
45
98
  FX_PREFIX,
46
99
  UNKNOWN
47
- };
100
+ });
101
+
102
+ /**
103
+ * A dictionary of the types used to identify AST node variants.
104
+ *
105
+ * @readonly
106
+ * @constant {Object<string>} nodeTypes
107
+ * @property {string} UNARY - A unary operation (`10%`)
108
+ * @property {string} BINARY - A binary operation (`10+10`)
109
+ * @property {string} REFERENCE - A range identifier (`A1`)
110
+ * @property {string} LITERAL - A literal (number, string, or boolean) (`123`, `"foo"`, `false`)
111
+ * @property {string} ERROR - An error literal (`#VALUE!`)
112
+ * @property {string} CALL - A function call expression (`SUM(1,2)`)
113
+ * @property {string} ARRAY - An array expression (`{1,2;3,4}`)
114
+ * @property {string} IDENTIFIER - A function name identifier (`SUM`)
115
+ * @see parse
116
+ */
117
+ export const nodeTypes = Object.freeze({
118
+ UNARY,
119
+ BINARY,
120
+ REFERENCE,
121
+ LITERAL,
122
+ ERROR: ERROR_LITERAL,
123
+ CALL,
124
+ ARRAY,
125
+ IDENTIFIER
126
+ });
package/lib/isType.js CHANGED
@@ -1,18 +1,129 @@
1
- import { RANGE, RANGE_BEAM, RANGE_NAMED, RANGE_TERNARY } from './constants.js';
1
+ import {
2
+ REF_RANGE, REF_BEAM, REF_NAMED, REF_TERNARY, REF_STRUCT,
3
+ FX_PREFIX, WHITESPACE, NEWLINE,
4
+ FUNCTION, OPERATOR,
5
+ ERROR, STRING, NUMBER, BOOLEAN
6
+ } from './constants.js';
2
7
 
8
+ /**
9
+ * Determines whether the specified token is a range.
10
+ *
11
+ * Returns `true` if the input is a token that has a type of either REF_RANGE
12
+ * (`A1` or `A1:B2`), REF_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), or
13
+ * REF_BEAM (`A:A` or `1:1`). In all other cases `false` is returned.
14
+ *
15
+ * @param {Object} token A token
16
+ * @return {boolean} True if the specified token is range, False otherwise.
17
+ */
3
18
  export function isRange (token) {
4
19
  return !!token && (
5
- token.type === RANGE ||
6
- token.type === RANGE_BEAM ||
7
- token.type === RANGE_TERNARY
20
+ token.type === REF_RANGE ||
21
+ token.type === REF_BEAM ||
22
+ token.type === REF_TERNARY
8
23
  );
9
24
  }
10
25
 
26
+ /**
27
+ * Determines whether the specified token is a reference.
28
+ *
29
+ * Returns `true` if the input is a token of type REF_RANGE (`A1` or `A1:B2`),
30
+ * REF_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), REF_BEAM (`A:A` or `1:1`),
31
+ * or REF_NAMED (`myrange`). In all other cases `false` is returned.
32
+ *
33
+ * @param {Object} token The token
34
+ * @return {boolean} True if the specified token is reference, False otherwise.
35
+ */
11
36
  export function isReference (token) {
12
37
  return !!token && (
13
- token.type === RANGE ||
14
- token.type === RANGE_BEAM ||
15
- token.type === RANGE_TERNARY ||
16
- token.type === RANGE_NAMED
38
+ token.type === REF_RANGE ||
39
+ token.type === REF_BEAM ||
40
+ token.type === REF_TERNARY ||
41
+ token.type === REF_STRUCT ||
42
+ token.type === REF_NAMED
17
43
  );
18
44
  }
45
+
46
+ /**
47
+ * Determines whether the specified token is a literal.
48
+ *
49
+ * Returns `true` if the input is a token of type BOOLEAN (`TRUE` or `FALSE`),
50
+ * ERROR (`#VALUE!`), NUMBER (123.4), or STRING (`"lorem ipsum"`). In all other
51
+ * cases `false` is returned.
52
+ *
53
+ * @param {Object} token The token
54
+ * @return {boolean} True if the specified token is literal, False otherwise.
55
+ */
56
+ export function isLiteral (token) {
57
+ return !!token && (
58
+ token.type === BOOLEAN ||
59
+ token.type === ERROR ||
60
+ token.type === NUMBER ||
61
+ token.type === STRING
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Determines whether the specified token is an error.
67
+ *
68
+ * Returns `true` if the input is a token of type ERROR (`#VALUE!`). In all
69
+ * other cases `false` is returned.
70
+ *
71
+ * @param {Object} token The token
72
+ * @return {boolean} True if the specified token is error, False otherwise.
73
+ */
74
+ export function isError (token) {
75
+ return !!token && token.type === ERROR;
76
+ }
77
+
78
+ /**
79
+ * Determines whether the specified token is whitespace.
80
+ *
81
+ * Returns `true` if the input is a token of type WHITESPACE (` `) or
82
+ * NEWLINE (`\n`). In all other cases `false` is returned.
83
+ *
84
+ * @param {Object} token The token
85
+ * @return {boolean} True if the specified token is whitespace, False otherwise.
86
+ */
87
+ export function isWhitespace (token) {
88
+ return !!token && (
89
+ token.type === WHITESPACE ||
90
+ token.type === NEWLINE
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Determines whether the specified token is a function.
96
+ *
97
+ * Returns `true` if the input is a token of type FUNCTION.
98
+ * In all other cases `false` is returned.
99
+ *
100
+ * @param {Object} token The token
101
+ * @return {boolean} True if the specified token is function, False otherwise.
102
+ */
103
+ export function isFunction (token) {
104
+ return !!token && token.type === FUNCTION;
105
+ }
106
+
107
+ /**
108
+ * Returns `true` if the input is a token of type FX_PREFIX (leading `=` in
109
+ * formula). In all other cases `false` is returned.
110
+ *
111
+ * @param {Object} token The token
112
+ * @return {boolean} True if the specified token is effects prefix, False otherwise.
113
+ */
114
+ export function isFxPrefix (token) {
115
+ return !!token && token.type === FX_PREFIX;
116
+ }
117
+
118
+ /**
119
+ * Determines whether the specified token is an operator.
120
+ *
121
+ * Returns `true` if the input is a token of type OPERATOR (`+` or `:`). In all
122
+ * other cases `false` is returned.
123
+ *
124
+ * @param {Object} token The token
125
+ * @return {boolean} True if the specified token is operator, False otherwise.
126
+ */
127
+ export function isOperator (token) {
128
+ return !!token && token.type === OPERATOR;
129
+ }