@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.
- 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.ts +18 -0
- package/lib/lexers/canEndRange.ts +25 -0
- package/lib/lexers/lexBoolean.ts +55 -0
- package/lib/lexers/lexContext.ts +104 -0
- package/lib/lexers/lexError.ts +15 -0
- package/lib/lexers/lexFunction.ts +37 -0
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/lexNamed.ts +60 -0
- package/lib/lexers/lexNewLine.ts +12 -0
- package/lib/lexers/lexNumber.ts +48 -0
- package/lib/lexers/lexOperator.ts +26 -0
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/lexRangeA1.ts +134 -0
- package/lib/lexers/lexRangeR1C1.ts +146 -0
- package/lib/lexers/lexRangeTrim.ts +26 -0
- package/lib/lexers/lexRefOp.ts +19 -0
- package/lib/lexers/lexString.ts +22 -0
- package/lib/lexers/lexStructured.ts +25 -0
- package/lib/lexers/lexWhitespace.ts +31 -0
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +47 -32
- 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.ts +240 -0
- 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 +46 -30
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- 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 -170
- 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 -283
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexerParts.js +0 -228
- 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/parseSRange.js +0 -167
- 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
package/lib/tokenize.ts
ADDED
|
@@ -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
|
+
});
|