@borgar/fx 4.12.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 (139) 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.ts +18 -0
  36. package/lib/lexers/canEndRange.ts +25 -0
  37. package/lib/lexers/lexBoolean.ts +55 -0
  38. package/lib/lexers/lexContext.ts +104 -0
  39. package/lib/lexers/lexError.ts +15 -0
  40. package/lib/lexers/lexFunction.ts +37 -0
  41. package/lib/lexers/lexNameFuncCntx.ts +112 -0
  42. package/lib/lexers/lexNamed.ts +60 -0
  43. package/lib/lexers/lexNewLine.ts +12 -0
  44. package/lib/lexers/lexNumber.ts +48 -0
  45. package/lib/lexers/lexOperator.ts +26 -0
  46. package/lib/lexers/lexRange.ts +15 -0
  47. package/lib/lexers/lexRangeA1.ts +134 -0
  48. package/lib/lexers/lexRangeR1C1.ts +146 -0
  49. package/lib/lexers/lexRangeTrim.ts +26 -0
  50. package/lib/lexers/lexRefOp.ts +19 -0
  51. package/lib/lexers/lexString.ts +22 -0
  52. package/lib/lexers/lexStructured.ts +25 -0
  53. package/lib/lexers/lexWhitespace.ts +31 -0
  54. package/lib/lexers/sets.ts +51 -0
  55. package/lib/mergeRefTokens.spec.ts +141 -0
  56. package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +47 -32
  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.ts +240 -0
  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 +46 -30
  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/dist/fx.d.ts +0 -823
  104. package/dist/fx.js +0 -2
  105. package/dist/package.json +0 -1
  106. package/lib/a1.js +0 -348
  107. package/lib/a1.spec.js +0 -458
  108. package/lib/addTokenMeta.spec.js +0 -153
  109. package/lib/astTypes.js +0 -96
  110. package/lib/extraTypes.js +0 -74
  111. package/lib/fixRanges.js +0 -104
  112. package/lib/fixRanges.spec.js +0 -170
  113. package/lib/fromCol.spec.js +0 -11
  114. package/lib/index.js +0 -134
  115. package/lib/index.spec.js +0 -67
  116. package/lib/isType.spec.js +0 -168
  117. package/lib/lexer-srefs.spec.js +0 -324
  118. package/lib/lexer.js +0 -283
  119. package/lib/lexer.spec.js +0 -1953
  120. package/lib/lexerParts.js +0 -228
  121. package/lib/mergeRefTokens.spec.js +0 -121
  122. package/lib/package.json +0 -1
  123. package/lib/parseRef.js +0 -157
  124. package/lib/parseRef.spec.js +0 -71
  125. package/lib/parseSRange.js +0 -167
  126. package/lib/parseStructRef.js +0 -48
  127. package/lib/parseStructRef.spec.js +0 -164
  128. package/lib/parser.spec.js +0 -1208
  129. package/lib/rc.js +0 -341
  130. package/lib/rc.spec.js +0 -403
  131. package/lib/stringifyStructRef.js +0 -80
  132. package/lib/stringifyStructRef.spec.js +0 -182
  133. package/lib/toCol.spec.js +0 -11
  134. package/lib/translate-toA1.spec.js +0 -214
  135. package/lib/translate-toRC.spec.js +0 -197
  136. package/lib/translate.js +0 -239
  137. package/lib/translate.spec.js +0 -21
  138. package/rollup.config.mjs +0 -22
  139. package/tsd.json +0 -12
