@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
@@ -0,0 +1,206 @@
1
+ import { MAX_COLS, MAX_ROWS } from './constants.ts';
2
+ import type { RangeA1 } from './types.ts';
3
+
4
+ type TrimString = 'both' | 'head' | 'tail';
5
+
6
+ export function fromRow (rowStr: string): number {
7
+ return +rowStr - 1;
8
+ }
9
+
10
+ const CHAR_DOLLAR = 36;
11
+ const CHAR_PERIOD = 46;
12
+ const CHAR_COLON = 58;
13
+ const CHAR_A_LC = 97;
14
+ const CHAR_A_UC = 65;
15
+ const CHAR_Z_LC = 122;
16
+ const CHAR_Z_UC = 90;
17
+ const CHAR_0 = 48;
18
+ const CHAR_1 = 49;
19
+ const CHAR_9 = 57;
20
+
21
+ function advRangeOp (str: string, pos: number): [ number, TrimString | '' ] {
22
+ const c0 = str.charCodeAt(pos);
23
+ if (c0 === CHAR_PERIOD) {
24
+ const c1 = str.charCodeAt(pos + 1);
25
+ if (c1 === CHAR_COLON) {
26
+ return str.charCodeAt(pos + 2) === CHAR_PERIOD
27
+ ? [ 3, 'both' ]
28
+ : [ 2, 'head' ];
29
+ }
30
+ }
31
+ else if (c0 === CHAR_COLON) {
32
+ const c1 = str.charCodeAt(pos + 1);
33
+ return c1 === CHAR_PERIOD
34
+ ? [ 2, 'tail' ]
35
+ : [ 1, '' ];
36
+ }
37
+ return [ 0, '' ];
38
+ }
39
+
40
+ function advA1Col (str: string, pos: number): [ number, number, boolean ] {
41
+ // [A-Z]{1,3}
42
+ const start = pos;
43
+ const lock = str.charCodeAt(pos) === CHAR_DOLLAR;
44
+ if (lock) { pos++; }
45
+ const stop = pos + 3;
46
+ let col = 0;
47
+ do {
48
+ const c = str.charCodeAt(pos);
49
+ if (c >= CHAR_A_UC && c <= CHAR_Z_UC) {
50
+ col = (26 * col) + c - (CHAR_A_UC - 1);
51
+ pos++;
52
+ }
53
+ else if (c >= CHAR_A_LC && c <= CHAR_Z_LC) {
54
+ col = (26 * col) + c - (CHAR_A_LC - 1);
55
+ pos++;
56
+ }
57
+ else {
58
+ break;
59
+ }
60
+ }
61
+ while (pos < stop && pos < str.length);
62
+ return (col && col <= MAX_COLS + 1)
63
+ ? [ pos - start, col - 1, lock ]
64
+ : [ 0, 0, false ];
65
+ }
66
+
67
+ function advA1Row (str: string, pos: number): [number, number, boolean] {
68
+ // [1-9][0-9]{0,6}
69
+ const start = pos;
70
+ const lock = str.charCodeAt(pos) === CHAR_DOLLAR;
71
+ if (lock) { pos++; }
72
+ const stop = pos + 7;
73
+ let row = 0;
74
+ let c = str.charCodeAt(pos);
75
+ if (c >= CHAR_1 && c <= CHAR_9) {
76
+ row = (row * 10) + c - CHAR_0;
77
+ pos++;
78
+ do {
79
+ c = str.charCodeAt(pos);
80
+ if (c >= CHAR_0 && c <= CHAR_9) {
81
+ row = (row * 10) + c - CHAR_0;
82
+ pos++;
83
+ }
84
+ else {
85
+ break;
86
+ }
87
+ }
88
+ while (pos < stop && pos < str.length);
89
+ }
90
+ return (row && row <= MAX_ROWS + 1)
91
+ ? [ pos - start, row - 1, lock ]
92
+ : [ 0, 0, false ];
93
+ }
94
+
95
+ function makeRange (
96
+ top: number | null,
97
+ $top: boolean | null,
98
+ left: number | null,
99
+ $left: boolean | null,
100
+ bottom: number | null,
101
+ $bottom: boolean | null,
102
+ right: number | null,
103
+ $right: boolean | null,
104
+ trim: TrimString | ''
105
+ ): RangeA1 {
106
+ // flip left/right and top/bottom as needed
107
+ // for partial ranges we perfer the coord on the left-side of the:
108
+ if (right != null && (left == null || (left != null && right < left))) {
109
+ [ left, right, $left, $right ] = [ right, left, $right, $left ];
110
+ }
111
+ if (bottom != null && (top == null || (top != null && bottom < top))) {
112
+ [ top, bottom, $top, $bottom ] = [ bottom, top, $bottom, $top ];
113
+ }
114
+ const range: RangeA1 = { top, left, bottom, right, $top, $left, $bottom, $right };
115
+ if (trim) {
116
+ range.trim = trim;
117
+ }
118
+ return range;
119
+ }
120
+
121
+ /**
122
+ * Parse A1-style range string into a RangeA1 object.
123
+ *
124
+ * @param rangeString A1-style range string.
125
+ * @param [allowTernary] Permit ternary ranges like A2:A or B2:2.
126
+ * @return A reference object.
127
+ */
128
+ export function parseA1Range (rangeString: string, allowTernary = true): RangeA1 | undefined {
129
+ let p = 0;
130
+ const [ leftChars, left, $left ] = advA1Col(rangeString, p);
131
+ let right = 0;
132
+ let $right = false;
133
+ let bottom = 0;
134
+ let $bottom = false;
135
+ let rightChars: number;
136
+ let bottomChars: number;
137
+ if (leftChars) {
138
+ // TLBR: could be A1:A1
139
+ // TL R: could be A1:A (if allowTernary)
140
+ // TLB : could be A1:1 (if allowTernary)
141
+ // LBR: could be A:A1 (if allowTernary)
142
+ // L R: could be A:A
143
+ p += leftChars;
144
+ const [ topChars, top, $top ] = advA1Row(rangeString, p);
145
+ p += topChars;
146
+ const [ op, trim ] = advRangeOp(rangeString, p);
147
+ if (op) {
148
+ p += op;
149
+ [ rightChars, right, $right ] = advA1Col(rangeString, p);
150
+ p += rightChars;
151
+ [ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p);
152
+ p += bottomChars;
153
+ if (topChars && bottomChars && rightChars) {
154
+ if (p === rangeString.length) {
155
+ return makeRange(top, $top, left, $left, bottom, $bottom, right, $right, trim);
156
+ }
157
+ }
158
+ else if (!topChars && !bottomChars) {
159
+ if (p === rangeString.length) {
160
+ return makeRange(null, false, left, $left, null, false, right, $right, trim);
161
+ }
162
+ }
163
+ else if (allowTernary && (bottomChars || rightChars) && p === rangeString.length) {
164
+ if (!topChars) {
165
+ return makeRange(null, false, left, $left, bottom, $bottom, right, $right, trim);
166
+ }
167
+ else if (!bottomChars) {
168
+ return makeRange(top, $top, left, $left, null, false, right, $right, trim);
169
+ }
170
+ else {
171
+ return makeRange(top, $top, left, $left, bottom, $bottom, null, false, trim);
172
+ }
173
+ }
174
+ }
175
+ // LT : this is A1
176
+ if (topChars && p === rangeString.length) {
177
+ return makeRange(top, $top, left, $left, top, $top, left, $left, trim);
178
+ }
179
+ }
180
+ else {
181
+ // T B : could be 1:1
182
+ // T BR: could be 1:A1 (if allowTernary)
183
+ const [ topChars, top, $top ] = advA1Row(rangeString, p);
184
+ if (topChars) {
185
+ p += topChars;
186
+ const [ op, trim ] = advRangeOp(rangeString, p);
187
+ if (op) {
188
+ p += op;
189
+ [ rightChars, right, $right ] = advA1Col(rangeString, p);
190
+ p += rightChars;
191
+ [ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p);
192
+ p += bottomChars;
193
+ if (rightChars && bottomChars && allowTernary) {
194
+ if (p === rangeString.length) {
195
+ return makeRange(top, $top, null, false, bottom, $bottom, right, $right, trim);
196
+ }
197
+ }
198
+ else if (!rightChars && bottomChars) {
199
+ if (p === rangeString.length) {
200
+ return makeRange(top, $top, null, false, bottom, $bottom, null, false, trim);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,337 @@
1
+ /* eslint-disable @stylistic/object-property-newline */
2
+ import { describe, test, expect } from 'vitest';
3
+ import { parseA1Ref, parseA1RefXlsx, type OptsParseA1Ref } from './parseA1Ref.ts';
4
+
5
+ type IsA1EqualOptions = OptsParseA1Ref & {
6
+ xlsx?: boolean,
7
+ };
8
+
9
+ function isA1Equal (expr: string, expected: any, opts?: IsA1EqualOptions) {
10
+ const xlsx = !!(opts?.xlsx);
11
+ if (expected) {
12
+ expected = xlsx
13
+ ? { workbookName: '', sheetName: '', ...expected }
14
+ : { context: [], ...expected };
15
+ Object.assign(expected, expected);
16
+ if (expected.range && typeof expected.range === 'object') {
17
+ // mix in some defaults so we don't have to write things out in full
18
+ expected.range = {
19
+ top: null, left: null, bottom: null, right: null,
20
+ $top: false, $left: false, $bottom: false, $right: false,
21
+ ...expected.range
22
+ };
23
+ }
24
+ }
25
+ expect(xlsx ? parseA1RefXlsx(expr, opts) : parseA1Ref(expr, opts)).toEqual(expected);
26
+ }
27
+
28
+ describe('parse A1 references', () => {
29
+ test('basic A1 references', () => {
30
+ isA1Equal('A1', { range: { top: 0, left: 0, bottom: 0, right: 0 } });
31
+ isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } });
32
+
33
+ isA1Equal('$A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $left: true } });
34
+ isA1Equal('A$1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $top: true } });
35
+ isA1Equal('A1:$B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $right: true } });
36
+ isA1Equal('A1:B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, $bottom: true } });
37
+ });
38
+
39
+ test('column and row ranges', () => {
40
+ isA1Equal('A:A', { range: { left: 0, right: 0 } });
41
+ isA1Equal('C:C', { range: { left: 2, right: 2 } });
42
+ isA1Equal('C:$C', { range: { left: 2, right: 2, $right: true } });
43
+ isA1Equal('$C:C', { range: { left: 2, right: 2, $left: true } });
44
+ isA1Equal('$C:$C', { range: { left: 2, right: 2, $left: true, $right: true } });
45
+
46
+ isA1Equal('1:1', { range: { top: 0, bottom: 0 } });
47
+ isA1Equal('10:10', { range: { top: 9, bottom: 9 } });
48
+ isA1Equal('10:$10', { range: { top: 9, bottom: 9, $bottom: true } });
49
+ isA1Equal('$10:10', { range: { top: 9, bottom: 9, $top: true } });
50
+ isA1Equal('$10:$10', { range: { top: 9, bottom: 9, $top: true, $bottom: true } });
51
+ });
52
+
53
+ test('maximum ranges', () => {
54
+ isA1Equal('XFD1048576', { range: { top: 1048575, left: 16383, bottom: 1048575, right: 16383 } });
55
+ });
56
+
57
+ test('sheet references', () => {
58
+ isA1Equal('Sheet1!A1', {
59
+ context: [ 'Sheet1' ],
60
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
61
+ });
62
+
63
+ isA1Equal('\'Sheet1\'!A1', {
64
+ context: [ 'Sheet1' ],
65
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
66
+ });
67
+
68
+ isA1Equal('\'Sheet1\'\'s\'!A1', {
69
+ context: [ 'Sheet1\'s' ],
70
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
71
+ });
72
+ });
73
+
74
+ test('workbook references', () => {
75
+ isA1Equal('[Workbook.xlsx]Sheet1!A1', {
76
+ context: [ 'Workbook.xlsx', 'Sheet1' ],
77
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
78
+ });
79
+
80
+ isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
81
+ context: [ 'Workbook.xlsx', 'Sheet1' ],
82
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
83
+ });
84
+
85
+ isA1Equal("='[Workbook.xlsx]Sheet1'!A1", {
86
+ context: [ 'Workbook.xlsx', 'Sheet1' ],
87
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
88
+ });
89
+
90
+ isA1Equal('[foo bar]Sheet1!A1', {
91
+ context: [ 'foo bar', 'Sheet1' ],
92
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
93
+ });
94
+
95
+ isA1Equal('[a "b" c]d!A1', {
96
+ context: [ 'a "b" c', 'd' ],
97
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
98
+ });
99
+ });
100
+
101
+ test('long context names', () => {
102
+ // unless we know the contexts available, we don't know that this is a sheet
103
+ // or a filename, so we can't reject it:
104
+ isA1Equal('0123456789abcdefghijklmnopqrstuvwxyz!A1', {
105
+ context: [ '0123456789abcdefghijklmnopqrstuvwxyz' ],
106
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
107
+ });
108
+ });
109
+
110
+ test('invalid references', () => {
111
+ isA1Equal('[Workbook.xlsx]!A1', undefined);
112
+ isA1Equal('[Workbook.xlsx]!A1:B2', undefined);
113
+ isA1Equal('[Workbook.xlsx]!A:A', undefined);
114
+ isA1Equal('[Workbook.xlsx]!1:1', undefined);
115
+ isA1Equal('[]Sheet1!A1', undefined);
116
+ });
117
+
118
+ test('named ranges', () => {
119
+ isA1Equal('namedrange', { name: 'namedrange' });
120
+
121
+ isA1Equal('Workbook.xlsx!namedrange', {
122
+ context: [ 'Workbook.xlsx' ],
123
+ name: 'namedrange'
124
+ });
125
+
126
+ isA1Equal("'Workbook.xlsx'!namedrange", {
127
+ context: [ 'Workbook.xlsx' ],
128
+ name: 'namedrange'
129
+ });
130
+
131
+ isA1Equal('[Workbook.xlsx]!namedrange', undefined);
132
+ isA1Equal('pensioneligibilitypartner1', { name: 'pensioneligibilitypartner1' });
133
+ isA1Equal('XFE1048577', { name: 'XFE1048577' });
134
+ });
135
+
136
+ test('named ranges with allowNamed: false', () => {
137
+ isA1Equal('namedrange', undefined, { allowNamed: false });
138
+ isA1Equal('Workbook.xlsx!namedrange', undefined, { allowNamed: false });
139
+ isA1Equal('pensioneligibilitypartner1', undefined, { allowNamed: false });
140
+ isA1Equal('XFE1048577', undefined, { allowNamed: false });
141
+ });
142
+ });
143
+
144
+ describe('parse A1 ranges in XLSX mode', () => {
145
+ const opts = { xlsx: true };
146
+
147
+ test('workbook references', () => {
148
+ isA1Equal('[1]!A1', {
149
+ workbookName: '1',
150
+ sheetName: '',
151
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
152
+ }, opts);
153
+
154
+ isA1Equal('[Workbook.xlsx]!A1', {
155
+ workbookName: 'Workbook.xlsx',
156
+ sheetName: '',
157
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
158
+ }, opts);
159
+
160
+ isA1Equal('[1]Sheet1!A1', {
161
+ workbookName: '1',
162
+ sheetName: 'Sheet1',
163
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
164
+ }, opts);
165
+
166
+ isA1Equal('[Workbook.xlsx]Sheet1!A1', {
167
+ workbookName: 'Workbook.xlsx',
168
+ sheetName: 'Sheet1',
169
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
170
+ }, opts);
171
+ });
172
+
173
+ test('named ranges in workbooks', () => {
174
+ isA1Equal('[4]!name', {
175
+ workbookName: '4',
176
+ sheetName: '',
177
+ name: 'name'
178
+ }, opts);
179
+
180
+ isA1Equal('[Workbook.xlsx]!name', {
181
+ workbookName: 'Workbook.xlsx',
182
+ sheetName: '',
183
+ name: 'name'
184
+ }, opts);
185
+
186
+ isA1Equal('[16]Sheet1!name', {
187
+ workbookName: '16',
188
+ sheetName: 'Sheet1',
189
+ name: 'name'
190
+ }, opts);
191
+
192
+ isA1Equal('[Workbook.xlsx]Sheet1!name', {
193
+ workbookName: 'Workbook.xlsx',
194
+ sheetName: 'Sheet1',
195
+ name: 'name'
196
+ }, opts);
197
+ });
198
+
199
+ test('quoted references', () => {
200
+ isA1Equal("='[1]'!A1", {
201
+ workbookName: '1',
202
+ sheetName: '',
203
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
204
+ }, opts);
205
+
206
+ isA1Equal("='[Workbook.xlsx]'!A1", {
207
+ workbookName: 'Workbook.xlsx',
208
+ sheetName: '',
209
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
210
+ }, opts);
211
+
212
+ isA1Equal("'[1]Sheet1'!A1", {
213
+ workbookName: '1',
214
+ sheetName: 'Sheet1',
215
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
216
+ }, opts);
217
+
218
+ isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
219
+ workbookName: 'Workbook.xlsx',
220
+ sheetName: 'Sheet1',
221
+ range: { top: 0, left: 0, bottom: 0, right: 0 }
222
+ }, opts);
223
+
224
+ isA1Equal("'[4]'!name", {
225
+ workbookName: '4',
226
+ sheetName: '',
227
+ name: 'name'
228
+ }, opts);
229
+
230
+ isA1Equal("'[Workbook.xlsx]'!name", {
231
+ workbookName: 'Workbook.xlsx',
232
+ sheetName: '',
233
+ name: 'name'
234
+ }, opts);
235
+
236
+ isA1Equal("'[16]Sheet1'!name", {
237
+ workbookName: '16',
238
+ sheetName: 'Sheet1',
239
+ name: 'name'
240
+ }, opts);
241
+
242
+ isA1Equal("'[Workbook.xlsx]Sheet1'!name", {
243
+ workbookName: 'Workbook.xlsx',
244
+ sheetName: 'Sheet1',
245
+ name: 'name'
246
+ }, opts);
247
+ });
248
+ });
249
+
250
+ describe('A1 partial ranges', () => {
251
+ const opt = { allowTernary: true };
252
+
253
+ test('partial ranges not allowed by default', () => {
254
+ isA1Equal('A10:A', undefined);
255
+ isA1Equal('B3:2', undefined);
256
+ });
257
+
258
+ test('unbounded bottom ranges', () => {
259
+ isA1Equal('A10:A', { range: { top: 9, left: 0, right: 0 } }, opt);
260
+ isA1Equal('A:A10', { range: { top: 9, left: 0, right: 0 } }, opt);
261
+ isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
262
+ isA1Equal('A:A$5', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
263
+ isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
264
+ isA1Equal('A:$B5', { range: { top: 4, left: 0, right: 1, $right: true } }, opt);
265
+ isA1Equal('$B:B3', { range: { top: 2, left: 1, right: 1, $left: true } }, opt);
266
+ isA1Equal('$B:C5', { range: { top: 4, left: 1, right: 2, $left: true } }, opt);
267
+ isA1Equal('C2:B', { range: { top: 1, left: 1, right: 2 } }, opt);
268
+ isA1Equal('C:B2', { range: { top: 1, left: 1, right: 2 } }, opt);
269
+ });
270
+
271
+ test('unbounded right ranges', () => {
272
+ isA1Equal('D1:1', { range: { top: 0, left: 3, bottom: 0 } }, opt);
273
+ isA1Equal('1:D2', { range: { top: 0, left: 3, bottom: 1 } }, opt);
274
+ isA1Equal('2:$D3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
275
+ isA1Equal('$D2:3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
276
+ isA1Equal('1:D$1', { range: { top: 0, left: 3, bottom: 0, $bottom: true } }, opt);
277
+ isA1Equal('$1:D1', { range: { top: 0, left: 3, bottom: 0, $top: true } }, opt);
278
+ isA1Equal('AA$3:4', { range: { top: 2, left: 26, bottom: 3, $top: true } }, opt);
279
+ isA1Equal('B3:2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
280
+ isA1Equal('3:B2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
281
+ });
282
+ });
283
+
284
+ describe('A1 trimmed ranges', () => {
285
+ const locks = { $top: true, $left: true, $bottom: true, $right: true };
286
+ const opts = [ {}, { xlsx: true } ];
287
+
288
+ for (const opt of opts) {
289
+ test(`trimmed ranges with ${opt.xlsx ? 'XLSX' : 'default'} options`, () => {
290
+ isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } }, opt);
291
+ isA1Equal('A1.:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head' } }, opt);
292
+ isA1Equal('A1:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail' } }, opt);
293
+ isA1Equal('A1.:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both' } }, opt);
294
+
295
+ isA1Equal('$A$1:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, ...locks } }, opt);
296
+ isA1Equal('$A$1.:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head', ...locks } }, opt);
297
+ isA1Equal('$A$1:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail', ...locks } }, opt);
298
+ isA1Equal('$A$1.:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both', ...locks } }, opt);
299
+
300
+ isA1Equal('J:J', { range: { top: null, left: 9, bottom: null, right: 9 } }, opt);
301
+ isA1Equal('J.:J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'head' } }, opt);
302
+ isA1Equal('J:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'tail' } }, opt);
303
+ isA1Equal('J.:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'both' } }, opt);
304
+
305
+ isA1Equal('10:10', { range: { top: 9, left: null, bottom: 9, right: null } }, opt);
306
+ isA1Equal('10.:10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'head' } }, opt);
307
+ isA1Equal('10:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'tail' } }, opt);
308
+ isA1Equal('10.:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'both' } }, opt);
309
+
310
+ isA1Equal('J10:J', undefined, { ...opt });
311
+ isA1Equal('J10:10', undefined, { ...opt });
312
+ isA1Equal('J10.:.J', undefined, { ...opt });
313
+ isA1Equal('J10.:.10', undefined, { ...opt });
314
+ isA1Equal('J10:J', { range: { top: 9, left: 9, bottom: null, right: 9 } }, { allowTernary: true, ...opt });
315
+ isA1Equal('J10:10', { range: { top: 9, left: 9, bottom: 9, right: null } }, { allowTernary: true, ...opt });
316
+ isA1Equal('J10.:.J', { range: { top: 9, left: 9, bottom: null, right: 9, trim: 'both' } }, { allowTernary: true, ...opt });
317
+ isA1Equal('J10.:.10', { range: { top: 9, left: 9, bottom: 9, right: null, trim: 'both' } }, { allowTernary: true, ...opt });
318
+ });
319
+ }
320
+ });
321
+
322
+ describe('A1 trimmed ranges vs named ranges', () => {
323
+ test('named ranges cannot be trimmed', () => {
324
+ isA1Equal('name1.:.name1', undefined);
325
+ isA1Equal('name1.:.foo', undefined);
326
+ isA1Equal('foo.:.name1', undefined);
327
+ });
328
+
329
+ test('trimmed column references', () => {
330
+ // prior to the intruduction of trimed ranges, the following would have
331
+ // been an expression: NAME:`foo.` OP:`:`, COLUMN:`bar`
332
+ isA1Equal('foo.:bar', { range: { left: 1395, right: 4460, trim: 'head' } });
333
+ // prior to the intruduction of trimed ranges, the following would have
334
+ // been an expression: NAME:`foo.` OP:`:`, CELL:`B2`
335
+ isA1Equal('foo.:B2', { range: { top: 1, left: 1, right: 4460, trim: 'head' } }, { allowTernary: true });
336
+ });
337
+ });
@@ -0,0 +1,115 @@
1
+ import { parseRefCtx, parseRefXlsx } from './parseRef.ts';
2
+ import { parseA1Range } from './parseA1Range.ts';
3
+ import type { ReferenceA1, ReferenceA1Xlsx, ReferenceName, ReferenceNameXlsx } from './types.ts';
4
+
5
+ /**
6
+ * Options for {@link parseA1Ref}.
7
+ */
8
+ export type OptsParseA1Ref = {
9
+ /**
10
+ * Enable parsing names as well as ranges.
11
+ * @defaultValue true
12
+ */
13
+ allowNamed?: boolean,
14
+ /**
15
+ * Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`.
16
+ * These are supported by Google Sheets but not Excel. See: [References.md](./References.md).
17
+ * @defaultValue false
18
+ */
19
+ allowTernary?: boolean,
20
+ };
21
+
22
+ /**
23
+ * Parse a string reference into an object representing it.
24
+ *
25
+ * ```js
26
+ * parseA1Ref('Sheet1!A$1:$B2');
27
+ * // => {
28
+ * // context: [ 'Sheet1' ],
29
+ * // range: {
30
+ * // top: 0,
31
+ * // left: 0,
32
+ * // bottom: 1,
33
+ * // right: 1
34
+ * // $top: true,
35
+ * // $left: false,
36
+ * // $bottom: false,
37
+ * // $right: true
38
+ * // }
39
+ * // }
40
+ * ```
41
+ *
42
+ * For A:A or A1:A style ranges, `null` will be used for any dimensions that the
43
+ * syntax does not specify.
44
+ *
45
+ * @see {@link OptsParseA1Ref}
46
+ * @param refString An A1-style reference string.
47
+ * @param options Options.
48
+ * @returns An object representing a valid reference or `undefined` if it is invalid.
49
+ */
50
+ export function parseA1Ref (
51
+ refString: string,
52
+ { allowNamed = true, allowTernary = false }: OptsParseA1Ref = {}
53
+ ): ReferenceA1 | ReferenceName | undefined {
54
+ const d = parseRefCtx(refString, { allowNamed, allowTernary, r1c1: false });
55
+ if (d) {
56
+ if (d.name) {
57
+ return { context: d.context, name: d.name };
58
+ }
59
+ else if (d.r0) {
60
+ const range = parseA1Range(d.r1 ? d.r0 + d.operator + d.r1 : d.r0);
61
+ if (range) {
62
+ return { context: d.context, range };
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Parse a string reference into an object representing it.
70
+ *
71
+ * ```js
72
+ * parseA1Ref('Sheet1!A$1:$B2');
73
+ * // => {
74
+ * // workbookName: '',
75
+ * // sheetName: 'Sheet1',
76
+ * // range: {
77
+ * // top: 0,
78
+ * // left: 0,
79
+ * // bottom: 1,
80
+ * // right: 1
81
+ * // $top: true,
82
+ * // $left: false,
83
+ * // $bottom: false,
84
+ * // $right: true
85
+ * // }
86
+ * // }
87
+ * ```
88
+ *
89
+ * For A:A or A1:A style ranges, `null` will be used for any dimensions that the
90
+ * syntax does not specify.
91
+ *
92
+ * @see {@link OptsParseA1Ref}
93
+ * @param refString An A1-style reference string.
94
+ * @param options Options.
95
+ * @returns An object representing a valid reference or `undefined` if it is invalid.
96
+ */
97
+ export function parseA1RefXlsx (
98
+ refString: string,
99
+ { allowNamed = true, allowTernary = false }: OptsParseA1Ref = {}
100
+ ): ReferenceA1Xlsx | ReferenceNameXlsx | undefined {
101
+ const d = parseRefXlsx(refString, { allowNamed, allowTernary, r1c1: false });
102
+ if (d) {
103
+ if (d.name) {
104
+ return { workbookName: d.workbookName, sheetName: d.sheetName, name: d.name };
105
+ }
106
+ else if (d.r0) {
107
+ if (d.r0) {
108
+ const range = parseA1Range(d.r1 ? d.r0 + d.operator + d.r1 : d.r0);
109
+ if (range) {
110
+ return { workbookName: d.workbookName, sheetName: d.sheetName, range };
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }