@borgar/fx 4.2.0 → 4.3.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/.jsdoc/publish.js +30 -8
- package/dist/fx.js +1 -1
- package/docs/API.md +74 -24
- package/docs/Prefixes.md +82 -0
- package/lib/a1.js +11 -5
- package/lib/a1.spec.js +143 -7
- package/lib/fixRanges.js +15 -8
- package/lib/fixRanges.spec.js +19 -0
- package/lib/lexer-srefs.spec.js +9 -1
- package/lib/lexer.js +1 -0
- package/lib/lexer.spec.js +72 -0
- package/lib/lexerParts.js +33 -6
- package/lib/parseRef.js +77 -15
- package/lib/parseRef.spec.js +60 -0
- package/lib/parser.js +3 -2
- package/lib/parser.spec.js +11 -0
- package/lib/rc.js +11 -5
- package/lib/rc.spec.js +145 -12
- package/lib/sr.js +24 -10
- package/lib/sr.spec.js +93 -7
- package/lib/stringifyPrefix.js +18 -0
- package/lib/translate-toA1.spec.js +23 -3
- package/lib/translate-toRC.spec.js +31 -2
- package/lib/translate.js +16 -11
- package/package.json +2 -2
package/lib/rc.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { MAX_ROWS, MAX_COLS } from './constants.js';
|
|
10
10
|
import { parseRef } from './parseRef.js';
|
|
11
|
-
import { stringifyPrefix } from './stringifyPrefix.js';
|
|
11
|
+
import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
|
|
12
12
|
|
|
13
13
|
const clamp = (min, val, max) => Math.min(Math.max(val, min), max);
|
|
14
14
|
|
|
@@ -268,10 +268,11 @@ export function fromR1C1 (ref) {
|
|
|
268
268
|
* @param {Object} [options={}] Options
|
|
269
269
|
* @param {boolean} [options.allowNamed=true] Enable parsing names as well as ranges.
|
|
270
270
|
* @param {boolean} [options.allowTernary=false] Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`. These are supported by Google Sheets but not Excel. See: References.md.
|
|
271
|
+
* @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)
|
|
271
272
|
* @return {(Object|null)} An object representing a valid reference or null if it is invalid.
|
|
272
273
|
*/
|
|
273
|
-
export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false } = {}) {
|
|
274
|
-
const d = parseRef(refString, { allowNamed, allowTernary, r1c1: true });
|
|
274
|
+
export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = false, xlsx = false } = {}) {
|
|
275
|
+
const d = parseRef(refString, { allowNamed, allowTernary, xlsx, r1c1: true });
|
|
275
276
|
if (d && (d.r0 || d.name)) {
|
|
276
277
|
const range = d.r1
|
|
277
278
|
? fromR1C1(d.r0 + ':' + d.r1)
|
|
@@ -310,10 +311,15 @@ export function parseR1C1Ref (refString, { allowNamed = true, allowTernary = fal
|
|
|
310
311
|
* ```
|
|
311
312
|
*
|
|
312
313
|
* @param {Object} refObject A reference object
|
|
314
|
+
* @param {Object} [options={}] Options
|
|
315
|
+
* @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)
|
|
313
316
|
* @return {Object} The reference in R1C1-style string format
|
|
314
317
|
*/
|
|
315
|
-
export function stringifyR1C1Ref (refObject) {
|
|
316
|
-
|
|
318
|
+
export function stringifyR1C1Ref (refObject, { xlsx = false } = {}) {
|
|
319
|
+
const prefix = xlsx
|
|
320
|
+
? stringifyPrefixAlt(refObject)
|
|
321
|
+
: stringifyPrefix(refObject);
|
|
322
|
+
return prefix + (
|
|
317
323
|
refObject.name ? refObject.name : toR1C1(refObject.range)
|
|
318
324
|
);
|
|
319
325
|
}
|
package/lib/rc.spec.js
CHANGED
|
@@ -3,24 +3,32 @@ import { test, Test } from 'tape';
|
|
|
3
3
|
import { MAX_COLS, MAX_ROWS } from './constants.js';
|
|
4
4
|
import { parseR1C1Ref, stringifyR1C1Ref, toR1C1 } from './rc.js';
|
|
5
5
|
|
|
6
|
-
Test.prototype.isRCEqual = function isTokens (expr,
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
Test.prototype.isRCEqual = function isTokens (expr, expect, opts) {
|
|
7
|
+
if (expect) {
|
|
8
|
+
expect = (opts?.xlsx)
|
|
9
|
+
? {
|
|
10
|
+
workbookName: '',
|
|
11
|
+
sheetName: '',
|
|
12
|
+
name: '',
|
|
13
|
+
range: null,
|
|
14
|
+
...expect
|
|
15
|
+
}
|
|
16
|
+
: {
|
|
17
|
+
context: [],
|
|
18
|
+
name: '',
|
|
19
|
+
range: null,
|
|
20
|
+
...expect
|
|
21
|
+
};
|
|
22
|
+
if (expect.range && typeof expect.range === 'object') {
|
|
15
23
|
// mix in some defaults so we don't have to write things out in full
|
|
16
|
-
|
|
24
|
+
expect.range = {
|
|
17
25
|
r0: null, c0: null, r1: null, c1: null,
|
|
18
26
|
$r0: false, $c0: false, $r1: false, $c1: false,
|
|
19
|
-
...
|
|
27
|
+
...expect.range
|
|
20
28
|
};
|
|
21
29
|
}
|
|
22
30
|
}
|
|
23
|
-
this.deepEqual(parseR1C1Ref(expr, opts),
|
|
31
|
+
this.deepEqual(parseR1C1Ref(expr, opts), expect, expr);
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
Test.prototype.isR1C1Rendered = function isTokens (range, expect, d) {
|
|
@@ -151,6 +159,108 @@ test('parse joined R1C1 references', t => {
|
|
|
151
159
|
t.end();
|
|
152
160
|
});
|
|
153
161
|
|
|
162
|
+
test('parse R1C1 ranges in XLSX mode', t => {
|
|
163
|
+
const opts = { xlsx: true };
|
|
164
|
+
const rcRange = { r0: 0, c0: 0, r1: 0, c1: 0 };
|
|
165
|
+
t.isRCEqual('[1]!RC', {
|
|
166
|
+
workbookName: '1',
|
|
167
|
+
sheetName: '',
|
|
168
|
+
range: rcRange
|
|
169
|
+
}, opts);
|
|
170
|
+
|
|
171
|
+
t.isRCEqual('[Workbook.xlsx]!RC', {
|
|
172
|
+
workbookName: 'Workbook.xlsx',
|
|
173
|
+
sheetName: '',
|
|
174
|
+
range: rcRange
|
|
175
|
+
}, opts);
|
|
176
|
+
|
|
177
|
+
t.isRCEqual('[1]Sheet1!RC', {
|
|
178
|
+
workbookName: '1',
|
|
179
|
+
sheetName: 'Sheet1',
|
|
180
|
+
range: rcRange
|
|
181
|
+
}, opts);
|
|
182
|
+
|
|
183
|
+
t.isRCEqual('[Workbook.xlsx]Sheet1!RC', {
|
|
184
|
+
workbookName: 'Workbook.xlsx',
|
|
185
|
+
sheetName: 'Sheet1',
|
|
186
|
+
range: rcRange
|
|
187
|
+
}, opts);
|
|
188
|
+
|
|
189
|
+
t.isRCEqual('[4]!name', {
|
|
190
|
+
workbookName: '4',
|
|
191
|
+
sheetName: '',
|
|
192
|
+
name: 'name'
|
|
193
|
+
}, opts);
|
|
194
|
+
|
|
195
|
+
t.isRCEqual('[Workbook.xlsx]!name', {
|
|
196
|
+
workbookName: 'Workbook.xlsx',
|
|
197
|
+
sheetName: '',
|
|
198
|
+
name: 'name'
|
|
199
|
+
}, opts);
|
|
200
|
+
|
|
201
|
+
t.isRCEqual('[16]Sheet1!name', {
|
|
202
|
+
workbookName: '16',
|
|
203
|
+
sheetName: 'Sheet1',
|
|
204
|
+
name: 'name'
|
|
205
|
+
}, opts);
|
|
206
|
+
|
|
207
|
+
t.isRCEqual('[Workbook.xlsx]Sheet1!name', {
|
|
208
|
+
workbookName: 'Workbook.xlsx',
|
|
209
|
+
sheetName: 'Sheet1',
|
|
210
|
+
name: 'name'
|
|
211
|
+
}, opts);
|
|
212
|
+
|
|
213
|
+
t.isRCEqual("='[1]'!RC", {
|
|
214
|
+
workbookName: '1',
|
|
215
|
+
sheetName: '',
|
|
216
|
+
range: rcRange
|
|
217
|
+
}, opts);
|
|
218
|
+
|
|
219
|
+
t.isRCEqual("='[Workbook.xlsx]'!RC", {
|
|
220
|
+
workbookName: 'Workbook.xlsx',
|
|
221
|
+
sheetName: '',
|
|
222
|
+
range: rcRange
|
|
223
|
+
}, opts);
|
|
224
|
+
|
|
225
|
+
t.isRCEqual("'[1]Sheet1'!RC", {
|
|
226
|
+
workbookName: '1',
|
|
227
|
+
sheetName: 'Sheet1',
|
|
228
|
+
range: rcRange
|
|
229
|
+
}, opts);
|
|
230
|
+
|
|
231
|
+
t.isRCEqual("'[Workbook.xlsx]Sheet1'!RC", {
|
|
232
|
+
workbookName: 'Workbook.xlsx',
|
|
233
|
+
sheetName: 'Sheet1',
|
|
234
|
+
range: rcRange
|
|
235
|
+
}, opts);
|
|
236
|
+
|
|
237
|
+
t.isRCEqual("'[4]'!name", {
|
|
238
|
+
workbookName: '4',
|
|
239
|
+
sheetName: '',
|
|
240
|
+
name: 'name'
|
|
241
|
+
}, opts);
|
|
242
|
+
|
|
243
|
+
t.isRCEqual("'[Workbook.xlsx]'!name", {
|
|
244
|
+
workbookName: 'Workbook.xlsx',
|
|
245
|
+
sheetName: '',
|
|
246
|
+
name: 'name'
|
|
247
|
+
}, opts);
|
|
248
|
+
|
|
249
|
+
t.isRCEqual("'[16]Sheet1'!name", {
|
|
250
|
+
workbookName: '16',
|
|
251
|
+
sheetName: 'Sheet1',
|
|
252
|
+
name: 'name'
|
|
253
|
+
}, opts);
|
|
254
|
+
|
|
255
|
+
t.isRCEqual("'[Workbook.xlsx]Sheet1'!name", {
|
|
256
|
+
workbookName: 'Workbook.xlsx',
|
|
257
|
+
sheetName: 'Sheet1',
|
|
258
|
+
name: 'name'
|
|
259
|
+
}, opts);
|
|
260
|
+
|
|
261
|
+
t.end();
|
|
262
|
+
});
|
|
263
|
+
|
|
154
264
|
test('R1C1 serialization', t => {
|
|
155
265
|
// ray
|
|
156
266
|
t.isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: MAX_COLS }, 'R');
|
|
@@ -229,3 +339,26 @@ test('stringifyR1C1Ref', t => {
|
|
|
229
339
|
testRef({ context: [ 'My File.xlsx' ], name: 'foo' }, "'My File.xlsx'!foo");
|
|
230
340
|
t.end();
|
|
231
341
|
});
|
|
342
|
+
|
|
343
|
+
test('stringifyR1C1Ref in XLSX mode', t => {
|
|
344
|
+
const rangeA1 = { r0: 2, c0: 4, r1: 2, c1: 4 };
|
|
345
|
+
const testRef = (ref, expect) => t.is(stringifyR1C1Ref(ref, { xlsx: true }), expect, expect);
|
|
346
|
+
testRef({ range: rangeA1 }, 'R[2]C[4]');
|
|
347
|
+
testRef({ sheetName: 'Sheet1', range: rangeA1 }, 'Sheet1!R[2]C[4]');
|
|
348
|
+
testRef({ sheetName: 'Sheet 1', range: rangeA1 }, "'Sheet 1'!R[2]C[4]");
|
|
349
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 }, '[MyFile.xlsx]Sheet1!R[2]C[4]');
|
|
350
|
+
testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', range: rangeA1 }, "'[My File.xlsx]Sheet1'!R[2]C[4]");
|
|
351
|
+
testRef({ workbookName: 'MyFile.xlsx', range: rangeA1 }, '[MyFile.xlsx]!R[2]C[4]');
|
|
352
|
+
testRef({ workbookName: 'My File.xlsx', range: rangeA1 }, "'[My File.xlsx]'!R[2]C[4]");
|
|
353
|
+
testRef({ name: 'foo' }, 'foo');
|
|
354
|
+
testRef({ sheetName: 'Sheet1', name: 'foo' }, 'Sheet1!foo');
|
|
355
|
+
testRef({ sheetName: 'Sheet 1', name: 'foo' }, "'Sheet 1'!foo");
|
|
356
|
+
testRef({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' }, '[MyFile.xlsx]Sheet1!foo');
|
|
357
|
+
testRef({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', name: 'foo' }, "'[My File.xlsx]Sheet1'!foo");
|
|
358
|
+
testRef({ workbookName: 'MyFile.xlsx', name: 'foo' }, '[MyFile.xlsx]!foo');
|
|
359
|
+
testRef({ workbookName: 'My File.xlsx', name: 'foo' }, "'[My File.xlsx]'!foo");
|
|
360
|
+
// ignore .context
|
|
361
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 }, 'R[2]C[4]');
|
|
362
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' }, 'foo');
|
|
363
|
+
t.end();
|
|
364
|
+
});
|
package/lib/sr.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseRef } from './parseRef.js';
|
|
2
|
-
import { stringifyPrefix } from './stringifyPrefix.js';
|
|
2
|
+
import { stringifyPrefix, stringifyPrefixAlt } from './stringifyPrefix.js';
|
|
3
3
|
|
|
4
4
|
const re_SRcolumnB = /^\[('['#@[\]]|[^'#@[\]])+\]/i;
|
|
5
5
|
const re_SRcolumnN = /^([^#@[\]:]+)/i;
|
|
@@ -186,19 +186,28 @@ export function parseSRange (raw) {
|
|
|
186
186
|
* @tutorial References.md
|
|
187
187
|
* @param {string} ref A structured reference string
|
|
188
188
|
* @param {Object} [options={}] Options
|
|
189
|
+
* @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)
|
|
189
190
|
* @return {(Object|null)} An object representing a valid reference or null if it is invalid.
|
|
190
191
|
*/
|
|
191
|
-
export function parseStructRef (ref, opts = {}) {
|
|
192
|
+
export function parseStructRef (ref, opts = { xlsx: false }) {
|
|
192
193
|
const r = parseRef(ref, opts);
|
|
193
194
|
if (r && r.struct) {
|
|
194
195
|
const structData = parseSRange(r.struct);
|
|
195
196
|
if (structData && structData.length === r.struct.length) {
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
return opts.xlsx
|
|
198
|
+
? {
|
|
199
|
+
workbookName: r.workbookName,
|
|
200
|
+
sheetName: r.sheetName,
|
|
201
|
+
table: r.name,
|
|
202
|
+
columns: structData.columns,
|
|
203
|
+
sections: structData.sections
|
|
204
|
+
}
|
|
205
|
+
: {
|
|
206
|
+
context: r.context,
|
|
207
|
+
table: r.name,
|
|
208
|
+
columns: structData.columns,
|
|
209
|
+
sections: structData.sections
|
|
210
|
+
};
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
return null;
|
|
@@ -230,10 +239,15 @@ function toSentenceCase (str) {
|
|
|
230
239
|
* ```
|
|
231
240
|
*
|
|
232
241
|
* @param {Object} refObject A structured reference object
|
|
242
|
+
* @param {Object} [options={}] Options
|
|
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)
|
|
233
244
|
* @return {Object} The structured reference in string format
|
|
234
245
|
*/
|
|
235
|
-
export function stringifyStructRef (ref) {
|
|
236
|
-
let s =
|
|
246
|
+
export function stringifyStructRef (ref, { xlsx = false } = {}) {
|
|
247
|
+
let s = xlsx
|
|
248
|
+
? stringifyPrefixAlt(ref)
|
|
249
|
+
: stringifyPrefix(ref);
|
|
250
|
+
|
|
237
251
|
if (ref.table) {
|
|
238
252
|
s += ref.table;
|
|
239
253
|
}
|
package/lib/sr.spec.js
CHANGED
|
@@ -4,13 +4,22 @@ import { parseStructRef, stringifyStructRef } from './sr.js';
|
|
|
4
4
|
|
|
5
5
|
Test.prototype.isSREqual = function isSREqual (expr, expect, opts) {
|
|
6
6
|
if (expect) {
|
|
7
|
-
expect =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
expect = opts?.xlsx
|
|
8
|
+
? {
|
|
9
|
+
workbookName: '',
|
|
10
|
+
sheetName: '',
|
|
11
|
+
table: '',
|
|
12
|
+
columns: [],
|
|
13
|
+
sections: [],
|
|
14
|
+
...expect
|
|
15
|
+
}
|
|
16
|
+
: {
|
|
17
|
+
context: [],
|
|
18
|
+
table: '',
|
|
19
|
+
columns: [],
|
|
20
|
+
sections: [],
|
|
21
|
+
...expect
|
|
22
|
+
};
|
|
14
23
|
}
|
|
15
24
|
this.deepEqual(parseStructRef(expr, opts), expect, expr);
|
|
16
25
|
};
|
|
@@ -67,6 +76,18 @@ test('parse structured references', t => {
|
|
|
67
76
|
sections: [ 'data', 'totals' ]
|
|
68
77
|
});
|
|
69
78
|
|
|
79
|
+
t.isSREqual("'Sheet'!Table[Column]", {
|
|
80
|
+
columns: [ 'Column' ],
|
|
81
|
+
table: 'Table',
|
|
82
|
+
context: [ 'Sheet' ]
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
t.isSREqual("Sheet1!Table1[foo '[bar']]", {
|
|
86
|
+
columns: [ 'foo [bar]' ],
|
|
87
|
+
table: 'Table1',
|
|
88
|
+
context: [ 'Sheet1' ]
|
|
89
|
+
});
|
|
90
|
+
|
|
70
91
|
t.end();
|
|
71
92
|
});
|
|
72
93
|
|
|
@@ -177,3 +198,68 @@ test('serialize structured references', t => {
|
|
|
177
198
|
t.end();
|
|
178
199
|
});
|
|
179
200
|
|
|
201
|
+
test('structured references parse and serialize in xlsx mode', t => {
|
|
202
|
+
t.isSREqual('[Workbook.xlsx]!Table[#Data]', {
|
|
203
|
+
workbookName: 'Workbook.xlsx',
|
|
204
|
+
table: 'Table',
|
|
205
|
+
sections: [ 'data' ]
|
|
206
|
+
}, { xlsx: true });
|
|
207
|
+
|
|
208
|
+
t.isSREqual('[Workbook.xlsx]Sheet1!Table[#Data]', {
|
|
209
|
+
workbookName: 'Workbook.xlsx',
|
|
210
|
+
sheetName: 'Sheet1',
|
|
211
|
+
table: 'Table',
|
|
212
|
+
sections: [ 'data' ]
|
|
213
|
+
}, { xlsx: true });
|
|
214
|
+
|
|
215
|
+
t.isSREqual('Sheet1!Table[#Data]', {
|
|
216
|
+
sheetName: 'Sheet1',
|
|
217
|
+
table: 'Table',
|
|
218
|
+
sections: [ 'data' ]
|
|
219
|
+
}, { xlsx: true });
|
|
220
|
+
|
|
221
|
+
t.is(
|
|
222
|
+
stringifyStructRef({
|
|
223
|
+
context: [ 'Lorem', 'Ipsum' ],
|
|
224
|
+
columns: [ 'foo' ]
|
|
225
|
+
}, { xlsx: true }),
|
|
226
|
+
'[foo]',
|
|
227
|
+
'context prop is ignored in xlsx mode'
|
|
228
|
+
);
|
|
229
|
+
t.is(
|
|
230
|
+
stringifyStructRef({
|
|
231
|
+
workbookName: 'Lorem',
|
|
232
|
+
sheetName: 'Ipsum',
|
|
233
|
+
columns: [ 'foo' ]
|
|
234
|
+
}, { xlsx: false }),
|
|
235
|
+
'[foo]',
|
|
236
|
+
'workbookName+sheetName props are ignored in default mode'
|
|
237
|
+
);
|
|
238
|
+
t.is(
|
|
239
|
+
stringifyStructRef({
|
|
240
|
+
workbookName: 'Lorem',
|
|
241
|
+
sheetName: 'Ipsum',
|
|
242
|
+
columns: [ 'foo' ]
|
|
243
|
+
}, { xlsx: true }),
|
|
244
|
+
'[Lorem]Ipsum![foo]',
|
|
245
|
+
'workbookName+sheetName props are rendered correctly'
|
|
246
|
+
);
|
|
247
|
+
t.is(
|
|
248
|
+
stringifyStructRef({
|
|
249
|
+
workbookName: 'Lorem',
|
|
250
|
+
columns: [ 'foo' ]
|
|
251
|
+
}, { xlsx: true }),
|
|
252
|
+
'[Lorem]![foo]',
|
|
253
|
+
'workbookName prop is rendered correctly'
|
|
254
|
+
);
|
|
255
|
+
t.is(
|
|
256
|
+
stringifyStructRef({
|
|
257
|
+
sheetName: 'Ipsum',
|
|
258
|
+
columns: [ 'foo' ]
|
|
259
|
+
}, { xlsx: true }),
|
|
260
|
+
'Ipsum![foo]',
|
|
261
|
+
'sheetName prop is rendered correctly'
|
|
262
|
+
);
|
|
263
|
+
t.end();
|
|
264
|
+
});
|
|
265
|
+
|
package/lib/stringifyPrefix.js
CHANGED
|
@@ -19,3 +19,21 @@ export function stringifyPrefix (ref) {
|
|
|
19
19
|
}
|
|
20
20
|
return pre ? pre + '!' : pre;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
export function stringifyPrefixAlt (ref) {
|
|
24
|
+
let pre = '';
|
|
25
|
+
let quote = 0;
|
|
26
|
+
const { workbookName, sheetName } = ref;
|
|
27
|
+
if (workbookName) {
|
|
28
|
+
pre += '[' + workbookName + ']';
|
|
29
|
+
quote += reBannedChars.test(workbookName);
|
|
30
|
+
}
|
|
31
|
+
if (sheetName) {
|
|
32
|
+
pre += sheetName;
|
|
33
|
+
quote += reBannedChars.test(sheetName);
|
|
34
|
+
}
|
|
35
|
+
if (quote) {
|
|
36
|
+
pre = "'" + pre.replace(/'/g, "''") + "'";
|
|
37
|
+
}
|
|
38
|
+
return pre ? pre + '!' : pre;
|
|
39
|
+
}
|
|
@@ -2,10 +2,10 @@ import { test, Test } from 'tape';
|
|
|
2
2
|
import { translateToA1 } from './translate.js';
|
|
3
3
|
import { tokenize } from './lexer.js';
|
|
4
4
|
import { addTokenMeta } from './addTokenMeta.js';
|
|
5
|
-
import { ERROR, FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM } from './constants.js';
|
|
5
|
+
import { ERROR, FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.js';
|
|
6
6
|
|
|
7
|
-
Test.prototype.isR2A = function isTokens (expr, anchor, result) {
|
|
8
|
-
this.is(translateToA1(expr, anchor), result, expr);
|
|
7
|
+
Test.prototype.isR2A = function isTokens (expr, anchor, result, opts) {
|
|
8
|
+
this.is(translateToA1(expr, anchor, opts), result, expr);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
test('translate absolute cells from RC to A1', t => {
|
|
@@ -175,3 +175,23 @@ test('translate works with merged ranges', t => {
|
|
|
175
175
|
t.deepEqual(translateToA1(tokens, 'D10'), expected, expr);
|
|
176
176
|
t.end();
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
test('translate works with xlsx mode references', t => {
|
|
180
|
+
const testExpr = (expr, anchor, expected) => {
|
|
181
|
+
const opts = { mergeRefs: true, xlsx: true, r1c1: true };
|
|
182
|
+
t.deepEqual(translateToA1(tokenize(expr, opts), anchor, opts), expected, expr);
|
|
183
|
+
};
|
|
184
|
+
testExpr("'[My Fancy Workbook.xlsx]'!R1C", 'B2', [
|
|
185
|
+
{ type: REF_RANGE, value: "'[My Fancy Workbook.xlsx]'!B$1" }
|
|
186
|
+
]);
|
|
187
|
+
testExpr('[Workbook.xlsx]!R1C', 'B2', [
|
|
188
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]!B$1' }
|
|
189
|
+
]);
|
|
190
|
+
testExpr('[Workbook.xlsx]Sheet1!R1C', 'B2', [
|
|
191
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!B$1' }
|
|
192
|
+
]);
|
|
193
|
+
testExpr('[Workbook.xlsx]!table[#data]', 'B2', [
|
|
194
|
+
{ type: REF_STRUCT, value: '[Workbook.xlsx]!table[#data]' }
|
|
195
|
+
]);
|
|
196
|
+
t.end();
|
|
197
|
+
});
|
|
@@ -2,7 +2,7 @@ import { test, Test } from 'tape';
|
|
|
2
2
|
import { translateToR1C1 } from './translate.js';
|
|
3
3
|
import { tokenize } from './lexer.js';
|
|
4
4
|
import { addTokenMeta } from './addTokenMeta.js';
|
|
5
|
-
import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM } from './constants.js';
|
|
5
|
+
import { FUNCTION, FX_PREFIX, OPERATOR, REF_RANGE, REF_BEAM, REF_STRUCT } from './constants.js';
|
|
6
6
|
|
|
7
7
|
Test.prototype.isA2R = function isTokens (expr, anchor, result) {
|
|
8
8
|
this.is(translateToR1C1(expr, anchor), result, expr);
|
|
@@ -115,9 +115,17 @@ test('translate mixed rel/abs coords from A1 to RC', t => {
|
|
|
115
115
|
t.end();
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
test('translate involved
|
|
118
|
+
test('translate involved cases from A1 to RC', t => {
|
|
119
119
|
t.isA2R('=SUM(IF(E10,$E$2,$E$3),Sheet1!$2:$2*Sheet2!B:B)', 'D10',
|
|
120
120
|
'=SUM(IF(RC[1],R2C5,R3C5),Sheet1!R2*Sheet2!C[-2])');
|
|
121
|
+
|
|
122
|
+
// make sure we don't get confused by structured, or named refs
|
|
123
|
+
t.isA2R('=A1+Table1[#Data]', 'D10', '=R[-9]C[-3]+Table1[#Data]');
|
|
124
|
+
t.isA2R('=A1+foobar', 'D10', '=R[-9]C[-3]+foobar');
|
|
125
|
+
|
|
126
|
+
// This [123]Sheet!A1 variant of the syntax is used internally in xlsx files
|
|
127
|
+
t.isA2R('=[2]Sheet1!A1', 'D10', '=[2]Sheet1!R[-9]C[-3]');
|
|
128
|
+
|
|
121
129
|
t.end();
|
|
122
130
|
});
|
|
123
131
|
|
|
@@ -150,3 +158,24 @@ test('translate works with merged ranges', t => {
|
|
|
150
158
|
t.deepEqual(translateToR1C1(tokens, 'D10'), expected, expr);
|
|
151
159
|
t.end();
|
|
152
160
|
});
|
|
161
|
+
|
|
162
|
+
test('translate works with xlsx mode', t => {
|
|
163
|
+
const testExpr = (expr, anchor, expected) => {
|
|
164
|
+
const opts = { mergeRefs: true, xlsx: true, r1c1: false };
|
|
165
|
+
const tokens = tokenize(expr, opts);
|
|
166
|
+
t.deepEqual(translateToR1C1(tokens, anchor, opts), expected, expr);
|
|
167
|
+
};
|
|
168
|
+
testExpr("'[My Fancy Workbook.xlsx]'!B$1", 'B2', [
|
|
169
|
+
{ type: REF_RANGE, value: "'[My Fancy Workbook.xlsx]'!R1C" }
|
|
170
|
+
]);
|
|
171
|
+
testExpr('[Workbook.xlsx]!B$1', 'B2', [
|
|
172
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]!R1C' }
|
|
173
|
+
]);
|
|
174
|
+
testExpr('[Workbook.xlsx]Sheet1!B$1', 'B2', [
|
|
175
|
+
{ type: REF_RANGE, value: '[Workbook.xlsx]Sheet1!R1C' }
|
|
176
|
+
]);
|
|
177
|
+
testExpr('[Workbook.xlsx]!table[#data]', 'B2', [
|
|
178
|
+
{ type: REF_STRUCT, value: '[Workbook.xlsx]!table[#data]' }
|
|
179
|
+
]);
|
|
180
|
+
t.end();
|
|
181
|
+
});
|
package/lib/translate.js
CHANGED
|
@@ -23,8 +23,7 @@ const settings = {
|
|
|
23
23
|
* relative R1C1 syntax.
|
|
24
24
|
*
|
|
25
25
|
* Returns the same formula with the ranges translated. If an array of tokens
|
|
26
|
-
* was supplied, then the same array is returned
|
|
27
|
-
* *must* be `false`).
|
|
26
|
+
* was supplied, then the same array is returned.
|
|
28
27
|
*
|
|
29
28
|
* ```js
|
|
30
29
|
* translateToR1C1("=SUM(E10,$E$2,Sheet!$E$3)", "D10");
|
|
@@ -33,21 +32,24 @@ const settings = {
|
|
|
33
32
|
*
|
|
34
33
|
* @param {(string | Array<Object>)} formula A string (an Excel formula) or a token list that should be adjusted.
|
|
35
34
|
* @param {string} anchorCell A simple string reference to an A1 cell ID (`AF123` or`$C$5`).
|
|
35
|
+
* @param {Object} [options={}] The options
|
|
36
|
+
* @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)
|
|
36
37
|
* @return {(string | Array<Object>)} A formula string or token list (depending on which was input)
|
|
37
38
|
*/
|
|
38
|
-
export function translateToR1C1 (fx, anchorCell) {
|
|
39
|
+
export function translateToR1C1 (fx, anchorCell, { xlsx = false } = {}) {
|
|
39
40
|
const { top, left } = fromA1(anchorCell);
|
|
40
41
|
const isString = typeof fx === 'string';
|
|
41
42
|
|
|
42
43
|
const tokens = isString
|
|
43
|
-
? tokenize(fx, settings)
|
|
44
|
+
? tokenize(fx, { ...settings, xlsx })
|
|
44
45
|
: fx;
|
|
45
46
|
|
|
46
47
|
let offsetSkew = 0;
|
|
48
|
+
const refOpts = { xlsx, allowTernary: true };
|
|
47
49
|
tokens.forEach(token => {
|
|
48
50
|
if (isRange(token)) {
|
|
49
51
|
const tokenValue = token.value;
|
|
50
|
-
const ref = parseA1Ref(tokenValue,
|
|
52
|
+
const ref = parseA1Ref(tokenValue, refOpts);
|
|
51
53
|
const d = ref.range;
|
|
52
54
|
const range = {};
|
|
53
55
|
range.r0 = calc(d.$top, d.top, top);
|
|
@@ -59,7 +61,7 @@ export function translateToR1C1 (fx, anchorCell) {
|
|
|
59
61
|
range.$c0 = d.$left;
|
|
60
62
|
range.$c1 = d.$right;
|
|
61
63
|
ref.range = range;
|
|
62
|
-
token.value = stringifyR1C1Ref(ref);
|
|
64
|
+
token.value = stringifyR1C1Ref(ref, refOpts);
|
|
63
65
|
// if token includes offsets, those offsets are now likely wrong!
|
|
64
66
|
if (token.loc) {
|
|
65
67
|
token.loc[0] += offsetSkew;
|
|
@@ -105,7 +107,8 @@ function toFixed (val, abs, base, max, wrapEdges = true) {
|
|
|
105
107
|
|
|
106
108
|
const defaultOptions = {
|
|
107
109
|
wrapEdges: true,
|
|
108
|
-
mergeRefs: true
|
|
110
|
+
mergeRefs: true,
|
|
111
|
+
xlsx: false
|
|
109
112
|
};
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -113,8 +116,7 @@ const defaultOptions = {
|
|
|
113
116
|
* absolute A1 syntax.
|
|
114
117
|
*
|
|
115
118
|
* Returns the same formula with the ranges translated. If an array of tokens
|
|
116
|
-
* was supplied, then the same array is returned
|
|
117
|
-
* *must* be `false`).
|
|
119
|
+
* was supplied, then the same array is returned.
|
|
118
120
|
*
|
|
119
121
|
* ```js
|
|
120
122
|
* translateToA1("=SUM(RC[1],R2C5,Sheet!R3C5)", "D10");
|
|
@@ -145,6 +147,7 @@ const defaultOptions = {
|
|
|
145
147
|
* @param {Object} [options={}] The options
|
|
146
148
|
* @param {boolean} [options.wrapEdges=true] Wrap out-of-bounds ranges around sheet edges rather than turning them to #REF! errors
|
|
147
149
|
* @param {boolean} [options.mergeRefs=true] Should ranges be treated as whole references (`Sheet1!A1:B2`) or as separate tokens for each part: (`Sheet1`,`!`,`A1`,`:`,`B2`).
|
|
150
|
+
* @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)
|
|
148
151
|
* @return {(string | Array<Object>)} A formula string or token list (depending on which was input)
|
|
149
152
|
*/
|
|
150
153
|
export function translateToA1 (formula, anchorCell, options = defaultOptions) {
|
|
@@ -156,16 +159,18 @@ export function translateToA1 (formula, anchorCell, options = defaultOptions) {
|
|
|
156
159
|
? tokenize(formula, {
|
|
157
160
|
withLocation: false,
|
|
158
161
|
mergeRefs: opts.mergeRefs,
|
|
162
|
+
xlsx: opts.xlsx,
|
|
159
163
|
allowTernary: true,
|
|
160
164
|
r1c1: true
|
|
161
165
|
})
|
|
162
166
|
: formula;
|
|
163
167
|
|
|
164
168
|
let offsetSkew = 0;
|
|
169
|
+
const refOpts = { xlsx: opts.xlsx, allowTernary: true };
|
|
165
170
|
tokens.forEach(token => {
|
|
166
171
|
if (isRange(token)) {
|
|
167
172
|
const tokenValue = token.value;
|
|
168
|
-
const ref = parseR1C1Ref(tokenValue,
|
|
173
|
+
const ref = parseR1C1Ref(tokenValue, refOpts);
|
|
169
174
|
const d = ref.range;
|
|
170
175
|
const range = {};
|
|
171
176
|
const r0 = toFixed(d.r0, d.$r0, anchor.top, MAX_ROWS, opts.wrapEdges);
|
|
@@ -204,7 +209,7 @@ export function translateToA1 (formula, anchorCell, options = defaultOptions) {
|
|
|
204
209
|
}
|
|
205
210
|
else {
|
|
206
211
|
ref.range = range;
|
|
207
|
-
token.value = stringifyA1Ref(ref);
|
|
212
|
+
token.value = stringifyA1Ref(ref, refOpts);
|
|
208
213
|
}
|
|
209
214
|
// if token includes offsets, those offsets are now likely wrong!
|
|
210
215
|
if (token.loc) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@borgar/fx",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Utilities for working with Excel formulas",
|
|
5
5
|
"main": "dist/fx.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"version": "npm run build",
|
|
16
16
|
"lint": "eslint lib/*.js",
|
|
17
17
|
"test": "tape lib/*.spec.js | tap-min",
|
|
18
|
-
"build:docs": "echo '#
|
|
18
|
+
"build:docs": "echo '# _Fx_ API\n'>docs/API.md; jsdoc -c .jsdoc/config.json -d console lib>>docs/API.md",
|
|
19
19
|
"build": "NODE_ENV=production rollup -c"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|