@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.
- package/dist/index-BMr6cTgc.d.cts +1444 -0
- package/dist/index-BMr6cTgc.d.ts +1444 -0
- package/dist/index.cjs +3054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2984 -0
- package/dist/index.js.map +1 -0
- package/dist/xlsx/index.cjs +3120 -0
- package/dist/xlsx/index.cjs.map +1 -0
- package/dist/xlsx/index.d.cts +55 -0
- package/dist/xlsx/index.d.ts +55 -0
- package/dist/xlsx/index.js +3049 -0
- package/dist/xlsx/index.js.map +1 -0
- package/docs/API.md +2959 -718
- package/docs/AST_format.md +2 -2
- package/eslint.config.mjs +40 -0
- package/lib/a1.spec.ts +32 -0
- package/lib/a1.ts +26 -0
- package/lib/addA1RangeBounds.ts +50 -0
- package/lib/addTokenMeta.spec.ts +166 -0
- package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
- package/lib/astTypes.ts +211 -0
- package/lib/cloneToken.ts +29 -0
- package/lib/{constants.js → constants.ts} +6 -3
- package/lib/fixRanges.spec.ts +220 -0
- package/lib/fixRanges.ts +260 -0
- package/lib/fromCol.spec.ts +15 -0
- package/lib/{fromCol.js → fromCol.ts} +1 -1
- package/lib/index.spec.ts +119 -0
- package/lib/index.ts +76 -0
- package/lib/isNodeType.ts +151 -0
- package/lib/isType.spec.ts +208 -0
- package/lib/{isType.js → isType.ts} +26 -25
- package/lib/lexers/{advRangeOp.js → advRangeOp.ts} +1 -1
- package/lib/lexers/{canEndRange.js → canEndRange.ts} +2 -2
- package/lib/lexers/{lexBoolean.js → lexBoolean.ts} +25 -6
- package/lib/lexers/{lexContext.js → lexContext.ts} +14 -6
- package/lib/lexers/{lexError.js → lexError.ts} +3 -3
- package/lib/lexers/{lexFunction.js → lexFunction.ts} +3 -2
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/{lexNamed.js → lexNamed.ts} +4 -4
- package/lib/lexers/{lexNewLine.js → lexNewLine.ts} +3 -2
- package/lib/lexers/{lexNumber.js → lexNumber.ts} +4 -3
- package/lib/lexers/{lexOperator.js → lexOperator.ts} +5 -4
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/{lexRangeA1.js → lexRangeA1.ts} +11 -7
- package/lib/lexers/{lexRangeR1C1.js → lexRangeR1C1.ts} +10 -6
- package/lib/lexers/{lexRangeTrim.js → lexRangeTrim.ts} +3 -2
- package/lib/lexers/{lexRefOp.js → lexRefOp.ts} +4 -3
- package/lib/lexers/{lexString.js → lexString.ts} +3 -3
- package/lib/lexers/{lexStructured.js → lexStructured.ts} +5 -5
- package/lib/lexers/{lexWhitespace.js → lexWhitespace.ts} +3 -2
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +14 -9
- package/lib/nodeTypes.ts +54 -0
- package/lib/parse.spec.ts +1410 -0
- package/lib/{parser.js → parse.ts} +81 -63
- package/lib/parseA1Range.spec.ts +233 -0
- package/lib/parseA1Range.ts +206 -0
- package/lib/parseA1Ref.spec.ts +337 -0
- package/lib/parseA1Ref.ts +115 -0
- package/lib/parseR1C1Range.ts +191 -0
- package/lib/parseR1C1Ref.spec.ts +323 -0
- package/lib/parseR1C1Ref.ts +127 -0
- package/lib/parseRef.spec.ts +90 -0
- package/lib/parseRef.ts +240 -0
- package/lib/{parseSRange.js → parseSRange.ts} +15 -10
- package/lib/parseStructRef.spec.ts +168 -0
- package/lib/parseStructRef.ts +76 -0
- package/lib/stringifyA1Range.spec.ts +72 -0
- package/lib/stringifyA1Range.ts +72 -0
- package/lib/stringifyA1Ref.spec.ts +64 -0
- package/lib/stringifyA1Ref.ts +59 -0
- package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
- package/lib/stringifyR1C1Range.spec.ts +92 -0
- package/lib/stringifyR1C1Range.ts +73 -0
- package/lib/stringifyR1C1Ref.spec.ts +63 -0
- package/lib/stringifyR1C1Ref.ts +67 -0
- package/lib/stringifyStructRef.spec.ts +124 -0
- package/lib/stringifyStructRef.ts +113 -0
- package/lib/stringifyTokens.ts +15 -0
- package/lib/toCol.spec.ts +11 -0
- package/lib/{toCol.js → toCol.ts} +4 -4
- package/lib/tokenTypes.ts +76 -0
- package/lib/tokenize-srefs.spec.ts +429 -0
- package/lib/tokenize.spec.ts +2103 -0
- package/lib/tokenize.ts +346 -0
- package/lib/translate.spec.ts +35 -0
- package/lib/translateToA1.spec.ts +247 -0
- package/lib/translateToA1.ts +231 -0
- package/lib/translateToR1C1.spec.ts +227 -0
- package/lib/translateToR1C1.ts +145 -0
- package/lib/types.ts +179 -0
- package/lib/xlsx/index.spec.ts +27 -0
- package/lib/xlsx/index.ts +32 -0
- package/package.json +45 -31
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- package/benchmark/benchmark.js +0 -48
- package/benchmark/formulas.json +0 -15677
- package/dist/fx.d.ts +0 -823
- package/dist/fx.js +0 -2
- package/dist/package.json +0 -1
- package/lib/a1.js +0 -348
- package/lib/a1.spec.js +0 -458
- package/lib/addTokenMeta.spec.js +0 -153
- package/lib/astTypes.js +0 -96
- package/lib/extraTypes.js +0 -74
- package/lib/fixRanges.js +0 -104
- package/lib/fixRanges.spec.js +0 -171
- package/lib/fromCol.spec.js +0 -11
- package/lib/index.js +0 -134
- package/lib/index.spec.js +0 -67
- package/lib/isType.spec.js +0 -168
- package/lib/lexer-srefs.spec.js +0 -324
- package/lib/lexer.js +0 -264
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexers/lexRange.js +0 -8
- package/lib/lexers/sets.js +0 -38
- package/lib/mergeRefTokens.spec.js +0 -121
- package/lib/package.json +0 -1
- package/lib/parseRef.js +0 -157
- package/lib/parseRef.spec.js +0 -71
- package/lib/parseStructRef.js +0 -48
- package/lib/parseStructRef.spec.js +0 -164
- package/lib/parser.spec.js +0 -1208
- package/lib/rc.js +0 -341
- package/lib/rc.spec.js +0 -403
- package/lib/stringifyStructRef.js +0 -80
- package/lib/stringifyStructRef.spec.js +0 -182
- package/lib/toCol.spec.js +0 -11
- package/lib/translate-toA1.spec.js +0 -214
- package/lib/translate-toRC.spec.js +0 -197
- package/lib/translate.js +0 -239
- package/lib/translate.spec.js +0 -21
- package/rollup.config.mjs +0 -22
- package/tsd.json +0 -12
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { MAX_ROWS, MAX_COLS, ERROR } from './constants.ts';
|
|
2
|
+
import { stringifyA1RefXlsx } from './stringifyA1Ref.ts';
|
|
3
|
+
import { parseR1C1RefXlsx } from './parseR1C1Ref.ts';
|
|
4
|
+
import { tokenizeXlsx } from './tokenize.ts';
|
|
5
|
+
import { isRange } from './isType.ts';
|
|
6
|
+
import { parseA1Range } from './parseA1Range.ts';
|
|
7
|
+
import type { RangeA1, ReferenceR1C1Xlsx, Token } from './types.ts';
|
|
8
|
+
import { stringifyTokens } from './stringifyTokens.ts';
|
|
9
|
+
import { cloneToken } from './cloneToken.ts';
|
|
10
|
+
|
|
11
|
+
// Turn on the most permissive setting when parsing ranges so we don't have to think about
|
|
12
|
+
// this option. We already know that range tokens are legal, so we're not going to encounter
|
|
13
|
+
// ternary ranges who's validity we need to worry about.
|
|
14
|
+
const REF_OPTS = { allowTernary: true };
|
|
15
|
+
|
|
16
|
+
function toFixed (val: number, abs: boolean, base: number, max: number, wrapEdges = true) {
|
|
17
|
+
let v = val;
|
|
18
|
+
if (v != null && !abs) {
|
|
19
|
+
v = base + val;
|
|
20
|
+
// Excel "wraps around" when value goes out of lower bounds.
|
|
21
|
+
// It's a bit quirky on entry as Excel _really wants_ to re-rewite the
|
|
22
|
+
// references but the behaviour is consistent with INDIRECT:
|
|
23
|
+
// ... In A1: RC[-1] => R1C[16383].
|
|
24
|
+
if (v < 0) {
|
|
25
|
+
if (!wrapEdges) {
|
|
26
|
+
return NaN;
|
|
27
|
+
}
|
|
28
|
+
v = max + v + 1;
|
|
29
|
+
}
|
|
30
|
+
// ... In B1: =RC[16383] => =RC[-1]
|
|
31
|
+
if (v > max) {
|
|
32
|
+
if (!wrapEdges) {
|
|
33
|
+
return NaN;
|
|
34
|
+
}
|
|
35
|
+
v -= max + 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options for {@link translateTokensToA1}
|
|
43
|
+
*/
|
|
44
|
+
export type OptsTranslateTokensToA1 = {
|
|
45
|
+
/**
|
|
46
|
+
* Wrap out-of-bounds ranges around sheet edges rather than turning them to #REF! errors.
|
|
47
|
+
* @defaultValue true
|
|
48
|
+
*/
|
|
49
|
+
wrapEdges?: boolean,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Translates ranges in a list of tokens from relative R1C1 syntax to absolute A1 syntax.
|
|
54
|
+
*
|
|
55
|
+
* ```js
|
|
56
|
+
* translateToA1("=SUM(RC[1],R2C5,Sheet!R3C5)", "D10");
|
|
57
|
+
* // => "=SUM(E10,$E$2,Sheet!$E$3)");
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* If an input range is -1,-1 relative rows/columns and the anchor is A1, the
|
|
61
|
+
* resulting range will (by default) wrap around to the bottom of the sheet
|
|
62
|
+
* resulting in the range XFD1048576. This may not be what you want so may set
|
|
63
|
+
* `{ wrapEdges }` to false which will instead turn the range into a `#REF!` error.
|
|
64
|
+
*
|
|
65
|
+
* ```js
|
|
66
|
+
* translateToA1("=R[-1]C[-1]", "A1");
|
|
67
|
+
* // => "=XFD1048576");
|
|
68
|
+
*
|
|
69
|
+
* translateToA1("=R[-1]C[-1]", "A1", { wrapEdges: false });
|
|
70
|
+
* // => "=#REF!");
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Note that if you are passing in a list of tokens that was not created using
|
|
74
|
+
* `mergeRefs` and you disable edge wrapping (or you simply set both options
|
|
75
|
+
* to false), you can end up with a formula such as `=#REF!:B2` or
|
|
76
|
+
* `=Sheet3!#REF!:F3`. These are valid formulas in the Excel formula language
|
|
77
|
+
* and Excel will accept them, but they are not supported in Google Sheets.
|
|
78
|
+
*
|
|
79
|
+
* @see {@link OptsTranslateTokensToA1}
|
|
80
|
+
* @param tokens A token list that should be adjusted.
|
|
81
|
+
* @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`).
|
|
82
|
+
* @param options Translation options.
|
|
83
|
+
* @returns A token list.
|
|
84
|
+
*/
|
|
85
|
+
export function translateTokensToA1 (
|
|
86
|
+
tokens: Token[],
|
|
87
|
+
anchorCell: string,
|
|
88
|
+
options: OptsTranslateTokensToA1 = {}
|
|
89
|
+
): Token[] {
|
|
90
|
+
const anchorRange = parseA1Range(anchorCell);
|
|
91
|
+
if (!anchorRange) {
|
|
92
|
+
throw new Error('translateToR1C1 got an invalid anchorCell: ' + anchorCell);
|
|
93
|
+
}
|
|
94
|
+
const { top, left } = anchorRange;
|
|
95
|
+
const { wrapEdges = true } = options;
|
|
96
|
+
|
|
97
|
+
let offsetSkew = 0;
|
|
98
|
+
const outTokens = [];
|
|
99
|
+
for (let token of tokens) {
|
|
100
|
+
if (isRange(token)) {
|
|
101
|
+
token = cloneToken(token);
|
|
102
|
+
const tokenValue = token.value;
|
|
103
|
+
// We can get away with using the xlsx ref-parser here because it is more permissive
|
|
104
|
+
// and we will end up with the same prefix after serialization anyway:
|
|
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);
|
|
149
|
+
}
|
|
150
|
+
// if token includes offsets, those offsets are now likely wrong!
|
|
151
|
+
if (token.loc) {
|
|
152
|
+
token.loc[0] += offsetSkew;
|
|
153
|
+
offsetSkew += token.value.length - tokenValue.length;
|
|
154
|
+
token.loc[1] += offsetSkew;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (offsetSkew && token.loc) {
|
|
158
|
+
token = cloneToken(token);
|
|
159
|
+
token.loc[0] += offsetSkew;
|
|
160
|
+
token.loc[1] += offsetSkew;
|
|
161
|
+
}
|
|
162
|
+
outTokens[outTokens.length] = token;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return outTokens;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Options for {@link translateFormulaToA1}.
|
|
170
|
+
*/
|
|
171
|
+
export type OptsTranslateFormulaToA1 = {
|
|
172
|
+
/**
|
|
173
|
+
* Wrap out-of-bounds ranges around sheet edges rather than turning them to #REF! errors.
|
|
174
|
+
* @defaultValue true
|
|
175
|
+
*/
|
|
176
|
+
wrapEdges?: boolean,
|
|
177
|
+
/**
|
|
178
|
+
* Should ranges be treated as whole references (`Sheet1!A1:B2`) or as separate tokens
|
|
179
|
+
* for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`).
|
|
180
|
+
* @defaultValue true
|
|
181
|
+
*/
|
|
182
|
+
mergeRefs?: boolean,
|
|
183
|
+
/**
|
|
184
|
+
* Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`.
|
|
185
|
+
* These are supported by Google Sheets but not Excel. See: References.md.
|
|
186
|
+
* @defaultValue true
|
|
187
|
+
*/
|
|
188
|
+
allowTernary?: boolean,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Translates ranges in a formula from relative R1C1 syntax to absolute A1 syntax.
|
|
193
|
+
*
|
|
194
|
+
* ```js
|
|
195
|
+
* translateToA1("=SUM(RC[1],R2C5,Sheet!R3C5)", "D10");
|
|
196
|
+
* // => "=SUM(E10,$E$2,Sheet!$E$3)");
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* If an input range is -1,-1 relative rows/columns and the anchor is A1, the
|
|
200
|
+
* resulting range will (by default) wrap around to the bottom of the sheet
|
|
201
|
+
* resulting in the range XFD1048576. This may not be what you want so you can set
|
|
202
|
+
* `{ wrapEdges }` to false which will instead turn the range into a `#REF!` error.
|
|
203
|
+
*
|
|
204
|
+
* ```js
|
|
205
|
+
* translateToA1("=R[-1]C[-1]", "A1");
|
|
206
|
+
* // => "=XFD1048576");
|
|
207
|
+
*
|
|
208
|
+
* translateToA1("=R[-1]C[-1]", "A1", { wrapEdges: false });
|
|
209
|
+
* // => "=#REF!");
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @see {@link OptsTranslateFormulaToA1}
|
|
213
|
+
* @param formula An Excel formula string that should be adjusted.
|
|
214
|
+
* @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`).
|
|
215
|
+
* @param options Translation options.
|
|
216
|
+
* @returns A formula string.
|
|
217
|
+
*/
|
|
218
|
+
export function translateFormulaToA1 (
|
|
219
|
+
formula: string,
|
|
220
|
+
anchorCell: string,
|
|
221
|
+
options: OptsTranslateFormulaToA1 = {}
|
|
222
|
+
): string {
|
|
223
|
+
if (typeof formula === 'string') {
|
|
224
|
+
return stringifyTokens(translateTokensToA1(tokenizeXlsx(formula, {
|
|
225
|
+
allowTernary: options.allowTernary ?? true,
|
|
226
|
+
mergeRefs: options.mergeRefs,
|
|
227
|
+
r1c1: true
|
|
228
|
+
}), anchorCell, options));
|
|
229
|
+
}
|
|
230
|
+
throw new Error('translateFormulaToA1 expects a formula string');
|
|
231
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { translateFormulaToR1C1, translateTokensToR1C1 } from './translateToR1C1.ts';
|
|
3
|
+
import { tokenize } from './tokenize.ts';
|
|
4
|
+
import { addTokenMeta } from './addTokenMeta.ts';
|
|
5
|
+
import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.ts';
|
|
6
|
+
|
|
7
|
+
function isA2R (expr: string, anchor: string, result: string) {
|
|
8
|
+
expect(translateFormulaToR1C1(expr, anchor)).toBe(result);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('translate absolute cells from A1 to RC', () => {
|
|
12
|
+
test('absolute cells with B2 anchor', () => {
|
|
13
|
+
isA2R('=$A$1', 'B2', '=R1C1');
|
|
14
|
+
isA2R('=$A$2', 'B2', '=R2C1');
|
|
15
|
+
isA2R('=$A$3', 'B2', '=R3C1');
|
|
16
|
+
isA2R('=$B$1', 'B2', '=R1C2');
|
|
17
|
+
isA2R('=$B$2', 'B2', '=R2C2');
|
|
18
|
+
isA2R('=$B$3', 'B2', '=R3C2');
|
|
19
|
+
isA2R('=$C$1', 'B2', '=R1C3');
|
|
20
|
+
isA2R('=$C$2', 'B2', '=R2C3');
|
|
21
|
+
isA2R('=$C$3', 'B2', '=R3C3');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('absolute cells with Z19 anchor', () => {
|
|
25
|
+
// absolute cells, anchor has no real effect
|
|
26
|
+
isA2R('=$A$1', 'Z19', '=R1C1');
|
|
27
|
+
isA2R('=$A$2', 'Z19', '=R2C1');
|
|
28
|
+
isA2R('=$A$3', 'Z19', '=R3C1');
|
|
29
|
+
isA2R('=$B$1', 'Z19', '=R1C2');
|
|
30
|
+
isA2R('=$B$2', 'Z19', '=R2C2');
|
|
31
|
+
isA2R('=$B$3', 'Z19', '=R3C2');
|
|
32
|
+
isA2R('=$C$1', 'Z19', '=R1C3');
|
|
33
|
+
isA2R('=$C$2', 'Z19', '=R2C3');
|
|
34
|
+
isA2R('=$C$3', 'Z19', '=R3C3');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('translate relative cells from A1 to RC', () => {
|
|
39
|
+
test('relative cells with B2 anchor', () => {
|
|
40
|
+
isA2R('=A1', 'B2', '=R[-1]C[-1]');
|
|
41
|
+
isA2R('=A2', 'B2', '=RC[-1]');
|
|
42
|
+
isA2R('=A3', 'B2', '=R[1]C[-1]');
|
|
43
|
+
isA2R('=B1', 'B2', '=R[-1]C');
|
|
44
|
+
isA2R('=B2', 'B2', '=RC');
|
|
45
|
+
isA2R('=B3', 'B2', '=R[1]C');
|
|
46
|
+
isA2R('=C1', 'B2', '=R[-1]C[1]');
|
|
47
|
+
isA2R('=C2', 'B2', '=RC[1]');
|
|
48
|
+
isA2R('=C3', 'B2', '=R[1]C[1]');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('relative cells with I12 anchor', () => {
|
|
52
|
+
// relative cells, but with [0] notation
|
|
53
|
+
isA2R('=H11', 'I12', '=R[-1]C[-1]');
|
|
54
|
+
isA2R('=H12', 'I12', '=RC[-1]');
|
|
55
|
+
isA2R('=H13', 'I12', '=R[1]C[-1]');
|
|
56
|
+
isA2R('=I11', 'I12', '=R[-1]C');
|
|
57
|
+
isA2R('=I12', 'I12', '=RC');
|
|
58
|
+
isA2R('=I13', 'I12', '=R[1]C');
|
|
59
|
+
isA2R('=J11', 'I12', '=R[-1]C[1]');
|
|
60
|
+
isA2R('=J12', 'I12', '=RC[1]');
|
|
61
|
+
isA2R('=J13', 'I12', '=R[1]C[1]');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('translate rows from A1 to RC', () => {
|
|
66
|
+
test('relative row references', () => {
|
|
67
|
+
isA2R('=2:2', 'B1', '=R[1]');
|
|
68
|
+
isA2R('=2:2', 'B2', '=R');
|
|
69
|
+
isA2R('=2:2', 'B3', '=R[-1]');
|
|
70
|
+
isA2R('=13:13', 'B13', '=R');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('mixed row references', () => {
|
|
74
|
+
isA2R('=$2:$2', 'B2', '=R2');
|
|
75
|
+
isA2R('=2:$2', 'B2', '=R:R2');
|
|
76
|
+
isA2R('=11:9', 'Z10', '=R[-1]:R[1]');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('translate cols from A1 to RC', () => {
|
|
81
|
+
test('relative column references', () => {
|
|
82
|
+
isA2R('=B:B', 'A2', '=C[1]');
|
|
83
|
+
isA2R('=B:B', 'B2', '=C');
|
|
84
|
+
isA2R('=B:B', 'C2', '=C[-1]');
|
|
85
|
+
isA2R('=Z:Z', 'Z2', '=C');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('mixed column references', () => {
|
|
89
|
+
isA2R('=$B:$B', 'B2', '=C2');
|
|
90
|
+
isA2R('=B:$B', 'B2', '=C:C2');
|
|
91
|
+
isA2R('=N:L', 'M10', '=C[-1]:C[1]');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('translate partials from A1 to RC', () => {
|
|
96
|
+
test('partial range references', () => {
|
|
97
|
+
isA2R('=A1:A', 'C6', '=R[-5]C[-2]:C[-2]');
|
|
98
|
+
isA2R('=A1:1', 'D6', '=R[-5]C[-3]:R[-5]');
|
|
99
|
+
isA2R('=$A1:$A', 'C7', '=R[-6]C1:C1');
|
|
100
|
+
isA2R('=$A:$A1', 'D7', '=R[-6]C1:C1');
|
|
101
|
+
isA2R('=$A1:1', 'C7', '=R[-6]C1:R[-6]');
|
|
102
|
+
isA2R('=1:$A1', 'C7', '=R[-6]C1:R[-6]');
|
|
103
|
+
isA2R('=A$1:A', 'C6', '=R1C[-2]:C[-2]');
|
|
104
|
+
isA2R('=A:A$1', 'C6', '=R1C[-2]:C[-2]');
|
|
105
|
+
isA2R('=A$1:$1', 'D6', '=R1C[-3]:R1');
|
|
106
|
+
isA2R('=$1:A$1', 'D6', '=R1C[-3]:R1');
|
|
107
|
+
isA2R('=$A$1:$A', 'D6', '=R1C1:C1');
|
|
108
|
+
isA2R('=$A:$A$1', 'D6', '=R1C1:C1');
|
|
109
|
+
isA2R('=$A$1:$1', 'D6', '=R1C1:R1');
|
|
110
|
+
isA2R('=$1:$A$1', 'D6', '=R1C1:R1');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('translate boundary coords from A1 to RC', () => {
|
|
115
|
+
test('boundary coordinate references', () => {
|
|
116
|
+
isA2R('=XFD:XFD', 'A1', '=C[16383]');
|
|
117
|
+
isA2R('=A1', 'B1', '=RC[-1]');
|
|
118
|
+
isA2R('=B1', 'C1', '=RC[-1]');
|
|
119
|
+
isA2R('=1048576:1048576', 'A1', '=R[1048575]');
|
|
120
|
+
isA2R('=$1:$1048576', 'A1', '=R1:R1048576');
|
|
121
|
+
isA2R('=$A:$XFD', 'A1', '=C1:C16384');
|
|
122
|
+
isA2R('=$A$1:$XFD$1048576', 'A1', '=R1C1:R1048576C16384');
|
|
123
|
+
isA2R('=A1', 'A2', '=R[-1]C');
|
|
124
|
+
isA2R('=A2', 'A3', '=R[-1]C');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('translate mixed rel/abs coords from A1 to RC', () => {
|
|
129
|
+
test('mixed relative/absolute references', () => {
|
|
130
|
+
isA2R('=B$1', 'B2', '=R1C');
|
|
131
|
+
isA2R('=$D8', 'B4', '=R[4]C4');
|
|
132
|
+
isA2R('=8:$10', 'B4', '=R[4]:R10');
|
|
133
|
+
isA2R('=$J:L', 'B4', '=C10:C[10]');
|
|
134
|
+
isA2R('=$A$1:$B$2', 'D4', '=R1C1:R2C2');
|
|
135
|
+
isA2R('=C3:F6', 'D4', '=R[-1]C[-1]:R[2]C[2]');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('translate involved cases from A1 to RC', () => {
|
|
140
|
+
test('complex function expressions', () => {
|
|
141
|
+
isA2R('=SUM(IF(E10,$E$2,$E$3),Sheet1!$2:$2*Sheet2!B:B)', 'D10',
|
|
142
|
+
'=SUM(IF(RC[1],R2C5,R3C5),Sheet1!R2*Sheet2!C[-2])');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('expressions with structured and named references', () => {
|
|
146
|
+
// make sure we don't get confused by structured, or named refs
|
|
147
|
+
isA2R('=A1+Table1[#Data]', 'D10', '=R[-9]C[-3]+Table1[#Data]');
|
|
148
|
+
isA2R('=A1+foobar', 'D10', '=R[-9]C[-3]+foobar');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('XLSX internal syntax', () => {
|
|
152
|
+
// This [123]Sheet!A1 variant of the syntax is used internally in xlsx files
|
|
153
|
+
isA2R('=[2]Sheet1!A1', 'D10', '=[2]Sheet1!R[-9]C[-3]');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('translate works with merged ranges', () => {
|
|
158
|
+
test('preserves token metadata and locations', () => {
|
|
159
|
+
// This tests that:
|
|
160
|
+
// - Translate works with ranges that have context attached
|
|
161
|
+
// - If input is a tokenlist, output is also a tokenlist
|
|
162
|
+
// - If tokens have ranges, those ranges are adjusted to new token lengths
|
|
163
|
+
// - Properties added by addTokenMeta are preserved
|
|
164
|
+
const expr = '=SUM(IF(E10,$E$2,$E$3),Sheet1!$2:$2*Sheet2!B:B)';
|
|
165
|
+
const tokens = addTokenMeta(tokenize(expr, { withLocation: true }));
|
|
166
|
+
const expected = [
|
|
167
|
+
{ type: FX_PREFIX, value: '=', loc: [ 0, 1 ], index: 0, depth: 0 },
|
|
168
|
+
{ type: FUNCTION, value: 'SUM', loc: [ 1, 4 ], index: 1, depth: 0 },
|
|
169
|
+
{ type: OPERATOR, value: '(', loc: [ 4, 5 ], index: 2, depth: 1, groupId: 'fxg7' },
|
|
170
|
+
{ type: FUNCTION, value: 'IF', loc: [ 5, 7 ], index: 3, depth: 1 },
|
|
171
|
+
{ type: OPERATOR, value: '(', loc: [ 7, 8 ], index: 4, depth: 2, groupId: 'fxg4' },
|
|
172
|
+
{ type: REF_RANGE, value: 'RC[1]', loc: [ 8, 13 ], index: 5, depth: 2, groupId: 'fxg1' },
|
|
173
|
+
{ type: OPERATOR, value: ',', loc: [ 13, 14 ], index: 6, depth: 2 },
|
|
174
|
+
{ type: REF_RANGE, value: 'R2C5', loc: [ 14, 18 ], index: 7, depth: 2, groupId: 'fxg2' },
|
|
175
|
+
{ type: OPERATOR, value: ',', loc: [ 18, 19 ], index: 8, depth: 2 },
|
|
176
|
+
{ type: REF_RANGE, value: 'R3C5', loc: [ 19, 23 ], index: 9, depth: 2, groupId: 'fxg3' },
|
|
177
|
+
{ type: OPERATOR, value: ')', loc: [ 23, 24 ], index: 10, depth: 2, groupId: 'fxg4' },
|
|
178
|
+
{ type: OPERATOR, value: ',', loc: [ 24, 25 ], index: 11, depth: 1 },
|
|
179
|
+
{ type: REF_BEAM, value: 'Sheet1!R2', loc: [ 25, 34 ], index: 12, depth: 1, groupId: 'fxg5' },
|
|
180
|
+
{ type: OPERATOR, value: '*', loc: [ 34, 35 ], index: 13, depth: 1 },
|
|
181
|
+
{ type: REF_BEAM, value: 'Sheet2!C[-2]', loc: [ 35, 47 ], index: 14, depth: 1, groupId: 'fxg6' },
|
|
182
|
+
{ type: OPERATOR, value: ')', loc: [ 47, 48 ], index: 15, depth: 1, groupId: 'fxg7' }
|
|
183
|
+
];
|
|
184
|
+
expect(translateTokensToR1C1(tokens, 'D10')).toEqual(expected);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('translate works with xlsx mode', () => {
|
|
189
|
+
function testExpr (expr: string, anchor: string, expected: any[]) {
|
|
190
|
+
const opts = { mergeRefs: true, xlsx: true, r1c1: false };
|
|
191
|
+
const tokens = tokenize(expr, opts);
|
|
192
|
+
expect(translateTokensToR1C1(tokens, anchor)).toEqual(expected);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test('XLSX workbook references', () => {
|
|
196
|
+
testExpr("'[My Fancy Workbook.xlsx]'!B$1", 'B2', [
|
|
197
|
+
{ type: REF_RANGE, value: "'[My Fancy Workbook.xlsx]'!R1C" }
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
testExpr('[Workbook.xlsx]!B$1', 'B2', [
|
|
201
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]!R1C' }
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
testExpr('[Workbook.xlsx]Sheet1!B$1', 'B2', [
|
|
205
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!R1C' }
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
testExpr('[Workbook.xlsx]!table[#data]', 'B2', [
|
|
209
|
+
{ type: REF_STRUCT, value: '[Workbook.xlsx]!table[#data]' }
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('translate works with trimmed ranges', () => {
|
|
215
|
+
function testExpr (expr: string, anchor: string, expected: any[]) {
|
|
216
|
+
const opts = { mergeRefs: true, xlsx: true, r1c1: false };
|
|
217
|
+
expect(translateTokensToR1C1(tokenize(expr, opts), anchor)).toEqual(expected);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test('trimmed range translation', () => {
|
|
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]' }
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { stringifyR1C1RefXlsx } from './stringifyR1C1Ref.ts';
|
|
2
|
+
import { tokenizeXlsx } from './tokenize.ts';
|
|
3
|
+
import { parseA1Range } from './parseA1Range.ts';
|
|
4
|
+
import type { RangeR1C1, ReferenceA1Xlsx, Token } from './types.ts';
|
|
5
|
+
import { stringifyTokens } from './stringifyTokens.ts';
|
|
6
|
+
import { cloneToken } from './cloneToken.ts';
|
|
7
|
+
import { REF_BEAM, REF_RANGE, REF_TERNARY } from './constants.ts';
|
|
8
|
+
import { splitContext } from './parseRef.ts';
|
|
9
|
+
|
|
10
|
+
const calc = (abs: boolean, vX: number, aX: number): number => {
|
|
11
|
+
if (vX == null) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return abs ? vX : vX - aX;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// We already know here that we're holding a token value from
|
|
18
|
+
// one of: REF_RANGE | REF_BEAM | REF_TERNARY
|
|
19
|
+
// So we can quickly scan for ! shortcut a bunch of parsing:
|
|
20
|
+
const unquote = d => d.slice(1, -1).replace(/''/g, "'");
|
|
21
|
+
function quickParseA1 (ref: string): ReferenceA1Xlsx {
|
|
22
|
+
const split = ref.lastIndexOf('!');
|
|
23
|
+
const data: Partial<ReferenceA1Xlsx> = {};
|
|
24
|
+
if (split > -1) {
|
|
25
|
+
if (ref.startsWith('\'')) {
|
|
26
|
+
splitContext(unquote(ref.slice(0, split)), data, true);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
splitContext(ref.slice(0, split), data, true);
|
|
30
|
+
}
|
|
31
|
+
data.range = parseA1Range(ref.slice(split + 1));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
data.range = parseA1Range(ref);
|
|
35
|
+
}
|
|
36
|
+
return data as ReferenceA1Xlsx;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for {@link translateFormulaToR1C1}.
|
|
41
|
+
*/
|
|
42
|
+
export type OptsTranslateToR1C1 = {
|
|
43
|
+
/**
|
|
44
|
+
* Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`.
|
|
45
|
+
* These are supported by Google Sheets but not Excel.
|
|
46
|
+
* See: [References.md](./References.md).
|
|
47
|
+
* @defaultValue true
|
|
48
|
+
*/
|
|
49
|
+
allowTernary?: boolean,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Translates ranges in a list of tokens from absolute A1 syntax to relative R1C1 syntax.
|
|
54
|
+
*
|
|
55
|
+
* ```js
|
|
56
|
+
* translateFormulaToR1C1("=SUM(E10,$E$2,Sheet!$E$3)", "D10");
|
|
57
|
+
* // => "=SUM(RC[1],R2C5,Sheet!R3C5)");
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @param tokens A token list that should be adjusted.
|
|
61
|
+
* @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`).
|
|
62
|
+
* @returns A token list.
|
|
63
|
+
*/
|
|
64
|
+
export function translateTokensToR1C1 (
|
|
65
|
+
tokens: Token[],
|
|
66
|
+
anchorCell: string
|
|
67
|
+
): Token[] {
|
|
68
|
+
const anchorRange = parseA1Range(anchorCell);
|
|
69
|
+
if (!anchorRange) {
|
|
70
|
+
throw new Error('translateTokensToR1C1 got an invalid anchorCell: ' + anchorCell);
|
|
71
|
+
}
|
|
72
|
+
const { top, left } = anchorRange;
|
|
73
|
+
|
|
74
|
+
let offsetSkew = 0;
|
|
75
|
+
const outTokens = [];
|
|
76
|
+
for (let token of tokens) {
|
|
77
|
+
const tokenType = token?.type;
|
|
78
|
+
if (tokenType === REF_RANGE || tokenType === REF_BEAM || tokenType === REF_TERNARY) {
|
|
79
|
+
token = cloneToken(token);
|
|
80
|
+
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:
|
|
83
|
+
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;
|
|
96
|
+
}
|
|
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
|
+
// if token includes offsets, those offsets are now likely wrong!
|
|
101
|
+
if (token.loc) {
|
|
102
|
+
token.loc[0] += offsetSkew;
|
|
103
|
+
offsetSkew += token.value.length - tokenValue.length;
|
|
104
|
+
token.loc[1] += offsetSkew;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (offsetSkew && token.loc) {
|
|
108
|
+
token = cloneToken(token);
|
|
109
|
+
token.loc[0] += offsetSkew;
|
|
110
|
+
token.loc[1] += offsetSkew;
|
|
111
|
+
}
|
|
112
|
+
outTokens[outTokens.length] = token;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return outTokens;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Translates ranges in a formula from absolute A1 syntax to relative R1C1 syntax.
|
|
120
|
+
*
|
|
121
|
+
* ```js
|
|
122
|
+
* translateFormulaToR1C1("=SUM(E10,$E$2,Sheet!$E$3)", "D10");
|
|
123
|
+
* // => "=SUM(RC[1],R2C5,Sheet!R3C5)");
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @see {@link OptsTranslateToR1C1}
|
|
127
|
+
* @param formula An Excel formula that should be adjusted.
|
|
128
|
+
* @param anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`).
|
|
129
|
+
* @param [options={}] The options
|
|
130
|
+
* @returns A formula string.
|
|
131
|
+
*/
|
|
132
|
+
export function translateFormulaToR1C1 (
|
|
133
|
+
formula: string,
|
|
134
|
+
anchorCell: string,
|
|
135
|
+
options: OptsTranslateToR1C1 = {}
|
|
136
|
+
): string {
|
|
137
|
+
if (typeof formula === 'string') {
|
|
138
|
+
const tokens = tokenizeXlsx(formula, {
|
|
139
|
+
mergeRefs: false,
|
|
140
|
+
allowTernary: options.allowTernary ?? true
|
|
141
|
+
});
|
|
142
|
+
return stringifyTokens(translateTokensToR1C1(tokens, anchorCell));
|
|
143
|
+
}
|
|
144
|
+
throw new Error('translateFormulaToA1 expects a formula string');
|
|
145
|
+
}
|