@@ -0,0 +1,346 @@
1
+ import {
2
+ FX_PREFIX,
3
+ NEWLINE,
4
+ NUMBER,
5
+ OPERATOR,
6
+ REF_NAMED,
7
+ UNKNOWN,
8
+ WHITESPACE,
9
+ FUNCTION,
10
+ OPERATOR_TRIM,
11
+ REF_RANGE
12
+ } from './constants.ts';
13
+ import { mergeRefTokens } from './mergeRefTokens.ts';
14
+ import { lexers, type PartLexer } from './lexers/sets.ts';
15
+ import type { Token } from './types.ts';
16
+
17
+ const reLetLambda = /^l(?:ambda|et)$/i;
18
+ const isType = (t: Token, type: string) => t && t.type === type;
19
+ const isTextTokenType = (tokenType: string) => tokenType === REF_NAMED || tokenType === FUNCTION;
20
+
21
+ const causesBinaryMinus = (token: Token) => {
22
+ return !isType(token, OPERATOR) || (
23
+ token.value === '%' ||
24
+ token.value === '}' ||
25
+ token.value === ')' ||
26
+ token.value === '#'
27
+ );
28
+ };
29
+
30
+ function fixRCNames (tokens: Token[]): Token[] {
31
+ let withinCall = 0;
32
+ let parenDepth = 0;
33
+ let lastToken: Token;
34
+ for (const token of tokens) {
35
+ if (token.type === OPERATOR) {
36
+ if (token.value === '(') {
37
+ parenDepth++;
38
+ if (lastToken.type === FUNCTION) {
39
+ if (reLetLambda.test(lastToken.value)) {
40
+ withinCall = parenDepth;
41
+ }
42
+ }
43
+ }
44
+ else if (token.value === ')') {
45
+ parenDepth--;
46
+ if (parenDepth < withinCall) {
47
+ withinCall = 0;
48
+ }
49
+ }
50
+ }
51
+ else if (withinCall && token.type === UNKNOWN && /^[rc]$/.test(token.value)) {
52
+ token.type = REF_NAMED;
53
+ }
54
+ lastToken = token;
55
+ }
56
+ return tokens;
57
+ }
58
+
59
+ type OptsGetTokens = {
60
+ withLocation?: boolean,
61
+ mergeRefs?: boolean,
62
+ negativeNumbers?: boolean
63
+ allowTernary?: boolean
64
+ r1c1?: boolean
65
+ xlsx?: boolean
66
+ };
67
+
68
+ export function getTokens (fx: string, tokenHandlers: PartLexer[], options: OptsGetTokens = {}) {
69
+ const {
70
+ withLocation = false,
71
+ mergeRefs = true,
72
+ negativeNumbers = true
73
+ } = options;
74
+ const opts = {
75
+ withLocation: withLocation,
76
+ mergeRefs: mergeRefs,
77
+ allowTernary: options.allowTernary ?? false,
78
+ negativeNumbers: negativeNumbers,
79
+ r1c1: options.r1c1 ?? false,
80
+ xlsx: options.xlsx ?? false
81
+ };
82
+
83
+ const tokens = [];
84
+ let pos = 0;
85
+ let letOrLambda = 0;
86
+ let unknownRC = 0;
87
+ const trimOps = [];
88
+
89
+ let tail0: Token; // last non-whitespace token
90
+ let tail1: Token; // penultimate non-whitespace token
91
+ let lastToken: Token; // last token
92
+ const pushToken = (token: Token) => {
93
+ let tokenType = token.type;
94
+ const isCurrUnknown = tokenType === UNKNOWN;
95
+ const isLastUnknown = lastToken && lastToken.type === UNKNOWN;
96
+ if (lastToken && (
97
+ (isCurrUnknown && isLastUnknown) ||
98
+ (isCurrUnknown && isTextTokenType(lastToken.type)) ||
99
+ (isLastUnknown && isTextTokenType(tokenType))
100
+ )) {
101
+ // UNKNOWN tokens "contaminate" sibling text tokens
102
+ lastToken.value += token.value;
103
+ lastToken.type = UNKNOWN;
104
+ if (withLocation) {
105
+ lastToken.loc[1] = token.loc[1];
106
+ }
107
+ }
108
+ else {
109
+ if (tokenType === OPERATOR_TRIM) {
110
+ trimOps.push(tokens.length);
111
+ tokenType = UNKNOWN;
112
+ token.type = UNKNOWN;
113
+ }
114
+ // push token as normally
115
+ tokens[tokens.length] = token;
116
+ lastToken = token;
117
+ if (tokenType !== WHITESPACE && tokenType !== NEWLINE) {
118
+ tail1 = tail0;
119
+ tail0 = token;
120
+ }
121
+ }
122
+ };
123
+
124
+ if (fx.startsWith('=')) {
125
+ const token: Token = { type: FX_PREFIX, value: '=' };
126
+ if (withLocation) {
127
+ token.loc = [ 0, 1 ];
128
+ }
129
+ pos++;
130
+ pushToken(token);
131
+ }
132
+
133
+ const numHandlers = tokenHandlers.length;
134
+ while (pos < fx.length) {
135
+ const startPos = pos;
136
+ let token;
137
+ for (let i = 0; i < numHandlers; i++) {
138
+ token = tokenHandlers[i](fx, pos, opts);
139
+ if (token) {
140
+ pos += token.value.length;
141
+ break;
142
+ }
143
+ }
144
+
145
+ if (!token) {
146
+ token = {
147
+ type: UNKNOWN,
148
+ value: fx[pos]
149
+ };
150
+ pos++;
151
+ }
152
+ if (withLocation) {
153
+ token.loc = [ startPos, pos ];
154
+ }
155
+
156
+ // make a note if we found a let/lambda call
157
+ if (lastToken && token.value === '(' && lastToken.type === FUNCTION) {
158
+ if (reLetLambda.test(lastToken.value)) {
159
+ letOrLambda++;
160
+ }
161
+ }
162
+ // make a note if we found a R or C unknown
163
+ if (token.type === UNKNOWN && token.value.length === 1) {
164
+ const valLC = token.value.toLowerCase();
165
+ unknownRC += (valLC === 'r' || valLC === 'c') ? 1 : 0;
166
+ }
167
+
168
+ if (negativeNumbers && token.type === NUMBER) {
169
+ const last1 = lastToken;
170
+ // do we have a number preceded by a minus?
171
+ if (last1?.type === OPERATOR && last1.value === '-') {
172
+ // missing tail1 means we are at the start of the stream
173
+ if (
174
+ !tail1 ||
175
+ tail1.type === FX_PREFIX ||
176
+ !causesBinaryMinus(tail1)
177
+ ) {
178
+ const minus = tokens.pop();
179
+ token.value = '-' + token.value;
180
+ if (token.loc) {
181
+ // ensure offsets are up to date
182
+ token.loc[0] = minus.loc[0];
183
+ }
184
+ // next step tries to counter the screwing around with the tailing
185
+ // it should be correct again once we pushToken()
186
+ tail0 = tail1;
187
+ lastToken = tokens[tokens.length - 1];
188
+ }
189
+ }
190
+ }
191
+
192
+ pushToken(token);
193
+ }
194
+
195
+ // if we encountered both a LAMBDA/LET call, and unknown 'r' or 'c' tokens
196
+ // we'll turn the unknown tokens into names within the call.
197
+ if (unknownRC && letOrLambda) {
198
+ fixRCNames(tokens);
199
+ }
200
+
201
+ // Any OPERATOR_TRIM tokens have been indexed already, they now need to be
202
+ // either turned into OPERATORs or UNKNOWNs. Trim operators are only allowed
203
+ // between two REF_RANGE tokens as they are not valid in expressions as full
204
+ // operators.
205
+ for (const index of trimOps) {
206
+ const before = tokens[index - 1];
207
+ const after = tokens[index + 1];
208
+ tokens[index].type = (before?.type === REF_RANGE && after?.type === REF_RANGE)
209
+ ? OPERATOR
210
+ : UNKNOWN;
211
+ }
212
+
213
+ if (mergeRefs) {
214
+ return mergeRefTokens(tokens);
215
+ }
216
+
217
+ return tokens;
218
+ }
219
+
220
+ /**
221
+ * Options for {@link tokenize}.
222
+ */
223
+ export type OptsTokenize = {
224
+ /**
225
+ * Nodes will include source position offsets to the tokens: `{ loc: [ start, end ] }`
226
+ * @defaultValue true
227
+ */
228
+ withLocation?: boolean,
229
+ /**
230
+ * Should ranges be returned as whole references (`Sheet1!A1:B2`) or as separate tokens for each
231
+ * part: (`Sheet1`,`!`,`A1`,`:`,`B2`). This is the same as calling [`mergeRefTokens`](#mergeRefTokens)
232
+ * @defaultValue true
233
+ */
234
+ mergeRefs?: boolean,
235
+ /**
236
+ * Merges unary minuses with their immediately following number tokens (`-`,`1`) => `-1`
237
+ * (alternatively these will be unary operations in the tree).
238
+ * @defaultValue true
239
+ */
240
+ negativeNumbers?: boolean
241
+ /**
242
+ * Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported
243
+ * by Google Sheets but not Excel. See: [References.md](./References.md).
244
+ * @defaultValue false
245
+ */
246
+ allowTernary?: boolean
247
+ /**
248
+ * Ranges are expected to be in the R1C1 style format rather than the more popular A1 style.
249
+ * @defaultValue false
250
+ */
251
+ r1c1?: boolean
252
+ };
253
+
254
+ /**
255
+ * Breaks a string formula into a list of tokens.
256
+ *
257
+ * The returned output will be an array of objects representing the tokens:
258
+ *
259
+ * ```js
260
+ * [
261
+ * { type: FX_PREFIX, value: '=' },
262
+ * { type: FUNCTION, value: 'SUM' },
263
+ * { type: OPERATOR, value: '(' },
264
+ * { type: REF_RANGE, value: 'A1:B2' },
265
+ * { type: OPERATOR, value: ')' }
266
+ * ]
267
+ * ```
268
+ *
269
+ * A collection of token types may be found as an object as the {@link tokenTypes}
270
+ * export on the package.
271
+ *
272
+ * _Warning:_ To support syntax highlighting as you type, `STRING` tokens are allowed to be
273
+ * "unterminated". For example, the incomplete formula `="Hello world` would be
274
+ * tokenized as:
275
+ *
276
+ * ```js
277
+ * [
278
+ * { type: FX_PREFIX, value: '=' },
279
+ * { type: STRING, value: '"Hello world', unterminated: true },
280
+ * ]
281
+ * ```
282
+ *
283
+ * Parsers will need to handle this.
284
+ *
285
+ * @see {@link OptsTokenize}
286
+ * @see {@link tokenTypes}
287
+ * @param formula An Excel formula string (an Excel expression).
288
+ * @param [options] Options
289
+ * @returns An array of Tokens
290
+ */
291
+ export function tokenize (
292
+ formula: string,
293
+ options: OptsTokenize = {}
294
+ ): Token[] {
295
+ return getTokens(formula, lexers, options);
296
+ }
297
+
298
+ /**
299
+ * Breaks a string formula into a list of tokens.
300
+ *
301
+ * The returned output will be an array of objects representing the tokens:
302
+ *
303
+ * ```js
304
+ * [
305
+ * { type: FX_PREFIX, value: '=' },
306
+ * { type: FUNCTION, value: 'SUM' },
307
+ * { type: OPERATOR, value: '(' },
308
+ * { type: REF_RANGE, value: 'A1:B2' },
309
+ * { type: OPERATOR, value: ')' }
310
+ * ]
311
+ * ```
312
+ *
313
+ * A collection of token types may be found as an object as the {@link tokenTypes}
314
+ * export on the package.
315
+ *
316
+ * _Warning:_ To support syntax highlighting as you type, `STRING` tokens are allowed to be
317
+ * "unterminated". For example, the incomplete formula `="Hello world` would be
318
+ * tokenized as:
319
+ *
320
+ * ```js
321
+ * [
322
+ * { type: FX_PREFIX, value: '=' },
323
+ * { type: STRING, value: '"Hello world', unterminated: true },
324
+ * ]
325
+ * ```
326
+ *
327
+ * @see {@link OptsTokenize}
328
+ * @see {@link tokenTypes}
329
+ * @param formula An Excel formula string (an Excel expression).
330
+ * @param [options] Options
331
+ * @returns An array of Tokens
332
+ */
333
+ export function tokenizeXlsx (
334
+ formula: string,
335
+ options: OptsTokenize = {}
336
+ ): Token[] {
337
+ const opts = {
338
+ withLocation: options.withLocation ?? false,
339
+ mergeRefs: options.mergeRefs ?? true,
340
+ allowTernary: options.allowTernary ?? false,
341
+ negativeNumbers: options.negativeNumbers ?? true,
342
+ r1c1: options.r1c1 ?? false,
343
+ xlsx: true
344
+ };
345
+ return getTokens(formula, lexers, opts);
346
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { translateFormulaToR1C1 } from './translateToR1C1.ts';
3
+ import { translateFormulaToA1 } from './translateToA1.ts';
4
+
5
+ function okayRoundTrip (expr: string, anchor: string, options?: any) {
6
+ const rc = translateFormulaToR1C1(expr, anchor, options);
7
+ const a1 = translateFormulaToA1(rc, anchor, options);
8
+ expect(a1).toBe(expr);
9
+ }
10
+
11
+ describe('translate absolute cells from A1 to RC', () => {
12
+ test('sheet qualified ranges', () => {
13
+ okayRoundTrip('=Sheet1!$1:$1048576', 'A1');
14
+ });
15
+
16
+ test('mixed absolute ranges', () => {
17
+ okayRoundTrip('=D$1:$BJ$1048576', 'A1');
18
+ });
19
+
20
+ test('function calls with ranges', () => {
21
+ okayRoundTrip('=VLOOKUP(C7,Röðun,4,0)', 'A1');
22
+ okayRoundTrip('=COUNTIF(B$1442:B$1048576,$G1442)', 'A1');
23
+ });
24
+
25
+ test('complex expressions', () => {
26
+ okayRoundTrip('=IF(p2m<=D5,10,0)*scene_spend', 'A1');
27
+ okayRoundTrip('=(kwh_used_daily*kwhbtu*co2btu)/1000000', 'A1');
28
+ okayRoundTrip('=NOPLATT1+g1_+ROIC1+WACC+G1+g1_+G130+ROIC2+WACC+g2_+WACC+N', 'A1');
29
+ });
30
+
31
+ test('ternary ranges with allowTernary disabled', () => {
32
+ // FIXME: translate needs to be be able to specify allowTernary=false
33
+ okayRoundTrip('=foo:C3:D4', 'A1', { allowTernary: false });
34
+ });
35
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { translateFormulaToA1, translateTokensToA1 } from './translateToA1.ts';
3
+ import { tokenize, tokenizeXlsx } from './tokenize.ts';
4
+ import { addTokenMeta } from './addTokenMeta.ts';
5
+ import { ERROR, FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.ts';
6
+
7
+ function isR2A (expr: string, anchor: string, result: string, opts?: any) {
8
+ expect(translateFormulaToA1(expr, anchor, opts)).toBe(result);
9
+ }
10
+
11
+ describe('translate absolute cells from RC to A1', () => {
12
+ test('absolute cells with B2 anchor', () => {
13
+ isR2A('=R1C1', 'B2', '=$A$1');
14
+ isR2A('=R2C1', 'B2', '=$A$2');
15
+ isR2A('=R3C1', 'B2', '=$A$3');
16
+ isR2A('=R1C2', 'B2', '=$B$1');
17
+ isR2A('=R2C2', 'B2', '=$B$2');
18
+ isR2A('=R3C2', 'B2', '=$B$3');
19
+ isR2A('=R1C3', 'B2', '=$C$1');
20
+ isR2A('=R2C3', 'B2', '=$C$2');
21
+ isR2A('=R3C3', 'B2', '=$C$3');
22
+ });
23
+
24
+ test('absolute cells with Z19 anchor', () => {
25
+ // absolute cells, anchor has no real effect
26
+ isR2A('=R1C1', 'Z19', '=$A$1');
27
+ isR2A('=R2C1', 'Z19', '=$A$2');
28
+ isR2A('=R3C1', 'Z19', '=$A$3');
29
+ isR2A('=R1C2', 'Z19', '=$B$1');
30
+ isR2A('=R2C2', 'Z19', '=$B$2');
31
+ isR2A('=R3C2', 'Z19', '=$B$3');
32
+ isR2A('=R1C3', 'Z19', '=$C$1');
33
+ isR2A('=R2C3', 'Z19', '=$C$2');
34
+ isR2A('=R3C3', 'Z19', '=$C$3');
35
+ });
36
+ });
37
+
38
+ describe('translate relative cells from RC to A1', () => {
39
+ test('relative cells with B2 anchor', () => {
40
+ isR2A('=R[-1]C[-1]', 'B2', '=A1');
41
+ isR2A('=RC[-1]', 'B2', '=A2');
42
+ isR2A('=R[1]C[-1]', 'B2', '=A3');
43
+ isR2A('=R[-1]C', 'B2', '=B1');
44
+ isR2A('=RC', 'B2', '=B2');
45
+ isR2A('=R[1]C', 'B2', '=B3');
46
+ isR2A('=R[-1]C[1]', 'B2', '=C1');
47
+ isR2A('=RC[1]', 'B2', '=C2');
48
+ isR2A('=R[1]C[1]', 'B2', '=C3');
49
+ });
50
+
51
+ test('relative cells with I12 anchor', () => {
52
+ // relative cells move with anchor
53
+ isR2A('=R[-1]C[-1]', 'I12', '=H11');
54
+ isR2A('=RC[-1]', 'I12', '=H12');
55
+ isR2A('=R[1]C[-1]', 'I12', '=H13');
56
+ isR2A('=R[-1]C', 'I12', '=I11');
57
+ isR2A('=RC', 'I12', '=I12');
58
+ isR2A('=R[1]C', 'I12', '=I13');
59
+ isR2A('=R[-1]C[1]', 'I12', '=J11');
60
+ isR2A('=RC[1]', 'I12', '=J12');
61
+ isR2A('=R[1]C[1]', 'I12', '=J13');
62
+ });
63
+
64
+ test('relative cells with explicit [0] notation', () => {
65
+ isR2A('=R[0]C[-1]', 'B2', '=A2');
66
+ isR2A('=R[-1]C[0]', 'B2', '=B1');
67
+ isR2A('=R[0]C[0]', 'B2', '=B2');
68
+ isR2A('=R[1]C[0]', 'B2', '=B3');
69
+ isR2A('=R[0]C[1]', 'B2', '=C2');
70
+ });
71
+ });
72
+
73
+ describe('translate rows from RC to A1', () => {
74
+ test('relative and absolute row references', () => {
75
+ isR2A('=R', 'B2', '=2:2');
76
+ isR2A('=R[0]', 'B2', '=2:2');
77
+ isR2A('=R', 'B13', '=13:13');
78
+ isR2A('=R:R', 'B2', '=2:2');
79
+ isR2A('=R2:R2', 'B2', '=$2:$2');
80
+ isR2A('=R:R2', 'B2', '=2:$2');
81
+ isR2A('=R[1]:R[-1]', 'Z10', '=9:11');
82
+ });
83
+ });
84
+
85
+ describe('translate cols from RC to A1', () => {
86
+ test('relative and absolute column references', () => {
87
+ isR2A('=C', 'B2', '=B:B');
88
+ isR2A('=C[0]', 'B2', '=B:B');
89
+ isR2A('=C', 'Z2', '=Z:Z');
90
+ isR2A('=C:C', 'B2', '=B:B');
91
+ isR2A('=C2:C2', 'B2', '=$B:$B');
92
+ isR2A('=C:C2', 'B2', '=B:$B');
93
+ isR2A('=C[1]:C[-1]', 'M10', '=L:N');
94
+ });
95
+ });
96
+
97
+ describe('translate partials from RC to A1', () => {
98
+ test('partial range references', () => {
99
+ isR2A('=R[-5]C[-2]:C[-2]', 'C6', '=A1:A');
100
+ isR2A('=R[-5]C[-3]:R[-5]', 'D6', '=A1:1');
101
+ isR2A('=R[-6]C1:C1', 'C7', '=$A1:$A');
102
+ isR2A('=C1:R[-6]C1', 'D7', '=$A1:$A');
103
+ isR2A('=R[-6]C1:R[-6]', 'C7', '=$A1:1');
104
+ isR2A('=R[-6]:R[-6]C1', 'C7', '=$A1:1');
105
+ isR2A('=R1C[-2]:C[-2]', 'C6', '=A$1:A');
106
+ isR2A('=C[-2]:R1C[-2]', 'C6', '=A$1:A');
107
+ isR2A('=R1C[-3]:R1', 'D6', '=A$1:$1');
108
+ isR2A('=R1C1:C1', 'D6', '=$A$1:$A');
109
+ isR2A('=C1:R1C1', 'D6', '=$A$1:$A');
110
+ isR2A('=R1C1:R1', 'D6', '=$A$1:$1');
111
+ isR2A('=R1:R1C1', 'D6', '=$A$1:$1');
112
+ });
113
+ });
114
+
115
+ describe('translate bounds coords from RC to A1', () => {
116
+ test('boundary coordinate references', () => {
117
+ isR2A('=C[-1]', 'A1', '=XFD:XFD');
118
+ isR2A('=C[-2]', 'A1', '=XFC:XFC');
119
+ isR2A('=RC[16383]', 'B1', '=A1');
120
+ isR2A('=RC[16383]', 'C1', '=B1');
121
+ isR2A('=R[-1]', 'A1', '=1048576:1048576');
122
+ isR2A('=R[-2]', 'A1', '=1048575:1048575');
123
+ isR2A('=R[1048575]C', 'A2', '=A1');
124
+ isR2A('=R[1048575]C', 'A3', '=A2');
125
+
126
+ isR2A('=R1:R1048576', 'A1', '=$1:$1048576');
127
+ isR2A('=C1:C16384', 'A1', '=$A:$XFD');
128
+ isR2A('=R1C1:R1048576C16384', 'A1', '=$A$1:$XFD$1048576');
129
+ });
130
+
131
+ test('out of bounds references with wrapEdges disabled', () => {
132
+ const f1 = '=R[-1]C[-1]';
133
+ expect(translateFormulaToA1(f1, 'A1', { wrapEdges: false })).toBe('=#REF!');
134
+
135
+ const tokens = addTokenMeta(tokenize('SUM(Sheet1!R[-1]C[-1])', { r1c1: true, withLocation: true }));
136
+ expect(translateTokensToA1(tokens, 'A1', { wrapEdges: false })).toEqual([
137
+ { type: FUNCTION, value: 'SUM', loc: [ 0, 3 ], index: 0, depth: 0 },
138
+ { type: OPERATOR, value: '(', loc: [ 3, 4 ], index: 1, depth: 1, groupId: 'fxg1' },
139
+ { type: ERROR, value: '#REF!', loc: [ 4, 9 ], index: 2, depth: 1 },
140
+ { type: OPERATOR, value: ')', loc: [ 9, 10 ], index: 3, depth: 1, groupId: 'fxg1' }
141
+ ]);
142
+
143
+ const f2 = '=Sheet4!R[-2]C[-2]:R[-1]C[-1]';
144
+ expect(translateFormulaToA1(f2, 'B2', { wrapEdges: false })).toBe('=#REF!');
145
+
146
+ const f3 = '=Sheet4!R[-2]C[-2]:R[-1]C[-1]';
147
+ expect(translateFormulaToA1(f3, 'B2', { wrapEdges: false, mergeRefs: false })).toBe('=Sheet4!#REF!:A1');
148
+ });
149
+ });
150
+
151
+ describe('translate mixed rel/abs coords from RC to A1', () => {
152
+ test('mixed relative/absolute references', () => {
153
+ isR2A('=R1C[0]', 'B2', '=B$1');
154
+ isR2A('=R[4]C4', 'B4', '=$D8');
155
+ isR2A('=R[4]:R10', 'B4', '=8:$10');
156
+ isR2A('=C10:C[10]', 'B4', '=$J:L');
157
+ isR2A('=R1C1:R2C2', 'D4', '=$A$1:$B$2');
158
+ isR2A('=R[-1]C[-1]:R[2]C[2]', 'D4', '=C3:F6');
159
+ });
160
+ });
161
+
162
+ describe('translate involved formula from RC to A1', () => {
163
+ test('complex function expressions', () => {
164
+ isR2A('=SUM(IF(RC[1],R2C5,R3C5),Sheet1!R2*Sheet2!C[-2])', 'D10',
165
+ '=SUM(IF(E10,$E$2,$E$3),Sheet1!$2:$2*Sheet2!B:B)');
166
+ });
167
+ });
168
+
169
+ describe('translate works with merged ranges', () => {
170
+ test('preserves token metadata and locations', () => {
171
+ // This tests that:
172
+ // - Translate works with ranges that have context attached
173
+ // - If input is a tokenlist, output is also a tokenlist
174
+ // - If tokens have ranges, those ranges are adjusted to new token lengths
175
+ // - Properties added by addTokenMeta are preserved
176
+ const expr = '=SUM(IF(RC[1],R2C5,R3C5),Sheet1!R2*Sheet2!C[-2])';
177
+ const tokens = addTokenMeta(tokenize(expr, { withLocation: true, r1c1: true }));
178
+ const expected = [
179
+ { type: FX_PREFIX, value: '=', loc: [ 0, 1 ], index: 0, depth: 0 },
180
+ { type: FUNCTION, value: 'SUM', loc: [ 1, 4 ], index: 1, depth: 0 },
181
+ { type: OPERATOR, value: '(', loc: [ 4, 5 ], index: 2, depth: 1, groupId: 'fxg3' },
182
+ { type: FUNCTION, value: 'IF', loc: [ 5, 7 ], index: 3, depth: 1 },
183
+ { type: OPERATOR, value: '(', loc: [ 7, 8 ], index: 4, depth: 2, groupId: 'fxg1' },
184
+ { type: REF_RANGE, value: 'E10', loc: [ 8, 11 ], index: 5, depth: 2 },
185
+ { type: OPERATOR, value: ',', loc: [ 11, 12 ], index: 6, depth: 2 },
186
+ { type: REF_RANGE, value: '$E$2', loc: [ 12, 16 ], index: 7, depth: 2 },
187
+ { type: OPERATOR, value: ',', loc: [ 16, 17 ], index: 8, depth: 2 },
188
+ { type: REF_RANGE, value: '$E$3', loc: [ 17, 21 ], index: 9, depth: 2 },
189
+ { type: OPERATOR, value: ')', loc: [ 21, 22 ], index: 10, depth: 2, groupId: 'fxg1' },
190
+ { type: OPERATOR, value: ',', loc: [ 22, 23 ], index: 11, depth: 1 },
191
+ { type: REF_BEAM, value: 'Sheet1!$2:$2', loc: [ 23, 35 ], index: 12, depth: 1, groupId: 'fxg2' },
192
+ { type: OPERATOR, value: '*', loc: [ 35, 36 ], index: 13, depth: 1 },
193
+ { type: REF_BEAM, value: 'Sheet2!B:B', loc: [ 36, 46 ], index: 14, depth: 1 },
194
+ { type: OPERATOR, value: ')', loc: [ 46, 47 ], index: 15, depth: 1, groupId: 'fxg3' }
195
+ ];
196
+ expect(translateTokensToA1(tokens, 'D10')).toEqual(expected);
197
+ });
198
+ });
199
+
200
+ describe('translate works with xlsx mode references', () => {
201
+ function testExpr (expr: string, anchor: string, expected: any[], xlsx = true) {
202
+ const tokens = xlsx
203
+ ? tokenizeXlsx(expr, { mergeRefs: true, r1c1: true })
204
+ : tokenize(expr, { mergeRefs: true, r1c1: true });
205
+ expect(translateTokensToA1(tokens, anchor)).toEqual(expected);
206
+ }
207
+
208
+ test('XLSX workbook references', () => {
209
+ testExpr("'[My Fancy Workbook.xlsx]'!R1C", 'B2', [
210
+ { type: REF_RANGE, value: "'[My Fancy Workbook.xlsx]'!B$1" }
211
+ ]);
212
+
213
+ expect(translateTokensToA1([ { type: 'range', value: 'foo!R1C' } ], 'B2'))
214
+ .toEqual([ { type: 'range', value: 'foo!B$1' } ]);
215
+ expect(translateTokensToA1([ { type: 'range', value: '[foo]!R1C' } ], 'B2'))
216
+ .toEqual([ { type: 'range', value: '[foo]!B$1' } ]);
217
+ expect(translateTokensToA1([ { type: 'range', value: '[foo]bar!R1C' } ], 'B2'))
218
+ .toEqual([ { type: 'range', value: '[foo]bar!B$1' } ]);
219
+
220
+ testExpr('[Workbook.xlsx]!R1C', 'B2', [
221
+ { type: REF_RANGE, value: '[Workbook.xlsx]!B$1' }
222
+ ]);
223
+
224
+ testExpr('[Workbook.xlsx]Sheet1!R1C', 'B2', [
225
+ { type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!B$1' }
226
+ ]);
227
+
228
+ testExpr('[Workbook.xlsx]!table[#data]', 'B2', [
229
+ { type: REF_STRUCT, value: '[Workbook.xlsx]!table[#data]' }
230
+ ]);
231
+ });
232
+ });
233
+
234
+ describe('translate works with trimmed ranges', () => {
235
+ function testExpr (expr: string, anchor: string, expected: any[]) {
236
+ const opts = { mergeRefs: true, r1c1: true };
237
+ expect(translateTokensToA1(tokenizeXlsx(expr, opts), anchor)).toEqual(expected);
238
+ }
239
+
240
+ test('trimmed range translation', () => {
241
+ testExpr('Sheet!R[-1]C[-1].:.RC*Sheet2!C[50].:.C[700]', 'B2', [
242
+ { type: 'range', value: 'Sheet!A1.:.B2' },
243
+ { type: 'operator', value: '*' },
244
+ { type: 'range_beam', value: 'Sheet2!AZ.:.ZZ' }
245
+ ]);
246
+ });
247
+ });