@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.
Files changed (141) hide show
  1. package/dist/index-BMr6cTgc.d.cts +1444 -0
  2. package/dist/index-BMr6cTgc.d.ts +1444 -0
  3. package/dist/index.cjs +3054 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +2984 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/xlsx/index.cjs +3120 -0
  10. package/dist/xlsx/index.cjs.map +1 -0
  11. package/dist/xlsx/index.d.cts +55 -0
  12. package/dist/xlsx/index.d.ts +55 -0
  13. package/dist/xlsx/index.js +3049 -0
  14. package/dist/xlsx/index.js.map +1 -0
  15. package/docs/API.md +2959 -718
  16. package/docs/AST_format.md +2 -2
  17. package/eslint.config.mjs +40 -0
  18. package/lib/a1.spec.ts +32 -0
  19. package/lib/a1.ts +26 -0
  20. package/lib/addA1RangeBounds.ts +50 -0
  21. package/lib/addTokenMeta.spec.ts +166 -0
  22. package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
  23. package/lib/astTypes.ts +211 -0
  24. package/lib/cloneToken.ts +29 -0
  25. package/lib/{constants.js → constants.ts} +6 -3
  26. package/lib/fixRanges.spec.ts +220 -0
  27. package/lib/fixRanges.ts +260 -0
  28. package/lib/fromCol.spec.ts +15 -0
  29. package/lib/{fromCol.js → fromCol.ts} +1 -1
  30. package/lib/index.spec.ts +119 -0
  31. package/lib/index.ts +76 -0
  32. package/lib/isNodeType.ts +151 -0
  33. package/lib/isType.spec.ts +208 -0
  34. package/lib/{isType.js → isType.ts} +26 -25
  35. package/lib/lexers/{advRangeOp.js → advRangeOp.ts} +1 -1
  36. package/lib/lexers/{canEndRange.js → canEndRange.ts} +2 -2
  37. package/lib/lexers/{lexBoolean.js → lexBoolean.ts} +25 -6
  38. package/lib/lexers/{lexContext.js → lexContext.ts} +14 -6
  39. package/lib/lexers/{lexError.js → lexError.ts} +3 -3
  40. package/lib/lexers/{lexFunction.js → lexFunction.ts} +3 -2
  41. package/lib/lexers/lexNameFuncCntx.ts +112 -0
  42. package/lib/lexers/{lexNamed.js → lexNamed.ts} +4 -4
  43. package/lib/lexers/{lexNewLine.js → lexNewLine.ts} +3 -2
  44. package/lib/lexers/{lexNumber.js → lexNumber.ts} +4 -3
  45. package/lib/lexers/{lexOperator.js → lexOperator.ts} +5 -4
  46. package/lib/lexers/lexRange.ts +15 -0
  47. package/lib/lexers/{lexRangeA1.js → lexRangeA1.ts} +11 -7
  48. package/lib/lexers/{lexRangeR1C1.js → lexRangeR1C1.ts} +10 -6
  49. package/lib/lexers/{lexRangeTrim.js → lexRangeTrim.ts} +3 -2
  50. package/lib/lexers/{lexRefOp.js → lexRefOp.ts} +4 -3
  51. package/lib/lexers/{lexString.js → lexString.ts} +3 -3
  52. package/lib/lexers/{lexStructured.js → lexStructured.ts} +5 -5
  53. package/lib/lexers/{lexWhitespace.js → lexWhitespace.ts} +3 -2
  54. package/lib/lexers/sets.ts +51 -0
  55. package/lib/mergeRefTokens.spec.ts +141 -0
  56. package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +14 -9
  57. package/lib/nodeTypes.ts +54 -0
  58. package/lib/parse.spec.ts +1410 -0
  59. package/lib/{parser.js → parse.ts} +81 -63
  60. package/lib/parseA1Range.spec.ts +233 -0
  61. package/lib/parseA1Range.ts +206 -0
  62. package/lib/parseA1Ref.spec.ts +337 -0
  63. package/lib/parseA1Ref.ts +115 -0
  64. package/lib/parseR1C1Range.ts +191 -0
  65. package/lib/parseR1C1Ref.spec.ts +323 -0
  66. package/lib/parseR1C1Ref.ts +127 -0
  67. package/lib/parseRef.spec.ts +90 -0
  68. package/lib/parseRef.ts +240 -0
  69. package/lib/{parseSRange.js → parseSRange.ts} +15 -10
  70. package/lib/parseStructRef.spec.ts +168 -0
  71. package/lib/parseStructRef.ts +76 -0
  72. package/lib/stringifyA1Range.spec.ts +72 -0
  73. package/lib/stringifyA1Range.ts +72 -0
  74. package/lib/stringifyA1Ref.spec.ts +64 -0
  75. package/lib/stringifyA1Ref.ts +59 -0
  76. package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
  77. package/lib/stringifyR1C1Range.spec.ts +92 -0
  78. package/lib/stringifyR1C1Range.ts +73 -0
  79. package/lib/stringifyR1C1Ref.spec.ts +63 -0
  80. package/lib/stringifyR1C1Ref.ts +67 -0
  81. package/lib/stringifyStructRef.spec.ts +124 -0
  82. package/lib/stringifyStructRef.ts +113 -0
  83. package/lib/stringifyTokens.ts +15 -0
  84. package/lib/toCol.spec.ts +11 -0
  85. package/lib/{toCol.js → toCol.ts} +4 -4
  86. package/lib/tokenTypes.ts +76 -0
  87. package/lib/tokenize-srefs.spec.ts +429 -0
  88. package/lib/tokenize.spec.ts +2103 -0
  89. package/lib/tokenize.ts +346 -0
  90. package/lib/translate.spec.ts +35 -0
  91. package/lib/translateToA1.spec.ts +247 -0
  92. package/lib/translateToA1.ts +231 -0
  93. package/lib/translateToR1C1.spec.ts +227 -0
  94. package/lib/translateToR1C1.ts +145 -0
  95. package/lib/types.ts +179 -0
  96. package/lib/xlsx/index.spec.ts +27 -0
  97. package/lib/xlsx/index.ts +32 -0
  98. package/package.json +45 -31
  99. package/tsconfig.json +28 -0
  100. package/typedoc-ignore-links.ts +17 -0
  101. package/typedoc.json +41 -0
  102. package/.eslintrc +0 -22
  103. package/benchmark/benchmark.js +0 -48
  104. package/benchmark/formulas.json +0 -15677
  105. package/dist/fx.d.ts +0 -823
  106. package/dist/fx.js +0 -2
  107. package/dist/package.json +0 -1
  108. package/lib/a1.js +0 -348
  109. package/lib/a1.spec.js +0 -458
  110. package/lib/addTokenMeta.spec.js +0 -153
  111. package/lib/astTypes.js +0 -96
  112. package/lib/extraTypes.js +0 -74
  113. package/lib/fixRanges.js +0 -104
  114. package/lib/fixRanges.spec.js +0 -171
  115. package/lib/fromCol.spec.js +0 -11
  116. package/lib/index.js +0 -134
  117. package/lib/index.spec.js +0 -67
  118. package/lib/isType.spec.js +0 -168
  119. package/lib/lexer-srefs.spec.js +0 -324
  120. package/lib/lexer.js +0 -264
  121. package/lib/lexer.spec.js +0 -1953
  122. package/lib/lexers/lexRange.js +0 -8
  123. package/lib/lexers/sets.js +0 -38
  124. package/lib/mergeRefTokens.spec.js +0 -121
  125. package/lib/package.json +0 -1
  126. package/lib/parseRef.js +0 -157
  127. package/lib/parseRef.spec.js +0 -71
  128. package/lib/parseStructRef.js +0 -48
  129. package/lib/parseStructRef.spec.js +0 -164
  130. package/lib/parser.spec.js +0 -1208
  131. package/lib/rc.js +0 -341
  132. package/lib/rc.spec.js +0 -403
  133. package/lib/stringifyStructRef.js +0 -80
  134. package/lib/stringifyStructRef.spec.js +0 -182
  135. package/lib/toCol.spec.js +0 -11
  136. package/lib/translate-toA1.spec.js +0 -214
  137. package/lib/translate-toRC.spec.js +0 -197
  138. package/lib/translate.js +0 -239
  139. package/lib/translate.spec.js +0 -21
  140. package/rollup.config.mjs +0 -22
  141. package/tsd.json +0 -12
@@ -16,7 +16,7 @@ import {
16
16
  isFxPrefix,
17
17
  isOperator,
18
18
  isError
19
- } from './isType.js';
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.js';
39
+ } from './constants.ts';
40
40
 
41
- import { tokenize } from './lexer.js';
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, true)) {
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 requires `mergeRefs` to have been `true` in tokenlist options,
679
- * because it does not recognize reference context tokens.
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 nodeTypes
685
- * @param {(string | Token[])} formula An Excel formula string (an Excel expression) or an array of tokens.
686
- * @param {object} [options={}] Options
687
- * @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
688
- * @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.
689
- * @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).
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 (formula, options) {
699
- if (typeof formula === 'string') {
700
- tokens = tokenize(formula, {
701
- withLocation: false,
702
- ...options,
703
- mergeRefs: true
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
+ });