@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/dist/index.cjs +145 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -85
- package/dist/index.js.map +1 -1
- package/dist/xlsx/index.cjs +144 -84
- package/dist/xlsx/index.cjs.map +1 -1
- package/dist/xlsx/index.js +144 -84
- package/dist/xlsx/index.js.map +1 -1
- package/lib/constants.ts +3 -0
- package/lib/fixRanges.spec.ts +14 -0
- package/lib/isRCTokenValue.ts +3 -0
- package/lib/lexers/lexRangeA1.ts +1 -1
- package/lib/lexers/lexRangeR1C1.ts +2 -1
- package/lib/mergeRefTokens.ts +23 -14
- package/lib/parseA1Ref.spec.ts +7 -0
- package/lib/stringifyA1Ref.spec.ts +18 -0
- package/lib/stringifyPrefix.ts +26 -5
- package/lib/tokenize.spec.ts +99 -0
- package/lib/tokenize.ts +17 -9
- package/lib/translateToA1.spec.ts +84 -0
- package/lib/translateToA1.ts +45 -43
- package/lib/translateToR1C1.spec.ts +68 -4
- package/lib/translateToR1C1.ts +46 -19
- package/package.json +7 -7
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
|
|
package/lib/fixRanges.spec.ts
CHANGED
|
@@ -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
|
|
package/lib/lexers/lexRangeA1.ts
CHANGED
|
@@ -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) {
|
package/lib/mergeRefTokens.ts
CHANGED
|
@@ -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
|
-
[
|
|
8
|
-
[
|
|
9
|
-
[
|
|
10
|
-
[
|
|
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, '!',
|
|
15
|
-
[ CONTEXT, '!',
|
|
16
|
-
[ CONTEXT, '!',
|
|
17
|
-
[ CONTEXT, '!',
|
|
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, '!',
|
|
22
|
-
[ CONTEXT_QUOTE, '!',
|
|
23
|
-
[ CONTEXT_QUOTE, '!',
|
|
24
|
-
[ CONTEXT_QUOTE, '!',
|
|
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
|
|
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;
|
package/lib/parseA1Ref.spec.ts
CHANGED
|
@@ -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
|
});
|
package/lib/stringifyPrefix.ts
CHANGED
|
@@ -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 +=
|
|
47
|
+
quote += needQuotes(scope, quote);
|
|
27
48
|
nth++;
|
|
28
49
|
}
|
|
29
50
|
}
|
|
30
51
|
if (quote) {
|
|
31
|
-
pre =
|
|
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 +=
|
|
65
|
+
quote += needQuotes(workbookName);
|
|
45
66
|
}
|
|
46
67
|
if (sheetName) {
|
|
47
68
|
pre += sheetName;
|
|
48
|
-
quote +=
|
|
69
|
+
quote += needQuotes(sheetName);
|
|
49
70
|
}
|
|
50
71
|
if (quote) {
|
|
51
|
-
pre =
|
|
72
|
+
pre = quotePrefix(pre);
|
|
52
73
|
}
|
|
53
74
|
return pre ? pre + '!' : pre;
|
|
54
75
|
}
|
package/lib/tokenize.spec.ts
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
});
|
package/lib/translateToA1.ts
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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) {
|