@borgar/fx 4.11.2 → 4.13.0

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/docs/API.md CHANGED
@@ -383,6 +383,7 @@ The AST Abstract Syntax Tree's format is documented in [AST_format.md](./AST_for
383
383
  | [options] | `object` | `{}` | Options |
384
384
  | [options].allowNamed | `boolean` | `true` | Enable parsing names as well as ranges. |
385
385
  | [options].allowTernary | `boolean` | `false` | Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md. |
386
+ | [options].looseRefCalls | `boolean` | `false` | Permits any function call where otherwise only functions that return references would be permitted. |
386
387
  | [options].negativeNumbers | `boolean` | `true` | Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1` (alternatively these will be unary operations in the tree). |
387
388
  | [options].permitArrayCalls | `boolean` | `false` | Function calls are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it. |
388
389
  | [options].permitArrayRanges | `boolean` | `false` | Ranges are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it. |
@@ -6,7 +6,8 @@ import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_STRUCT, REF_TERNARY } fro
6
6
 
7
7
  Test.prototype.isFixed = function (expr, expected, options = {}) {
8
8
  const result = fixRanges(expr, options);
9
- this.is(result, expected, expr + ' → ' + expected);
9
+ this.is(result, expected,
10
+ `\x1b[36m${expr} → ${expected} \x1b[37mopts=${JSON.stringify(options)}\x1b[0m`);
10
11
  };
11
12
 
12
13
  test('fixRanges basics', t => {
package/lib/lexer.js CHANGED
@@ -4,15 +4,14 @@ import {
4
4
  NUMBER,
5
5
  OPERATOR,
6
6
  REF_NAMED,
7
- STRING,
8
7
  UNKNOWN,
9
8
  WHITESPACE,
10
9
  FUNCTION,
11
10
  OPERATOR_TRIM,
12
11
  REF_RANGE
13
12
  } from './constants.js';
14
- import { lexers } from './lexerParts.js';
15
13
  import { mergeRefTokens } from './mergeRefTokens.js';
14
+ import { lexers } from './lexers/sets.js';
16
15
 
17
16
  const isType = (t, type) => t && t.type === type;
18
17
 
@@ -71,7 +70,14 @@ function fixRCNames (tokens) {
71
70
  }
72
71
 
73
72
  export function getTokens (fx, tokenHandlers, options = {}) {
74
- const opts = Object.assign({}, defaultOptions, options);
73
+ const opts = { ...defaultOptions, ...options };
74
+ // const opts = {
75
+ // withLocation: options.withLocation ?? false,
76
+ // mergeRefs: options.mergeRefs ?? true,
77
+ // allowTernary: options.allowTernary ?? false,
78
+ // negativeNumbers: options.negativeNumbers ?? true,
79
+ // r1c1: options.r1c1 ?? false
80
+ // };
75
81
  const { withLocation, mergeRefs, negativeNumbers } = opts;
76
82
  const tokens = [];
77
83
  let pos = 0;
@@ -103,7 +109,8 @@ export function getTokens (fx, tokenHandlers, options = {}) {
103
109
  token.type = UNKNOWN;
104
110
  }
105
111
  // push token as normally
106
- tokens.push(token);
112
+ // tokens.push(token);
113
+ tokens[tokens.length] = token;
107
114
  lastToken = token;
108
115
  if (token.type !== WHITESPACE && token.type !== NEWLINE) {
109
116
  tail1 = tail0;
@@ -112,77 +119,51 @@ export function getTokens (fx, tokenHandlers, options = {}) {
112
119
  }
113
120
  };
114
121
 
115
- if (fx[0] === '=') {
116
- const token = {
117
- type: FX_PREFIX,
118
- value: '=',
119
- ...(withLocation ? { loc: [ 0, 1 ] } : {})
120
- };
122
+ if (fx.startsWith('=')) {
123
+ const token = { type: FX_PREFIX, value: '=' };
124
+ if (withLocation) {
125
+ token.loc = [ 0, 1 ];
126
+ }
121
127
  pos++;
122
128
  pushToken(token);
123
129
  }
124
130
 
131
+ const numHandlers = tokenHandlers.length;
125
132
  while (pos < fx.length) {
126
133
  const startPos = pos;
127
- const s = fx.slice(pos);
128
- let tokenType = '';
129
- let tokenValue = '';
130
- for (let i = 0; i < tokenHandlers.length; i++) {
131
- const t = tokenHandlers[i](s, opts);
132
- if (t) {
133
- tokenType = t.type;
134
- tokenValue = t.value;
135
- pos += tokenValue.length;
134
+ let token;
135
+ for (let i = 0; i < numHandlers; i++) {
136
+ token = tokenHandlers[i](fx, pos, opts);
137
+ if (token) {
138
+ pos += token.value.length;
136
139
  break;
137
140
  }
138
141
  }
139
142
 
140
- if (!tokenType) {
141
- tokenType = UNKNOWN;
142
- tokenValue = fx[pos];
143
+ if (!token) {
144
+ token = {
145
+ type: UNKNOWN,
146
+ value: fx[pos]
147
+ };
143
148
  pos++;
144
149
  }
145
-
146
- const token = {
147
- type: tokenType,
148
- value: tokenValue,
149
- ...(withLocation ? { loc: [ startPos, pos ] } : {})
150
- };
150
+ if (withLocation) {
151
+ token.loc = [ startPos, pos ];
152
+ }
151
153
 
152
154
  // make a note if we found a let/lambda call
153
- if (lastToken && lastToken.type === FUNCTION && tokenValue === '(') {
154
- const lastLC = lastToken.value.toLowerCase();
155
- if (lastLC === 'lambda' || lastLC === 'let') {
155
+ if (lastToken && token.value === '(' && lastToken.type === FUNCTION) {
156
+ if (/^l(?:ambda|et)$/i.test(lastToken.value)) {
156
157
  letOrLambda++;
157
158
  }
158
159
  }
159
160
  // make a note if we found a R or C unknown
160
- if (tokenType === UNKNOWN) {
161
- const valLC = tokenValue.toLowerCase();
161
+ if (token.type === UNKNOWN && token.value.length === 1) {
162
+ const valLC = token.value.toLowerCase();
162
163
  unknownRC += (valLC === 'r' || valLC === 'c') ? 1 : 0;
163
164
  }
164
165
 
165
- // check for termination
166
- if (tokenType === STRING) {
167
- const l = tokenValue.length;
168
- if (tokenValue === '""') {
169
- // common case that IS terminated
170
- }
171
- else if (tokenValue === '"' || tokenValue[l - 1] !== '"') {
172
- token.unterminated = true;
173
- }
174
- else if (tokenValue !== '""' && tokenValue[l - 2] === '"') {
175
- let p = l - 1;
176
- while (tokenValue[p] === '"') { p--; }
177
- const atStart = (p + 1);
178
- const oddNum = ((l - p + 1) % 2 === 0);
179
- if (!atStart ^ oddNum) {
180
- token.unterminated = true;
181
- }
182
- }
183
- }
184
-
185
- if (negativeNumbers && tokenType === NUMBER) {
166
+ if (negativeNumbers && token.type === NUMBER) {
186
167
  const last1 = lastToken;
187
168
  // do we have a number preceded by a minus?
188
169
  if (last1 && isType(last1, OPERATOR) && last1.value === '-') {
@@ -193,8 +174,8 @@ export function getTokens (fx, tokenHandlers, options = {}) {
193
174
  !causesBinaryMinus(tail1)
194
175
  ) {
195
176
  const minus = tokens.pop();
196
- token.value = '-' + tokenValue;
197
- if (withLocation) {
177
+ token.value = '-' + token.value;
178
+ if (token.loc) {
198
179
  // ensure offsets are up to date
199
180
  token.loc[0] = minus.loc[0];
200
181
  }
@@ -221,7 +202,7 @@ export function getTokens (fx, tokenHandlers, options = {}) {
221
202
  // operators.
222
203
  for (const index of trimOps) {
223
204
  const before = tokens[index - 1];
224
- const after = tokens[index - 1];
205
+ const after = tokens[index + 1];
225
206
  if (before && before.type === REF_RANGE && after && after.type === REF_RANGE) {
226
207
  tokens[index].type = OPERATOR;
227
208
  }
@@ -0,0 +1,18 @@
1
+ const PERIOD = 46;
2
+ const COLON = 58;
3
+
4
+ export function advRangeOp (str, pos) {
5
+ const c0 = str.charCodeAt(pos);
6
+ if (c0 === PERIOD) {
7
+ const c1 = str.charCodeAt(pos + 1);
8
+ if (c1 === COLON) {
9
+ return str.charCodeAt(pos + 2) === PERIOD ? 3 : 2;
10
+ }
11
+ }
12
+ else if (c0 === COLON) {
13
+ const c1 = str.charCodeAt(pos + 1);
14
+ return c1 === PERIOD ? 2 : 1;
15
+ }
16
+ return 0;
17
+ }
18
+
@@ -0,0 +1,25 @@
1
+ // regular: [A-Za-z0-9_\u00a1-\uffff]
2
+ export function canEndRange (str, pos) {
3
+ const c = str.charCodeAt(pos);
4
+ return !(
5
+ (c >= 65 && c <= 90) || // A-Z
6
+ (c >= 97 && c <= 122) || // a-z
7
+ (c >= 48 && c <= 57) || // 0-9
8
+ (c === 95) || // _
9
+ (c > 0xA0) // \u00a1-\uffff
10
+ );
11
+ }
12
+
13
+ // partial: [A-Za-z0-9_($.]
14
+ export function canEndPartialRange (str, pos) {
15
+ const c = str.charCodeAt(pos);
16
+ return !(
17
+ (c >= 65 && c <= 90) || // A-Z
18
+ (c >= 97 && c <= 122) || // a-z
19
+ (c >= 48 && c <= 57) || // 0-9
20
+ (c === 95) || // _
21
+ (c === 40) || // (
22
+ (c === 36) || // $
23
+ (c === 46) // .
24
+ );
25
+ }
@@ -0,0 +1,36 @@
1
+ import { BOOLEAN } from '../constants.js';
2
+
3
+ export function lexBoolean (str, pos) {
4
+ // "true" (case insensitive)
5
+ const c0 = str.charCodeAt(pos);
6
+ if (c0 === 84 || c0 === 116) {
7
+ const c1 = str.charCodeAt(pos + 1);
8
+ if (c1 === 82 || c1 === 114) {
9
+ const c2 = str.charCodeAt(pos + 2);
10
+ if (c2 === 85 || c2 === 117) {
11
+ const c3 = str.charCodeAt(pos + 3);
12
+ if (c3 === 69 || c3 === 101) {
13
+ // non char to follow?
14
+ return { type: BOOLEAN, value: str.slice(pos, pos + 4) };
15
+ }
16
+ }
17
+ }
18
+ }
19
+ // "false" (case insensitive)
20
+ if (c0 === 70 || c0 === 102) {
21
+ const c1 = str.charCodeAt(pos + 1);
22
+ if (c1 === 65 || c1 === 97) {
23
+ const c2 = str.charCodeAt(pos + 2);
24
+ if (c2 === 76 || c2 === 108) {
25
+ const c3 = str.charCodeAt(pos + 3);
26
+ if (c3 === 83 || c3 === 115) {
27
+ const c4 = str.charCodeAt(pos + 4);
28
+ if (c4 === 69 || c4 === 101) {
29
+ // non char to follow?
30
+ return { type: BOOLEAN, value: str.slice(pos, pos + 5) };
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,96 @@
1
+ import { CONTEXT, CONTEXT_QUOTE } from '../constants.js';
2
+
3
+ const QUOT_SINGLE = 39; // '
4
+ const BR_OPEN = 91; // [
5
+ const BR_CLOSE = 93; // ]
6
+ const EXCL = 33; // !
7
+
8
+ // xlsx xml uses a variant of the syntax that has external references in
9
+ // bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named
10
+ export function lexContext (str, pos, options) {
11
+ const c0 = str.charCodeAt(pos);
12
+ let br1;
13
+ let br2;
14
+ // quoted context: '(?:''|[^'])*('|$)(?=!)
15
+ if (c0 === QUOT_SINGLE) {
16
+ const start = pos;
17
+ pos++;
18
+ while (pos < str.length) {
19
+ const c = str.charCodeAt(pos);
20
+ if (c === BR_OPEN) {
21
+ if (br1) { return; } // only 1 allowed
22
+ br1 = pos;
23
+ }
24
+ else if (c === BR_CLOSE) {
25
+ if (br2) { return; } // only 1 allowed
26
+ br2 = pos;
27
+ }
28
+ else if (c === QUOT_SINGLE) {
29
+ pos++;
30
+ if (str.charCodeAt(pos) !== QUOT_SINGLE) {
31
+ let valid = br1 == null && br2 == null;
32
+ if (options.xlsx && (br1 === start + 1) && (br2 === pos - 2)) {
33
+ valid = true;
34
+ }
35
+ if ((br1 >= start + 1) && (br2 < pos - 2) && (br2 > br1 + 1)) {
36
+ valid = true;
37
+ }
38
+ if (valid && str.charCodeAt(pos) === EXCL) {
39
+ return { type: CONTEXT_QUOTE, value: str.slice(start, pos) };
40
+ }
41
+ return;
42
+ }
43
+ }
44
+ pos++;
45
+ }
46
+ }
47
+ // unquoted context
48
+ else if (c0 !== EXCL) {
49
+ const start = pos;
50
+ while (pos < str.length) {
51
+ const c = str.charCodeAt(pos);
52
+ if (c === BR_OPEN) {
53
+ if (br1) { return; } // only 1 allowed
54
+ br1 = pos;
55
+ }
56
+ else if (c === BR_CLOSE) {
57
+ if (br2) { return; } // only 1 allowed
58
+ br2 = pos;
59
+ }
60
+ else if (c === EXCL) {
61
+ let valid = br1 == null && br2 == null;
62
+ if (options.xlsx && (br1 === start) && (br2 === pos - 1)) {
63
+ valid = true;
64
+ }
65
+ if ((br1 >= start) && (br2 < pos - 1) && (br2 > br1 + 1)) {
66
+ valid = true;
67
+ }
68
+ if (valid) {
69
+ return { type: CONTEXT, value: str.slice(start, pos) };
70
+ }
71
+ }
72
+ else if (
73
+ (br1 == null || br2 != null) &&
74
+ // [0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]
75
+ !(
76
+ (c >= 65 && c <= 90) || // A-Z
77
+ (c >= 97 && c <= 122) || // a-z
78
+ (c >= 48 && c <= 57) || // 0-9
79
+ (c === 46) || // .
80
+ (c === 95) || // _
81
+ (c === 161) || // ¡
82
+ (c === 164) || // ¤
83
+ (c === 167) || // §
84
+ (c === 168) || // ¨
85
+ (c === 170) || // ª
86
+ (c === 173) || // \u00ad
87
+ (c >= 175) // ¯-\uffff
88
+ )
89
+ ) {
90
+ return;
91
+ }
92
+ // 0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff
93
+ pos++;
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,15 @@
1
+ /* eslint-disable no-mixed-operators */
2
+ import { ERROR } from '../constants.js';
3
+
4
+ const re_ERROR = /#(?:NAME\?|FIELD!|CALC!|VALUE!|REF!|DIV\/0!|NULL!|NUM!|N\/A|GETTING_DATA\b|SPILL!|UNKNOWN!|SYNTAX\?|ERROR!|CONNECT!|BLOCKED!|EXTERNAL!)/iy;
5
+ const HASH = 35;
6
+
7
+ export function lexError (str, pos) {
8
+ if (str.charCodeAt(pos) === HASH) {
9
+ re_ERROR.lastIndex = pos;
10
+ const m = re_ERROR.exec(str);
11
+ if (m) {
12
+ return { type: ERROR, value: m[0] };
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,36 @@
1
+ import { FUNCTION } from '../constants.js';
2
+
3
+ const PAREN_OPEN = 40;
4
+
5
+ // [A-Za-z_]+[A-Za-z\d_.]*(?=\()
6
+ export function lexFunction (str, pos) {
7
+ const start = pos;
8
+ // starts with: a-zA-Z_
9
+ let c = str.charCodeAt(pos);
10
+ if (
11
+ (c < 65 || c > 90) && // A-Z
12
+ (c < 97 || c > 122) && // a-z
13
+ (c !== 95) // _
14
+ ) {
15
+ return;
16
+ }
17
+ pos++;
18
+ // has any number of: a-zA-Z0-9_.
19
+ do {
20
+ c = str.charCodeAt(pos);
21
+ if (
22
+ (c < 65 || c > 90) && // A-Z
23
+ (c < 97 || c > 122) && // a-z
24
+ (c < 48 || c > 57) && // 0-9
25
+ (c !== 95) && // _
26
+ (c !== 46) // .
27
+ ) {
28
+ break;
29
+ }
30
+ pos++;
31
+ } while (pos < str.length);
32
+ // followed by a (
33
+ if (str.charCodeAt(pos) === PAREN_OPEN) {
34
+ return { type: FUNCTION, value: str.slice(start, pos) };
35
+ }
36
+ }
@@ -0,0 +1,60 @@
1
+ /* eslint-disable max-len */
2
+ import { REF_NAMED } from '../constants.js';
3
+
4
+ // The advertized named ranges rules are a bit off from what Excel seems to do.
5
+ // In the "extended range" of chars, it looks like it allows most things above
6
+ // U+00B0 with the range between U+00A0-U+00AF rather random:
7
+ // /^[a-zA-Z\\_¡¤§¨ª\u00ad¯\u00b0-\uffff][a-zA-Z0-9\\_.?¡¤§¨ª\u00ad¯\u00b0-\uffff]{0,254}/
8
+ //
9
+ // I've simplified to allowing everything above U+00A1:
10
+ // /^[a-zA-Z\\_\u00a1-\uffff][a-zA-Z0-9\\_.?\u00a1-\uffff]{0,254}/
11
+ export function lexNamed (str, pos) {
12
+ const start = pos;
13
+ // starts with: [a-zA-Z\\_\u00a1-\uffff]
14
+ const s = str.charCodeAt(pos);
15
+ if (
16
+ (s >= 65 && s <= 90) || // A-Z
17
+ (s >= 97 && s <= 122) || // a-z
18
+ (s === 95) || // _
19
+ (s === 92) || // \
20
+ (s > 0xA0) // \u00a1-\uffff
21
+ ) {
22
+ pos++;
23
+ }
24
+ else {
25
+ return;
26
+ }
27
+ // has any number of: [a-zA-Z0-9\\_.?\u00a1-\uffff]
28
+ let c;
29
+ do {
30
+ c = str.charCodeAt(pos);
31
+ if (
32
+ (c >= 65 && c <= 90) || // A-Z
33
+ (c >= 97 && c <= 122) || // a-z
34
+ (c >= 48 && c <= 57) || // 0-9
35
+ (c === 95) || // _
36
+ (c === 92) || // \
37
+ (c === 46) || // .
38
+ (c === 63) || // ?
39
+ (c > 0xA0) // \u00a1-\uffff
40
+ ) {
41
+ pos++;
42
+ }
43
+ else {
44
+ break;
45
+ }
46
+ } while (isFinite(c));
47
+
48
+ const len = pos - start;
49
+ if (len && len < 255) {
50
+ // names starting with \ must be at least 3 char long
51
+ if (s === 92 && len < 3) {
52
+ return;
53
+ }
54
+ // single characters R and C are forbidden as names
55
+ if (len === 1 && (s === 114 || s === 82 || s === 99 || s === 67)) {
56
+ return;
57
+ }
58
+ return { type: REF_NAMED, value: str.slice(start, pos) };
59
+ }
60
+ }
@@ -0,0 +1,11 @@
1
+ import { NEWLINE } from '../constants.js';
2
+
3
+ export function lexNewLine (str, pos) {
4
+ const start = pos;
5
+ while (str.charCodeAt(pos) === 10) {
6
+ pos++;
7
+ }
8
+ if (pos !== start) {
9
+ return { type: NEWLINE, value: str.slice(start, pos) };
10
+ }
11
+ }
@@ -0,0 +1,47 @@
1
+ import { NUMBER } from '../constants.js';
2
+
3
+ function advDigits (str, pos) {
4
+ const start = pos;
5
+ do {
6
+ const c = str.charCodeAt(pos);
7
+ if (c < 48 || c > 57) { // 0-9
8
+ break;
9
+ }
10
+ pos++;
11
+ }
12
+ while (pos < str.length);
13
+ return pos - start;
14
+ }
15
+
16
+ // \d+(\.\d+)?(?:[eE][+-]?\d+)?
17
+ export function lexNumber (str, pos) {
18
+ const start = pos;
19
+
20
+ // integer
21
+ const lead = advDigits(str, pos);
22
+ if (!lead) { return; }
23
+ pos += lead;
24
+
25
+ // optional fraction part
26
+ const c0 = str.charCodeAt(pos);
27
+ if (c0 === 46) { // .
28
+ pos++;
29
+ const frac = advDigits(str, pos);
30
+ if (!frac) { return; }
31
+ pos += frac;
32
+ }
33
+ // optional exponent part
34
+ const c1 = str.charCodeAt(pos);
35
+ if (c1 === 69 || c1 === 101) { // E e
36
+ pos++;
37
+ const sign = str.charCodeAt(pos);
38
+ if (sign === 43 || sign === 45) { // + -
39
+ pos++;
40
+ }
41
+ const exp = advDigits(str, pos);
42
+ if (!exp) { return; }
43
+ pos += exp;
44
+ }
45
+
46
+ return { type: NUMBER, value: str.slice(start, pos) };
47
+ }
@@ -0,0 +1,25 @@
1
+ import { OPERATOR } from '../constants.js';
2
+
3
+ export function lexOperator (str, pos) {
4
+ const c0 = str.charCodeAt(pos);
5
+ const c1 = str.charCodeAt(pos + 1);
6
+ if (
7
+ (c0 === 60 && c1 === 61) || // <=
8
+ (c0 === 62 && c1 === 61) || // >=
9
+ (c0 === 60 && c1 === 62) // <>
10
+ ) {
11
+ return { type: OPERATOR, value: str.slice(pos, pos + 2) };
12
+ }
13
+ if (
14
+ // { } ! # % &
15
+ c0 === 123 || c0 === 125 || c0 === 33 || c0 === 35 || c0 === 37 || c0 === 38 ||
16
+ // ( ) * + , -
17
+ c0 === 40 || c0 === 41 || c0 === 42 || c0 === 43 || c0 === 44 || c0 === 45 ||
18
+ // / : ; < = >
19
+ c0 === 47 || c0 === 58 || c0 === 59 || c0 === 60 || c0 === 61 || c0 === 62 ||
20
+ // @ ^
21
+ c0 === 64 || c0 === 94
22
+ ) {
23
+ return { type: OPERATOR, value: str[pos] };
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ import { lexRangeA1 } from './lexRangeA1.js';
2
+ import { lexRangeR1C1 } from './lexRangeR1C1.js';
3
+
4
+ export function lexRange (str, pos, options) {
5
+ return options.r1c1
6
+ ? lexRangeR1C1(str, pos, options)
7
+ : lexRangeA1(str, pos, options);
8
+ }