@borgar/fx 4.13.0 → 5.0.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/index-BMr6cTgc.d.cts +1444 -0
- package/dist/index-BMr6cTgc.d.ts +1444 -0
- package/dist/index.cjs +3054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2984 -0
- package/dist/index.js.map +1 -0
- package/dist/xlsx/index.cjs +3120 -0
- package/dist/xlsx/index.cjs.map +1 -0
- package/dist/xlsx/index.d.cts +55 -0
- package/dist/xlsx/index.d.ts +55 -0
- package/dist/xlsx/index.js +3049 -0
- package/dist/xlsx/index.js.map +1 -0
- package/docs/API.md +2959 -718
- package/docs/AST_format.md +2 -2
- package/eslint.config.mjs +40 -0
- package/lib/a1.spec.ts +32 -0
- package/lib/a1.ts +26 -0
- package/lib/addA1RangeBounds.ts +50 -0
- package/lib/addTokenMeta.spec.ts +166 -0
- package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
- package/lib/astTypes.ts +211 -0
- package/lib/cloneToken.ts +29 -0
- package/lib/{constants.js → constants.ts} +6 -3
- package/lib/fixRanges.spec.ts +220 -0
- package/lib/fixRanges.ts +260 -0
- package/lib/fromCol.spec.ts +15 -0
- package/lib/{fromCol.js → fromCol.ts} +1 -1
- package/lib/index.spec.ts +119 -0
- package/lib/index.ts +76 -0
- package/lib/isNodeType.ts +151 -0
- package/lib/isType.spec.ts +208 -0
- package/lib/{isType.js → isType.ts} +26 -25
- package/lib/lexers/{advRangeOp.js → advRangeOp.ts} +1 -1
- package/lib/lexers/{canEndRange.js → canEndRange.ts} +2 -2
- package/lib/lexers/{lexBoolean.js → lexBoolean.ts} +25 -6
- package/lib/lexers/{lexContext.js → lexContext.ts} +14 -6
- package/lib/lexers/{lexError.js → lexError.ts} +3 -3
- package/lib/lexers/{lexFunction.js → lexFunction.ts} +3 -2
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/{lexNamed.js → lexNamed.ts} +4 -4
- package/lib/lexers/{lexNewLine.js → lexNewLine.ts} +3 -2
- package/lib/lexers/{lexNumber.js → lexNumber.ts} +4 -3
- package/lib/lexers/{lexOperator.js → lexOperator.ts} +5 -4
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/{lexRangeA1.js → lexRangeA1.ts} +11 -7
- package/lib/lexers/{lexRangeR1C1.js → lexRangeR1C1.ts} +10 -6
- package/lib/lexers/{lexRangeTrim.js → lexRangeTrim.ts} +3 -2
- package/lib/lexers/{lexRefOp.js → lexRefOp.ts} +4 -3
- package/lib/lexers/{lexString.js → lexString.ts} +3 -3
- package/lib/lexers/{lexStructured.js → lexStructured.ts} +5 -5
- package/lib/lexers/{lexWhitespace.js → lexWhitespace.ts} +3 -2
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +14 -9
- package/lib/nodeTypes.ts +54 -0
- package/lib/parse.spec.ts +1410 -0
- package/lib/{parser.js → parse.ts} +81 -63
- package/lib/parseA1Range.spec.ts +233 -0
- package/lib/parseA1Range.ts +206 -0
- package/lib/parseA1Ref.spec.ts +337 -0
- package/lib/parseA1Ref.ts +115 -0
- package/lib/parseR1C1Range.ts +191 -0
- package/lib/parseR1C1Ref.spec.ts +323 -0
- package/lib/parseR1C1Ref.ts +127 -0
- package/lib/parseRef.spec.ts +90 -0
- package/lib/parseRef.ts +240 -0
- package/lib/{parseSRange.js → parseSRange.ts} +15 -10
- package/lib/parseStructRef.spec.ts +168 -0
- package/lib/parseStructRef.ts +76 -0
- package/lib/stringifyA1Range.spec.ts +72 -0
- package/lib/stringifyA1Range.ts +72 -0
- package/lib/stringifyA1Ref.spec.ts +64 -0
- package/lib/stringifyA1Ref.ts +59 -0
- package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
- package/lib/stringifyR1C1Range.spec.ts +92 -0
- package/lib/stringifyR1C1Range.ts +73 -0
- package/lib/stringifyR1C1Ref.spec.ts +63 -0
- package/lib/stringifyR1C1Ref.ts +67 -0
- package/lib/stringifyStructRef.spec.ts +124 -0
- package/lib/stringifyStructRef.ts +113 -0
- package/lib/stringifyTokens.ts +15 -0
- package/lib/toCol.spec.ts +11 -0
- package/lib/{toCol.js → toCol.ts} +4 -4
- package/lib/tokenTypes.ts +76 -0
- package/lib/tokenize-srefs.spec.ts +429 -0
- package/lib/tokenize.spec.ts +2103 -0
- package/lib/tokenize.ts +346 -0
- package/lib/translate.spec.ts +35 -0
- package/lib/translateToA1.spec.ts +247 -0
- package/lib/translateToA1.ts +231 -0
- package/lib/translateToR1C1.spec.ts +227 -0
- package/lib/translateToR1C1.ts +145 -0
- package/lib/types.ts +179 -0
- package/lib/xlsx/index.spec.ts +27 -0
- package/lib/xlsx/index.ts +32 -0
- package/package.json +45 -31
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- package/benchmark/benchmark.js +0 -48
- package/benchmark/formulas.json +0 -15677
- package/dist/fx.d.ts +0 -823
- package/dist/fx.js +0 -2
- package/dist/package.json +0 -1
- package/lib/a1.js +0 -348
- package/lib/a1.spec.js +0 -458
- package/lib/addTokenMeta.spec.js +0 -153
- package/lib/astTypes.js +0 -96
- package/lib/extraTypes.js +0 -74
- package/lib/fixRanges.js +0 -104
- package/lib/fixRanges.spec.js +0 -171
- package/lib/fromCol.spec.js +0 -11
- package/lib/index.js +0 -134
- package/lib/index.spec.js +0 -67
- package/lib/isType.spec.js +0 -168
- package/lib/lexer-srefs.spec.js +0 -324
- package/lib/lexer.js +0 -264
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexers/lexRange.js +0 -8
- package/lib/lexers/sets.js +0 -38
- package/lib/mergeRefTokens.spec.js +0 -121
- package/lib/package.json +0 -1
- package/lib/parseRef.js +0 -157
- package/lib/parseRef.spec.js +0 -71
- package/lib/parseStructRef.js +0 -48
- package/lib/parseStructRef.spec.js +0 -164
- package/lib/parser.spec.js +0 -1208
- package/lib/rc.js +0 -341
- package/lib/rc.spec.js +0 -403
- package/lib/stringifyStructRef.js +0 -80
- package/lib/stringifyStructRef.spec.js +0 -182
- package/lib/toCol.spec.js +0 -11
- package/lib/translate-toA1.spec.js +0 -214
- package/lib/translate-toRC.spec.js +0 -197
- package/lib/translate.js +0 -239
- package/lib/translate.spec.js +0 -21
- package/rollup.config.mjs +0 -22
- package/tsd.json +0 -12
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
isFxPrefix,
|
|
17
17
|
isOperator,
|
|
18
18
|
isError
|
|
19
|
-
} from './isType.
|
|
19
|
+
} from './isType.ts';
|
|
20
20
|
import {
|
|
21
21
|
UNARY,
|
|
22
22
|
BINARY,
|
|
@@ -36,9 +36,10 @@ import {
|
|
|
36
36
|
REF_NAMED,
|
|
37
37
|
REF_STRUCT,
|
|
38
38
|
REF_BEAM
|
|
39
|
-
} from './constants.
|
|
39
|
+
} from './constants.ts';
|
|
40
40
|
|
|
41
|
-
import {
|
|
41
|
+
import type { Token } from './types.ts';
|
|
42
|
+
import type { ArrayExpression, AstExpression, BinaryExpression, CallExpression, Identifier, LambdaExpression, LetDeclarator, LetExpression, UnaryExpression } from './astTypes.ts';
|
|
42
43
|
|
|
43
44
|
const END = '(END)';
|
|
44
45
|
const FUNCTION = '(FUNCTION)';
|
|
@@ -65,17 +66,17 @@ const refFunctions = [
|
|
|
65
66
|
|
|
66
67
|
const symbolTable = {};
|
|
67
68
|
let currentNode;
|
|
68
|
-
let tokens;
|
|
69
|
-
let tokenIndex;
|
|
69
|
+
let tokens: Token[];
|
|
70
|
+
let tokenIndex: number;
|
|
70
71
|
let permitArrayRanges = false;
|
|
71
72
|
let permitArrayCalls = false;
|
|
72
73
|
let looseRefCalls = false;
|
|
73
74
|
|
|
74
|
-
const isReferenceFunctionName = fnName => {
|
|
75
|
+
const isReferenceFunctionName = (fnName: string) => {
|
|
75
76
|
return looseRefCalls || refFunctions.includes(fnName.toUpperCase());
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
const isReferenceToken = (token, allowOperators = false) => {
|
|
79
|
+
const isReferenceToken = (token: Token, allowOperators = false) => {
|
|
79
80
|
const value = (token && token.value) + '';
|
|
80
81
|
if (isReference(token)) {
|
|
81
82
|
return true;
|
|
@@ -106,9 +107,11 @@ const isReferenceNode = node => {
|
|
|
106
107
|
);
|
|
107
108
|
};
|
|
108
109
|
|
|
109
|
-
function halt (message, atIndex = null) {
|
|
110
|
+
function halt (message: string, atIndex = null) {
|
|
110
111
|
const err = new Error(message);
|
|
112
|
+
// @ts-ignore -- FIXME: use a dedicated error class
|
|
111
113
|
err.source = tokens.map(d => d.value).join('');
|
|
114
|
+
// @ts-ignore
|
|
112
115
|
err.sourceOffset = tokens
|
|
113
116
|
.slice(0, atIndex ?? tokenIndex)
|
|
114
117
|
.reduce((a, d) => a + d.value.length, 0);
|
|
@@ -116,9 +119,9 @@ function halt (message, atIndex = null) {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
// A1 A1 | A1 (A1) | A1 ((A1)) | A1 ( (A1) ) | ...
|
|
119
|
-
function refIsUpcoming (allowOperators = false) {
|
|
122
|
+
function refIsUpcoming (allowOperators = false): boolean {
|
|
120
123
|
let i = tokenIndex;
|
|
121
|
-
let next;
|
|
124
|
+
let next: Token;
|
|
122
125
|
do {
|
|
123
126
|
next = tokens[++i];
|
|
124
127
|
}
|
|
@@ -193,7 +196,7 @@ function advance (expectNext = null, leftNode = null) {
|
|
|
193
196
|
return currentNode;
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
function expression (rbp) {
|
|
199
|
+
function expression (rbp: number) {
|
|
197
200
|
let t = currentNode;
|
|
198
201
|
advance(null, t);
|
|
199
202
|
let left = t.nud();
|
|
@@ -213,7 +216,7 @@ const original_symbol = {
|
|
|
213
216
|
};
|
|
214
217
|
|
|
215
218
|
// bp = binding power
|
|
216
|
-
function symbol (id, bp = 0) {
|
|
219
|
+
function symbol (id: string, bp = 0) {
|
|
217
220
|
let s = symbolTable[id];
|
|
218
221
|
if (s) {
|
|
219
222
|
if (bp >= s.lbp) {
|
|
@@ -230,10 +233,11 @@ function symbol (id, bp = 0) {
|
|
|
230
233
|
return s;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
function infix (id, bp, led) {
|
|
236
|
+
function infix (id: string, bp: number, led?) {
|
|
234
237
|
const s = symbol(id, bp);
|
|
235
|
-
s.led = led || function (left) {
|
|
238
|
+
s.led = led || function (this: BinaryExpression & { value?: string }, left) {
|
|
236
239
|
this.type = BINARY;
|
|
240
|
+
// @ts-expect-error -- we know this is going to be a valid operator
|
|
237
241
|
this.operator = this.value;
|
|
238
242
|
delete this.value;
|
|
239
243
|
const right = expression(bp);
|
|
@@ -246,11 +250,12 @@ function infix (id, bp, led) {
|
|
|
246
250
|
return s;
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
function postfix (id, led) {
|
|
253
|
+
function postfix (id: string, led?) {
|
|
250
254
|
const s = symbol(id, 0);
|
|
251
255
|
s.lbp = 70;
|
|
252
|
-
s.led = led || function (left) {
|
|
256
|
+
s.led = led || function (this: UnaryExpression & { value?: string }, left) {
|
|
253
257
|
this.type = UNARY;
|
|
258
|
+
// @ts-expect-error -- we know this is going to be a valid operator
|
|
254
259
|
this.operator = this.value;
|
|
255
260
|
delete this.value;
|
|
256
261
|
this.arguments = [ left ];
|
|
@@ -262,10 +267,11 @@ function postfix (id, led) {
|
|
|
262
267
|
return s;
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
function prefix (id, nud) {
|
|
270
|
+
function prefix (id, nud?) {
|
|
266
271
|
const s = symbol(id);
|
|
267
|
-
s.nud = nud || function () {
|
|
272
|
+
s.nud = nud || function (this: UnaryExpression & { value?: string }) {
|
|
268
273
|
this.type = UNARY;
|
|
274
|
+
// @ts-expect-error -- we know this is going to be a valid operator
|
|
269
275
|
this.operator = this.value;
|
|
270
276
|
delete this.value;
|
|
271
277
|
const subexpr = expression(70);
|
|
@@ -279,15 +285,16 @@ function prefix (id, nud) {
|
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
function rangeInfix (id, bp) {
|
|
282
|
-
return infix(id, bp, function (left) {
|
|
288
|
+
return infix(id, bp, function (this: BinaryExpression & { id?: string, value?: string }, left) {
|
|
283
289
|
if (!isReferenceNode(left)) {
|
|
284
290
|
halt(`Unexpected ${id} operator`);
|
|
285
291
|
}
|
|
286
292
|
const right = expression(bp);
|
|
287
|
-
if (!isReferenceNode(right
|
|
293
|
+
if (!isReferenceNode(right)) {
|
|
288
294
|
halt(`Unexpected ${currentNode.type} following ${this.id}`);
|
|
289
295
|
}
|
|
290
296
|
this.type = BINARY;
|
|
297
|
+
// @ts-expect-error -- we know this is going to be a valid operator
|
|
291
298
|
this.operator = this.value.trim() ? this.value : ' '; // hack around whitespace op
|
|
292
299
|
delete this.value;
|
|
293
300
|
this.arguments = [ left, right ];
|
|
@@ -307,7 +314,7 @@ rangeInfix(WHITESPACE, 80); // intersect: =B7:D7 C6:C8
|
|
|
307
314
|
|
|
308
315
|
// Excel's grammar is ambiguous. This turns the , operator's left binding
|
|
309
316
|
// power on/off which allows us to treat , as a symbol where we need.
|
|
310
|
-
const unionRefs = enable => {
|
|
317
|
+
const unionRefs = (enable?: boolean) => {
|
|
311
318
|
const currState = comma.lbp > 0;
|
|
312
319
|
if (enable != null) { comma.lbp = enable ? 80 : 0; }
|
|
313
320
|
return currState;
|
|
@@ -315,7 +322,7 @@ const unionRefs = enable => {
|
|
|
315
322
|
|
|
316
323
|
// arithmetic and string operations
|
|
317
324
|
postfix('%'); // percent
|
|
318
|
-
postfix('#', function (left) { // spilled range (_xlfn.ANCHORARRAY)
|
|
325
|
+
postfix('#', function (this: Token, left) { // spilled range (_xlfn.ANCHORARRAY)
|
|
319
326
|
if (!isReferenceNode(left)) {
|
|
320
327
|
halt('# expects a reference');
|
|
321
328
|
}
|
|
@@ -396,8 +403,8 @@ prefix('(', function () {
|
|
|
396
403
|
symbol(FUNCTION).nud = function () {
|
|
397
404
|
return this;
|
|
398
405
|
};
|
|
399
|
-
infix('(', 90, function (left) {
|
|
400
|
-
let callee = {
|
|
406
|
+
infix('(', 90, function (this: CallExpression & { value?: string }, left) {
|
|
407
|
+
let callee: Identifier = {
|
|
401
408
|
type: IDENTIFIER,
|
|
402
409
|
name: left.value
|
|
403
410
|
};
|
|
@@ -470,10 +477,10 @@ infix('(', 90, function (left) {
|
|
|
470
477
|
return this;
|
|
471
478
|
});
|
|
472
479
|
|
|
473
|
-
function parseLambda (left) {
|
|
480
|
+
function parseLambda (this: LambdaExpression & { value?: string }, left) {
|
|
474
481
|
const args = [];
|
|
475
482
|
const argNames = {};
|
|
476
|
-
let body;
|
|
483
|
+
let body: AstExpression | null;
|
|
477
484
|
let done = false;
|
|
478
485
|
const prevState = unionRefs(false);
|
|
479
486
|
if (currentNode.id !== ')') {
|
|
@@ -492,7 +499,7 @@ function parseLambda (left) {
|
|
|
492
499
|
halt('Duplicate name: ' + arg.value);
|
|
493
500
|
}
|
|
494
501
|
argNames[currName] = 1;
|
|
495
|
-
const a = { type: IDENTIFIER, name: arg.value };
|
|
502
|
+
const a: Identifier = { type: IDENTIFIER, name: arg.value };
|
|
496
503
|
if (arg.loc) { a.loc = arg.loc; }
|
|
497
504
|
args.push(a);
|
|
498
505
|
}
|
|
@@ -520,13 +527,13 @@ function parseLambda (left) {
|
|
|
520
527
|
return this;
|
|
521
528
|
}
|
|
522
529
|
|
|
523
|
-
function parseLet (left) {
|
|
530
|
+
function parseLet (this: LetExpression & { value?: string }, left) {
|
|
524
531
|
const args = [];
|
|
525
532
|
const vals = [];
|
|
526
533
|
const argNames = {};
|
|
527
|
-
let body;
|
|
534
|
+
let body: AstExpression | null;
|
|
528
535
|
let argCounter = 0;
|
|
529
|
-
const addArgument = (arg, lastArg) => {
|
|
536
|
+
const addArgument = (arg, lastArg?) => {
|
|
530
537
|
if (body) {
|
|
531
538
|
halt('Unexpected argument following calculation');
|
|
532
539
|
}
|
|
@@ -585,7 +592,6 @@ function parseLet (left) {
|
|
|
585
592
|
if (lastWasComma) {
|
|
586
593
|
addArgument(null, true);
|
|
587
594
|
}
|
|
588
|
-
// eslint-disable-next-line no-undefined
|
|
589
595
|
if (body === undefined) {
|
|
590
596
|
halt('Unexpected end of arguments');
|
|
591
597
|
}
|
|
@@ -597,7 +603,7 @@ function parseLet (left) {
|
|
|
597
603
|
halt('Unexpected end of arguments');
|
|
598
604
|
}
|
|
599
605
|
for (let i = 0; i < args.length; i++) {
|
|
600
|
-
const s = {
|
|
606
|
+
const s: LetDeclarator = {
|
|
601
607
|
type: LET_DECL,
|
|
602
608
|
id: args[i],
|
|
603
609
|
init: vals[i],
|
|
@@ -616,7 +622,7 @@ function parseLet (left) {
|
|
|
616
622
|
// array literal
|
|
617
623
|
symbol('}');
|
|
618
624
|
symbol(';');
|
|
619
|
-
prefix('{', function () {
|
|
625
|
+
prefix('{', function (this: ArrayExpression & { value?: string }) {
|
|
620
626
|
if (currentNode.id === '}') { // arrays must not be empty
|
|
621
627
|
halt('Unexpected empty array');
|
|
622
628
|
}
|
|
@@ -672,42 +678,53 @@ prefix('{', function () {
|
|
|
672
678
|
return this;
|
|
673
679
|
});
|
|
674
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Options for {@link parse}.
|
|
683
|
+
*/
|
|
684
|
+
export type OptsParse = {
|
|
685
|
+
/**
|
|
686
|
+
* Ranges are allowed as elements of arrays. This is a feature in Google Sheets while Excel
|
|
687
|
+
* does not allow it.
|
|
688
|
+
* @defaultValue false
|
|
689
|
+
*/
|
|
690
|
+
permitArrayRanges?: boolean,
|
|
691
|
+
/**
|
|
692
|
+
* Function calls are allowed as elements of arrays. This is a feature in Google Sheets
|
|
693
|
+
* while Excel does not allow it.
|
|
694
|
+
* @defaultValue false
|
|
695
|
+
*/
|
|
696
|
+
permitArrayCalls?: boolean,
|
|
697
|
+
/**
|
|
698
|
+
* Permits any function call where otherwise only functions that return references would
|
|
699
|
+
* be permitted.
|
|
700
|
+
* @defaultValue false
|
|
701
|
+
*/
|
|
702
|
+
looseRefCalls?: boolean,
|
|
703
|
+
};
|
|
704
|
+
|
|
675
705
|
/**
|
|
676
706
|
* Parses a string formula or list of tokens into an AST.
|
|
677
707
|
*
|
|
678
|
-
* The parser
|
|
679
|
-
*
|
|
708
|
+
* The parser assumes `mergeRefs` and `negativeNumbers` were `true` when the tokens were generated.
|
|
709
|
+
* It does not yet recognize reference context tokens or know how to deal with unary minuses in
|
|
710
|
+
* arrays.
|
|
680
711
|
*
|
|
681
712
|
* The AST Abstract Syntax Tree's format is documented in
|
|
682
|
-
* [AST_format.md](./AST_format.md)
|
|
713
|
+
* [AST_format.md](./AST_format.md).
|
|
683
714
|
*
|
|
684
|
-
* @see
|
|
685
|
-
* @
|
|
686
|
-
* @
|
|
687
|
-
* @param
|
|
688
|
-
* @param
|
|
689
|
-
* @
|
|
690
|
-
* @param {boolean} [options.permitArrayRanges=false] Ranges are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it.
|
|
691
|
-
* @param {boolean} [options.permitArrayCalls=false] Function calls are allowed as elements of arrays. This is a feature in Google Sheets while Excel does not allow it.
|
|
692
|
-
* @param {boolean} [options.looseRefCalls=false] Permits any function call where otherwise only functions that return references would be permitted.
|
|
693
|
-
* @param {boolean} [options.r1c1=false] Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
|
|
694
|
-
* @param {boolean} [options.withLocation=false] Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
|
|
695
|
-
* @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)
|
|
696
|
-
* @returns {AstExpression} An AST of nodes
|
|
715
|
+
* @see {@link OptsParse}
|
|
716
|
+
* @see {@link nodeTypes}
|
|
717
|
+
* @see {@link tokenize}
|
|
718
|
+
* @param tokenlist An array of tokens.
|
|
719
|
+
* @param options Options for the parsers behavior.
|
|
720
|
+
* @returns An AST of nodes.
|
|
697
721
|
*/
|
|
698
|
-
export function parse (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
else if (Array.isArray(formula)) {
|
|
707
|
-
tokens = formula;
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
throw new Error('Parse requires a string or array of tokens.');
|
|
722
|
+
export function parse (
|
|
723
|
+
tokenlist: Token[],
|
|
724
|
+
options: OptsParse = {}
|
|
725
|
+
): AstExpression {
|
|
726
|
+
if (!Array.isArray(tokenlist)) {
|
|
727
|
+
throw new Error('Parse requires an array of tokens.');
|
|
711
728
|
}
|
|
712
729
|
// allow ranges in array "literals"?
|
|
713
730
|
permitArrayRanges = options?.permitArrayRanges;
|
|
@@ -715,7 +732,8 @@ export function parse (formula, options) {
|
|
|
715
732
|
permitArrayCalls = options?.permitArrayCalls;
|
|
716
733
|
// allow any function call in range operations?
|
|
717
734
|
looseRefCalls = options?.looseRefCalls;
|
|
718
|
-
// set index to start
|
|
735
|
+
// assign the tokenlist and set index to start
|
|
736
|
+
tokens = tokenlist;
|
|
719
737
|
tokenIndex = 0;
|
|
720
738
|
// discard redundant whitespace and = prefix
|
|
721
739
|
while (isWhitespace(tokens[tokenIndex]) || isFxPrefix(tokens[tokenIndex])) {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable @stylistic/object-property-newline */
|
|
2
|
+
import { describe, test, expect } from 'vitest';
|
|
3
|
+
import { fromRow, parseA1Range } from './parseA1Range.ts';
|
|
4
|
+
|
|
5
|
+
describe('fromRow', () => {
|
|
6
|
+
test('fromRow converts row strings to zero-based indices', () => {
|
|
7
|
+
expect(fromRow('1')).toBe(0);
|
|
8
|
+
expect(fromRow('2')).toBe(1);
|
|
9
|
+
expect(fromRow('10')).toBe(9);
|
|
10
|
+
expect(fromRow('100')).toBe(99);
|
|
11
|
+
expect(fromRow('9999999')).toBe(9999998);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('parseA1Range', () => {
|
|
16
|
+
test('parseA1Range parses simple cell references', () => {
|
|
17
|
+
expect(parseA1Range('A1')).toEqual({
|
|
18
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
19
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(parseA1Range('B2')).toEqual({
|
|
23
|
+
top: 1, left: 1, bottom: 1, right: 1,
|
|
24
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(parseA1Range('Z10')).toEqual({
|
|
28
|
+
top: 9, left: 25, bottom: 9, right: 25,
|
|
29
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(parseA1Range('AA100')).toEqual({
|
|
33
|
+
top: 99, left: 26, bottom: 99, right: 26,
|
|
34
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('parseA1Range parses absolute cell references', () => {
|
|
40
|
+
expect(parseA1Range('$A$1')).toEqual({
|
|
41
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
42
|
+
$top: true, $left: true, $bottom: true, $right: true
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(parseA1Range('$A1')).toEqual({
|
|
46
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
47
|
+
$top: false, $left: true, $bottom: false, $right: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(parseA1Range('A$1')).toEqual({
|
|
51
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
52
|
+
$top: true, $left: false, $bottom: true, $right: false
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('parseA1Range parses range references', () => {
|
|
57
|
+
expect(parseA1Range('A1:B2')).toEqual({
|
|
58
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
59
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(parseA1Range('A1:Z10')).toEqual({
|
|
63
|
+
top: 0, left: 0, bottom: 9, right: 25,
|
|
64
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(parseA1Range('B2:D4')).toEqual({
|
|
68
|
+
top: 1, left: 1, bottom: 3, right: 3,
|
|
69
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('parseA1Range parses range references with mixed absolute/relative', () => {
|
|
74
|
+
expect(parseA1Range('$A$1:B2')).toEqual({
|
|
75
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
76
|
+
$top: true, $left: true, $bottom: false, $right: false
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(parseA1Range('A1:$B$2')).toEqual({
|
|
80
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
81
|
+
$top: false, $left: false, $bottom: true, $right: true
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(parseA1Range('$A1:B$2')).toEqual({
|
|
85
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
86
|
+
$top: false, $left: true, $bottom: true, $right: false
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('parseA1Range normalizes reversed ranges', () => {
|
|
91
|
+
expect(parseA1Range('B2:A1')).toEqual({
|
|
92
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
93
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(parseA1Range('Z10:A1')).toEqual({
|
|
97
|
+
top: 0, left: 0, bottom: 9, right: 25,
|
|
98
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('parseA1Range parses column ranges', () => {
|
|
103
|
+
expect(parseA1Range('A:A')).toEqual({
|
|
104
|
+
top: null, left: 0, bottom: null, right: 0,
|
|
105
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(parseA1Range('A:C')).toEqual({
|
|
109
|
+
top: null, left: 0, bottom: null, right: 2,
|
|
110
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(parseA1Range('C:A')).toEqual({
|
|
114
|
+
top: null, left: 0, bottom: null, right: 2,
|
|
115
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(parseA1Range('$A:C')).toEqual({
|
|
119
|
+
top: null, left: 0, bottom: null, right: 2,
|
|
120
|
+
$top: false, $left: true, $bottom: false, $right: false
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(parseA1Range('A:$C')).toEqual({
|
|
124
|
+
top: null, left: 0, bottom: null, right: 2,
|
|
125
|
+
$top: false, $left: false, $bottom: false, $right: true
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('parseA1Range parses row ranges', () => {
|
|
130
|
+
expect(parseA1Range('1:1')).toEqual({
|
|
131
|
+
top: 0, left: null, bottom: 0, right: null,
|
|
132
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(parseA1Range('1:3')).toEqual({
|
|
136
|
+
top: 0, left: null, bottom: 2, right: null,
|
|
137
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(parseA1Range('3:1')).toEqual({
|
|
141
|
+
top: 0, left: null, bottom: 2, right: null,
|
|
142
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(parseA1Range('$1:3')).toEqual({
|
|
146
|
+
top: 0, left: null, bottom: 2, right: null,
|
|
147
|
+
$top: true, $left: false, $bottom: false, $right: false
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(parseA1Range('1:$3')).toEqual({
|
|
151
|
+
top: 0, left: null, bottom: 2, right: null,
|
|
152
|
+
$top: false, $left: false, $bottom: true, $right: false
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('parseA1Range parses trimmed ranges', () => {
|
|
157
|
+
expect(parseA1Range('A1.:B2')).toEqual({
|
|
158
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
159
|
+
$top: false, $left: false, $bottom: false, $right: false,
|
|
160
|
+
trim: 'head'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(parseA1Range('A1:.B2')).toEqual({
|
|
164
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
165
|
+
$top: false, $left: false, $bottom: false, $right: false,
|
|
166
|
+
trim: 'tail'
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(parseA1Range('A1.:.B2')).toEqual({
|
|
170
|
+
top: 0, left: 0, bottom: 1, right: 1,
|
|
171
|
+
$top: false, $left: false, $bottom: false, $right: false,
|
|
172
|
+
trim: 'both'
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('parseA1Range handles partial column ranges', () => {
|
|
177
|
+
const range = {
|
|
178
|
+
top: 0, left: 0, bottom: null, right: 2,
|
|
179
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
180
|
+
};
|
|
181
|
+
expect(parseA1Range('A1:C')).toEqual(range);
|
|
182
|
+
expect(parseA1Range('C:A1')).toEqual(range);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('parseA1Range handles partial row ranges', () => {
|
|
186
|
+
const range = {
|
|
187
|
+
top: 0, left: 0, bottom: 2, right: null,
|
|
188
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
189
|
+
};
|
|
190
|
+
expect(parseA1Range('A1:3')).toEqual(range);
|
|
191
|
+
expect(parseA1Range('3:A1')).toEqual(range);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('parseA1Range returns null for invalid references', () => {
|
|
195
|
+
expect(parseA1Range('')).toBe(undefined);
|
|
196
|
+
expect(parseA1Range('A')).toBe(undefined);
|
|
197
|
+
expect(parseA1Range('1')).toBe(undefined);
|
|
198
|
+
expect(parseA1Range('$A')).toBe(undefined);
|
|
199
|
+
expect(parseA1Range('$1')).toBe(undefined);
|
|
200
|
+
expect(parseA1Range('AAAA1')).toBe(undefined);
|
|
201
|
+
expect(parseA1Range('A0')).toBe(undefined);
|
|
202
|
+
expect(parseA1Range('A10000000')).toBe(undefined);
|
|
203
|
+
expect(parseA1Range('123ABC')).toBe(undefined);
|
|
204
|
+
expect(parseA1Range('A1:B2:C3')).toBe(undefined);
|
|
205
|
+
expect(parseA1Range('A1::B2')).toBe(undefined);
|
|
206
|
+
expect(parseA1Range('A1B2')).toBe(undefined);
|
|
207
|
+
expect(parseA1Range('$$$A1')).toBe(undefined);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('parseA1Range handles maximum valid values', () => {
|
|
211
|
+
expect(parseA1Range('XFD1048576')).toEqual({
|
|
212
|
+
top: 1048575, left: 16383, bottom: 1048575, right: 16383,
|
|
213
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
214
|
+
});
|
|
215
|
+
expect(parseA1Range('XFD1048577')).toBe(undefined);
|
|
216
|
+
expect(parseA1Range('XFE1048576')).toBe(undefined);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('parseA1Range handles case insensitivity', () => {
|
|
220
|
+
const lower = parseA1Range('a1');
|
|
221
|
+
const upper = parseA1Range('A1');
|
|
222
|
+
const mixed = parseA1Range('aA1');
|
|
223
|
+
|
|
224
|
+
expect(lower).toEqual(upper);
|
|
225
|
+
expect(lower).toEqual({
|
|
226
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
227
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
228
|
+
});
|
|
229
|
+
expect(mixed).toEqual({
|
|
230
|
+
top: 0, left: 26, bottom: 0, right: 26,
|
|
231
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
232
|
+
});
|
|
233
|
+
});
|