@borgar/fx 5.0.0 → 5.0.2

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/lib/constants.ts CHANGED
@@ -14,6 +14,9 @@ export const REF_BEAM = 'range_beam';
14
14
  export const REF_TERNARY = 'range_ternary';
15
15
  export const REF_NAMED = 'range_named';
16
16
  export const REF_STRUCT = 'structured';
17
+ // TODO: in future, we should type the difference between A1:B1 (REF_RANGE) and
18
+ // A1 (REF_CELL) but this will require a major version bump.
19
+ export const REF_CELL = 'cell'; // internal only
17
20
  export const FX_PREFIX = 'fx_prefix';
18
21
  export const UNKNOWN = 'unknown';
19
22
 
@@ -66,6 +66,20 @@ describe('fixRanges basics', () => {
66
66
  });
67
67
  });
68
68
 
69
+ describe('fixRanges prefixes', () => {
70
+ test('Quotes prefixes as needed', () => {
71
+ isFixed('=Sch1!B2', "='Sch1'!B2");
72
+ isFixed('=[Foo]Ab12x!B2', '=[Foo]Ab12x!B2');
73
+ isFixed('=[Foo]Ab12!B2', "='[Foo]Ab12'!B2");
74
+ isFixed('=ABC123!B2', "='ABC123'!B2");
75
+ isFixed('=abc123!B2', "='abc123'!B2");
76
+ isFixed('=C!B2', "='C'!B2");
77
+ isFixed('=R!B2', "='R'!B2");
78
+ isFixed('=RC!B2', "='RC'!B2");
79
+ isFixed('=CR!B2', '=CR!B2');
80
+ });
81
+ });
82
+
69
83
  describe('fixRanges A1', () => {
70
84
  const opt = { allowTernary: true };
71
85
 
@@ -0,0 +1,3 @@
1
+ export function isRCTokenValue (value: string): boolean {
2
+ return value === 'r' || value === 'R' || value === 'c' || value === 'C';
3
+ }
@@ -99,7 +99,7 @@ export function lexRangeA1 (
99
99
  }
100
100
  }
101
101
  // LT : this is A1
102
- if (top && canEndRange(str, preOp)) {
102
+ if (top && canEndRange(str, preOp) && str.charCodeAt(preOp) !== 33) { // 33 = "!"
103
103
  return { type: REF_RANGE, value: str.slice(pos, preOp) };
104
104
  }
105
105
  }
@@ -11,6 +11,7 @@ const UC_C = 67;
11
11
  const LC_C = 99;
12
12
  const PLUS = 43;
13
13
  const MINUS = 45;
14
+ const EXCL = 33;
14
15
 
15
16
  // C
16
17
  // C\[[+-]?\d+\]
@@ -97,7 +98,7 @@ export function lexRangeR1C1 (
97
98
  p += r1;
98
99
  const c1 = lexR1C1Part(str, p);
99
100
  p += c1;
100
- if (c1 || r1) {
101
+ if ((c1 || r1) && str.charCodeAt(p) !== EXCL) {
101
102
  const op = advRangeOp(str, p);
102
103
  const preOp = p;
103
104
  if (op) {
@@ -1,27 +1,29 @@
1
- import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT } from './constants.ts';
1
+ import { CONTEXT, CONTEXT_QUOTE, REF_RANGE, REF_NAMED, REF_BEAM, REF_TERNARY, OPERATOR, REF_STRUCT, REF_CELL } from './constants.ts';
2
2
  import type { Token } from './types.ts';
3
3
 
4
4
  const END = '$';
5
5
 
6
6
  const validRunsMerge = [
7
- [ REF_RANGE, ':', REF_RANGE ],
8
- [ REF_RANGE, '.:', REF_RANGE ],
9
- [ REF_RANGE, ':.', REF_RANGE ],
10
- [ REF_RANGE, '.:.', REF_RANGE ],
7
+ [ REF_CELL, ':', REF_CELL ],
8
+ [ REF_CELL, '.:', REF_CELL ],
9
+ [ REF_CELL, ':.', REF_CELL ],
10
+ [ REF_CELL, '.:.', REF_CELL ],
11
11
  [ REF_RANGE ],
12
12
  [ REF_BEAM ],
13
13
  [ REF_TERNARY ],
14
- [ CONTEXT, '!', REF_RANGE, ':', REF_RANGE ],
15
- [ CONTEXT, '!', REF_RANGE, '.:', REF_RANGE ],
16
- [ CONTEXT, '!', REF_RANGE, ':.', REF_RANGE ],
17
- [ CONTEXT, '!', REF_RANGE, '.:.', REF_RANGE ],
14
+ [ CONTEXT, '!', REF_CELL, ':', REF_CELL ],
15
+ [ CONTEXT, '!', REF_CELL, '.:', REF_CELL ],
16
+ [ CONTEXT, '!', REF_CELL, ':.', REF_CELL ],
17
+ [ CONTEXT, '!', REF_CELL, '.:.', REF_CELL ],
18
+ [ CONTEXT, '!', REF_CELL ],
18
19
  [ CONTEXT, '!', REF_RANGE ],
19
20
  [ CONTEXT, '!', REF_BEAM ],
20
21
  [ CONTEXT, '!', REF_TERNARY ],
21
- [ CONTEXT_QUOTE, '!', REF_RANGE, ':', REF_RANGE ],
22
- [ CONTEXT_QUOTE, '!', REF_RANGE, '.:', REF_RANGE ],
23
- [ CONTEXT_QUOTE, '!', REF_RANGE, ':.', REF_RANGE ],
24
- [ CONTEXT_QUOTE, '!', REF_RANGE, '.:.', REF_RANGE ],
22
+ [ CONTEXT_QUOTE, '!', REF_CELL, ':', REF_CELL ],
23
+ [ CONTEXT_QUOTE, '!', REF_CELL, '.:', REF_CELL ],
24
+ [ CONTEXT_QUOTE, '!', REF_CELL, ':.', REF_CELL ],
25
+ [ CONTEXT_QUOTE, '!', REF_CELL, '.:.', REF_CELL ],
26
+ [ CONTEXT_QUOTE, '!', REF_CELL ],
25
27
  [ CONTEXT_QUOTE, '!', REF_RANGE ],
26
28
  [ CONTEXT_QUOTE, '!', REF_BEAM ],
27
29
  [ CONTEXT_QUOTE, '!', REF_TERNARY ],
@@ -62,7 +64,14 @@ const matcher = (tokens: Token[], currNode, anchorIndex, index = 0) => {
62
64
  while (i <= max) {
63
65
  const token = tokens[anchorIndex - i];
64
66
  if (token) {
65
- const key = (token.type === OPERATOR) ? token.value : token.type;
67
+ const value = token.value;
68
+ let key = (token.type === OPERATOR) ? value : token.type;
69
+ // we need to prevent merging ["A1:B2" ":" "C3"] as a range is only
70
+ // allowed to contain a single ":" operator even if "A1:B2:C3" is
71
+ // valid Excel syntax
72
+ if (key === REF_RANGE && !value.includes(':')) {
73
+ key = REF_CELL;
74
+ }
66
75
  if (key in node) {
67
76
  node = node[key];
68
77
  i += 1;
@@ -335,3 +335,10 @@ describe('A1 trimmed ranges vs named ranges', () => {
335
335
  isA1Equal('foo.:B2', { range: { top: 1, left: 1, right: 4460, trim: 'head' } }, { allowTernary: true });
336
336
  });
337
337
  });
338
+
339
+ describe('Sheet name that looks like an A1 ref', () => {
340
+ test('parse correctly', () => {
341
+ isA1Equal("'Sch1'!B2", { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } });
342
+ isA1Equal('Sch1!B2', { context: [ 'Sch1' ], range: { top: 1, left: 1, bottom: 1, right: 1 } });
343
+ });
344
+ });
@@ -30,6 +30,12 @@ describe('stringifyA1Ref', () => {
30
30
  // @ts-expect-error -- testing invalid input
31
31
  expect(stringifyA1Ref({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' })).toBe('foo');
32
32
  });
33
+
34
+ test('should quote prefixes that look like A1 ranges', () => {
35
+ expect(stringifyA1Ref({ context: [ 'Ab12' ], range: rangeA1 })).toBe("'Ab12'!A1");
36
+ expect(stringifyA1Ref({ context: [ 'Sch1' ], range: rangeA1 })).toBe("'Sch1'!A1");
37
+ expect(stringifyA1Ref({ context: [ 'Foo12345' ], range: rangeA1 })).toBe("'Foo12345'!A1");
38
+ });
33
39
  });
34
40
 
35
41
  describe('stringifyA1Ref in XLSX mode', () => {
@@ -61,4 +67,16 @@ describe('stringifyA1Ref in XLSX mode', () => {
61
67
  // @ts-expect-error -- testing invalid input
62
68
  expect(stringifyA1RefXlsx({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' })).toBe('foo');
63
69
  });
70
+
71
+ test('should quote prefixes that look like ranges', () => {
72
+ expect(stringifyA1RefXlsx({ sheetName: 'C', range: rangeA1 })).toBe("'C'!A1");
73
+ expect(stringifyA1RefXlsx({ sheetName: 'R', range: rangeA1 })).toBe("'R'!A1");
74
+ expect(stringifyA1RefXlsx({ sheetName: 'RC', range: rangeA1 })).toBe("'RC'!A1");
75
+ expect(stringifyA1RefXlsx({ sheetName: 'Ab12', range: rangeA1 })).toBe("'Ab12'!A1");
76
+ expect(stringifyA1RefXlsx({ sheetName: 'Sch1', range: rangeA1 })).toBe("'Sch1'!A1");
77
+ expect(stringifyA1RefXlsx({ sheetName: 'Foo12345', range: rangeA1 })).toBe("'Foo12345'!A1");
78
+ expect(stringifyA1RefXlsx({ workbookName: 'Ab12', range: rangeA1 })).toBe("'[Ab12]'!A1");
79
+ expect(stringifyA1RefXlsx({ workbookName: 'Sch1', range: rangeA1 })).toBe("'[Sch1]'!A1");
80
+ expect(stringifyA1RefXlsx({ workbookName: 'Foo12345', range: rangeA1 })).toBe("'[Foo12345]'!A1");
81
+ });
64
82
  });
@@ -10,6 +10,27 @@ import type {
10
10
  } from './types.ts';
11
11
 
12
12
  const reBannedChars = /[^0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]/;
13
+ // A1-XFD1048575 | R | C | RC
14
+ const reIsRangelike = /^(R|C|RC|[A-Z]{1,3}\d{1,7})$/i;
15
+
16
+ export function needQuotes (scope: string, yesItDoes = 0): number {
17
+ if (yesItDoes) {
18
+ return 1;
19
+ }
20
+ if (scope) {
21
+ if (reBannedChars.test(scope)) {
22
+ return 1;
23
+ }
24
+ if (reIsRangelike.test(scope)) {
25
+ return 1;
26
+ }
27
+ }
28
+ return 0;
29
+ }
30
+
31
+ export function quotePrefix (prefix) {
32
+ return "'" + prefix.replace(/'/g, "''") + "'";
33
+ }
13
34
 
14
35
  export function stringifyPrefix (
15
36
  ref: ReferenceA1 | ReferenceName | ReferenceStruct | ReferenceR1C1
@@ -23,12 +44,12 @@ export function stringifyPrefix (
23
44
  if (scope) {
24
45
  const part = (nth % 2) ? '[' + scope + ']' : scope;
25
46
  pre = part + pre;
26
- quote += +reBannedChars.test(scope);
47
+ quote += needQuotes(scope, quote);
27
48
  nth++;
28
49
  }
29
50
  }
30
51
  if (quote) {
31
- pre = "'" + pre.replace(/'/g, "''") + "'";
52
+ pre = quotePrefix(pre);
32
53
  }
33
54
  return pre ? pre + '!' : pre;
34
55
  }
@@ -41,14 +62,14 @@ export function stringifyPrefixXlsx (
41
62
  const { workbookName, sheetName } = ref;
42
63
  if (workbookName) {
43
64
  pre += '[' + workbookName + ']';
44
- quote += +reBannedChars.test(workbookName);
65
+ quote += needQuotes(workbookName);
45
66
  }
46
67
  if (sheetName) {
47
68
  pre += sheetName;
48
- quote += +reBannedChars.test(sheetName);
69
+ quote += needQuotes(sheetName);
49
70
  }
50
71
  if (quote) {
51
- pre = "'" + pre.replace(/'/g, "''") + "'";
72
+ pre = quotePrefix(pre);
52
73
  }
53
74
  return pre ? pre + '!' : pre;
54
75
  }
@@ -1062,6 +1062,13 @@ describe('lexer', () => {
1062
1062
  { type: FX_PREFIX, value: '=' },
1063
1063
  { type: REF_RANGE, value: 'A1:C1' }
1064
1064
  ]);
1065
+
1066
+ isTokens('=A1:C1:D1', [
1067
+ { type: FX_PREFIX, value: '=' },
1068
+ { type: REF_RANGE, value: 'A1:C1' },
1069
+ { type: OPERATOR, value: ':' },
1070
+ { type: REF_RANGE, value: 'D1' }
1071
+ ]);
1065
1072
  });
1066
1073
 
1067
1074
  test('spill range syntax', () => {
@@ -2100,4 +2107,96 @@ describe('lexer', () => {
2100
2107
  expect(tokenizeXlsx('[foo]!A1')).toEqual([ { type: REF_RANGE, value: '[foo]!A1' } ]);
2101
2108
  expect(tokenizeXlsx('foo!A1')).toEqual([ { type: REF_RANGE, value: 'foo!A1' } ]);
2102
2109
  });
2110
+
2111
+ test('r and c as LET arguments in R1C1 mode', () => {
2112
+ // Unlike with LET(c,1,c) is not valid syntax with the R1C1 notation in Excel.
2113
+ //
2114
+ // If you create a cell with this expression in A1 mode and flip to R1C1, Excel
2115
+ // will not change it when expressing it, but will not allow you to re-enter it.
2116
+ //
2117
+ // Excel will always save the formula such as the arguments will have a "_xlpm."
2118
+ // prefix: _xlfn.LET(_xlpm.c,1,_xlpm.c)
2119
+ //
2120
+ // However, that is also invalid syntax in the exposed/common Excel formula syntax.
2121
+ // To counter this, fx does the following:
2122
+ //
2123
+ // tokenize:
2124
+ // Supports _xlpm.c in both modes.
2125
+ // Assumes c, C, r and R are names when encountered as tokens within LET functions.
2126
+ // translateTokensToR1C1:
2127
+ // Tries to be unabiguous by serializing "c" ranges in within LET as C[0].
2128
+ // Same goes for "r" to R[0]. Prefixed names are left as they are.
2129
+ // This way round-tripping is possible.
2130
+ expect(tokenize('LET(c,1,c)', { r1c1: true })).toEqual([
2131
+ { type: FUNCTION, value: 'LET' },
2132
+ { type: OPERATOR, value: '(' },
2133
+ { type: REF_NAMED, value: 'c' },
2134
+ { type: OPERATOR, value: ',' },
2135
+ { type: NUMBER, value: '1' },
2136
+ { type: OPERATOR, value: ',' },
2137
+ { type: REF_NAMED, value: 'c' },
2138
+ { type: OPERATOR, value: ')' }
2139
+ ]);
2140
+ expect(tokenize('LET(r,1,r)', { r1c1: true })).toEqual([
2141
+ { type: FUNCTION, value: 'LET' },
2142
+ { type: OPERATOR, value: '(' },
2143
+ { type: REF_NAMED, value: 'r' },
2144
+ { type: OPERATOR, value: ',' },
2145
+ { type: NUMBER, value: '1' },
2146
+ { type: OPERATOR, value: ',' },
2147
+ { type: REF_NAMED, value: 'r' },
2148
+ { type: OPERATOR, value: ')' }
2149
+ ]);
2150
+ // Even if the second C could be identified as a range,
2151
+ // which requires a parse-tree of some sort, the the "c+C"
2152
+ // would both have to be names as arguments are
2153
+ // case-insensitive:
2154
+ expect(tokenize('LET(c,C,c+C)', { r1c1: true })).toEqual([
2155
+ { type: FUNCTION, value: 'LET' },
2156
+ { type: OPERATOR, value: '(' },
2157
+ { type: REF_NAMED, value: 'c' },
2158
+ { type: OPERATOR, value: ',' },
2159
+ { type: REF_NAMED, value: 'C' }, // beam
2160
+ { type: OPERATOR, value: ',' },
2161
+ { type: REF_NAMED, value: 'c' },
2162
+ { type: OPERATOR, value: '+' },
2163
+ { type: REF_NAMED, value: 'C' }, // beam
2164
+ { type: OPERATOR, value: ')' }
2165
+ ]);
2166
+ expect(tokenize('LET(c,C,SUM(c,C))', { r1c1: true })).toEqual([
2167
+ { type: FUNCTION, value: 'LET' },
2168
+ { type: OPERATOR, value: '(' },
2169
+ { type: REF_NAMED, value: 'c' },
2170
+ { type: OPERATOR, value: ',' },
2171
+ { type: REF_NAMED, value: 'C' },
2172
+ { type: OPERATOR, value: ',' },
2173
+ { type: FUNCTION, value: 'SUM' },
2174
+ { type: OPERATOR, value: '(' },
2175
+ { type: REF_NAMED, value: 'c' },
2176
+ { type: OPERATOR, value: ',' },
2177
+ { type: REF_NAMED, value: 'C' },
2178
+ { type: OPERATOR, value: ')' },
2179
+ { type: OPERATOR, value: ')' }
2180
+ ]);
2181
+ });
2182
+
2183
+ describe('Sheet name that looks like an A1 ref', () => {
2184
+ test('Sheet name that looks like an A1 ref', () => {
2185
+ expect(tokenize("'Sch1'!B2")).toEqual([
2186
+ { type: REF_RANGE, value: "'Sch1'!B2" }
2187
+ ]);
2188
+ expect(tokenize('Sch1!B2')).toEqual([
2189
+ { type: REF_RANGE, value: 'Sch1!B2' }
2190
+ ]);
2191
+ });
2192
+
2193
+ test('Sheet name that is a R or C ref', () => {
2194
+ expect(tokenize("'C'!R[-9]C[-3]", { r1c1: true })).toEqual([
2195
+ { type: REF_RANGE, value: "'C'!R[-9]C[-3]" }
2196
+ ]);
2197
+ expect(tokenize('C!R[-9]C[-3]', { r1c1: true })).toEqual([
2198
+ { type: REF_RANGE, value: 'C!R[-9]C[-3]' }
2199
+ ]);
2200
+ });
2201
+ });
2103
2202
  });
package/lib/tokenize.ts CHANGED
@@ -8,11 +8,13 @@ import {
8
8
  WHITESPACE,
9
9
  FUNCTION,
10
10
  OPERATOR_TRIM,
11
- REF_RANGE
11
+ REF_RANGE,
12
+ REF_BEAM
12
13
  } from './constants.ts';
13
14
  import { mergeRefTokens } from './mergeRefTokens.ts';
14
15
  import { lexers, type PartLexer } from './lexers/sets.ts';
15
16
  import type { Token } from './types.ts';
17
+ import { isRCTokenValue } from './isRCTokenValue.ts';
16
18
 
17
19
  const reLetLambda = /^l(?:ambda|et)$/i;
18
20
  const isType = (t: Token, type: string) => t && t.type === type;
@@ -27,12 +29,13 @@ const causesBinaryMinus = (token: Token) => {
27
29
  );
28
30
  };
29
31
 
30
- function fixRCNames (tokens: Token[]): Token[] {
32
+ function fixRCNames (tokens: Token[], r1c1Mode?: boolean): Token[] {
31
33
  let withinCall = 0;
32
34
  let parenDepth = 0;
33
35
  let lastToken: Token;
34
36
  for (const token of tokens) {
35
- if (token.type === OPERATOR) {
37
+ const tokenType = token.type;
38
+ if (tokenType === OPERATOR) {
36
39
  if (token.value === '(') {
37
40
  parenDepth++;
38
41
  if (lastToken.type === FUNCTION) {
@@ -48,7 +51,10 @@ function fixRCNames (tokens: Token[]): Token[] {
48
51
  }
49
52
  }
50
53
  }
51
- else if (withinCall && token.type === UNKNOWN && /^[rc]$/.test(token.value)) {
54
+ else if (withinCall && tokenType === UNKNOWN && isRCTokenValue(token.value)) {
55
+ token.type = REF_NAMED;
56
+ }
57
+ else if (withinCall && r1c1Mode && tokenType === REF_BEAM && isRCTokenValue(token.value)) {
52
58
  token.type = REF_NAMED;
53
59
  }
54
60
  lastToken = token;
@@ -159,10 +165,12 @@ export function getTokens (fx: string, tokenHandlers: PartLexer[], options: Opts
159
165
  letOrLambda++;
160
166
  }
161
167
  }
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;
168
+ // Make a note if we found a R or C unknown or REF_BEAM token in R1C1 mode.
169
+ // It seemse unlikely that anyone does `F2 = LET(c,1,c+F:F)` as this is a
170
+ // circular reference (and not a very useful one), so we're assuming that
171
+ // all "c" or "r" tokens found within the LET are names.
172
+ if (token.value.length === 1 && (token.type === UNKNOWN || (opts.r1c1 && token.type === REF_BEAM))) {
173
+ unknownRC += isRCTokenValue(token.value) ? 1 : 0;
166
174
  }
167
175
 
168
176
  if (negativeNumbers && token.type === NUMBER) {
@@ -195,7 +203,7 @@ export function getTokens (fx: string, tokenHandlers: PartLexer[], options: Opts
195
203
  // if we encountered both a LAMBDA/LET call, and unknown 'r' or 'c' tokens
196
204
  // we'll turn the unknown tokens into names within the call.
197
205
  if (unknownRC && letOrLambda) {
198
- fixRCNames(tokens);
206
+ fixRCNames(tokens, opts.r1c1);
199
207
  }
200
208
 
201
209
  // Any OPERATOR_TRIM tokens have been indexed already, they now need to be
@@ -245,3 +245,87 @@ describe('translate works with trimmed ranges', () => {
245
245
  ]);
246
246
  });
247
247
  });
248
+
249
+ describe('translate r & c as LET parameters', () => {
250
+ // Unlike in A1, LET(c,1,c) is not valid syntax with the R1C1 notation in Excel.
251
+ // If you create a cell with this expression in A1 mode and flip to R1C1, Excel
252
+ // will not change it when expressing it, but will not allow you to re-enter it.
253
+ //
254
+ // Excel will always save the formula such as the arguments will have a "_xlpm."
255
+ // prefix: _xlfn.LET(_xlpm.c,1,_xlpm.c)
256
+ //
257
+ // However, that is also invalid syntax in the exposed/common Excel formula syntax.
258
+ // To counter this, fx does the following:
259
+ //
260
+ // tokenize:
261
+ // Supports _xlpm.c in both modes.
262
+ // Assumes c, C, r and R are names when encountered as tokens within LET functions.
263
+ // translateTokensToR1C1:
264
+ // Tries to be unambiguous by serializing "c" ranges in within LET as C[0].
265
+ // Same goes for "r" to R[0]. Prefixed names are left as they are.
266
+ // This way round-tripping is possible.
267
+ function testExpr (expr: string, anchor: string, expected: any[]) {
268
+ const opts = { mergeRefs: true, r1c1: true };
269
+ expect(translateTokensToA1(tokenizeXlsx(expr, opts), anchor)).toEqual(expected);
270
+ }
271
+
272
+ test('translate prefixed r & c in LET', () => {
273
+ testExpr('_xlfn.LET(_xlpm.c,3,_xlpm.c)', 'B2', [
274
+ { type: 'func', value: '_xlfn.LET' },
275
+ { type: 'operator', value: '(' },
276
+ { type: 'range_named', value: '_xlpm.c' },
277
+ { type: 'operator', value: ',' },
278
+ { type: 'number', value: '3' },
279
+ { type: 'operator', value: ',' },
280
+ { type: 'range_named', value: '_xlpm.c' },
281
+ { type: 'operator', value: ')' }
282
+ ]);
283
+
284
+ testExpr('_xlfn.LET(_xlpm.r,3,_xlpm.r)', 'B2', [
285
+ { type: 'func', value: '_xlfn.LET' },
286
+ { type: 'operator', value: '(' },
287
+ { type: 'range_named', value: '_xlpm.r' },
288
+ { type: 'operator', value: ',' },
289
+ { type: 'number', value: '3' },
290
+ { type: 'operator', value: ',' },
291
+ { type: 'range_named', value: '_xlpm.r' },
292
+ { type: 'operator', value: ')' }
293
+ ]);
294
+ });
295
+
296
+ test('Converting ', () => {
297
+ // The syntax is invalid so it will regress:
298
+ isR2A('=LET(r,R,r+R)', 'B4', '=LET(r,R,r+R)');
299
+ isR2A('=LET(c,C,c+C)', 'B4', '=LET(c,C,c+C)');
300
+ // R[0] and C[0] work as expected
301
+ isR2A('=LET(r,R[0],r+R[0])', 'B4', '=LET(r,4:4,r+4:4)');
302
+ isR2A('=LET(c,C[0],c+C[0])', 'B4', '=LET(c,B:B,c+B:B)');
303
+ // prefixed parameters work too
304
+ isR2A('=LET(_xlpm.r,R[0],_xlpm.r+R[0])', 'B4', '=LET(_xlpm.r,4:4,_xlpm.r+4:4)');
305
+ isR2A('=LET(_xlpm.c,C[0],_xlpm.c+C[0])', 'B4', '=LET(_xlpm.c,B:B,_xlpm.c+B:B)');
306
+ });
307
+
308
+ test('translate r & c in LET', () => {
309
+ testExpr('LET(c,3,c)', 'B2', [
310
+ { type: 'func', value: 'LET' },
311
+ { type: 'operator', value: '(' },
312
+ { type: 'range_named', value: 'c' },
313
+ { type: 'operator', value: ',' },
314
+ { type: 'number', value: '3' },
315
+ { type: 'operator', value: ',' },
316
+ { type: 'range_named', value: 'c' },
317
+ { type: 'operator', value: ')' }
318
+ ]);
319
+
320
+ testExpr('LET(r,3,r)', 'B2', [
321
+ { type: 'func', value: 'LET' },
322
+ { type: 'operator', value: '(' },
323
+ { type: 'range_named', value: 'r' },
324
+ { type: 'operator', value: ',' },
325
+ { type: 'number', value: '3' },
326
+ { type: 'operator', value: ',' },
327
+ { type: 'range_named', value: 'r' },
328
+ { type: 'operator', value: ')' }
329
+ ]);
330
+ });
331
+ });
@@ -103,49 +103,51 @@ export function translateTokensToA1 (
103
103
  // We can get away with using the xlsx ref-parser here because it is more permissive
104
104
  // and we will end up with the same prefix after serialization anyway:
105
105
  const ref = parseR1C1RefXlsx(tokenValue, REF_OPTS) as ReferenceR1C1Xlsx;
106
- const d = ref.range;
107
- const range: RangeA1 = { top: 0, left: 0 };
108
- const r0 = toFixed(d.r0, d.$r0, top, MAX_ROWS, wrapEdges);
109
- const r1 = toFixed(d.r1, d.$r1, top, MAX_ROWS, wrapEdges);
110
- if (r0 > r1) {
111
- range.top = r1;
112
- range.$top = d.$r1;
113
- range.bottom = r0;
114
- range.$bottom = d.$r0;
115
- }
116
- else {
117
- range.top = r0;
118
- range.$top = d.$r0;
119
- range.bottom = r1;
120
- range.$bottom = d.$r1;
121
- }
122
- const c0 = toFixed(d.c0, d.$c0, left, MAX_COLS, wrapEdges);
123
- const c1 = toFixed(d.c1, d.$c1, left, MAX_COLS, wrapEdges);
124
- if (c0 > c1) {
125
- range.left = c1;
126
- range.$left = d.$c1;
127
- range.right = c0;
128
- range.$right = d.$c0;
129
- }
130
- else {
131
- range.left = c0;
132
- range.$left = d.$c0;
133
- range.right = c1;
134
- range.$right = d.$c1;
135
- }
136
- if (d.trim) {
137
- range.trim = d.trim;
138
- }
139
- if (isNaN(r0) || isNaN(r1) || isNaN(c0) || isNaN(c1)) {
140
- // convert to ref error
141
- token.type = ERROR;
142
- token.value = '#REF!';
143
- delete token.groupId;
144
- }
145
- else {
146
- ref.range = range;
147
- // @ts-expect-error -- reusing the object, switching it to A1 by swapping the range
148
- token.value = stringifyA1RefXlsx(ref);
106
+ if (ref) {
107
+ const d = ref.range;
108
+ const range: RangeA1 = { top: 0, left: 0 };
109
+ const r0 = toFixed(d.r0, d.$r0, top, MAX_ROWS, wrapEdges);
110
+ const r1 = toFixed(d.r1, d.$r1, top, MAX_ROWS, wrapEdges);
111
+ if (r0 > r1) {
112
+ range.top = r1;
113
+ range.$top = d.$r1;
114
+ range.bottom = r0;
115
+ range.$bottom = d.$r0;
116
+ }
117
+ else {
118
+ range.top = r0;
119
+ range.$top = d.$r0;
120
+ range.bottom = r1;
121
+ range.$bottom = d.$r1;
122
+ }
123
+ const c0 = toFixed(d.c0, d.$c0, left, MAX_COLS, wrapEdges);
124
+ const c1 = toFixed(d.c1, d.$c1, left, MAX_COLS, wrapEdges);
125
+ if (c0 > c1) {
126
+ range.left = c1;
127
+ range.$left = d.$c1;
128
+ range.right = c0;
129
+ range.$right = d.$c0;
130
+ }
131
+ else {
132
+ range.left = c0;
133
+ range.$left = d.$c0;
134
+ range.right = c1;
135
+ range.$right = d.$c1;
136
+ }
137
+ if (d.trim) {
138
+ range.trim = d.trim;
139
+ }
140
+ if (isNaN(r0) || isNaN(r1) || isNaN(c0) || isNaN(c1)) {
141
+ // convert to ref error
142
+ token.type = ERROR;
143
+ token.value = '#REF!';
144
+ delete token.groupId;
145
+ }
146
+ else {
147
+ ref.range = range;
148
+ // @ts-expect-error -- reusing the object, switching it to A1 by swapping the range
149
+ token.value = stringifyA1RefXlsx(ref);
150
+ }
149
151
  }
150
152
  // if token includes offsets, those offsets are now likely wrong!
151
153
  if (token.loc) {