@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.
@@ -2,7 +2,7 @@ import { describe, test, expect } from 'vitest';
2
2
  import { translateFormulaToR1C1, translateTokensToR1C1 } from './translateToR1C1.ts';
3
3
  import { tokenize } from './tokenize.ts';
4
4
  import { addTokenMeta } from './addTokenMeta.ts';
5
- import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.ts';
5
+ import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT, REF_NAMED, NUMBER } from './constants.ts';
6
6
 
7
7
  function isA2R (expr: string, anchor: string, result: string) {
8
8
  expect(translateFormulaToR1C1(expr, anchor)).toBe(result);
@@ -219,9 +219,73 @@ describe('translate works with trimmed ranges', () => {
219
219
 
220
220
  test('trimmed range translation', () => {
221
221
  testExpr('Sheet!A1.:.B2*Sheet2!AZ.:.ZZ', 'B2', [
222
- { type: 'range', value: 'Sheet!R[-1]C[-1].:.RC' },
223
- { type: 'operator', value: '*' },
224
- { type: 'range_beam', value: 'Sheet2!C[50].:.C[700]' }
222
+ { type: REF_RANGE, value: 'Sheet!R[-1]C[-1].:.RC' },
223
+ { type: OPERATOR, value: '*' },
224
+ { type: REF_BEAM, value: 'Sheet2!C[50].:.C[700]' }
225
+ ]);
226
+ });
227
+ });
228
+
229
+ describe('translate does not create invalid LET arguments', () => {
230
+ // Unlike in A1, LET(c,1,c) is not valid syntax with the R1C1 notation in Excel.
231
+ // If you create a cell with this expression in A1 mode and flip to R1C1, Excel
232
+ // will not change it when expressing it, but will not allow you to re-enter it.
233
+ //
234
+ // Excel will always save the formula such as the arguments will have a "_xlpm."
235
+ // prefix: _xlfn.LET(_xlpm.c,1,_xlpm.c)
236
+ //
237
+ // However, that is also invalid syntax in the exposed/common Excel formula syntax.
238
+ // To counter this, fx does the following:
239
+ //
240
+ // tokenize:
241
+ // Supports _xlpm.c in both modes.
242
+ // Assumes c, C, r and R are names when encountered as tokens within LET functions.
243
+ // translateTokensToR1C1:
244
+ // Tries to be unambiguous by serializing "c" ranges in within LET as C[0].
245
+ // Same goes for "r" to R[0]. Prefixed names are left as they are.
246
+ // This way round-tripping is possible.
247
+ function testExpr (expr: string, anchor: string, expected: any[]) {
248
+ const opts = { mergeRefs: true, xlsx: true, r1c1: false };
249
+ expect(translateTokensToR1C1(tokenize(expr, opts), anchor)).toEqual(expected);
250
+ }
251
+
252
+ test('preserve C + R argument names', () => {
253
+ isA2R('=LET(c,1,c)', 'B2', '=LET(c,1,c)');
254
+ isA2R('=LET(r,1,r)', 'B2', '=LET(r,1,r)');
255
+ // ensure that we disambiguate C and R ranges
256
+ isA2R('=LET(c,B:B,c+B:B)', 'B2', '=LET(c,C[0],c+C[0])');
257
+ isA2R('=LET(r,2:2,r+2:2)', 'B2', '=LET(r,R[0],r+R[0])');
258
+ // prefixed parameters work too
259
+ isA2R('=LET(_xlpm.c,1,_xlpm.c)', 'B2', '=LET(_xlpm.c,1,_xlpm.c)');
260
+ isA2R('=LET(_xlpm.r,1,_xlpm.r)', 'B2', '=LET(_xlpm.r,1,_xlpm.r)');
261
+
262
+ testExpr('=LET(c,1,c)', 'B2', [
263
+ { type: FX_PREFIX, value: '=' },
264
+ { type: FUNCTION, value: 'LET' },
265
+ { type: OPERATOR, value: '(' },
266
+ { type: REF_NAMED, value: 'c' },
267
+ { type: OPERATOR, value: ',' },
268
+ { type: NUMBER, value: '1' },
269
+ { type: OPERATOR, value: ',' },
270
+ { type: REF_NAMED, value: 'c' },
271
+ { type: OPERATOR, value: ')' }
272
+ ]);
273
+ });
274
+
275
+ test('ensure that C + R ranges are unambiguous', () => {
276
+ isA2R('=LET(c,B:B,c)', 'B2', '=LET(c,C[0],c)');
277
+ isA2R('=LET(r,2:2,r)', 'B2', '=LET(r,R[0],r)');
278
+
279
+ testExpr('=LET(c,B:B,c)', 'B2', [
280
+ { type: FX_PREFIX, value: '=' },
281
+ { type: FUNCTION, value: 'LET' },
282
+ { type: OPERATOR, value: '(' },
283
+ { type: REF_NAMED, value: 'c' },
284
+ { type: OPERATOR, value: ',' },
285
+ { type: REF_BEAM, value: 'C[0]' },
286
+ { type: OPERATOR, value: ',' },
287
+ { type: REF_NAMED, value: 'c' },
288
+ { type: OPERATOR, value: ')' }
225
289
  ]);
226
290
  });
227
291
  });
@@ -4,8 +4,11 @@ import { parseA1Range } from './parseA1Range.ts';
4
4
  import type { RangeR1C1, ReferenceA1Xlsx, Token } from './types.ts';
5
5
  import { stringifyTokens } from './stringifyTokens.ts';
6
6
  import { cloneToken } from './cloneToken.ts';
7
- import { REF_BEAM, REF_RANGE, REF_TERNARY } from './constants.ts';
7
+ import { FUNCTION, OPERATOR, REF_BEAM, REF_RANGE, REF_TERNARY } from './constants.ts';
8
8
  import { splitContext } from './parseRef.ts';
9
+ import { isRCTokenValue } from './isRCTokenValue.ts';
10
+
11
+ const reLetLambda = /^l(?:ambda|et)$/i;
9
12
 
10
13
  const calc = (abs: boolean, vX: number, aX: number): number => {
11
14
  if (vX == null) {
@@ -70,33 +73,57 @@ export function translateTokensToR1C1 (
70
73
  throw new Error('translateTokensToR1C1 got an invalid anchorCell: ' + anchorCell);
71
74
  }
72
75
  const { top, left } = anchorRange;
76
+ let withinCall = 0;
77
+ let parenDepth = 0;
73
78
 
74
79
  let offsetSkew = 0;
75
- const outTokens = [];
80
+ const outTokens: Token[] = [];
76
81
  for (let token of tokens) {
77
82
  const tokenType = token?.type;
83
+ if (tokenType === OPERATOR) {
84
+ if (token.value === '(') {
85
+ parenDepth++;
86
+ const lastToken = outTokens[outTokens.length - 1];
87
+ if (lastToken && lastToken.type === FUNCTION) {
88
+ if (reLetLambda.test(lastToken.value)) {
89
+ withinCall = parenDepth;
90
+ }
91
+ }
92
+ }
93
+ else if (token.value === ')') {
94
+ parenDepth--;
95
+ if (parenDepth < withinCall) {
96
+ withinCall = 0;
97
+ }
98
+ }
99
+ }
78
100
  if (tokenType === REF_RANGE || tokenType === REF_BEAM || tokenType === REF_TERNARY) {
79
101
  token = cloneToken(token);
80
102
  const tokenValue = token.value;
81
- // We can get away with using the xlsx ref-parser here because it is more permissive
82
- // and we will end up with the same prefix after serialization anyway:
103
+ // We can get away with using the xlsx ref-parser here because it is more permissive:
83
104
  const ref = quickParseA1(tokenValue);
84
- const d = ref.range;
85
- const range: RangeR1C1 = {};
86
- range.r0 = calc(d.$top, d.top, top);
87
- range.r1 = calc(d.$bottom, d.bottom, top);
88
- range.c0 = calc(d.$left, d.left, left);
89
- range.c1 = calc(d.$right, d.right, left);
90
- range.$r0 = d.$top;
91
- range.$r1 = d.$bottom;
92
- range.$c0 = d.$left;
93
- range.$c1 = d.$right;
94
- if (d.trim) {
95
- range.trim = d.trim;
105
+ if (ref) {
106
+ const d = ref.range;
107
+ const range: RangeR1C1 = {};
108
+ range.r0 = calc(d.$top, d.top, top);
109
+ range.r1 = calc(d.$bottom, d.bottom, top);
110
+ range.c0 = calc(d.$left, d.left, left);
111
+ range.c1 = calc(d.$right, d.right, left);
112
+ range.$r0 = d.$top;
113
+ range.$r1 = d.$bottom;
114
+ range.$c0 = d.$left;
115
+ range.$c1 = d.$right;
116
+ if (d.trim) {
117
+ range.trim = d.trim;
118
+ }
119
+ // @ts-expect-error -- reusing the object, switching it to R1C1 by swapping the range
120
+ ref.range = range;
121
+ let val = stringifyR1C1RefXlsx(ref);
122
+ if (isRCTokenValue(val) && withinCall) {
123
+ val += '[0]';
124
+ }
125
+ token.value = val;
96
126
  }
97
- // @ts-expect-error -- reusing the object, switching it to R1C1 by swapping the range
98
- ref.range = range;
99
- token.value = stringifyR1C1RefXlsx(ref);
100
127
  // if token includes offsets, those offsets are now likely wrong!
101
128
  if (token.loc) {
102
129
  token.loc[0] += offsetSkew;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@borgar/fx",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Utilities for working with Excel formulas",
5
5
  "type": "module",
6
6
  "exports": {
@@ -68,11 +68,11 @@
68
68
  "benchmark": "~2.1.4",
69
69
  "eslint": "~9.39.0",
70
70
  "typescript": "~5.9.3",
71
- "typescript-eslint": "~8.46.2",
72
- "vitest": "~4.0.6",
73
- "globals": "~16.5.0",
74
- "typedoc": "~0.28.14",
75
- "typedoc-plugin-markdown": "~4.9.0",
76
- "tsup": "~8.5.0"
71
+ "typescript-eslint": "~8.55.0",
72
+ "vitest": "~4.0.18",
73
+ "globals": "~17.3.0",
74
+ "typedoc": "~0.28.17",
75
+ "typedoc-plugin-markdown": "~4.10.0",
76
+ "tsup": "~8.5.1"
77
77
  }
78
78
  }