@borgar/fx 4.4.0 → 4.6.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/.eslintrc +3 -12
- package/dist/fx.d.ts +153 -34
- package/dist/fx.js +1 -1
- package/docs/API.md +248 -119
- package/lib/a1.js +32 -23
- package/lib/addTokenMeta.js +6 -6
- package/lib/constants.js +2 -2
- package/lib/extraTypes.js +73 -0
- package/lib/fixRanges.js +8 -8
- package/lib/isType.js +16 -16
- package/lib/lexer-srefs.spec.js +5 -0
- package/lib/lexer.js +2 -2
- package/lib/lexerParts.js +24 -12
- package/lib/mergeRefTokens.js +2 -2
- package/lib/parseRef.spec.js +22 -11
- package/lib/parser.js +8 -8
- package/lib/parser.spec.js +90 -39
- package/lib/rc.js +14 -12
- package/lib/sr.js +22 -22
- package/lib/sr.spec.js +7 -0
- package/lib/translate-toA1.spec.js +5 -1
- package/lib/translate-toRC.spec.js +4 -1
- package/lib/translate.js +16 -13
- package/lib/translate.spec.js +21 -0
- package/package.json +9 -8
package/lib/a1.js
CHANGED
|
@@ -5,6 +5,7 @@ import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
|
|
|
5
5
|
const clamp = (min, val, max) => Math.min(Math.max(val, min), max);
|
|
6
6
|
const toColStr = (c, a) => (a ? '$' : '') + toCol(c);
|
|
7
7
|
const toRowStr = (r, a) => (a ? '$' : '') + toRow(r);
|
|
8
|
+
const charFrom = String.fromCharCode;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Convert a column string representation to a 0 based
|
|
@@ -15,7 +16,7 @@ const toRowStr = (r, a) => (a ? '$' : '') + toRow(r);
|
|
|
15
16
|
* return garbage.
|
|
16
17
|
*
|
|
17
18
|
* @param {string} columnString The column string identifier
|
|
18
|
-
* @
|
|
19
|
+
* @returns {number} Zero based column index number
|
|
19
20
|
*/
|
|
20
21
|
export function fromCol (columnString) {
|
|
21
22
|
const x = (columnString || '');
|
|
@@ -47,13 +48,17 @@ export function fromCol (columnString) {
|
|
|
47
48
|
* return garbage.
|
|
48
49
|
*
|
|
49
50
|
* @param {number} columnIndex Zero based column index number
|
|
50
|
-
* @
|
|
51
|
+
* @returns {string} The column string identifier
|
|
51
52
|
*/
|
|
52
53
|
export function toCol (columnIndex) {
|
|
53
54
|
return (
|
|
54
|
-
(columnIndex >= 702
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
(columnIndex >= 702
|
|
56
|
+
? charFrom(((((columnIndex - 702) / 676) - 0) % 26) + 65)
|
|
57
|
+
: '') +
|
|
58
|
+
(columnIndex >= 26
|
|
59
|
+
? charFrom(((((columnIndex / 26) - 1) % 26) + 65))
|
|
60
|
+
: '') +
|
|
61
|
+
charFrom((columnIndex % 26) + 65)
|
|
57
62
|
);
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -80,8 +85,8 @@ export function toAbsolute (range) {
|
|
|
80
85
|
*
|
|
81
86
|
* @private
|
|
82
87
|
* @see parseA1Ref
|
|
83
|
-
* @param {
|
|
84
|
-
* @
|
|
88
|
+
* @param {RangeA1} range A range object
|
|
89
|
+
* @returns {string} An A1-style string represenation of a range
|
|
85
90
|
*/
|
|
86
91
|
export function toA1 (range) {
|
|
87
92
|
let { top, left, bottom, right } = range;
|
|
@@ -102,27 +107,31 @@ export function toA1 (range) {
|
|
|
102
107
|
right = clamp(0, right | 0, MAX_COLS);
|
|
103
108
|
}
|
|
104
109
|
// A:A
|
|
105
|
-
|
|
110
|
+
const allRows = top === 0 && bottom >= MAX_ROWS;
|
|
111
|
+
const haveAbsCol = ($left && !noLeft) || ($right && !noRight);
|
|
112
|
+
if ((allRows && !noLeft && !noRight && (!haveAbsCol || left === right)) || (noTop && noBottom)) {
|
|
106
113
|
return toColStr(left, $left) + ':' + toColStr(right, $right);
|
|
107
114
|
}
|
|
108
115
|
// 1:1
|
|
109
|
-
|
|
116
|
+
const allCols = left === 0 && right >= MAX_COLS;
|
|
117
|
+
const haveAbsRow = ($top && !noTop) || ($bottom && !noBottom);
|
|
118
|
+
if ((allCols && !noTop && !noBottom && (!haveAbsRow || top === bottom)) || (noLeft && noRight)) {
|
|
110
119
|
return toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
111
120
|
}
|
|
112
121
|
// A1:1
|
|
113
|
-
|
|
122
|
+
if (!noLeft && !noTop && !noRight && noBottom) {
|
|
114
123
|
return toColStr(left, $left) + toRowStr(top, $top) + ':' + toColStr(right, $right);
|
|
115
124
|
}
|
|
116
125
|
// A:A1 => A1:1
|
|
117
|
-
|
|
126
|
+
if (!noLeft && noTop && !noRight && !noBottom) {
|
|
118
127
|
return toColStr(left, $left) + toRowStr(bottom, $bottom) + ':' + toColStr(right, $right);
|
|
119
128
|
}
|
|
120
129
|
// A1:A
|
|
121
|
-
|
|
130
|
+
if (!noLeft && !noTop && noRight && !noBottom) {
|
|
122
131
|
return toColStr(left, $left) + toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
123
132
|
}
|
|
124
133
|
// A:A1 => A1:A
|
|
125
|
-
|
|
134
|
+
if (noLeft && !noTop && !noRight && !noBottom) {
|
|
126
135
|
return toColStr(right, $right) + toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
127
136
|
}
|
|
128
137
|
// A1:A1
|
|
@@ -154,9 +163,9 @@ function splitA1 (str) {
|
|
|
154
163
|
* @private
|
|
155
164
|
* @see parseA1Ref
|
|
156
165
|
* @param {string} rangeString A range string
|
|
157
|
-
* @
|
|
166
|
+
* @returns {(RangeA1|null)} An object representing a valid range or null if it is invalid.
|
|
158
167
|
*/
|
|
159
|
-
export function fromA1 (
|
|
168
|
+
export function fromA1 (rangeString) {
|
|
160
169
|
let top = null;
|
|
161
170
|
let left = null;
|
|
162
171
|
let bottom = null;
|
|
@@ -165,7 +174,7 @@ export function fromA1 (rangeStr) {
|
|
|
165
174
|
let $left = false;
|
|
166
175
|
let $bottom = false;
|
|
167
176
|
let $right = false;
|
|
168
|
-
const [ part1, part2, part3 ] =
|
|
177
|
+
const [ part1, part2, part3 ] = rangeString.split(':');
|
|
169
178
|
if (part3) {
|
|
170
179
|
return null;
|
|
171
180
|
}
|
|
@@ -241,11 +250,11 @@ export function fromA1 (rangeStr) {
|
|
|
241
250
|
* syntax does not specify:
|
|
242
251
|
*
|
|
243
252
|
* @param {string} refString An A1-style reference string
|
|
244
|
-
* @param {
|
|
253
|
+
* @param {object} [options={}] Options
|
|
245
254
|
* @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
|
|
246
255
|
* @param {boolean} [options.allowTernary=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.
|
|
247
256
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
248
|
-
* @
|
|
257
|
+
* @returns {(ReferenceA1|null)} An object representing a valid reference or null if it is invalid.
|
|
249
258
|
*/
|
|
250
259
|
export function parseA1Ref (refString, { allowNamed = true, allowTernary = false, xlsx = false } = {}) {
|
|
251
260
|
const d = parseRef(refString, { allowNamed, allowTernary, xlsx, r1c1: false });
|
|
@@ -287,10 +296,10 @@ export function parseA1Ref (refString, { allowNamed = true, allowTernary = false
|
|
|
287
296
|
* // => 'Sheet1!A$1:$B2'
|
|
288
297
|
* ```
|
|
289
298
|
*
|
|
290
|
-
* @param {
|
|
291
|
-
* @param {
|
|
299
|
+
* @param {ReferenceA1} refObject A reference object
|
|
300
|
+
* @param {object} [options={}] Options
|
|
292
301
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
293
|
-
* @
|
|
302
|
+
* @returns {string} The reference in A1-style string format
|
|
294
303
|
*/
|
|
295
304
|
export function stringifyA1Ref (refObject, { xlsx = false } = {}) {
|
|
296
305
|
const prefix = xlsx
|
|
@@ -332,8 +341,8 @@ export function stringifyA1Ref (refObject, { xlsx = false } = {}) {
|
|
|
332
341
|
* // }
|
|
333
342
|
* ```
|
|
334
343
|
*
|
|
335
|
-
* @param {
|
|
336
|
-
* @
|
|
344
|
+
* @param {RangeA1} range The range part of a reference object.
|
|
345
|
+
* @returns {RangeA1} same range with missing bounds filled in.
|
|
337
346
|
*/
|
|
338
347
|
export function addA1RangeBounds (range) {
|
|
339
348
|
if (range.top == null) {
|
package/lib/addTokenMeta.js
CHANGED
|
@@ -121,13 +121,13 @@ function addContext (ref, sheetName, workbookName) {
|
|
|
121
121
|
*
|
|
122
122
|
* All will be tagged with `.error` (boolean `true`).
|
|
123
123
|
*
|
|
124
|
-
* @param {Array<
|
|
125
|
-
* @param {
|
|
124
|
+
* @param {Array<Token>} tokenlist An array of tokens (from `tokenize()`)
|
|
125
|
+
* @param {object} [context={}] A contest used to match `A1` to `Sheet1!A1`.
|
|
126
126
|
* @param {string} [context.sheetName=''] An implied sheet name ('Sheet1')
|
|
127
127
|
* @param {string} [context.workbookName=''] An implied workbook name ('report.xlsx')
|
|
128
|
-
* @
|
|
128
|
+
* @returns {Array<TokenEnhanced>} The input array with the enchanced tokens
|
|
129
129
|
*/
|
|
130
|
-
export function addTokenMeta (
|
|
130
|
+
export function addTokenMeta (tokenlist, { sheetName = '', workbookName = '' } = {}) {
|
|
131
131
|
const parenStack = [];
|
|
132
132
|
let arrayStart = null;
|
|
133
133
|
const uid = getIDer();
|
|
@@ -135,7 +135,7 @@ export function addTokenMeta (tokens, { sheetName = '', workbookName = '' } = {}
|
|
|
135
135
|
|
|
136
136
|
const getCurrDepth = () => parenStack.length + (arrayStart ? 1 : 0);
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
tokenlist.forEach((token, i) => {
|
|
139
139
|
token.index = i;
|
|
140
140
|
token.depth = getCurrDepth();
|
|
141
141
|
if (token.value === '(') {
|
|
@@ -202,5 +202,5 @@ export function addTokenMeta (tokens, { sheetName = '', workbookName = '' } = {}
|
|
|
202
202
|
token.error = true;
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
|
-
return
|
|
205
|
+
return tokenlist;
|
|
206
206
|
}
|
package/lib/constants.js
CHANGED
|
@@ -25,5 +25,5 @@ export const CALL = 'CallExpression';
|
|
|
25
25
|
export const ARRAY = 'ArrayExpression';
|
|
26
26
|
export const IDENTIFIER = 'Identifier';
|
|
27
27
|
|
|
28
|
-
export const MAX_COLS = 2 ** 14 - 1; // 16383
|
|
29
|
-
export const MAX_ROWS = 2 ** 20 - 1; // 1048575
|
|
28
|
+
export const MAX_COLS = (2 ** 14) - 1; // 16383
|
|
29
|
+
export const MAX_ROWS = (2 ** 20) - 1; // 1048575
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Token A formula language token.
|
|
3
|
+
* @augments Record<string,any>
|
|
4
|
+
* @property {string} type The type of the token
|
|
5
|
+
* @property {string} value The value of the token
|
|
6
|
+
* @property {boolean} [unterminated] Signifies an unterminated string token
|
|
7
|
+
* @property {Array<number>} [loc] Source position offsets to the token
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} TokenEnhanced A token with extra meta data.
|
|
12
|
+
* @augments Token
|
|
13
|
+
* @property {number} index A zero based position in a token list
|
|
14
|
+
* @property {string} [groupId] The ID of a group which this token belongs (e.g. matching parens)
|
|
15
|
+
* @property {number} [depth] This token's level of nesting inside parentheses
|
|
16
|
+
* @property {boolean} [error] Token is of unknown type or a paren without a match
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Record<string, any>} RangeR1C1 A range in R1C1 style coordinates.
|
|
21
|
+
* @property {number | null} [r0] Top row of the range
|
|
22
|
+
* @property {number | null} [r1] Bottom row of the range
|
|
23
|
+
* @property {number | null} [c0] Left column of the range
|
|
24
|
+
* @property {number | null} [c1] Right column of the range
|
|
25
|
+
* @property {boolean | null} [$r0] Signifies that r0 is an absolute value
|
|
26
|
+
* @property {boolean | null} [$r1] Signifies that r1 is an absolute value
|
|
27
|
+
* @property {boolean | null} [$c0] Signifies that c0 is an absolute value
|
|
28
|
+
* @property {boolean | null} [$c1] Signifies that c1 is an absolute value
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Record<string, any>} RangeA1 A range in A1 style coordinates.
|
|
33
|
+
* @property {number | null} [top] Top row of the range
|
|
34
|
+
* @property {number | null} [bottom] Bottom row of the range
|
|
35
|
+
* @property {number | null} [left] Left column of the range
|
|
36
|
+
* @property {number | null} [right] Right column of the range
|
|
37
|
+
* @property {boolean | null} [$top] Signifies that top is a "locked" value
|
|
38
|
+
* @property {boolean | null} [$bottom] Signifies that bottom is a "locked" value
|
|
39
|
+
* @property {boolean | null} [$left] Signifies that left is a "locked" value
|
|
40
|
+
* @property {boolean | null} [$right] Signifies that right is a "locked" value
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Record<string, any>} ReferenceA1
|
|
45
|
+
* A reference containing an A1 style range. See [Prefixes.md] for
|
|
46
|
+
* documentation on how scopes work in Fx.
|
|
47
|
+
* @property {Array<string>} [context] A collection of scopes for the reference
|
|
48
|
+
* @property {string} [workbookName] A context workbook scope
|
|
49
|
+
* @property {string} [sheetName] A context sheet scope
|
|
50
|
+
* @property {RangeA1} [range] The reference's range
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Record<string, any>} ReferenceR1C1
|
|
55
|
+
* A reference containing a R1C1 style range. See [Prefixes.md] for
|
|
56
|
+
* documentation on how scopes work in Fx.
|
|
57
|
+
* @property {Array<string>} [context] A collection of scopes for the reference
|
|
58
|
+
* @property {string} [workbookName] A context workbook scope
|
|
59
|
+
* @property {string} [sheetName] A context sheet scope
|
|
60
|
+
* @property {RangeR1C1} [range] The reference's range
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {Record<string, any>} ReferenceStruct
|
|
65
|
+
* A reference containing a table slice definition. See [Prefixes.md] for
|
|
66
|
+
* documentation on how scopes work in Fx.
|
|
67
|
+
* @property {Array<string>} [context] A collection of scopes for the reference
|
|
68
|
+
* @property {string} [workbookName] A context workbook scope
|
|
69
|
+
* @property {string} [sheetName] A context sheet scope
|
|
70
|
+
* @property {Array<string>} [sections] The sections this reference targets
|
|
71
|
+
* @property {Array<string>} [columns] The sections this reference targets
|
|
72
|
+
* @property {string} [table] The table this reference targets
|
|
73
|
+
*/
|
package/lib/fixRanges.js
CHANGED
|
@@ -40,19 +40,19 @@ import { REF_STRUCT } from './constants.js';
|
|
|
40
40
|
* Returns the same formula with the ranges updated. If an array of tokens was
|
|
41
41
|
* supplied, then a new array is returned.
|
|
42
42
|
*
|
|
43
|
-
* @param {(string | Array<
|
|
44
|
-
* @param {
|
|
43
|
+
* @param {(string | Array<Token>)} formula A string (an Excel formula) or a token list that should be adjusted.
|
|
44
|
+
* @param {object} [options={}] Options
|
|
45
45
|
* @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.
|
|
46
46
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
47
|
-
* @
|
|
47
|
+
* @returns {(string | Array<Token>)} A formula string or token list (depending on which was input)
|
|
48
48
|
*/
|
|
49
|
-
export function fixRanges (
|
|
50
|
-
if (typeof
|
|
51
|
-
return fixRanges(tokenize(
|
|
49
|
+
export function fixRanges (formula, options = { addBounds: false }) {
|
|
50
|
+
if (typeof formula === 'string') {
|
|
51
|
+
return fixRanges(tokenize(formula, options), options)
|
|
52
52
|
.map(d => d.value)
|
|
53
53
|
.join('');
|
|
54
54
|
}
|
|
55
|
-
if (!Array.isArray(
|
|
55
|
+
if (!Array.isArray(formula)) {
|
|
56
56
|
throw new Error('fixRanges expects an array of tokens');
|
|
57
57
|
}
|
|
58
58
|
const { addBounds, r1c1, xlsx } = options;
|
|
@@ -60,7 +60,7 @@ export function fixRanges (tokens, options = { addBounds: false }) {
|
|
|
60
60
|
throw new Error('fixRanges does not have an R1C1 mode');
|
|
61
61
|
}
|
|
62
62
|
let offsetSkew = 0;
|
|
63
|
-
return
|
|
63
|
+
return formula.map(t => {
|
|
64
64
|
const token = { ...t };
|
|
65
65
|
if (t.loc) {
|
|
66
66
|
token.loc = [ ...t.loc ];
|
package/lib/isType.js
CHANGED
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
* (`A1` or `A1:B2`), REF_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), or
|
|
13
13
|
* REF_BEAM (`A:A` or `1:1`). In all other cases `false` is returned.
|
|
14
14
|
*
|
|
15
|
-
* @param {
|
|
16
|
-
* @
|
|
15
|
+
* @param {any} token A token
|
|
16
|
+
* @returns {boolean} True if the specified token is range, False otherwise.
|
|
17
17
|
*/
|
|
18
18
|
export function isRange (token) {
|
|
19
19
|
return !!token && (
|
|
@@ -30,8 +30,8 @@ export function isRange (token) {
|
|
|
30
30
|
* REF_TERNARY (`A1:A`, `A1:1`, `1:A1`, or `A:A1`), REF_BEAM (`A:A` or `1:1`),
|
|
31
31
|
* or REF_NAMED (`myrange`). In all other cases `false` is returned.
|
|
32
32
|
*
|
|
33
|
-
* @param {
|
|
34
|
-
* @
|
|
33
|
+
* @param {any} token The token
|
|
34
|
+
* @returns {boolean} True if the specified token is reference, False otherwise.
|
|
35
35
|
*/
|
|
36
36
|
export function isReference (token) {
|
|
37
37
|
return !!token && (
|
|
@@ -50,8 +50,8 @@ export function isReference (token) {
|
|
|
50
50
|
* ERROR (`#VALUE!`), NUMBER (123.4), or STRING (`"lorem ipsum"`). In all other
|
|
51
51
|
* cases `false` is returned.
|
|
52
52
|
*
|
|
53
|
-
* @param {
|
|
54
|
-
* @
|
|
53
|
+
* @param {any} token The token
|
|
54
|
+
* @returns {boolean} True if the specified token is literal, False otherwise.
|
|
55
55
|
*/
|
|
56
56
|
export function isLiteral (token) {
|
|
57
57
|
return !!token && (
|
|
@@ -68,8 +68,8 @@ export function isLiteral (token) {
|
|
|
68
68
|
* Returns `true` if the input is a token of type ERROR (`#VALUE!`). In all
|
|
69
69
|
* other cases `false` is returned.
|
|
70
70
|
*
|
|
71
|
-
* @param {
|
|
72
|
-
* @
|
|
71
|
+
* @param {any} token The token
|
|
72
|
+
* @returns {boolean} True if the specified token is error, False otherwise.
|
|
73
73
|
*/
|
|
74
74
|
export function isError (token) {
|
|
75
75
|
return !!token && token.type === ERROR;
|
|
@@ -81,8 +81,8 @@ export function isError (token) {
|
|
|
81
81
|
* Returns `true` if the input is a token of type WHITESPACE (` `) or
|
|
82
82
|
* NEWLINE (`\n`). In all other cases `false` is returned.
|
|
83
83
|
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @
|
|
84
|
+
* @param {any} token The token
|
|
85
|
+
* @returns {boolean} True if the specified token is whitespace, False otherwise.
|
|
86
86
|
*/
|
|
87
87
|
export function isWhitespace (token) {
|
|
88
88
|
return !!token && (
|
|
@@ -97,8 +97,8 @@ export function isWhitespace (token) {
|
|
|
97
97
|
* Returns `true` if the input is a token of type FUNCTION.
|
|
98
98
|
* In all other cases `false` is returned.
|
|
99
99
|
*
|
|
100
|
-
* @param {
|
|
101
|
-
* @
|
|
100
|
+
* @param {any} token The token
|
|
101
|
+
* @returns {boolean} True if the specified token is function, False otherwise.
|
|
102
102
|
*/
|
|
103
103
|
export function isFunction (token) {
|
|
104
104
|
return !!token && token.type === FUNCTION;
|
|
@@ -108,8 +108,8 @@ export function isFunction (token) {
|
|
|
108
108
|
* Returns `true` if the input is a token of type FX_PREFIX (leading `=` in
|
|
109
109
|
* formula). In all other cases `false` is returned.
|
|
110
110
|
*
|
|
111
|
-
* @param {
|
|
112
|
-
* @
|
|
111
|
+
* @param {any} token The token
|
|
112
|
+
* @returns {boolean} True if the specified token is effects prefix, False otherwise.
|
|
113
113
|
*/
|
|
114
114
|
export function isFxPrefix (token) {
|
|
115
115
|
return !!token && token.type === FX_PREFIX;
|
|
@@ -121,8 +121,8 @@ export function isFxPrefix (token) {
|
|
|
121
121
|
* Returns `true` if the input is a token of type OPERATOR (`+` or `:`). In all
|
|
122
122
|
* other cases `false` is returned.
|
|
123
123
|
*
|
|
124
|
-
* @param {
|
|
125
|
-
* @
|
|
124
|
+
* @param {any} token The token
|
|
125
|
+
* @returns {boolean} True if the specified token is operator, False otherwise.
|
|
126
126
|
*/
|
|
127
127
|
export function isOperator (token) {
|
|
128
128
|
return !!token && token.type === OPERATOR;
|
package/lib/lexer-srefs.spec.js
CHANGED
|
@@ -315,5 +315,10 @@ test('tokenize structured references (merges on)', t => {
|
|
|
315
315
|
{ type: FX_PREFIX, value: '=' },
|
|
316
316
|
{ type: REF_STRUCT, value: '\'Sales - May2020.xlsx\'!Table1[ [#Data], [#Totals], [Surf]:[Rod] ]' }
|
|
317
317
|
]);
|
|
318
|
+
|
|
319
|
+
t.isTokens('=[myworkbook.xlsx]Sheet1!TMP8w0habhr[#All]', [
|
|
320
|
+
{ type: FX_PREFIX, value: '=' },
|
|
321
|
+
{ type: REF_STRUCT, value: '[myworkbook.xlsx]Sheet1!TMP8w0habhr[#All]' }
|
|
322
|
+
]);
|
|
318
323
|
t.end();
|
|
319
324
|
});
|
package/lib/lexer.js
CHANGED
|
@@ -196,14 +196,14 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
196
196
|
*
|
|
197
197
|
* @see tokenTypes
|
|
198
198
|
* @param {string} formula An Excel formula string (an Excel expression) or an array of tokens.
|
|
199
|
-
* @param {
|
|
199
|
+
* @param {object} [options={}] Options
|
|
200
200
|
* @param {boolean} [options.allowTernary=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.
|
|
201
201
|
* @param {boolean} [options.negativeNumbers=true] Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1` (alternatively these will be unary operations in the tree).
|
|
202
202
|
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
203
203
|
* @param {boolean} [options.withLocation=true] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
204
204
|
* @param {boolean} [options.mergeRefs=true] Should ranges be returned as whole references (`Sheet1!A1:B2`) or as separate tokens for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`). This is the same as calling [`mergeRefTokens`](#mergeRefTokens)
|
|
205
205
|
* @param {boolean} [options.xlsx=false] Enables a `[1]Sheet1!A1` or `[1]!name` syntax form for external workbooks found only in XLSX files.
|
|
206
|
-
* @
|
|
206
|
+
* @returns {Array<Token>} An AST of nodes
|
|
207
207
|
*/
|
|
208
208
|
export function tokenize (formula, options = {}) {
|
|
209
209
|
return getTokens(formula, lexers, options);
|
package/lib/lexerParts.js
CHANGED
|
@@ -34,16 +34,17 @@ const re_CONTEXT_QUOTE = /^'(?:''|[^'])*('|$)(?=!)/;
|
|
|
34
34
|
const rngPart = '\\$?[A-Z]{1,3}\\$?[1-9][0-9]{0,6}';
|
|
35
35
|
const colPart = '\\$?[A-Z]{1,3}';
|
|
36
36
|
const rowPart = '\\$?[1-9][0-9]{0,6}';
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
37
|
+
const nextNotChar = '(?![a-z0-9_\\u00a1-\\uffff])';
|
|
38
|
+
const re_A1COL = new RegExp(`^${colPart}:${colPart}${nextNotChar}`, 'i');
|
|
39
|
+
const re_A1ROW = new RegExp(`^${rowPart}:${rowPart}${nextNotChar}`, 'i');
|
|
40
|
+
const re_A1RANGE = new RegExp(`^${rngPart}${nextNotChar}`, 'i');
|
|
40
41
|
const re_A1PARTIAL = new RegExp(`^((${colPart}|${rowPart}):${rngPart}|${rngPart}:(${colPart}|${rowPart}))(?![\\w($.])`, 'i');
|
|
41
42
|
const rPart = '(?:R(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,6})?)';
|
|
42
43
|
const cPart = '(?:C(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,4})?)';
|
|
43
|
-
const re_RCCOL = new RegExp(`^${cPart}(:${cPart})
|
|
44
|
-
const re_RCROW = new RegExp(`^${rPart}(:${rPart})
|
|
45
|
-
const re_RCRANGE = new RegExp(`^(?:(?=[RC])${rPart}${cPart})`, 'i');
|
|
46
|
-
const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(:${cPart}|:${rPart})(?![[\\d])|(${rPart}|${cPart})(:${rPart}${cPart}))
|
|
44
|
+
const re_RCCOL = new RegExp(`^${cPart}(:${cPart})?${nextNotChar}`, 'i');
|
|
45
|
+
const re_RCROW = new RegExp(`^${rPart}(:${rPart})?${nextNotChar}`, 'i');
|
|
46
|
+
const re_RCRANGE = new RegExp(`^(?:(?=[RC])${rPart}${cPart})${nextNotChar}`, 'i');
|
|
47
|
+
const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(:${cPart}|:${rPart})(?![[\\d])|(${rPart}|${cPart})(:${rPart}${cPart}))${nextNotChar}`, 'i');
|
|
47
48
|
|
|
48
49
|
// The advertized named ranges rules are a bit off from what Excel seems to do:
|
|
49
50
|
// in the "extended range" of chars, it looks like it allows most things above
|
|
@@ -51,7 +52,7 @@ const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(:${cPart}|:${rPart})(?![[\\d
|
|
|
51
52
|
// eslint-disable-next-line
|
|
52
53
|
// const re_NAMED = /^[a-zA-Z\\_¡¤§¨ª\u00ad¯\u00b0-\uffff][a-zA-Z0-9\\_.?¡¤§¨ª\u00ad¯\u00b0-\uffff]{0,254}/i;
|
|
53
54
|
// I've simplified to allowing everything above U+00A1:
|
|
54
|
-
const re_NAMED = /^
|
|
55
|
+
const re_NAMED = /^[a-zA-Z\\_\u00a1-\uffff][a-zA-Z0-9\\_.?\u00a1-\uffff]{0,254}/i;
|
|
55
56
|
|
|
56
57
|
function makeHandler (type, re) {
|
|
57
58
|
return str => {
|
|
@@ -62,6 +63,17 @@ function makeHandler (type, re) {
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function lexNamed (str) {
|
|
67
|
+
const m = re_NAMED.exec(str);
|
|
68
|
+
if (m) {
|
|
69
|
+
const lc = m[0].toLowerCase();
|
|
70
|
+
if (lc === 'r' || lc === 'c') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return { type: REF_NAMED, value: m[0] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
const re_QUOTED_VALUE = /^'(?:[^[\]]+?)?(?:\[(.+?)\])?(?:[^[\]]+?)'$/;
|
|
66
78
|
const re_QUOTED_VALUE_XLSX = /^'\[(.+?)\]'$/;
|
|
67
79
|
function lexContext (str, options) {
|
|
@@ -130,9 +142,9 @@ function lexRange (str, options) {
|
|
|
130
142
|
if (t) {
|
|
131
143
|
reRCNums.lastIndex = 0;
|
|
132
144
|
while ((m = reRCNums.exec(t.value)) !== null) {
|
|
133
|
-
const x = (m[1] === 'R' ? MAX_ROWS : MAX_COLS) + (m[2] ?
|
|
145
|
+
const x = (m[1] === 'R' ? MAX_ROWS : MAX_COLS) + (m[2] ? 0 : 1);
|
|
134
146
|
const val = parseInt(m[3], 10);
|
|
135
|
-
if (val
|
|
147
|
+
if (val > x || val < -x) {
|
|
136
148
|
return null;
|
|
137
149
|
}
|
|
138
150
|
}
|
|
@@ -192,7 +204,7 @@ export const lexers = [
|
|
|
192
204
|
lexRange,
|
|
193
205
|
lexStructured,
|
|
194
206
|
makeHandler(NUMBER, re_NUMBER),
|
|
195
|
-
|
|
207
|
+
lexNamed
|
|
196
208
|
];
|
|
197
209
|
|
|
198
210
|
export const lexersRefs = [
|
|
@@ -200,5 +212,5 @@ export const lexersRefs = [
|
|
|
200
212
|
lexContext,
|
|
201
213
|
lexRange,
|
|
202
214
|
lexStructured,
|
|
203
|
-
|
|
215
|
+
lexNamed
|
|
204
216
|
];
|
package/lib/mergeRefTokens.js
CHANGED
|
@@ -63,8 +63,8 @@ const matcher = (tokens, currNode, anchorIndex, index = 0) => {
|
|
|
63
63
|
* as whole references (`Sheet1!A1:B2`) rather than separate tokens for each
|
|
64
64
|
* part: (`Sheet1`,`!`,`A1`,`:`,`B2`).
|
|
65
65
|
*
|
|
66
|
-
* @param {Array<
|
|
67
|
-
* @
|
|
66
|
+
* @param {Array<Token>} tokenlist An array of tokens (from `tokenize()`)
|
|
67
|
+
* @returns {Array<Token>} A new list of tokens with range parts merged.
|
|
68
68
|
*/
|
|
69
69
|
export function mergeRefTokens (tokenlist) {
|
|
70
70
|
const finalTokens = [];
|
package/lib/parseRef.spec.js
CHANGED
|
@@ -21,40 +21,51 @@ test('splitPrefix', t => {
|
|
|
21
21
|
testStr('[foo][bar][baz]', false, [
|
|
22
22
|
{ value: 'foo', braced: true },
|
|
23
23
|
{ value: 'bar', braced: true },
|
|
24
|
-
{ value: 'baz', braced: true }
|
|
24
|
+
{ value: 'baz', braced: true }
|
|
25
|
+
]);
|
|
25
26
|
testStr('foo[bar][baz]', false, [
|
|
26
27
|
{ value: 'foo', braced: false },
|
|
27
28
|
{ value: 'bar', braced: true },
|
|
28
|
-
{ value: 'baz', braced: true }
|
|
29
|
+
{ value: 'baz', braced: true }
|
|
30
|
+
]);
|
|
29
31
|
testStr('[foo]bar[baz]', false, [
|
|
30
32
|
{ value: 'foo', braced: true },
|
|
31
33
|
{ value: 'bar', braced: false },
|
|
32
|
-
{ value: 'baz', braced: true }
|
|
34
|
+
{ value: 'baz', braced: true }
|
|
35
|
+
]);
|
|
33
36
|
testStr('[foo][bar]baz', false, [
|
|
34
37
|
{ value: 'foo', braced: true },
|
|
35
38
|
{ value: 'bar', braced: true },
|
|
36
|
-
{ value: 'baz', braced: false }
|
|
39
|
+
{ value: 'baz', braced: false }
|
|
40
|
+
]);
|
|
37
41
|
testStr('foo[bar]baz', false, [
|
|
38
42
|
{ value: 'foo', braced: false },
|
|
39
43
|
{ value: 'bar', braced: true },
|
|
40
|
-
{ value: 'baz', braced: false }
|
|
44
|
+
{ value: 'baz', braced: false }
|
|
45
|
+
]);
|
|
41
46
|
testStr('[foo]bar[baz]', false, [
|
|
42
47
|
{ value: 'foo', braced: true },
|
|
43
48
|
{ value: 'bar', braced: false },
|
|
44
|
-
{ value: 'baz', braced: true }
|
|
49
|
+
{ value: 'baz', braced: true }
|
|
50
|
+
]);
|
|
45
51
|
testStr('[foo]bar', false, [
|
|
46
52
|
{ value: 'foo', braced: true },
|
|
47
|
-
{ value: 'bar', braced: false }
|
|
53
|
+
{ value: 'bar', braced: false }
|
|
54
|
+
]);
|
|
48
55
|
testStr('foo[bar]', false, [
|
|
49
56
|
{ value: 'foo', braced: false },
|
|
50
|
-
{ value: 'bar', braced: true }
|
|
57
|
+
{ value: 'bar', braced: true }
|
|
58
|
+
]);
|
|
51
59
|
testStr('[foo][bar]', false, [
|
|
52
60
|
{ value: 'foo', braced: true },
|
|
53
|
-
{ value: 'bar', braced: true }
|
|
61
|
+
{ value: 'bar', braced: true }
|
|
62
|
+
]);
|
|
54
63
|
testStr('[foo]', false, [
|
|
55
|
-
{ value: 'foo', braced: true }
|
|
64
|
+
{ value: 'foo', braced: true }
|
|
65
|
+
]);
|
|
56
66
|
testStr('foo', false, [
|
|
57
|
-
{ value: 'foo', braced: false }
|
|
67
|
+
{ value: 'foo', braced: false }
|
|
68
|
+
]);
|
|
58
69
|
|
|
59
70
|
t.end();
|
|
60
71
|
});
|
package/lib/parser.js
CHANGED
|
@@ -478,8 +478,8 @@ prefix('{', function () {
|
|
|
478
478
|
* [AST_format.md](./AST_format.md)
|
|
479
479
|
*
|
|
480
480
|
* @see nodeTypes
|
|
481
|
-
* @param {(string | Array<
|
|
482
|
-
* @param {
|
|
481
|
+
* @param {(string | Array<Token>)} formula An Excel formula string (an Excel expression) or an array of tokens.
|
|
482
|
+
* @param {object} [options={}] Options
|
|
483
483
|
* @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
|
|
484
484
|
* @param {boolean} [options.allowTernary=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.
|
|
485
485
|
* @param {boolean} [options.negativeNumbers=true] Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1` (alternatively these will be unary operations in the tree).
|
|
@@ -488,18 +488,18 @@ prefix('{', function () {
|
|
|
488
488
|
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
489
489
|
* @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
490
490
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
491
|
-
* @
|
|
491
|
+
* @returns {object} An AST of nodes
|
|
492
492
|
*/
|
|
493
|
-
export function parse (
|
|
494
|
-
if (typeof
|
|
495
|
-
tokens = tokenize(
|
|
493
|
+
export function parse (formula, options) {
|
|
494
|
+
if (typeof formula === 'string') {
|
|
495
|
+
tokens = tokenize(formula, {
|
|
496
496
|
withLocation: false,
|
|
497
497
|
...options,
|
|
498
498
|
mergeRefs: true
|
|
499
499
|
});
|
|
500
500
|
}
|
|
501
|
-
else if (Array.isArray(
|
|
502
|
-
tokens =
|
|
501
|
+
else if (Array.isArray(formula)) {
|
|
502
|
+
tokens = formula;
|
|
503
503
|
}
|
|
504
504
|
else {
|
|
505
505
|
throw new Error('Parse requires a string or array of tokens.');
|