@borgar/fx 4.8.0 → 4.10.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/fx.d.ts +8 -0
- package/dist/fx.js +1 -1
- package/docs/API.md +18 -15
- package/lib/a1.js +58 -23
- package/lib/a1.spec.js +75 -13
- package/lib/addTokenMeta.js +1 -1
- package/lib/addTokenMeta.spec.js +12 -0
- package/lib/constants.js +1 -0
- package/lib/extraTypes.js +1 -0
- package/lib/fixRanges.js +3 -2
- package/lib/fixRanges.spec.js +3 -0
- package/lib/lexer.js +24 -2
- package/lib/lexer.spec.js +106 -0
- package/lib/lexerParts.js +18 -11
- package/lib/mergeRefTokens.js +9 -0
- package/lib/mergeRefTokens.spec.js +3 -0
- package/lib/parseRef.js +5 -3
- package/lib/parser.js +1 -0
- package/lib/parser.spec.js +1 -0
- package/lib/rc.js +28 -16
- package/lib/rc.spec.js +52 -13
- package/lib/sr.js +5 -3
- package/lib/sr.spec.js +50 -0
- package/lib/stringifyPrefix.js +3 -3
- package/lib/translate-toA1.spec.js +13 -0
- package/lib/translate-toRC.spec.js +13 -0
- package/lib/translate.js +6 -0
- package/package.json +1 -1
package/lib/lexer.js
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
STRING,
|
|
8
8
|
UNKNOWN,
|
|
9
9
|
WHITESPACE,
|
|
10
|
-
FUNCTION
|
|
10
|
+
FUNCTION,
|
|
11
|
+
OPERATOR_TRIM,
|
|
12
|
+
REF_RANGE
|
|
11
13
|
} from './constants.js';
|
|
12
14
|
import { lexers } from './lexerParts.js';
|
|
13
15
|
import { mergeRefTokens } from './mergeRefTokens.js';
|
|
@@ -75,6 +77,7 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
75
77
|
let pos = 0;
|
|
76
78
|
let letOrLambda = 0;
|
|
77
79
|
let unknownRC = 0;
|
|
80
|
+
const trimOps = [];
|
|
78
81
|
|
|
79
82
|
let tail0 = null; // last non-whitespace token
|
|
80
83
|
let tail1 = null; // penultimate non-whitespace token
|
|
@@ -95,6 +98,10 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
else {
|
|
101
|
+
if (token.type === OPERATOR_TRIM) {
|
|
102
|
+
trimOps.push(tokens.length);
|
|
103
|
+
token.type = UNKNOWN;
|
|
104
|
+
}
|
|
98
105
|
// push token as normally
|
|
99
106
|
tokens.push(token);
|
|
100
107
|
lastToken = token;
|
|
@@ -105,7 +112,7 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
105
112
|
}
|
|
106
113
|
};
|
|
107
114
|
|
|
108
|
-
if (
|
|
115
|
+
if (fx.at(0) === '=') {
|
|
109
116
|
const token = {
|
|
110
117
|
type: FX_PREFIX,
|
|
111
118
|
value: '=',
|
|
@@ -208,6 +215,21 @@ export function getTokens (fx, tokenHandlers, options = {}) {
|
|
|
208
215
|
fixRCNames(tokens);
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// Any OPERATOR_TRIM tokens have been indexed already, they now need to be
|
|
219
|
+
// either turned into OPERATORs or UNKNOWNs. Trim operators are only allowed
|
|
220
|
+
// between two REF_RANGE tokens as they are not valid in expressions as full
|
|
221
|
+
// operators.
|
|
222
|
+
for (const index of trimOps) {
|
|
223
|
+
const before = tokens[index - 1];
|
|
224
|
+
const after = tokens[index - 1];
|
|
225
|
+
if (before && before.type === REF_RANGE && after && after.type === REF_RANGE) {
|
|
226
|
+
tokens[index].type = OPERATOR;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
tokens[index].type = UNKNOWN;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
211
233
|
if (mergeRefs) {
|
|
212
234
|
return mergeRefTokens(tokens);
|
|
213
235
|
}
|
package/lib/lexer.spec.js
CHANGED
|
@@ -969,6 +969,21 @@ test('tokenize A1 style references', t => {
|
|
|
969
969
|
{ type: REF_RANGE, value: 'A10:A20' }
|
|
970
970
|
]);
|
|
971
971
|
|
|
972
|
+
t.isTokens('=A10.:A20', [
|
|
973
|
+
{ type: FX_PREFIX, value: '=' },
|
|
974
|
+
{ type: REF_RANGE, value: 'A10.:A20' }
|
|
975
|
+
]);
|
|
976
|
+
|
|
977
|
+
t.isTokens('=A10:.A20', [
|
|
978
|
+
{ type: FX_PREFIX, value: '=' },
|
|
979
|
+
{ type: REF_RANGE, value: 'A10:.A20' }
|
|
980
|
+
]);
|
|
981
|
+
|
|
982
|
+
t.isTokens('=A10.:.A20', [
|
|
983
|
+
{ type: FX_PREFIX, value: '=' },
|
|
984
|
+
{ type: REF_RANGE, value: 'A10.:.A20' }
|
|
985
|
+
]);
|
|
986
|
+
|
|
972
987
|
t.isTokens('=A10:E20', [
|
|
973
988
|
{ type: FX_PREFIX, value: '=' },
|
|
974
989
|
{ type: REF_RANGE, value: 'A10:E20' }
|
|
@@ -984,6 +999,21 @@ test('tokenize A1 style references', t => {
|
|
|
984
999
|
{ type: REF_BEAM, value: '5:5' }
|
|
985
1000
|
]);
|
|
986
1001
|
|
|
1002
|
+
t.isTokens('=5.:5', [
|
|
1003
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1004
|
+
{ type: REF_BEAM, value: '5.:5' }
|
|
1005
|
+
]);
|
|
1006
|
+
|
|
1007
|
+
t.isTokens('=5:.5', [
|
|
1008
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1009
|
+
{ type: REF_BEAM, value: '5:.5' }
|
|
1010
|
+
]);
|
|
1011
|
+
|
|
1012
|
+
t.isTokens('=5.:.5', [
|
|
1013
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1014
|
+
{ type: REF_BEAM, value: '5.:.5' }
|
|
1015
|
+
]);
|
|
1016
|
+
|
|
987
1017
|
t.isTokens('=15:15', [
|
|
988
1018
|
{ type: FX_PREFIX, value: '=' },
|
|
989
1019
|
{ type: REF_BEAM, value: '15:15' }
|
|
@@ -994,6 +1024,21 @@ test('tokenize A1 style references', t => {
|
|
|
994
1024
|
{ type: REF_BEAM, value: 'H:H' }
|
|
995
1025
|
]);
|
|
996
1026
|
|
|
1027
|
+
t.isTokens('=H.:H', [
|
|
1028
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1029
|
+
{ type: REF_BEAM, value: 'H.:H' }
|
|
1030
|
+
]);
|
|
1031
|
+
|
|
1032
|
+
t.isTokens('=H:.H', [
|
|
1033
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1034
|
+
{ type: REF_BEAM, value: 'H:.H' }
|
|
1035
|
+
]);
|
|
1036
|
+
|
|
1037
|
+
t.isTokens('=H.:.H', [
|
|
1038
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1039
|
+
{ type: REF_BEAM, value: 'H.:.H' }
|
|
1040
|
+
]);
|
|
1041
|
+
|
|
997
1042
|
t.isTokens('=AA:JJ', [
|
|
998
1043
|
{ type: FX_PREFIX, value: '=' },
|
|
999
1044
|
{ type: REF_BEAM, value: 'AA:JJ' }
|
|
@@ -1222,6 +1267,15 @@ test('tokenize A1 style references', t => {
|
|
|
1222
1267
|
{ type: REF_BEAM, value: 'B:B' }
|
|
1223
1268
|
], { mergeRefs: false });
|
|
1224
1269
|
|
|
1270
|
+
t.isTokens('=Sheet1!A.:.A:B.:.B', [
|
|
1271
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1272
|
+
{ type: CONTEXT, value: 'Sheet1' },
|
|
1273
|
+
{ type: OPERATOR, value: '!' },
|
|
1274
|
+
{ type: REF_BEAM, value: 'A.:.A' },
|
|
1275
|
+
{ type: OPERATOR, value: ':' },
|
|
1276
|
+
{ type: REF_BEAM, value: 'B.:.B' }
|
|
1277
|
+
], { mergeRefs: false });
|
|
1278
|
+
|
|
1225
1279
|
t.isTokens('=Sheet1!#REF!:A1', [
|
|
1226
1280
|
{ type: FX_PREFIX, value: '=' },
|
|
1227
1281
|
{ type: CONTEXT, value: 'Sheet1' },
|
|
@@ -1832,3 +1886,55 @@ test('tokenize r and c as names within LET and LAMBDA calls', t => {
|
|
|
1832
1886
|
|
|
1833
1887
|
t.end();
|
|
1834
1888
|
});
|
|
1889
|
+
|
|
1890
|
+
test('trim operators are not valid outside literal ranges', t => {
|
|
1891
|
+
// when not merging refs, trim ops are allowed between ranges
|
|
1892
|
+
t.isTokens('=Sheet1!A1.:.B2', [
|
|
1893
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1894
|
+
{ type: CONTEXT, value: 'Sheet1' },
|
|
1895
|
+
{ type: OPERATOR, value: '!' },
|
|
1896
|
+
{ type: REF_RANGE, value: 'A1' },
|
|
1897
|
+
{ type: OPERATOR, value: '.:.' },
|
|
1898
|
+
{ type: REF_RANGE, value: 'B2' }
|
|
1899
|
+
], { mergeRefs: false });
|
|
1900
|
+
t.isTokens('A1:.B2', [
|
|
1901
|
+
{ type: REF_RANGE, value: 'A1' },
|
|
1902
|
+
{ type: OPERATOR, value: ':.' },
|
|
1903
|
+
{ type: REF_RANGE, value: 'B2' }
|
|
1904
|
+
], { mergeRefs: false });
|
|
1905
|
+
t.isTokens('A1.:B2', [
|
|
1906
|
+
{ type: REF_RANGE, value: 'A1' },
|
|
1907
|
+
{ type: OPERATOR, value: '.:' },
|
|
1908
|
+
{ type: REF_RANGE, value: 'B2' }
|
|
1909
|
+
], { mergeRefs: false });
|
|
1910
|
+
|
|
1911
|
+
t.isTokens('=Sheet1!A.:.A.:.B.:.B', [
|
|
1912
|
+
{ type: FX_PREFIX, value: '=' },
|
|
1913
|
+
{ type: REF_BEAM, value: 'Sheet1!A.:.A' },
|
|
1914
|
+
{ type: UNKNOWN, value: '.:.' },
|
|
1915
|
+
{ type: REF_BEAM, value: 'B.:.B' }
|
|
1916
|
+
]);
|
|
1917
|
+
|
|
1918
|
+
t.isTokens('=name1.:.name2', [
|
|
1919
|
+
{ type: 'fx_prefix', value: '=' },
|
|
1920
|
+
{ type: REF_NAMED, value: 'name1.' },
|
|
1921
|
+
{ type: 'unknown', value: ':.name2' }
|
|
1922
|
+
]);
|
|
1923
|
+
|
|
1924
|
+
t.isTokens('=OFFSET(A1,1,1).:.INDIRECT("A1")', [
|
|
1925
|
+
{ type: 'fx_prefix', value: '=' },
|
|
1926
|
+
{ type: 'func', value: 'OFFSET' },
|
|
1927
|
+
{ type: 'operator', value: '(' },
|
|
1928
|
+
{ type: 'range', value: 'A1' },
|
|
1929
|
+
{ type: 'operator', value: ',' },
|
|
1930
|
+
{ type: 'number', value: '1' },
|
|
1931
|
+
{ type: 'operator', value: ',' },
|
|
1932
|
+
{ type: 'number', value: '1' },
|
|
1933
|
+
{ type: 'operator', value: ')' },
|
|
1934
|
+
{ type: 'unknown', value: '.:.INDIRECT' },
|
|
1935
|
+
{ type: 'operator', value: '(' },
|
|
1936
|
+
{ type: 'string', value: '"A1"' },
|
|
1937
|
+
{ type: 'operator', value: ')' }
|
|
1938
|
+
]);
|
|
1939
|
+
t.end();
|
|
1940
|
+
});
|
package/lib/lexerParts.js
CHANGED
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
REF_TERNARY,
|
|
16
16
|
REF_STRUCT,
|
|
17
17
|
MAX_COLS,
|
|
18
|
-
MAX_ROWS
|
|
18
|
+
MAX_ROWS,
|
|
19
|
+
OPERATOR_TRIM
|
|
19
20
|
} from './constants.js';
|
|
20
21
|
import { fromCol } from './a1.js';
|
|
21
22
|
import { parseSRange } from './sr.js';
|
|
@@ -30,21 +31,23 @@ const re_STRING = /^"(?:""|[^"])*("|$)/;
|
|
|
30
31
|
const re_NUMBER = /^(?:\d+(\.\d+)?(?:[eE][+-]?\d+)?|\d+)/;
|
|
31
32
|
const re_CONTEXT = /^(?!!)(\[(?:[^\]])+\])?([0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]+)?(?=!)/;
|
|
32
33
|
const re_CONTEXT_QUOTE = /^'(?:''|[^'])*('|$)(?=!)/;
|
|
34
|
+
const re_RANGE_TRIM = /^(\.:\.|\.:|:\.)/;
|
|
33
35
|
|
|
34
36
|
const rngPart = '\\$?[A-Z]{1,3}\\$?[1-9][0-9]{0,6}';
|
|
35
37
|
const colPart = '\\$?[A-Z]{1,3}';
|
|
36
38
|
const rowPart = '\\$?[1-9][0-9]{0,6}';
|
|
39
|
+
const rangeOp = '\\.?:\\.?';
|
|
37
40
|
const nextNotChar = '(?![a-z0-9_\\u00a1-\\uffff])';
|
|
38
|
-
const re_A1COL = new RegExp(`^${colPart}
|
|
39
|
-
const re_A1ROW = new RegExp(`^${rowPart}
|
|
41
|
+
const re_A1COL = new RegExp(`^${colPart}${rangeOp}${colPart}${nextNotChar}`, 'i');
|
|
42
|
+
const re_A1ROW = new RegExp(`^${rowPart}${rangeOp}${rowPart}${nextNotChar}`, 'i');
|
|
40
43
|
const re_A1RANGE = new RegExp(`^${rngPart}${nextNotChar}`, 'i');
|
|
41
|
-
const re_A1PARTIAL = new RegExp(`^((${colPart}|${rowPart})
|
|
44
|
+
const re_A1PARTIAL = new RegExp(`^((${colPart}|${rowPart})${rangeOp}${rngPart}|${rngPart}${rangeOp}(${colPart}|${rowPart}))(?![\\w($.])`, 'i');
|
|
42
45
|
const rPart = '(?:R(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,6})?)';
|
|
43
46
|
const cPart = '(?:C(?:\\[[+-]?\\d+\\]|[1-9][0-9]{0,4})?)';
|
|
44
|
-
const re_RCCOL = new RegExp(`^${cPart}(
|
|
45
|
-
const re_RCROW = new RegExp(`^${rPart}(
|
|
47
|
+
const re_RCCOL = new RegExp(`^${cPart}(${rangeOp}${cPart})?${nextNotChar}`, 'i');
|
|
48
|
+
const re_RCROW = new RegExp(`^${rPart}(${rangeOp}${rPart})?${nextNotChar}`, 'i');
|
|
46
49
|
const re_RCRANGE = new RegExp(`^(?:(?=[RC])${rPart}${cPart})${nextNotChar}`, 'i');
|
|
47
|
-
const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(
|
|
50
|
+
const re_RCPARTIAL = new RegExp(`^(${rPart}${cPart}(${rangeOp}${cPart}|${rangeOp}${rPart})(?![[\\d])|(${rPart}|${cPart})(${rangeOp}${rPart}${cPart}))${nextNotChar}`, 'i');
|
|
48
51
|
|
|
49
52
|
// The advertized named ranges rules are a bit off from what Excel seems to do:
|
|
50
53
|
// in the "extended range" of chars, it looks like it allows most things above
|
|
@@ -169,6 +172,7 @@ function lexRange (str, options) {
|
|
|
169
172
|
}
|
|
170
173
|
if (t) {
|
|
171
174
|
reA1Nums.lastIndex = 0;
|
|
175
|
+
// XXX: can probably optimize this as we know letters can only be 3 at max
|
|
172
176
|
while ((m = reA1Nums.exec(t.value)) !== null) {
|
|
173
177
|
if (/^\d/.test(m[1])) { // row
|
|
174
178
|
if ((parseInt(m[1], 10) - 1) > MAX_ROWS) {
|
|
@@ -191,14 +195,17 @@ function lexRefOp (s, opts) {
|
|
|
191
195
|
? { type: OPERATOR, value: s[0] }
|
|
192
196
|
: null;
|
|
193
197
|
}
|
|
194
|
-
// in A1 mode we allow !
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
:
|
|
198
|
+
// in A1 mode we allow [ '!', ':', '.:', ':.', '.:.']
|
|
199
|
+
const m = /^(!|\.?:\.?)/.exec(s);
|
|
200
|
+
if (m) {
|
|
201
|
+
return { type: OPERATOR, value: m[1] };
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
export const lexers = [
|
|
201
207
|
makeHandler(ERROR, re_ERROR),
|
|
208
|
+
makeHandler(OPERATOR_TRIM, re_RANGE_TRIM),
|
|
202
209
|
makeHandler(OPERATOR, re_OPERATOR),
|
|
203
210
|
makeHandler(FUNCTION, re_FUNCTION),
|
|
204
211
|
makeHandler(BOOLEAN, re_BOOLEAN),
|
package/lib/mergeRefTokens.js
CHANGED
|
@@ -4,14 +4,23 @@ const END = '$';
|
|
|
4
4
|
|
|
5
5
|
const validRunsMerge = [
|
|
6
6
|
[ REF_RANGE, ':', REF_RANGE ],
|
|
7
|
+
[ REF_RANGE, '.:', REF_RANGE ],
|
|
8
|
+
[ REF_RANGE, ':.', REF_RANGE ],
|
|
9
|
+
[ REF_RANGE, '.:.', REF_RANGE ],
|
|
7
10
|
[ REF_RANGE ],
|
|
8
11
|
[ REF_BEAM ],
|
|
9
12
|
[ REF_TERNARY ],
|
|
10
13
|
[ CONTEXT, '!', REF_RANGE, ':', REF_RANGE ],
|
|
14
|
+
[ CONTEXT, '!', REF_RANGE, '.:', REF_RANGE ],
|
|
15
|
+
[ CONTEXT, '!', REF_RANGE, ':.', REF_RANGE ],
|
|
16
|
+
[ CONTEXT, '!', REF_RANGE, '.:.', REF_RANGE ],
|
|
11
17
|
[ CONTEXT, '!', REF_RANGE ],
|
|
12
18
|
[ CONTEXT, '!', REF_BEAM ],
|
|
13
19
|
[ CONTEXT, '!', REF_TERNARY ],
|
|
14
20
|
[ CONTEXT_QUOTE, '!', REF_RANGE, ':', REF_RANGE ],
|
|
21
|
+
[ CONTEXT_QUOTE, '!', REF_RANGE, '.:', REF_RANGE ],
|
|
22
|
+
[ CONTEXT_QUOTE, '!', REF_RANGE, ':.', REF_RANGE ],
|
|
23
|
+
[ CONTEXT_QUOTE, '!', REF_RANGE, '.:.', REF_RANGE ],
|
|
15
24
|
[ CONTEXT_QUOTE, '!', REF_RANGE ],
|
|
16
25
|
[ CONTEXT_QUOTE, '!', REF_BEAM ],
|
|
17
26
|
[ CONTEXT_QUOTE, '!', REF_TERNARY ],
|
|
@@ -95,6 +95,9 @@ test('mergeRefTokens cases', t => {
|
|
|
95
95
|
t.deepEqual(tokenize('[WB]Sheet1!A1:A', opts), [
|
|
96
96
|
{ type: REF_TERNARY, value: '[WB]Sheet1!A1:A' }
|
|
97
97
|
]);
|
|
98
|
+
t.deepEqual(tokenize('[WB]Sheet1!A1.:.C3', opts), [
|
|
99
|
+
{ type: REF_RANGE, value: '[WB]Sheet1!A1.:.C3' }
|
|
100
|
+
]);
|
|
98
101
|
|
|
99
102
|
t.deepEqual(tokenize('foo', opts), [
|
|
100
103
|
{ type: REF_NAMED, value: 'foo' }
|
package/lib/parseRef.js
CHANGED
|
@@ -71,7 +71,7 @@ function splitContextXlsx (contextString) {
|
|
|
71
71
|
|
|
72
72
|
const unquote = d => d.slice(1, -1).replace(/''/g, "'");
|
|
73
73
|
|
|
74
|
-
const pRangeOp = t => t && t.value === ':' && {};
|
|
74
|
+
const pRangeOp = t => t && (t.value === ':' || t.value === '.:' || t.value === ':.' || t.value === '.:.') && { operator: t.value };
|
|
75
75
|
const pRange = t => t && t.type === REF_RANGE && { r0: t.value };
|
|
76
76
|
const pPartial = t => t && t.type === REF_TERNARY && { r0: t.value };
|
|
77
77
|
const pRange2 = t => t && t.type === REF_RANGE && { r1: t.value };
|
|
@@ -125,13 +125,15 @@ export function parseRef (ref, opts) {
|
|
|
125
125
|
sheetName: '',
|
|
126
126
|
r0: '',
|
|
127
127
|
r1: '',
|
|
128
|
-
name: ''
|
|
128
|
+
name: '',
|
|
129
|
+
operator: ''
|
|
129
130
|
}
|
|
130
131
|
: {
|
|
131
132
|
context: [],
|
|
132
133
|
r0: '',
|
|
133
134
|
r1: '',
|
|
134
|
-
name: ''
|
|
135
|
+
name: '',
|
|
136
|
+
operator: ''
|
|
135
137
|
};
|
|
136
138
|
// discard the "="-prefix if it is there
|
|
137
139
|
if (tokens.length && tokens[0].type === FX_PREFIX) {
|
package/lib/parser.js
CHANGED
package/lib/parser.spec.js
CHANGED
|
@@ -70,6 +70,7 @@ test('parse ranges', t => {
|
|
|
70
70
|
t.isParsed('1:2', { type: 'ReferenceIdentifier', value: '1:2', kind: 'beam' });
|
|
71
71
|
t.isParsed('A1:2', { type: 'ReferenceIdentifier', value: 'A1:2', kind: 'range' });
|
|
72
72
|
t.isParsed('1:A2', { type: 'ReferenceIdentifier', value: '1:A2', kind: 'range' });
|
|
73
|
+
t.isParsed('A1.:.B2', { type: 'ReferenceIdentifier', value: 'A1.:.B2', kind: 'range' });
|
|
73
74
|
t.isParsed('Sheet!A1', { type: 'ReferenceIdentifier', value: 'Sheet!A1', kind: 'range' });
|
|
74
75
|
t.isParsed('[Workbook]Sheet!A1', { type: 'ReferenceIdentifier', value: '[Workbook]Sheet!A1', kind: 'range' });
|
|
75
76
|
t.isParsed('\'Sheet\'!A1', { type: 'ReferenceIdentifier', value: '\'Sheet\'!A1', kind: 'range' });
|
package/lib/rc.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
** - R[1]C1:R[2]C2 will also work, but
|
|
7
7
|
** - R[1]C[1]:R2C2 doesn't have a direct rectangle represention without context.
|
|
8
8
|
*/
|
|
9
|
+
import { rangeOperator, trimDirection } from './a1.js';
|
|
9
10
|
import { MAX_ROWS, MAX_COLS } from './constants.js';
|
|
10
11
|
import { parseRef } from './parseRef.js';
|
|
11
12
|
import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
|
|
@@ -35,6 +36,8 @@ export function toR1C1 (range) {
|
|
|
35
36
|
const nullC0 = c0 == null;
|
|
36
37
|
let nullR1 = r1 == null;
|
|
37
38
|
let nullC1 = c1 == null;
|
|
39
|
+
const op = rangeOperator(range.trim);
|
|
40
|
+
const hasTrim = !!range.trim;
|
|
38
41
|
r0 = clamp($r0 ? 0 : -MAX_ROWS, r0 | 0, MAX_ROWS);
|
|
39
42
|
c0 = clamp($c0 ? 0 : -MAX_COLS, c0 | 0, MAX_COLS);
|
|
40
43
|
if (!nullR0 && nullR1 && !nullC0 && nullC1) {
|
|
@@ -52,14 +55,14 @@ export function toR1C1 (range) {
|
|
|
52
55
|
if ((allRows && !nullC0 && !nullC1) || (nullR0 && nullR1)) {
|
|
53
56
|
const a = toCoord(c0, $c0);
|
|
54
57
|
const b = toCoord(c1, $c1);
|
|
55
|
-
return 'C' + (a === b ? a : a + '
|
|
58
|
+
return 'C' + (a === b && !hasTrim ? a : a + op + 'C' + b);
|
|
56
59
|
}
|
|
57
60
|
// R:R
|
|
58
61
|
const allCols = c0 === 0 && c1 >= MAX_COLS;
|
|
59
62
|
if ((allCols && !nullR0 && !nullR1) || (nullC0 && nullC1)) {
|
|
60
63
|
const a = toCoord(r0, $r0);
|
|
61
64
|
const b = toCoord(r1, $r1);
|
|
62
|
-
return 'R' + (a === b ? a : a + '
|
|
65
|
+
return 'R' + (a === b && !hasTrim ? a : a + op + 'R' + b);
|
|
63
66
|
}
|
|
64
67
|
const s_r0 = toCoord(r0, $r0);
|
|
65
68
|
const s_r1 = toCoord(r1, $r1);
|
|
@@ -70,14 +73,14 @@ export function toR1C1 (range) {
|
|
|
70
73
|
return (
|
|
71
74
|
(nullR0 ? '' : 'R' + s_r0) +
|
|
72
75
|
(nullC0 ? '' : 'C' + s_c0) +
|
|
73
|
-
|
|
76
|
+
op +
|
|
74
77
|
(nullR1 ? '' : 'R' + s_r1) +
|
|
75
78
|
(nullC1 ? '' : 'C' + s_c1)
|
|
76
79
|
);
|
|
77
80
|
}
|
|
78
81
|
// RC:RC
|
|
79
82
|
if (s_r0 !== s_r1 || s_c0 !== s_c1) {
|
|
80
|
-
return 'R' + s_r0 + 'C' + s_c0 + '
|
|
83
|
+
return 'R' + s_r0 + 'C' + s_c0 + op + 'R' + s_r1 + 'C' + s_c1;
|
|
81
84
|
}
|
|
82
85
|
// RC
|
|
83
86
|
return 'R' + s_r0 + 'C' + s_c0;
|
|
@@ -140,8 +143,12 @@ function parseR1C1Part (ref) {
|
|
|
140
143
|
*/
|
|
141
144
|
export function fromR1C1 (rangeString) {
|
|
142
145
|
let final = null;
|
|
143
|
-
const [ part1, part2 ] = rangeString.split(
|
|
146
|
+
const [ part1, op, part2, overflow ] = rangeString.split(/(\.?:\.?)/);
|
|
147
|
+
if (overflow) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
144
150
|
const range = parseR1C1Part(part1);
|
|
151
|
+
const trim = trimDirection(!!op && op.at(0) === '.', !!op && op.at(-1) === '.');
|
|
145
152
|
if (range) {
|
|
146
153
|
const [ r0, c0, $r0, $c0 ] = range;
|
|
147
154
|
if (part2) {
|
|
@@ -209,7 +216,7 @@ export function fromR1C1 (rangeString) {
|
|
|
209
216
|
}
|
|
210
217
|
// range only - no second part
|
|
211
218
|
else if (r0 != null && c0 == null) {
|
|
212
|
-
|
|
219
|
+
final = {
|
|
213
220
|
r0: r0,
|
|
214
221
|
c0: null,
|
|
215
222
|
r1: r0,
|
|
@@ -221,7 +228,7 @@ export function fromR1C1 (rangeString) {
|
|
|
221
228
|
};
|
|
222
229
|
}
|
|
223
230
|
else if (r0 == null && c0 != null) {
|
|
224
|
-
|
|
231
|
+
final = {
|
|
225
232
|
r0: null,
|
|
226
233
|
c0: c0,
|
|
227
234
|
r1: null,
|
|
@@ -233,7 +240,7 @@ export function fromR1C1 (rangeString) {
|
|
|
233
240
|
};
|
|
234
241
|
}
|
|
235
242
|
else {
|
|
236
|
-
|
|
243
|
+
final = {
|
|
237
244
|
r0: r0 || 0,
|
|
238
245
|
c0: c0 || 0,
|
|
239
246
|
r1: r0 || 0,
|
|
@@ -245,6 +252,9 @@ export function fromR1C1 (rangeString) {
|
|
|
245
252
|
};
|
|
246
253
|
}
|
|
247
254
|
}
|
|
255
|
+
if (final && trim) {
|
|
256
|
+
final.trim = trim;
|
|
257
|
+
}
|
|
248
258
|
return final;
|
|
249
259
|
}
|
|
250
260
|
|
|
@@ -279,17 +289,19 @@ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = fal
|
|
|
279
289
|
const d = parseRef(refString, { allowNamed, allowTernary, xlsx, r1c1: true });
|
|
280
290
|
if (d && (d.r0 || d.name)) {
|
|
281
291
|
const range = d.r1
|
|
282
|
-
? fromR1C1(d.r0 +
|
|
292
|
+
? fromR1C1(d.r0 + d.operator + d.r1)
|
|
283
293
|
: fromR1C1(d.r0);
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return d;
|
|
294
|
+
if (range) {
|
|
295
|
+
return xlsx
|
|
296
|
+
? { workbookName: d.workbookName, sheetName: d.sheetName, range }
|
|
297
|
+
: { context: d.context, range };
|
|
289
298
|
}
|
|
290
|
-
|
|
291
|
-
return
|
|
299
|
+
if (d.name) {
|
|
300
|
+
return xlsx
|
|
301
|
+
? { workbookName: d.workbookName, sheetName: d.sheetName, name: d.name }
|
|
302
|
+
: { context: d.context, name: d.name };
|
|
292
303
|
}
|
|
304
|
+
return null;
|
|
293
305
|
}
|
|
294
306
|
return null;
|
|
295
307
|
}
|
package/lib/rc.spec.js
CHANGED
|
@@ -6,19 +6,8 @@ import { parseR1C1Ref, stringifyR1C1Ref, toR1C1 } from './rc.js';
|
|
|
6
6
|
Test.prototype.isRCEqual = function isTokens (expr, expect, opts) {
|
|
7
7
|
if (expect) {
|
|
8
8
|
expect = (opts?.xlsx)
|
|
9
|
-
? {
|
|
10
|
-
|
|
11
|
-
sheetName: '',
|
|
12
|
-
name: '',
|
|
13
|
-
range: null,
|
|
14
|
-
...expect
|
|
15
|
-
}
|
|
16
|
-
: {
|
|
17
|
-
context: [],
|
|
18
|
-
name: '',
|
|
19
|
-
range: null,
|
|
20
|
-
...expect
|
|
21
|
-
};
|
|
9
|
+
? { workbookName: '', sheetName: '', ...expect }
|
|
10
|
+
: { context: [], ...expect };
|
|
22
11
|
if (expect.range && typeof expect.range === 'object') {
|
|
23
12
|
// mix in some defaults so we don't have to write things out in full
|
|
24
13
|
expect.range = {
|
|
@@ -261,6 +250,43 @@ test('parse R1C1 ranges in XLSX mode', t => {
|
|
|
261
250
|
t.end();
|
|
262
251
|
});
|
|
263
252
|
|
|
253
|
+
test('R1C1 trimmed ranges', t => {
|
|
254
|
+
const locks = { $r0: true, $r1: true, $c0: true, $c1: true };
|
|
255
|
+
const opts = [ {}, { xlsx: true } ];
|
|
256
|
+
for (const opt of opts) {
|
|
257
|
+
t.isRCEqual('R[1]C[1]:R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2 } }, opt);
|
|
258
|
+
t.isRCEqual('R[1]C[1].:R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'head' } }, opt);
|
|
259
|
+
t.isRCEqual('R[1]C[1]:.R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'tail' } }, opt);
|
|
260
|
+
t.isRCEqual('R[1]C[1].:.R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'both' } }, opt);
|
|
261
|
+
|
|
262
|
+
t.isRCEqual('R2C2:R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, ...locks } }, opt);
|
|
263
|
+
t.isRCEqual('R2C2.:R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'head', ...locks } }, opt);
|
|
264
|
+
t.isRCEqual('R2C2:.R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'tail', ...locks } }, opt);
|
|
265
|
+
t.isRCEqual('R2C2.:.R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'both', ...locks } }, opt);
|
|
266
|
+
|
|
267
|
+
t.isRCEqual('C[1]:C[2]', { range: { c0: 1, c1: 2 } }, opt);
|
|
268
|
+
t.isRCEqual('C[1].:C[2]', { range: { c0: 1, c1: 2, trim: 'head' } }, opt);
|
|
269
|
+
t.isRCEqual('C[1]:.C[2]', { range: { c0: 1, c1: 2, trim: 'tail' } }, opt);
|
|
270
|
+
t.isRCEqual('C[1].:.C[2]', { range: { c0: 1, c1: 2, trim: 'both' } }, opt);
|
|
271
|
+
|
|
272
|
+
t.isRCEqual('R[10]:R[10]', { range: { r0: 10, r1: 10 } }, opt);
|
|
273
|
+
t.isRCEqual('R[10].:R[10]', { range: { r0: 10, r1: 10, trim: 'head' } }, opt);
|
|
274
|
+
t.isRCEqual('R[10]:.R[10]', { range: { r0: 10, r1: 10, trim: 'tail' } }, opt);
|
|
275
|
+
t.isRCEqual('R[10].:.R[10]', { range: { r0: 10, r1: 10, trim: 'both' } }, opt);
|
|
276
|
+
|
|
277
|
+
t.isRCEqual('R[2]C[2]:R[4]', null, { ...opt });
|
|
278
|
+
t.isRCEqual('R[2]C[2]:C[4]', null, { ...opt });
|
|
279
|
+
t.isRCEqual('R[2]C[2].:.R[4]', null, { ...opt });
|
|
280
|
+
t.isRCEqual('R[2]C[2].:.C[4]', null, { ...opt });
|
|
281
|
+
|
|
282
|
+
t.isRCEqual('R[2]C[2]:R[4]', { range: { r0: 2, r1: 4, c0: 2 } }, { allowTernary: true, ...opt });
|
|
283
|
+
t.isRCEqual('R[2]C[2]:C[4]', { range: { r0: 2, c0: 2, c1: 4 } }, { allowTernary: true, ...opt });
|
|
284
|
+
t.isRCEqual('R[2]C[2].:.R[4]', { range: { r0: 2, r1: 4, c0: 2, trim: 'both' } }, { allowTernary: true, ...opt });
|
|
285
|
+
t.isRCEqual('R[2]C[2].:.C[4]', { range: { r0: 2, c0: 2, c1: 4, trim: 'both' } }, { allowTernary: true, ...opt });
|
|
286
|
+
}
|
|
287
|
+
t.end();
|
|
288
|
+
});
|
|
289
|
+
|
|
264
290
|
test('R1C1 serialization', t => {
|
|
265
291
|
// ray
|
|
266
292
|
t.isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: MAX_COLS }, 'R');
|
|
@@ -317,6 +343,19 @@ test('R1C1 serialization', t => {
|
|
|
317
343
|
t.isR1C1Rendered({ r0: -15e5, c0: 0, r1: 15e5, c1: 0 }, 'R[-1048575]C:R[1048575]C');
|
|
318
344
|
t.isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5, ...abs }, 'R1C1');
|
|
319
345
|
t.isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5 }, 'RC');
|
|
346
|
+
// trimming
|
|
347
|
+
t.isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2 }, 'R[1]C[1]:R[2]C[2]');
|
|
348
|
+
t.isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'head' }, 'R[1]C[1].:R[2]C[2]');
|
|
349
|
+
t.isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'tail' }, 'R[1]C[1]:.R[2]C[2]');
|
|
350
|
+
t.isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'both' }, 'R[1]C[1].:.R[2]C[2]');
|
|
351
|
+
t.isR1C1Rendered({ r0: 1, c0: 1, r1: 1, c1: 1, trim: 'both' }, 'R[1]C[1]');
|
|
352
|
+
t.isR1C1Rendered({ r0: 1, r1: 1 }, 'R[1]');
|
|
353
|
+
t.isR1C1Rendered({ r0: 1, r1: 1, trim: 'head' }, 'R[1].:R[1]');
|
|
354
|
+
t.isR1C1Rendered({ r0: 1, r1: 1, trim: 'both' }, 'R[1].:.R[1]');
|
|
355
|
+
t.isR1C1Rendered({ c0: 1, c1: 1 }, 'C[1]');
|
|
356
|
+
t.isR1C1Rendered({ c0: 1, c1: 1, trim: 'tail' }, 'C[1]:.C[1]');
|
|
357
|
+
t.isR1C1Rendered({ c0: 1, c1: 1, trim: 'both' }, 'C[1].:.C[1]');
|
|
358
|
+
t.isR1C1Rendered({ r0: -5, c0: -2, c1: -2, trim: 'both' }, 'R[-5]C[-2].:.C[-2]');
|
|
320
359
|
t.end();
|
|
321
360
|
});
|
|
322
361
|
|
package/lib/sr.js
CHANGED
|
@@ -241,9 +241,11 @@ function toSentenceCase (str) {
|
|
|
241
241
|
* @param {ReferenceStruct} refObject A structured reference object
|
|
242
242
|
* @param {object} [options={}] Options
|
|
243
243
|
* @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
|
|
244
|
+
* @param {boolean} [options.thisRow=false] Enforces using the `[#This Row]` instead of the `@` shorthand when serializing structured ranges.
|
|
244
245
|
* @returns {string} The structured reference in string format
|
|
245
246
|
*/
|
|
246
|
-
export function stringifyStructRef (refObject,
|
|
247
|
+
export function stringifyStructRef (refObject, options = {}) {
|
|
248
|
+
const { xlsx, thisRow } = options;
|
|
247
249
|
let s = xlsx
|
|
248
250
|
? stringifyPrefixAlt(refObject)
|
|
249
251
|
: stringifyPrefix(refObject);
|
|
@@ -263,8 +265,8 @@ export function stringifyStructRef (refObject, { xlsx = false } = {}) {
|
|
|
263
265
|
}
|
|
264
266
|
else {
|
|
265
267
|
s += '[';
|
|
266
|
-
// single [#this row] sections get normalized to an @
|
|
267
|
-
const singleAt = numSections === 1 && refObject.sections[0].toLowerCase() === 'this row';
|
|
268
|
+
// single [#this row] sections get normalized to an @ by default
|
|
269
|
+
const singleAt = !thisRow && numSections === 1 && refObject.sections[0].toLowerCase() === 'this row';
|
|
268
270
|
if (singleAt) {
|
|
269
271
|
s += '@';
|
|
270
272
|
}
|
package/lib/sr.spec.js
CHANGED
|
@@ -278,3 +278,53 @@ test('structured references parse and serialize in xlsx mode', t => {
|
|
|
278
278
|
t.end();
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
test.only('longform serialize (in xlsx mode)', t => {
|
|
282
|
+
// thisRow should have no effect when parsing
|
|
283
|
+
t.isSREqual('Table2[[#This Row],[col1]]', {
|
|
284
|
+
table: 'Table2',
|
|
285
|
+
columns: [ 'col1' ],
|
|
286
|
+
sections: [ 'this row' ]
|
|
287
|
+
}, { xlsx: true, thisRow: true });
|
|
288
|
+
|
|
289
|
+
t.isSREqual('Table2[[#This Row],[col1]]', {
|
|
290
|
+
table: 'Table2',
|
|
291
|
+
columns: [ 'col1' ],
|
|
292
|
+
sections: [ 'this row' ]
|
|
293
|
+
}, { xlsx: true, thisRow: false });
|
|
294
|
+
|
|
295
|
+
t.isSREqual('Table2[[#This Row],[col1]]', {
|
|
296
|
+
table: 'Table2',
|
|
297
|
+
columns: [ 'col1' ],
|
|
298
|
+
sections: [ 'this row' ]
|
|
299
|
+
}, { xlsx: false, thisRow: true });
|
|
300
|
+
|
|
301
|
+
t.isSREqual('Table2[[#This Row],[col1]]', {
|
|
302
|
+
table: 'Table2',
|
|
303
|
+
columns: [ 'col1' ],
|
|
304
|
+
sections: [ 'this row' ]
|
|
305
|
+
}, { xlsx: false, thisRow: false });
|
|
306
|
+
|
|
307
|
+
// thisRow should mean we don't see @'s in output
|
|
308
|
+
t.is(
|
|
309
|
+
stringifyStructRef({
|
|
310
|
+
table: 'Table2',
|
|
311
|
+
columns: [ 'col1' ],
|
|
312
|
+
sections: [ 'this row' ]
|
|
313
|
+
}, { xlsx: true, thisRow: true }),
|
|
314
|
+
'Table2[[#This row],[col1]]',
|
|
315
|
+
'Table2[[#This row],[col1]] (xlsx mode)'
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
t.is(
|
|
319
|
+
stringifyStructRef({
|
|
320
|
+
table: 'Table2',
|
|
321
|
+
columns: [ 'col1' ],
|
|
322
|
+
sections: [ 'this row' ]
|
|
323
|
+
}, { xlsx: false, thisRow: true }),
|
|
324
|
+
'Table2[[#This row],[col1]]',
|
|
325
|
+
'Table2[[#This row],[col1]] (non xlsx mode)'
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
t.end();
|
|
329
|
+
});
|
|
330
|
+
|
package/lib/stringifyPrefix.js
CHANGED
|
@@ -10,7 +10,7 @@ export function stringifyPrefix (ref) {
|
|
|
10
10
|
if (scope) {
|
|
11
11
|
const part = (nth % 2) ? '[' + scope + ']' : scope;
|
|
12
12
|
pre = part + pre;
|
|
13
|
-
quote += reBannedChars.test(scope);
|
|
13
|
+
quote += +reBannedChars.test(scope);
|
|
14
14
|
nth++;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -26,11 +26,11 @@ export function stringifyPrefixAlt (ref) {
|
|
|
26
26
|
const { workbookName, sheetName } = ref;
|
|
27
27
|
if (workbookName) {
|
|
28
28
|
pre += '[' + workbookName + ']';
|
|
29
|
-
quote += reBannedChars.test(workbookName);
|
|
29
|
+
quote += +reBannedChars.test(workbookName);
|
|
30
30
|
}
|
|
31
31
|
if (sheetName) {
|
|
32
32
|
pre += sheetName;
|
|
33
|
-
quote += reBannedChars.test(sheetName);
|
|
33
|
+
quote += +reBannedChars.test(sheetName);
|
|
34
34
|
}
|
|
35
35
|
if (quote) {
|
|
36
36
|
pre = "'" + pre.replace(/'/g, "''") + "'";
|