@borgar/fx 4.12.0 → 4.13.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/benchmark/benchmark.js +48 -0
- package/benchmark/formulas.json +15677 -0
- package/dist/fx.js +2 -2
- package/lib/fixRanges.spec.js +2 -1
- package/lib/lexer.js +38 -57
- package/lib/lexers/advRangeOp.js +18 -0
- package/lib/lexers/canEndRange.js +25 -0
- package/lib/lexers/lexBoolean.js +36 -0
- package/lib/lexers/lexContext.js +96 -0
- package/lib/lexers/lexError.js +15 -0
- package/lib/lexers/lexFunction.js +36 -0
- package/lib/lexers/lexNamed.js +60 -0
- package/lib/lexers/lexNewLine.js +11 -0
- package/lib/lexers/lexNumber.js +47 -0
- package/lib/lexers/lexOperator.js +25 -0
- package/lib/lexers/lexRange.js +8 -0
- package/lib/lexers/lexRangeA1.js +130 -0
- package/lib/lexers/lexRangeR1C1.js +142 -0
- package/lib/lexers/lexRangeTrim.js +25 -0
- package/lib/lexers/lexRefOp.js +18 -0
- package/lib/lexers/lexString.js +22 -0
- package/lib/lexers/lexStructured.js +25 -0
- package/lib/lexers/lexWhitespace.js +30 -0
- package/lib/lexers/sets.js +38 -0
- package/lib/mergeRefTokens.js +33 -23
- package/lib/parseRef.js +1 -1
- package/lib/parseSRange.js +184 -116
- package/lib/parseStructRef.spec.js +1 -1
- package/package.json +12 -10
- package/lib/lexerParts.js +0 -228
package/lib/fixRanges.spec.js
CHANGED
|
@@ -6,7 +6,8 @@ import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_STRUCT, REF_TERNARY } fro
|
|
|
6
6
|
|
|
7
7
|
Test.prototype.isFixed = function (expr, expected, options = {}) {
|
|
8
8
|
const result = fixRanges(expr, options);
|
|
9
|
-
this.is(result, expected,
|
|
9
|
+
this.is(result, expected,
|
|
10
|
+
`\x1b[36m${expr} → ${expected} \x1b[37mopts=${JSON.stringify(options)}\x1b[0m`);
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
test('fixRanges basics', t => {
|
package/lib/lexer.js
CHANGED
|
@@ -4,15 +4,14 @@ import {
|
|
|
4
4
|
NUMBER,
|
|
5
5
|
OPERATOR,
|
|
6
6
|
REF_NAMED,
|
|
7
|
-
STRING,
|
|
8
7
|
UNKNOWN,
|
|
9
8
|
WHITESPACE,
|
|
10
9
|
FUNCTION,
|
|
11
10
|
OPERATOR_TRIM,
|
|
12
11
|
REF_RANGE
|
|
13
12
|
} from './constants.js';
|
|
14
|
-
import { lexers } from './lexerParts.js';
|
|
15
13
|
import { mergeRefTokens } from './mergeRefTokens.js';
|
|
14
|
+
import { lexers } from './lexers/sets.js';
|
|
16
15
|
|
|
17
16
|
const isType = (t, type) => t && t.type === type;
|
|
18
17
|
|
|
@@ -71,7 +70,14 @@ function fixRCNames (tokens) {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
export function getTokens (fx, tokenHandlers, options = {}) {
|
|
74
|
-
const opts =
|
|
73
|
+
const opts = { ...defaultOptions, ...options };
|
|
74
|
+
// const opts = {
|
|
75
|
+
// withLocation: options.withLocation ?? false,
|
|
76
|
+
// mergeRefs: options.mergeRefs ?? true,
|
|
77
|
+
// allowTernary: options.allowTernary ?? false,
|
|
78
|
+
// negativeNumbers: options.negativeNumbers ?? true,
|
|
79
|
+
// r1c1: options.r1c1 ?? false
|
|
80
|
+
// };
|
|
75
81
|
const { withLocation, mergeRefs, negativeNumbers } = opts;
|
|
76
82
|
const tokens = [];
|
|
77
83
|
let pos = 0;
|
|
@@ -103,7 +109,8 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
103
109
|
token.type = UNKNOWN;
|
|
104
110
|
}
|
|
105
111
|
// push token as normally
|
|
106
|
-
tokens.push(token);
|
|
112
|
+
// tokens.push(token);
|
|
113
|
+
tokens[tokens.length] = token;
|
|
107
114
|
lastToken = token;
|
|
108
115
|
if (token.type !== WHITESPACE && token.type !== NEWLINE) {
|
|
109
116
|
tail1 = tail0;
|
|
@@ -112,77 +119,51 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
112
119
|
}
|
|
113
120
|
};
|
|
114
121
|
|
|
115
|
-
if (fx
|
|
116
|
-
const token = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
122
|
+
if (fx.startsWith('=')) {
|
|
123
|
+
const token = { type: FX_PREFIX, value: '=' };
|
|
124
|
+
if (withLocation) {
|
|
125
|
+
token.loc = [ 0, 1 ];
|
|
126
|
+
}
|
|
121
127
|
pos++;
|
|
122
128
|
pushToken(token);
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
const numHandlers = tokenHandlers.length;
|
|
125
132
|
while (pos < fx.length) {
|
|
126
133
|
const startPos = pos;
|
|
127
|
-
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (t) {
|
|
133
|
-
tokenType = t.type;
|
|
134
|
-
tokenValue = t.value;
|
|
135
|
-
pos += tokenValue.length;
|
|
134
|
+
let token;
|
|
135
|
+
for (let i = 0; i < numHandlers; i++) {
|
|
136
|
+
token = tokenHandlers[i](fx, pos, opts);
|
|
137
|
+
if (token) {
|
|
138
|
+
pos += token.value.length;
|
|
136
139
|
break;
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
if (!
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
if (!token) {
|
|
144
|
+
token = {
|
|
145
|
+
type: UNKNOWN,
|
|
146
|
+
value: fx[pos]
|
|
147
|
+
};
|
|
143
148
|
pos++;
|
|
144
149
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
value: tokenValue,
|
|
149
|
-
...(withLocation ? { loc: [ startPos, pos ] } : {})
|
|
150
|
-
};
|
|
150
|
+
if (withLocation) {
|
|
151
|
+
token.loc = [ startPos, pos ];
|
|
152
|
+
}
|
|
151
153
|
|
|
152
154
|
// make a note if we found a let/lambda call
|
|
153
|
-
if (lastToken &&
|
|
154
|
-
|
|
155
|
-
if (lastLC === 'lambda' || lastLC === 'let') {
|
|
155
|
+
if (lastToken && token.value === '(' && lastToken.type === FUNCTION) {
|
|
156
|
+
if (/^l(?:ambda|et)$/i.test(lastToken.value)) {
|
|
156
157
|
letOrLambda++;
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
// make a note if we found a R or C unknown
|
|
160
|
-
if (
|
|
161
|
-
const valLC =
|
|
161
|
+
if (token.type === UNKNOWN && token.value.length === 1) {
|
|
162
|
+
const valLC = token.value.toLowerCase();
|
|
162
163
|
unknownRC += (valLC === 'r' || valLC === 'c') ? 1 : 0;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
if (tokenType === STRING) {
|
|
167
|
-
const l = tokenValue.length;
|
|
168
|
-
if (tokenValue === '""') {
|
|
169
|
-
// common case that IS terminated
|
|
170
|
-
}
|
|
171
|
-
else if (tokenValue === '"' || tokenValue[l - 1] !== '"') {
|
|
172
|
-
token.unterminated = true;
|
|
173
|
-
}
|
|
174
|
-
else if (tokenValue !== '""' && tokenValue[l - 2] === '"') {
|
|
175
|
-
let p = l - 1;
|
|
176
|
-
while (tokenValue[p] === '"') { p--; }
|
|
177
|
-
const atStart = (p + 1);
|
|
178
|
-
const oddNum = ((l - p + 1) % 2 === 0);
|
|
179
|
-
if (!atStart ^ oddNum) {
|
|
180
|
-
token.unterminated = true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (negativeNumbers && tokenType === NUMBER) {
|
|
166
|
+
if (negativeNumbers && token.type === NUMBER) {
|
|
186
167
|
const last1 = lastToken;
|
|
187
168
|
// do we have a number preceded by a minus?
|
|
188
169
|
if (last1 && isType(last1, OPERATOR) && last1.value === '-') {
|
|
@@ -193,8 +174,8 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
193
174
|
!causesBinaryMinus(tail1)
|
|
194
175
|
) {
|
|
195
176
|
const minus = tokens.pop();
|
|
196
|
-
token.value = '-' +
|
|
197
|
-
if (
|
|
177
|
+
token.value = '-' + token.value;
|
|
178
|
+
if (token.loc) {
|
|
198
179
|
// ensure offsets are up to date
|
|
199
180
|
token.loc[0] = minus.loc[0];
|
|
200
181
|
}
|
|
@@ -221,7 +202,7 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
221
202
|
// operators.
|
|
222
203
|
for (const index of trimOps) {
|
|
223
204
|
const before = tokens[index - 1];
|
|
224
|
-
const after = tokens[index
|
|
205
|
+
const after = tokens[index + 1];
|
|
225
206
|
if (before && before.type === REF_RANGE && after && after.type === REF_RANGE) {
|
|
226
207
|
tokens[index].type = OPERATOR;
|
|
227
208
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const PERIOD = 46;
|
|
2
|
+
const COLON = 58;
|
|
3
|
+
|
|
4
|
+
export function advRangeOp (str, pos) {
|
|
5
|
+
const c0 = str.charCodeAt(pos);
|
|
6
|
+
if (c0 === PERIOD) {
|
|
7
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
8
|
+
if (c1 === COLON) {
|
|
9
|
+
return str.charCodeAt(pos + 2) === PERIOD ? 3 : 2;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
else if (c0 === COLON) {
|
|
13
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
14
|
+
return c1 === PERIOD ? 2 : 1;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// regular: [A-Za-z0-9_\u00a1-\uffff]
|
|
2
|
+
export function canEndRange (str, pos) {
|
|
3
|
+
const c = str.charCodeAt(pos);
|
|
4
|
+
return !(
|
|
5
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
6
|
+
(c >= 97 && c <= 122) || // a-z
|
|
7
|
+
(c >= 48 && c <= 57) || // 0-9
|
|
8
|
+
(c === 95) || // _
|
|
9
|
+
(c > 0xA0) // \u00a1-\uffff
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// partial: [A-Za-z0-9_($.]
|
|
14
|
+
export function canEndPartialRange (str, pos) {
|
|
15
|
+
const c = str.charCodeAt(pos);
|
|
16
|
+
return !(
|
|
17
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
18
|
+
(c >= 97 && c <= 122) || // a-z
|
|
19
|
+
(c >= 48 && c <= 57) || // 0-9
|
|
20
|
+
(c === 95) || // _
|
|
21
|
+
(c === 40) || // (
|
|
22
|
+
(c === 36) || // $
|
|
23
|
+
(c === 46) // .
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BOOLEAN } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
export function lexBoolean (str, pos) {
|
|
4
|
+
// "true" (case insensitive)
|
|
5
|
+
const c0 = str.charCodeAt(pos);
|
|
6
|
+
if (c0 === 84 || c0 === 116) {
|
|
7
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
8
|
+
if (c1 === 82 || c1 === 114) {
|
|
9
|
+
const c2 = str.charCodeAt(pos + 2);
|
|
10
|
+
if (c2 === 85 || c2 === 117) {
|
|
11
|
+
const c3 = str.charCodeAt(pos + 3);
|
|
12
|
+
if (c3 === 69 || c3 === 101) {
|
|
13
|
+
// non char to follow?
|
|
14
|
+
return { type: BOOLEAN, value: str.slice(pos, pos + 4) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// "false" (case insensitive)
|
|
20
|
+
if (c0 === 70 || c0 === 102) {
|
|
21
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
22
|
+
if (c1 === 65 || c1 === 97) {
|
|
23
|
+
const c2 = str.charCodeAt(pos + 2);
|
|
24
|
+
if (c2 === 76 || c2 === 108) {
|
|
25
|
+
const c3 = str.charCodeAt(pos + 3);
|
|
26
|
+
if (c3 === 83 || c3 === 115) {
|
|
27
|
+
const c4 = str.charCodeAt(pos + 4);
|
|
28
|
+
if (c4 === 69 || c4 === 101) {
|
|
29
|
+
// non char to follow?
|
|
30
|
+
return { type: BOOLEAN, value: str.slice(pos, pos + 5) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { CONTEXT, CONTEXT_QUOTE } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
const QUOT_SINGLE = 39; // '
|
|
4
|
+
const BR_OPEN = 91; // [
|
|
5
|
+
const BR_CLOSE = 93; // ]
|
|
6
|
+
const EXCL = 33; // !
|
|
7
|
+
|
|
8
|
+
// xlsx xml uses a variant of the syntax that has external references in
|
|
9
|
+
// bracets. Any of: [1]Sheet1!A1, '[1]Sheet one'!A1, [1]!named
|
|
10
|
+
export function lexContext (str, pos, options) {
|
|
11
|
+
const c0 = str.charCodeAt(pos);
|
|
12
|
+
let br1;
|
|
13
|
+
let br2;
|
|
14
|
+
// quoted context: '(?:''|[^'])*('|$)(?=!)
|
|
15
|
+
if (c0 === QUOT_SINGLE) {
|
|
16
|
+
const start = pos;
|
|
17
|
+
pos++;
|
|
18
|
+
while (pos < str.length) {
|
|
19
|
+
const c = str.charCodeAt(pos);
|
|
20
|
+
if (c === BR_OPEN) {
|
|
21
|
+
if (br1) { return; } // only 1 allowed
|
|
22
|
+
br1 = pos;
|
|
23
|
+
}
|
|
24
|
+
else if (c === BR_CLOSE) {
|
|
25
|
+
if (br2) { return; } // only 1 allowed
|
|
26
|
+
br2 = pos;
|
|
27
|
+
}
|
|
28
|
+
else if (c === QUOT_SINGLE) {
|
|
29
|
+
pos++;
|
|
30
|
+
if (str.charCodeAt(pos) !== QUOT_SINGLE) {
|
|
31
|
+
let valid = br1 == null && br2 == null;
|
|
32
|
+
if (options.xlsx && (br1 === start + 1) && (br2 === pos - 2)) {
|
|
33
|
+
valid = true;
|
|
34
|
+
}
|
|
35
|
+
if ((br1 >= start + 1) && (br2 < pos - 2) && (br2 > br1 + 1)) {
|
|
36
|
+
valid = true;
|
|
37
|
+
}
|
|
38
|
+
if (valid && str.charCodeAt(pos) === EXCL) {
|
|
39
|
+
return { type: CONTEXT_QUOTE, value: str.slice(start, pos) };
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
pos++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// unquoted context
|
|
48
|
+
else if (c0 !== EXCL) {
|
|
49
|
+
const start = pos;
|
|
50
|
+
while (pos < str.length) {
|
|
51
|
+
const c = str.charCodeAt(pos);
|
|
52
|
+
if (c === BR_OPEN) {
|
|
53
|
+
if (br1) { return; } // only 1 allowed
|
|
54
|
+
br1 = pos;
|
|
55
|
+
}
|
|
56
|
+
else if (c === BR_CLOSE) {
|
|
57
|
+
if (br2) { return; } // only 1 allowed
|
|
58
|
+
br2 = pos;
|
|
59
|
+
}
|
|
60
|
+
else if (c === EXCL) {
|
|
61
|
+
let valid = br1 == null && br2 == null;
|
|
62
|
+
if (options.xlsx && (br1 === start) && (br2 === pos - 1)) {
|
|
63
|
+
valid = true;
|
|
64
|
+
}
|
|
65
|
+
if ((br1 >= start) && (br2 < pos - 1) && (br2 > br1 + 1)) {
|
|
66
|
+
valid = true;
|
|
67
|
+
}
|
|
68
|
+
if (valid) {
|
|
69
|
+
return { type: CONTEXT, value: str.slice(start, pos) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (
|
|
73
|
+
(br1 == null || br2 != null) &&
|
|
74
|
+
// [0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]
|
|
75
|
+
!(
|
|
76
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
77
|
+
(c >= 97 && c <= 122) || // a-z
|
|
78
|
+
(c >= 48 && c <= 57) || // 0-9
|
|
79
|
+
(c === 46) || // .
|
|
80
|
+
(c === 95) || // _
|
|
81
|
+
(c === 161) || // ¡
|
|
82
|
+
(c === 164) || // ¤
|
|
83
|
+
(c === 167) || // §
|
|
84
|
+
(c === 168) || // ¨
|
|
85
|
+
(c === 170) || // ª
|
|
86
|
+
(c === 173) || // \u00ad
|
|
87
|
+
(c >= 175) // ¯-\uffff
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// 0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff
|
|
93
|
+
pos++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* eslint-disable no-mixed-operators */
|
|
2
|
+
import { ERROR } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
const re_ERROR = /#(?:NAME\?|FIELD!|CALC!|VALUE!|REF!|DIV\/0!|NULL!|NUM!|N\/A|GETTING_DATA\b|SPILL!|UNKNOWN!|SYNTAX\?|ERROR!|CONNECT!|BLOCKED!|EXTERNAL!)/iy;
|
|
5
|
+
const HASH = 35;
|
|
6
|
+
|
|
7
|
+
export function lexError (str, pos) {
|
|
8
|
+
if (str.charCodeAt(pos) === HASH) {
|
|
9
|
+
re_ERROR.lastIndex = pos;
|
|
10
|
+
const m = re_ERROR.exec(str);
|
|
11
|
+
if (m) {
|
|
12
|
+
return { type: ERROR, value: m[0] };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FUNCTION } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
const PAREN_OPEN = 40;
|
|
4
|
+
|
|
5
|
+
// [A-Za-z_]+[A-Za-z\d_.]*(?=\()
|
|
6
|
+
export function lexFunction (str, pos) {
|
|
7
|
+
const start = pos;
|
|
8
|
+
// starts with: a-zA-Z_
|
|
9
|
+
let c = str.charCodeAt(pos);
|
|
10
|
+
if (
|
|
11
|
+
(c < 65 || c > 90) && // A-Z
|
|
12
|
+
(c < 97 || c > 122) && // a-z
|
|
13
|
+
(c !== 95) // _
|
|
14
|
+
) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
pos++;
|
|
18
|
+
// has any number of: a-zA-Z0-9_.
|
|
19
|
+
do {
|
|
20
|
+
c = str.charCodeAt(pos);
|
|
21
|
+
if (
|
|
22
|
+
(c < 65 || c > 90) && // A-Z
|
|
23
|
+
(c < 97 || c > 122) && // a-z
|
|
24
|
+
(c < 48 || c > 57) && // 0-9
|
|
25
|
+
(c !== 95) && // _
|
|
26
|
+
(c !== 46) // .
|
|
27
|
+
) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
pos++;
|
|
31
|
+
} while (pos < str.length);
|
|
32
|
+
// followed by a (
|
|
33
|
+
if (str.charCodeAt(pos) === PAREN_OPEN) {
|
|
34
|
+
return { type: FUNCTION, value: str.slice(start, pos) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
import { REF_NAMED } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
// The advertized named ranges rules are a bit off from what Excel seems to do.
|
|
5
|
+
// In the "extended range" of chars, it looks like it allows most things above
|
|
6
|
+
// U+00B0 with the range between U+00A0-U+00AF rather random:
|
|
7
|
+
// /^[a-zA-Z\\_¡¤§¨ª\u00ad¯\u00b0-\uffff][a-zA-Z0-9\\_.?¡¤§¨ª\u00ad¯\u00b0-\uffff]{0,254}/
|
|
8
|
+
//
|
|
9
|
+
// I've simplified to allowing everything above U+00A1:
|
|
10
|
+
// /^[a-zA-Z\\_\u00a1-\uffff][a-zA-Z0-9\\_.?\u00a1-\uffff]{0,254}/
|
|
11
|
+
export function lexNamed (str, pos) {
|
|
12
|
+
const start = pos;
|
|
13
|
+
// starts with: [a-zA-Z\\_\u00a1-\uffff]
|
|
14
|
+
const s = str.charCodeAt(pos);
|
|
15
|
+
if (
|
|
16
|
+
(s >= 65 && s <= 90) || // A-Z
|
|
17
|
+
(s >= 97 && s <= 122) || // a-z
|
|
18
|
+
(s === 95) || // _
|
|
19
|
+
(s === 92) || // \
|
|
20
|
+
(s > 0xA0) // \u00a1-\uffff
|
|
21
|
+
) {
|
|
22
|
+
pos++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// has any number of: [a-zA-Z0-9\\_.?\u00a1-\uffff]
|
|
28
|
+
let c;
|
|
29
|
+
do {
|
|
30
|
+
c = str.charCodeAt(pos);
|
|
31
|
+
if (
|
|
32
|
+
(c >= 65 && c <= 90) || // A-Z
|
|
33
|
+
(c >= 97 && c <= 122) || // a-z
|
|
34
|
+
(c >= 48 && c <= 57) || // 0-9
|
|
35
|
+
(c === 95) || // _
|
|
36
|
+
(c === 92) || // \
|
|
37
|
+
(c === 46) || // .
|
|
38
|
+
(c === 63) || // ?
|
|
39
|
+
(c > 0xA0) // \u00a1-\uffff
|
|
40
|
+
) {
|
|
41
|
+
pos++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
} while (isFinite(c));
|
|
47
|
+
|
|
48
|
+
const len = pos - start;
|
|
49
|
+
if (len && len < 255) {
|
|
50
|
+
// names starting with \ must be at least 3 char long
|
|
51
|
+
if (s === 92 && len < 3) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// single characters R and C are forbidden as names
|
|
55
|
+
if (len === 1 && (s === 114 || s === 82 || s === 99 || s === 67)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
return { type: REF_NAMED, value: str.slice(start, pos) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { NUMBER } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
function advDigits (str, pos) {
|
|
4
|
+
const start = pos;
|
|
5
|
+
do {
|
|
6
|
+
const c = str.charCodeAt(pos);
|
|
7
|
+
if (c < 48 || c > 57) { // 0-9
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
pos++;
|
|
11
|
+
}
|
|
12
|
+
while (pos < str.length);
|
|
13
|
+
return pos - start;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// \d+(\.\d+)?(?:[eE][+-]?\d+)?
|
|
17
|
+
export function lexNumber (str, pos) {
|
|
18
|
+
const start = pos;
|
|
19
|
+
|
|
20
|
+
// integer
|
|
21
|
+
const lead = advDigits(str, pos);
|
|
22
|
+
if (!lead) { return; }
|
|
23
|
+
pos += lead;
|
|
24
|
+
|
|
25
|
+
// optional fraction part
|
|
26
|
+
const c0 = str.charCodeAt(pos);
|
|
27
|
+
if (c0 === 46) { // .
|
|
28
|
+
pos++;
|
|
29
|
+
const frac = advDigits(str, pos);
|
|
30
|
+
if (!frac) { return; }
|
|
31
|
+
pos += frac;
|
|
32
|
+
}
|
|
33
|
+
// optional exponent part
|
|
34
|
+
const c1 = str.charCodeAt(pos);
|
|
35
|
+
if (c1 === 69 || c1 === 101) { // E e
|
|
36
|
+
pos++;
|
|
37
|
+
const sign = str.charCodeAt(pos);
|
|
38
|
+
if (sign === 43 || sign === 45) { // + -
|
|
39
|
+
pos++;
|
|
40
|
+
}
|
|
41
|
+
const exp = advDigits(str, pos);
|
|
42
|
+
if (!exp) { return; }
|
|
43
|
+
pos += exp;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { type: NUMBER, value: str.slice(start, pos) };
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { OPERATOR } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
export function lexOperator (str, pos) {
|
|
4
|
+
const c0 = str.charCodeAt(pos);
|
|
5
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
6
|
+
if (
|
|
7
|
+
(c0 === 60 && c1 === 61) || // <=
|
|
8
|
+
(c0 === 62 && c1 === 61) || // >=
|
|
9
|
+
(c0 === 60 && c1 === 62) // <>
|
|
10
|
+
) {
|
|
11
|
+
return { type: OPERATOR, value: str.slice(pos, pos + 2) };
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
// { } ! # % &
|
|
15
|
+
c0 === 123 || c0 === 125 || c0 === 33 || c0 === 35 || c0 === 37 || c0 === 38 ||
|
|
16
|
+
// ( ) * + , -
|
|
17
|
+
c0 === 40 || c0 === 41 || c0 === 42 || c0 === 43 || c0 === 44 || c0 === 45 ||
|
|
18
|
+
// / : ; < = >
|
|
19
|
+
c0 === 47 || c0 === 58 || c0 === 59 || c0 === 60 || c0 === 61 || c0 === 62 ||
|
|
20
|
+
// @ ^
|
|
21
|
+
c0 === 64 || c0 === 94
|
|
22
|
+
) {
|
|
23
|
+
return { type: OPERATOR, value: str[pos] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* eslint-disable no-mixed-operators */
|
|
2
|
+
import { REF_RANGE, REF_BEAM, REF_TERNARY, MAX_COLS, MAX_ROWS } from '../constants.js';
|
|
3
|
+
import { advRangeOp } from './advRangeOp.js';
|
|
4
|
+
import { canEndRange, canEndPartialRange } from './canEndRange.js';
|
|
5
|
+
|
|
6
|
+
function advA1Col (str, pos) {
|
|
7
|
+
// [A-Z]{1,3}
|
|
8
|
+
const start = pos;
|
|
9
|
+
if (str.charCodeAt(pos) === 36) { // $
|
|
10
|
+
pos++;
|
|
11
|
+
}
|
|
12
|
+
const stop = pos + 3;
|
|
13
|
+
let col = 0;
|
|
14
|
+
do {
|
|
15
|
+
const c = str.charCodeAt(pos);
|
|
16
|
+
if (c >= 65 && c <= 90) { // A-Z
|
|
17
|
+
col = 26 * col + c - 64;
|
|
18
|
+
pos++;
|
|
19
|
+
}
|
|
20
|
+
else if (c >= 97 && c <= 122) { // a-z
|
|
21
|
+
col = 26 * col + c - 96;
|
|
22
|
+
pos++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
while (pos < stop && pos < str.length);
|
|
29
|
+
return (col && col <= MAX_COLS + 1) ? pos - start : 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function advA1Row (str, pos) {
|
|
33
|
+
// [1-9][0-9]{0,6}
|
|
34
|
+
const start = pos;
|
|
35
|
+
if (str.charCodeAt(pos) === 36) { // $
|
|
36
|
+
pos++;
|
|
37
|
+
}
|
|
38
|
+
const stop = pos + 7;
|
|
39
|
+
let row = 0;
|
|
40
|
+
let c = str.charCodeAt(pos);
|
|
41
|
+
if (c >= 49 && c <= 57) { // 1-9
|
|
42
|
+
row = row * 10 + c - 48;
|
|
43
|
+
pos++;
|
|
44
|
+
do {
|
|
45
|
+
c = str.charCodeAt(pos);
|
|
46
|
+
if (c >= 48 && c <= 57) { // 0-9
|
|
47
|
+
row = row * 10 + c - 48;
|
|
48
|
+
pos++;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
while (pos < stop && pos < str.length);
|
|
55
|
+
}
|
|
56
|
+
return (row && row <= MAX_ROWS + 1) ? pos - start : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function lexRangeA1 (str, pos, options) {
|
|
60
|
+
let p = pos;
|
|
61
|
+
const left = advA1Col(str, p);
|
|
62
|
+
let right = 0;
|
|
63
|
+
let bottom = 0;
|
|
64
|
+
if (left) {
|
|
65
|
+
// TLBR: could be A1:A1
|
|
66
|
+
// TL R: could be A1:A (if allowTernary)
|
|
67
|
+
// TLB : could be A1:1 (if allowTernary)
|
|
68
|
+
// LBR: could be A:A1 (if allowTernary)
|
|
69
|
+
// L R: could be A:A
|
|
70
|
+
p += left;
|
|
71
|
+
const top = advA1Row(str, p);
|
|
72
|
+
p += top;
|
|
73
|
+
const op = advRangeOp(str, p);
|
|
74
|
+
const preOp = p;
|
|
75
|
+
if (op) {
|
|
76
|
+
p += op;
|
|
77
|
+
right = advA1Col(str, p);
|
|
78
|
+
p += right;
|
|
79
|
+
bottom = advA1Row(str, p);
|
|
80
|
+
p += bottom;
|
|
81
|
+
if (top && bottom && right) {
|
|
82
|
+
if (canEndRange(str, p) && options.mergeRefs) {
|
|
83
|
+
return { type: REF_RANGE, value: str.slice(pos, p) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (!top && !bottom) {
|
|
87
|
+
if (canEndRange(str, p)) {
|
|
88
|
+
return { type: REF_BEAM, value: str.slice(pos, p) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (options.allowTernary && (bottom || right)) {
|
|
92
|
+
if (canEndPartialRange(str, p)) {
|
|
93
|
+
return { type: REF_TERNARY, value: str.slice(pos, p) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// LT : this is A1
|
|
98
|
+
if (top && canEndRange(str, preOp)) {
|
|
99
|
+
return { type: REF_RANGE, value: str.slice(pos, preOp) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// T B : could be 1:1
|
|
104
|
+
// T BR: could be 1:A1 (if allowTernary)
|
|
105
|
+
const top = advA1Row(str, p);
|
|
106
|
+
if (top) {
|
|
107
|
+
p += top;
|
|
108
|
+
const op = advRangeOp(str, p);
|
|
109
|
+
if (op) {
|
|
110
|
+
p += op;
|
|
111
|
+
right = advA1Col(str, p);
|
|
112
|
+
if (right) {
|
|
113
|
+
p += right;
|
|
114
|
+
}
|
|
115
|
+
bottom = advA1Row(str, p);
|
|
116
|
+
p += bottom;
|
|
117
|
+
if (right && bottom && options.allowTernary) {
|
|
118
|
+
if (canEndPartialRange(str, p)) {
|
|
119
|
+
return { type: REF_TERNARY, value: str.slice(pos, p) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!right && bottom) {
|
|
123
|
+
if (canEndRange(str, p)) {
|
|
124
|
+
return { type: REF_BEAM, value: str.slice(pos, p) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|