@borgar/fx 4.7.1 → 4.9.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/dist/fx.d.ts +86 -1
- package/dist/fx.js +1 -1
- package/docs/API.md +171 -7
- package/docs/AST_format.md +45 -15
- package/lib/astTypes.js +96 -0
- package/lib/constants.js +3 -0
- package/lib/fixRanges.js +3 -2
- package/lib/lexer.js +51 -0
- package/lib/lexer.spec.js +63 -0
- package/lib/lexerParts.js +5 -0
- package/lib/parser.js +235 -33
- package/lib/parser.spec.js +380 -89
- package/lib/sr.js +5 -3
- package/lib/sr.spec.js +50 -0
- package/package.json +1 -1
package/lib/parser.js
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
* Beutiful Code (http://crockford.com/javascript/tdop/tdop.html).
|
|
7
7
|
*
|
|
8
8
|
* The parser handles most basic things Excel/Sheets do except:
|
|
9
|
-
*
|
|
10
|
-
* - LAMBDA expressions: =LAMBDA(x, x*x)(2)
|
|
11
|
-
* https://support.microsoft.com/en-us/office/lambda-function-bd212d27-1cd1-4321-a34a-ccbf254b8b67
|
|
12
|
-
* - LET expressions: LET(x, 5, SUM(x, 1))
|
|
13
|
-
* https://support.microsoft.com/en-us/office/let-function-34842dd8-b92b-4d3f-b325-b8b8f9908999
|
|
14
|
-
* - Sheet1:Sheet2!A1 references cross contexts (3D references)
|
|
9
|
+
* `Sheet1:Sheet2!A1` references cross contexts (3D references)
|
|
15
10
|
*/
|
|
16
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isReference,
|
|
13
|
+
isLiteral,
|
|
14
|
+
isFunction,
|
|
15
|
+
isWhitespace,
|
|
16
|
+
isFxPrefix,
|
|
17
|
+
isOperator,
|
|
18
|
+
isError
|
|
19
|
+
} from './isType.js';
|
|
17
20
|
import {
|
|
18
21
|
UNARY,
|
|
19
22
|
BINARY,
|
|
@@ -21,12 +24,18 @@ import {
|
|
|
21
24
|
LITERAL,
|
|
22
25
|
ERROR_LITERAL,
|
|
23
26
|
CALL,
|
|
27
|
+
LAMBDA,
|
|
24
28
|
ARRAY,
|
|
25
29
|
IDENTIFIER,
|
|
26
30
|
NUMBER,
|
|
27
31
|
BOOLEAN,
|
|
28
32
|
ERROR,
|
|
29
|
-
STRING
|
|
33
|
+
STRING,
|
|
34
|
+
LET,
|
|
35
|
+
LET_DECL,
|
|
36
|
+
REF_NAMED,
|
|
37
|
+
REF_STRUCT,
|
|
38
|
+
REF_BEAM
|
|
30
39
|
} from './constants.js';
|
|
31
40
|
|
|
32
41
|
import { tokenize } from './lexer.js';
|
|
@@ -53,12 +62,24 @@ const refFunctions = [
|
|
|
53
62
|
'XLOOKUP'
|
|
54
63
|
];
|
|
55
64
|
|
|
56
|
-
const
|
|
65
|
+
const isReferenceFunctionName = fnName => {
|
|
66
|
+
return refFunctions.includes(fnName.toUpperCase());
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isReferenceToken = (token, allowOperators = false) => {
|
|
57
70
|
const value = (token && token.value) + '';
|
|
58
|
-
if (isReference(token)) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
71
|
+
if (isReference(token)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (allowOperators && isOperator(token) && (value === ':' || value === ',' || !value.trim())) {
|
|
75
|
+
return true; // join, union, intersection
|
|
76
|
+
}
|
|
77
|
+
if (isFunction(token) && isReferenceFunctionName(value)) {
|
|
78
|
+
return true; // function that yields reference
|
|
79
|
+
}
|
|
80
|
+
if (isError(token) && value === '#REF!') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
62
83
|
return false;
|
|
63
84
|
};
|
|
64
85
|
|
|
@@ -71,7 +92,8 @@ const isReferenceNode = node => {
|
|
|
71
92
|
node.operator === ' ' ||
|
|
72
93
|
node.operator === ',')
|
|
73
94
|
) ||
|
|
74
|
-
(node
|
|
95
|
+
isReference(node) ||
|
|
96
|
+
(node.type === CALL && isReferenceFunctionName(node.callee.name))
|
|
75
97
|
);
|
|
76
98
|
};
|
|
77
99
|
|
|
@@ -82,15 +104,17 @@ let tokenIndex;
|
|
|
82
104
|
let permitArrayRanges = false;
|
|
83
105
|
let permitArrayCalls = false;
|
|
84
106
|
|
|
85
|
-
function halt (message) {
|
|
107
|
+
function halt (message, atIndex = null) {
|
|
86
108
|
const err = new Error(message);
|
|
87
109
|
err.source = tokens.map(d => d.value).join('');
|
|
88
|
-
err.sourceOffset = tokens
|
|
110
|
+
err.sourceOffset = tokens
|
|
111
|
+
.slice(0, atIndex ?? tokenIndex)
|
|
112
|
+
.reduce((a, d) => a + d.value.length, 0);
|
|
89
113
|
throw err;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
// A1 A1 | A1 (A1) | A1 ((A1)) | A1 ( (A1) ) | ...
|
|
93
|
-
function refIsUpcoming () {
|
|
117
|
+
function refIsUpcoming (allowOperators = false) {
|
|
94
118
|
let i = tokenIndex;
|
|
95
119
|
let next;
|
|
96
120
|
do {
|
|
@@ -102,7 +126,7 @@ function refIsUpcoming () {
|
|
|
102
126
|
(isOperator(next) && next.value === '(')
|
|
103
127
|
)
|
|
104
128
|
);
|
|
105
|
-
return isReferenceToken(next);
|
|
129
|
+
return isReferenceToken(next, allowOperators);
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
function advance (expectNext = null, leftNode = null) {
|
|
@@ -111,9 +135,11 @@ function advance (expectNext = null, leftNode = null) {
|
|
|
111
135
|
}
|
|
112
136
|
// look ahead to see if we have ( ( " ", "(" )+ REF )
|
|
113
137
|
if (isWhitespace(tokens[tokenIndex])) {
|
|
114
|
-
// potential intersection operation
|
|
115
|
-
const
|
|
116
|
-
|
|
138
|
+
// potential intersection operation (so don't allow operators as upcoming)
|
|
139
|
+
const haveRef = isReferenceNode(leftNode);
|
|
140
|
+
const possibleWSOp = haveRef && refIsUpcoming(false);
|
|
141
|
+
const nextIsCall = haveRef && tokens[tokenIndex + 1].value === '(';
|
|
142
|
+
if (!possibleWSOp && !nextIsCall) {
|
|
117
143
|
// ignore whitespace
|
|
118
144
|
while (isWhitespace(tokens[tokenIndex])) {
|
|
119
145
|
tokenIndex++;
|
|
@@ -134,7 +160,6 @@ function advance (expectNext = null, leftNode = null) {
|
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
let node;
|
|
137
|
-
let type = token.type;
|
|
138
163
|
if (isOperator(token)) {
|
|
139
164
|
node = symbolTable[token.value];
|
|
140
165
|
if (!node) {
|
|
@@ -149,7 +174,6 @@ function advance (expectNext = null, leftNode = null) {
|
|
|
149
174
|
}
|
|
150
175
|
else if (isReference(token)) {
|
|
151
176
|
node = symbolTable[REFERENCE];
|
|
152
|
-
type = REFERENCE;
|
|
153
177
|
}
|
|
154
178
|
else if (isFunction(token)) {
|
|
155
179
|
node = symbolTable[FUNCTION];
|
|
@@ -159,7 +183,7 @@ function advance (expectNext = null, leftNode = null) {
|
|
|
159
183
|
}
|
|
160
184
|
|
|
161
185
|
currentNode = Object.create(node);
|
|
162
|
-
currentNode.type = type;
|
|
186
|
+
currentNode.type = token.type;
|
|
163
187
|
currentNode.value = token.value;
|
|
164
188
|
if (token.loc) {
|
|
165
189
|
currentNode.loc = [ ...token.loc ];
|
|
@@ -289,7 +313,7 @@ const unionRefs = enable => {
|
|
|
289
313
|
|
|
290
314
|
// arithmetic and string operations
|
|
291
315
|
postfix('%'); // percent
|
|
292
|
-
postfix('#', function (left) {
|
|
316
|
+
postfix('#', function (left) { // spilled range (_xlfn.ANCHORARRAY)
|
|
293
317
|
if (!isReferenceNode(left)) {
|
|
294
318
|
halt('# expects a reference');
|
|
295
319
|
}
|
|
@@ -340,6 +364,18 @@ symbol(LITERAL).nud = function () {
|
|
|
340
364
|
return this;
|
|
341
365
|
};
|
|
342
366
|
symbol(REFERENCE).nud = function () {
|
|
367
|
+
if (this.type === REF_NAMED) {
|
|
368
|
+
this.kind = 'name';
|
|
369
|
+
}
|
|
370
|
+
else if (this.type === REF_STRUCT) {
|
|
371
|
+
this.kind = 'table'; // structured ?
|
|
372
|
+
}
|
|
373
|
+
else if (this.type === REF_BEAM) {
|
|
374
|
+
this.kind = 'beam';
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
this.kind = 'range';
|
|
378
|
+
}
|
|
343
379
|
this.type = REFERENCE;
|
|
344
380
|
return this;
|
|
345
381
|
};
|
|
@@ -359,8 +395,35 @@ symbol(FUNCTION).nud = function () {
|
|
|
359
395
|
return this;
|
|
360
396
|
};
|
|
361
397
|
infix('(', 90, function (left) {
|
|
398
|
+
let callee = {
|
|
399
|
+
type: IDENTIFIER,
|
|
400
|
+
name: left.value
|
|
401
|
+
};
|
|
362
402
|
if (left.id !== FUNCTION) {
|
|
363
|
-
|
|
403
|
+
if (
|
|
404
|
+
left.type === LAMBDA ||
|
|
405
|
+
// Excel only allows calls to "names" and ref functions. Since we don't
|
|
406
|
+
// differentiate between the two (this requires a table of function names)
|
|
407
|
+
// we're overly permissive here:
|
|
408
|
+
left.type === CALL ||
|
|
409
|
+
left.type === LET ||
|
|
410
|
+
left.type === REFERENCE ||
|
|
411
|
+
(left.type === UNARY && left.value === '#') || // Because it's really SINGLE(...)()
|
|
412
|
+
(left.type === ERROR_LITERAL && left.value === '#REF!')
|
|
413
|
+
) {
|
|
414
|
+
// in the case of REFERENCE, do we want to set the node to Identifier?
|
|
415
|
+
callee = left;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
halt('Unexpected call', tokenIndex - 1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const lcFn = left.value.toLowerCase();
|
|
422
|
+
if (lcFn === 'lambda') {
|
|
423
|
+
return parseLambda.call(this, left);
|
|
424
|
+
}
|
|
425
|
+
if (lcFn === 'let') {
|
|
426
|
+
return parseLet.call(this, left);
|
|
364
427
|
}
|
|
365
428
|
const args = [];
|
|
366
429
|
let lastWasComma = false;
|
|
@@ -393,10 +456,7 @@ infix('(', 90, function (left) {
|
|
|
393
456
|
const closeParen = currentNode;
|
|
394
457
|
delete this.value;
|
|
395
458
|
this.type = CALL;
|
|
396
|
-
this.callee =
|
|
397
|
-
type: IDENTIFIER,
|
|
398
|
-
name: left.value
|
|
399
|
-
};
|
|
459
|
+
this.callee = callee;
|
|
400
460
|
if (left.loc) {
|
|
401
461
|
this.callee.loc = [ ...left.loc ];
|
|
402
462
|
}
|
|
@@ -408,6 +468,149 @@ infix('(', 90, function (left) {
|
|
|
408
468
|
return this;
|
|
409
469
|
});
|
|
410
470
|
|
|
471
|
+
function parseLambda (left) {
|
|
472
|
+
const args = [];
|
|
473
|
+
const argNames = {};
|
|
474
|
+
let body;
|
|
475
|
+
let done = false;
|
|
476
|
+
const prevState = unionRefs(false);
|
|
477
|
+
if (currentNode.id !== ')') {
|
|
478
|
+
while (!done) {
|
|
479
|
+
if (isWhitespace(currentNode)) {
|
|
480
|
+
advance();
|
|
481
|
+
}
|
|
482
|
+
const argTokenIndex = tokenIndex;
|
|
483
|
+
const arg = expression(0);
|
|
484
|
+
if (currentNode.id === ',') {
|
|
485
|
+
// all but last args must be names
|
|
486
|
+
if (arg.type === REFERENCE && arg.kind === 'name') {
|
|
487
|
+
// names may not be duplicates
|
|
488
|
+
const currName = arg.value.toLowerCase();
|
|
489
|
+
if (currName in argNames) {
|
|
490
|
+
halt('Duplicate name: ' + arg.value);
|
|
491
|
+
}
|
|
492
|
+
argNames[currName] = 1;
|
|
493
|
+
const a = { type: IDENTIFIER, name: arg.value };
|
|
494
|
+
if (arg.loc) { a.loc = arg.loc; }
|
|
495
|
+
args.push(a);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
tokenIndex = argTokenIndex;
|
|
499
|
+
halt('LAMBDA argument is not a name');
|
|
500
|
+
}
|
|
501
|
+
advance(',');
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
body = arg;
|
|
505
|
+
done = true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
unionRefs(prevState);
|
|
510
|
+
delete this.value;
|
|
511
|
+
this.type = LAMBDA;
|
|
512
|
+
this.params = args;
|
|
513
|
+
this.body = body || null;
|
|
514
|
+
if (left.loc) {
|
|
515
|
+
this.loc = [ left.loc[0], currentNode.loc[1] ];
|
|
516
|
+
}
|
|
517
|
+
advance(')', this);
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function parseLet (left) {
|
|
522
|
+
const args = [];
|
|
523
|
+
const vals = [];
|
|
524
|
+
const argNames = {};
|
|
525
|
+
let body;
|
|
526
|
+
let argCounter = 0;
|
|
527
|
+
const addArgument = (arg, lastArg) => {
|
|
528
|
+
if (body) {
|
|
529
|
+
halt('Unexpected argument following calculation');
|
|
530
|
+
}
|
|
531
|
+
if (lastArg && argCounter >= 2) {
|
|
532
|
+
body = arg;
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
const wantName = !(argCounter % 2);
|
|
536
|
+
if (wantName) {
|
|
537
|
+
if (arg && (arg.type === REFERENCE && arg.kind === 'name')) {
|
|
538
|
+
// names may not be duplicates
|
|
539
|
+
const currName = arg.value.toLowerCase();
|
|
540
|
+
if (currName in argNames) {
|
|
541
|
+
halt('Duplicate name: ' + arg.value);
|
|
542
|
+
}
|
|
543
|
+
argNames[currName] = 1;
|
|
544
|
+
args.push({ type: IDENTIFIER, name: arg.value, loc: arg.loc });
|
|
545
|
+
}
|
|
546
|
+
else if (argCounter >= 2) {
|
|
547
|
+
body = arg;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
halt('Argument is not a name');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
vals.push(arg);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
argCounter++;
|
|
558
|
+
};
|
|
559
|
+
const prevState = unionRefs(false);
|
|
560
|
+
let lastWasComma = false;
|
|
561
|
+
if (currentNode.id !== ')') {
|
|
562
|
+
while (currentNode.id !== ')') {
|
|
563
|
+
if (isWhitespace(currentNode)) {
|
|
564
|
+
advance();
|
|
565
|
+
}
|
|
566
|
+
if (currentNode.id === ',') {
|
|
567
|
+
addArgument(null);
|
|
568
|
+
lastWasComma = true;
|
|
569
|
+
advance();
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const arg = expression(0);
|
|
573
|
+
addArgument(arg, currentNode.id !== ',');
|
|
574
|
+
lastWasComma = false;
|
|
575
|
+
if (currentNode.id === ',') {
|
|
576
|
+
advance(',');
|
|
577
|
+
lastWasComma = true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
unionRefs(prevState);
|
|
582
|
+
}
|
|
583
|
+
if (lastWasComma) {
|
|
584
|
+
addArgument(null, true);
|
|
585
|
+
}
|
|
586
|
+
// eslint-disable-next-line no-undefined
|
|
587
|
+
if (body === undefined) {
|
|
588
|
+
halt('Unexpected end of arguments');
|
|
589
|
+
}
|
|
590
|
+
unionRefs(prevState);
|
|
591
|
+
delete this.value;
|
|
592
|
+
this.type = LET;
|
|
593
|
+
this.declarations = [];
|
|
594
|
+
if (!args.length) {
|
|
595
|
+
halt('Unexpected end of arguments');
|
|
596
|
+
}
|
|
597
|
+
for (let i = 0; i < args.length; i++) {
|
|
598
|
+
const s = {
|
|
599
|
+
type: LET_DECL,
|
|
600
|
+
id: args[i],
|
|
601
|
+
init: vals[i],
|
|
602
|
+
loc: args[i].loc && [ args[i].loc[0], vals[i].loc[1] ]
|
|
603
|
+
};
|
|
604
|
+
this.declarations.push(s);
|
|
605
|
+
}
|
|
606
|
+
this.body = body;
|
|
607
|
+
if (left.loc) {
|
|
608
|
+
this.loc = [ left.loc[0], currentNode.loc[1] ];
|
|
609
|
+
}
|
|
610
|
+
advance(')', this);
|
|
611
|
+
return this;
|
|
612
|
+
}
|
|
613
|
+
|
|
411
614
|
// array literal
|
|
412
615
|
symbol('}');
|
|
413
616
|
symbol(';');
|
|
@@ -437,7 +640,6 @@ prefix('{', function () {
|
|
|
437
640
|
else if (permitArrayCalls && isFunction(currentNode)) {
|
|
438
641
|
const arg = expression(0);
|
|
439
642
|
row.push(arg);
|
|
440
|
-
// FIXME: need to skip WS here?
|
|
441
643
|
}
|
|
442
644
|
else {
|
|
443
645
|
halt(`Unexpected ${currentNode.type} in array: ${currentNode.value}`);
|
|
@@ -478,7 +680,7 @@ prefix('{', function () {
|
|
|
478
680
|
* [AST_format.md](./AST_format.md)
|
|
479
681
|
*
|
|
480
682
|
* @see nodeTypes
|
|
481
|
-
* @param {(string |
|
|
683
|
+
* @param {(string | Token[])} formula An Excel formula string (an Excel expression) or an array of tokens.
|
|
482
684
|
* @param {object} [options={}] Options
|
|
483
685
|
* @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
|
|
484
686
|
* @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.
|
|
@@ -488,7 +690,7 @@ prefix('{', function () {
|
|
|
488
690
|
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
489
691
|
* @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
490
692
|
* @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
|
-
* @returns {
|
|
693
|
+
* @returns {AstExpression} An AST of nodes
|
|
492
694
|
*/
|
|
493
695
|
export function parse (formula, options) {
|
|
494
696
|
if (typeof formula === 'string') {
|