@borgar/fx 2.1.1 → 3.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/README.md +139 -24
- package/References.md +39 -0
- package/dist/fx.js +1 -1
- package/lib/a1.js +152 -87
- package/lib/a1.spec.js +264 -0
- package/lib/addMeta.js +72 -13
- package/lib/{addMeta-test.js → addMeta.spec.js} +39 -6
- package/lib/constants.js +7 -89
- package/lib/fixRanges.js +41 -0
- package/lib/fixRanges.spec.js +111 -0
- package/lib/index.js +9 -6
- package/lib/isType.js +18 -0
- package/lib/lexer.js +99 -70
- package/lib/{lexer-test.js → lexer.spec.js} +445 -142
- package/lib/lexerParts.js +153 -0
- package/lib/mergeRefTokens.js +77 -0
- package/lib/mergeRefTokens.spec.js +118 -0
- package/lib/parseRef.js +44 -40
- package/lib/rc.js +154 -49
- package/lib/rc.spec.js +220 -0
- package/lib/stringifyPrefix.js +21 -0
- package/lib/{translate-toA1-test.js → translate-toA1.spec.js} +20 -2
- package/lib/{translate-toRC-test.js → translate-toRC.spec.js} +18 -1
- package/lib/translate.js +20 -32
- package/package.json +12 -10
- package/lib/a1-test.js +0 -158
- package/lib/quickVerify.js +0 -35
- package/lib/rc-test.js +0 -111
package/lib/a1.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
import { MAX_ROWS, MAX_COLS
|
|
1
|
+
import { MAX_ROWS, MAX_COLS } from './constants.js';
|
|
2
2
|
import { parseRef } from './parseRef.js';
|
|
3
|
+
import { stringifyPrefix } from './stringifyPrefix.js';
|
|
3
4
|
|
|
4
|
-
export function fromCol (
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export function fromCol (columnId) {
|
|
6
|
+
const x = (columnId || '');
|
|
7
|
+
const l = x.length;
|
|
8
|
+
let n = 0;
|
|
9
|
+
if (l > 2) {
|
|
10
|
+
const c = x.charCodeAt(l - 3);
|
|
11
|
+
const a = c > 95 ? 32 : 0;
|
|
12
|
+
n += (1 + c - a - 65) * 676;
|
|
13
|
+
}
|
|
14
|
+
if (l > 1) {
|
|
15
|
+
const c = x.charCodeAt(l - 2);
|
|
16
|
+
const a = c > 95 ? 32 : 0;
|
|
17
|
+
n += (1 + c - a - 65) * 26;
|
|
13
18
|
}
|
|
14
|
-
|
|
19
|
+
if (l) {
|
|
20
|
+
const c = x.charCodeAt(l - 1);
|
|
21
|
+
const a = c > 95 ? 32 : 0;
|
|
22
|
+
n += (c - a) - 65;
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export function toCol (left) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
return c;
|
|
28
|
+
return (
|
|
29
|
+
(left >= 702 ? String.fromCharCode((((left - 702) / 676) - 0) % 26 + 65) : '') +
|
|
30
|
+
(left >= 26 ? String.fromCharCode(Math.floor(((left / 26) - 1) % 26 + 65)) : '') +
|
|
31
|
+
String.fromCharCode((left % 26 + 65))
|
|
32
|
+
);
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export function fromRow (rowStr) {
|
|
@@ -42,98 +50,127 @@ export function toAbsolute (range) {
|
|
|
42
50
|
return { top, left, bottom, right, $left: true, $right: true, $top: true, $bottom: true };
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
// TODO: add a setting for partials?
|
|
54
|
+
const toColStr = (c, a) => (a ? '$' : '') + toCol(c);
|
|
55
|
+
const toRowStr = (r, a) => (a ? '$' : '') + toRow(r);
|
|
45
56
|
export function toA1 (range) {
|
|
46
|
-
const toAbs = d => (d ? '$' : '');
|
|
47
57
|
const { top, left, bottom, right, $left, $right, $top, $bottom } = range;
|
|
58
|
+
const noLeft = left == null;
|
|
59
|
+
const noRight = right == null;
|
|
60
|
+
const noTop = top == null;
|
|
61
|
+
const noBottom = bottom == null;
|
|
48
62
|
// A:A
|
|
49
|
-
if (top === 0 && bottom
|
|
50
|
-
return
|
|
63
|
+
if ((top === 0 && bottom >= MAX_ROWS) || (noTop && noBottom)) {
|
|
64
|
+
return toColStr(left, $left) + ':' + toColStr(right, $right);
|
|
51
65
|
}
|
|
52
66
|
// 1:1
|
|
53
|
-
if (left === 0 && right
|
|
54
|
-
return
|
|
67
|
+
else if ((left === 0 && right >= MAX_COLS) || (noLeft && noRight)) {
|
|
68
|
+
return toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
69
|
+
}
|
|
70
|
+
// A1:1
|
|
71
|
+
else if (!noLeft && !noTop && !noRight && noBottom) {
|
|
72
|
+
return toColStr(left, $left) + toRowStr(top, $top) + ':' + toColStr(right, $right);
|
|
73
|
+
}
|
|
74
|
+
// A:A1 => A1:1
|
|
75
|
+
else if (!noLeft && noTop && !noRight && !noBottom) {
|
|
76
|
+
return toColStr(left, $left) + toRowStr(bottom, $bottom) + ':' + toColStr(right, $right);
|
|
77
|
+
}
|
|
78
|
+
// A1:A
|
|
79
|
+
else if (!noLeft && !noTop && noRight && !noBottom) {
|
|
80
|
+
return toColStr(left, $left) + toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
81
|
+
}
|
|
82
|
+
// A:A1 => A1:A
|
|
83
|
+
else if (noLeft && !noTop && !noRight && !noBottom) {
|
|
84
|
+
return toColStr(right, $right) + toRowStr(top, $top) + ':' + toRowStr(bottom, $bottom);
|
|
55
85
|
}
|
|
56
86
|
// A1:A1
|
|
57
|
-
if (right
|
|
58
|
-
return
|
|
87
|
+
else if (right !== left || bottom !== top || $right !== $left || $bottom !== $top) {
|
|
88
|
+
return toColStr(left, $left) + toRowStr(top, $top) + ':' + toColStr(right, $right) + toRowStr(bottom, $bottom);
|
|
59
89
|
}
|
|
60
90
|
// A1
|
|
61
|
-
return
|
|
91
|
+
return toColStr(left, $left) + toRowStr(top, $top);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function splitA1 (str) {
|
|
95
|
+
const m = /^(?=.)(\$(?=\D))?([A-Za-z]{0,3})?(\$)?([1-9][0-9]{0,6})?$/.exec(str);
|
|
96
|
+
if (!m || (!m[2] && !m[4])) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return [
|
|
100
|
+
m[4] ? fromRow(m[4]) : null, // row index or null
|
|
101
|
+
m[2] ? fromCol(m[2]) : null, // col index or null
|
|
102
|
+
!!m[3], // is row absolute?
|
|
103
|
+
!!m[1] // is col absolute?
|
|
104
|
+
];
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
export function fromA1 (rangeStr) {
|
|
65
|
-
let
|
|
66
|
-
let
|
|
67
|
-
let
|
|
68
|
-
let
|
|
69
|
-
let right = MAX_COLS;
|
|
108
|
+
let top = null;
|
|
109
|
+
let left = null;
|
|
110
|
+
let bottom = null;
|
|
111
|
+
let right = null;
|
|
70
112
|
let $top = false;
|
|
71
113
|
let $left = false;
|
|
72
114
|
let $bottom = false;
|
|
73
115
|
let $right = false;
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
const b = fromCol(m[4]);
|
|
78
|
-
left = Math.min(a, b);
|
|
79
|
-
right = Math.max(a, b);
|
|
80
|
-
$left = !!m[a <= b ? 1 : 3];
|
|
81
|
-
$right = !!m[a <= b ? 3 : 1];
|
|
82
|
-
$top = true;
|
|
83
|
-
$bottom = true;
|
|
84
|
-
return { top, left, bottom, right, $top, $left, $bottom, $right };
|
|
116
|
+
const [ part1, part2, part3 ] = rangeStr.split(':');
|
|
117
|
+
if (part3) {
|
|
118
|
+
return null;
|
|
85
119
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
$left =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
else {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
bottom = fromRow(m[4]);
|
|
109
|
-
$right = !!m[1];
|
|
110
|
-
$bottom = !!m[3];
|
|
111
|
-
// need to flip?
|
|
112
|
-
if (bottom < top) {
|
|
113
|
-
[ top, bottom, $top, $bottom ] = [ bottom, top, $bottom, $top ];
|
|
114
|
-
}
|
|
115
|
-
if (right < left) {
|
|
116
|
-
[ left, right, $left, $right ] = [ right, left, $right, $left ];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
bottom = top;
|
|
121
|
-
right = left;
|
|
122
|
-
$bottom = $top;
|
|
123
|
-
$right = $left;
|
|
124
|
-
}
|
|
125
|
-
return { top, left, bottom, right, $top, $left, $bottom, $right };
|
|
120
|
+
const p1 = splitA1(part1);
|
|
121
|
+
const p2 = part2 ? splitA1(part2) : null;
|
|
122
|
+
if (!p1 || (part2 && !p2)) {
|
|
123
|
+
// invalid section
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// part 1 bits
|
|
127
|
+
if (p1[0] != null && p1[1] != null) {
|
|
128
|
+
[ top, left, $top, $left ] = p1;
|
|
129
|
+
}
|
|
130
|
+
else if (p1[0] == null && p1[1] != null) {
|
|
131
|
+
[ , left, , $left ] = p1;
|
|
132
|
+
}
|
|
133
|
+
else if (p1[0] != null && p1[1] == null) {
|
|
134
|
+
[ top, , $top ] = p1;
|
|
135
|
+
}
|
|
136
|
+
// part 2 bits
|
|
137
|
+
if (!part2) {
|
|
138
|
+
// part 2 must exist if either top or left is null:
|
|
139
|
+
// this disallows a single num or col patterns
|
|
140
|
+
if (top == null || left == null) {
|
|
141
|
+
return null;
|
|
126
142
|
}
|
|
143
|
+
bottom = top;
|
|
144
|
+
right = left;
|
|
145
|
+
$bottom = $top;
|
|
146
|
+
$right = $left;
|
|
127
147
|
}
|
|
128
|
-
|
|
148
|
+
else if (p2[0] != null && p2[1] != null) {
|
|
149
|
+
[ bottom, right, $bottom, $right ] = p2;
|
|
150
|
+
}
|
|
151
|
+
else if (p2[0] == null && p2[1] != null) {
|
|
152
|
+
[ , right, , $right ] = p2;
|
|
153
|
+
}
|
|
154
|
+
else if (p2[0] != null && p2[1] == null) {
|
|
155
|
+
[ bottom, , $bottom ] = p2;
|
|
156
|
+
}
|
|
157
|
+
// flip left/right and top/bottom as needed
|
|
158
|
+
// for partial ranges we perfer the coord on the left-side of the :
|
|
159
|
+
if (right != null && (left == null || (left != null && right < left))) {
|
|
160
|
+
[ left, right, $left, $right ] = [ right, left, $right, $left ];
|
|
161
|
+
}
|
|
162
|
+
if (bottom != null && (top == null || (top != null && bottom < top))) {
|
|
163
|
+
[ top, bottom, $top, $bottom ] = [ bottom, top, $bottom, $top ];
|
|
164
|
+
}
|
|
165
|
+
return { top, left, bottom, right, $top, $left, $bottom, $right };
|
|
129
166
|
}
|
|
130
167
|
|
|
131
|
-
export function parseA1Ref (ref,
|
|
132
|
-
const d = parseRef(ref,
|
|
168
|
+
export function parseA1Ref (ref, { allowNamed = true, allowTernary = false } = {}) {
|
|
169
|
+
const d = parseRef(ref, { allowNamed, allowTernary, r1c1: false });
|
|
133
170
|
if (d && (d.r0 || d.name)) {
|
|
134
171
|
let range = null;
|
|
135
172
|
if (d.r0) {
|
|
136
|
-
range = d.r1 ?
|
|
173
|
+
range = fromA1(d.r1 ? d.r0 + ':' + d.r1 : d.r0);
|
|
137
174
|
}
|
|
138
175
|
if (d.name || range) {
|
|
139
176
|
d.range = range;
|
|
@@ -148,6 +185,32 @@ export function parseA1Ref (ref, allow_named = true) {
|
|
|
148
185
|
return null;
|
|
149
186
|
}
|
|
150
187
|
|
|
188
|
+
export function stringifyA1Ref (ref) {
|
|
189
|
+
return stringifyPrefix(ref) + (
|
|
190
|
+
ref.name ? ref.name : toA1(ref.range)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function addRangeBounds (range) {
|
|
195
|
+
if (range.top == null) {
|
|
196
|
+
range.top = 0;
|
|
197
|
+
range.$top = false;
|
|
198
|
+
}
|
|
199
|
+
if (range.bottom == null) {
|
|
200
|
+
range.bottom = MAX_ROWS;
|
|
201
|
+
range.$bottom = false;
|
|
202
|
+
}
|
|
203
|
+
if (range.left == null) {
|
|
204
|
+
range.left = 0;
|
|
205
|
+
range.$left = false;
|
|
206
|
+
}
|
|
207
|
+
if (range.right == null) {
|
|
208
|
+
range.right = MAX_COLS;
|
|
209
|
+
range.$right = false;
|
|
210
|
+
}
|
|
211
|
+
return range;
|
|
212
|
+
}
|
|
213
|
+
|
|
151
214
|
export default {
|
|
152
215
|
fromCol,
|
|
153
216
|
toCol,
|
|
@@ -155,5 +218,7 @@ export default {
|
|
|
155
218
|
toAbsolute,
|
|
156
219
|
to: toA1,
|
|
157
220
|
from: fromA1,
|
|
158
|
-
parse: parseA1Ref
|
|
221
|
+
parse: parseA1Ref,
|
|
222
|
+
addBounds: addRangeBounds,
|
|
223
|
+
stringify: stringifyA1Ref
|
|
159
224
|
};
|
package/lib/a1.spec.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/* eslint-disable object-property-newline, object-curly-newline */
|
|
2
|
+
import { test, Test } from 'tape';
|
|
3
|
+
import {
|
|
4
|
+
fromCol,
|
|
5
|
+
toCol,
|
|
6
|
+
fromRow,
|
|
7
|
+
toRow,
|
|
8
|
+
toRelative,
|
|
9
|
+
toAbsolute,
|
|
10
|
+
parseA1Ref,
|
|
11
|
+
stringifyA1Ref,
|
|
12
|
+
toA1
|
|
13
|
+
} from './a1.js';
|
|
14
|
+
import { MAX_COLS, MAX_ROWS } from './constants.js';
|
|
15
|
+
|
|
16
|
+
Test.prototype.isA1Equal = function isTokens (expr, expect, opts) {
|
|
17
|
+
if (expect) {
|
|
18
|
+
expect = {
|
|
19
|
+
context: [],
|
|
20
|
+
name: '',
|
|
21
|
+
range: null,
|
|
22
|
+
...expect
|
|
23
|
+
};
|
|
24
|
+
if (expect.range && typeof expect.range === 'object') {
|
|
25
|
+
// mix in some defaults so we don't have to write things out in full
|
|
26
|
+
expect.range = {
|
|
27
|
+
top: null, left: null, bottom: null, right: null,
|
|
28
|
+
$top: false, $left: false, $bottom: false, $right: false,
|
|
29
|
+
...expect.range
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.deepEqual(parseA1Ref(expr, opts), expect, expr);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// What happens when B2:A1 -> should work!
|
|
37
|
+
test('convert to and from column and row ids', t => {
|
|
38
|
+
t.is(fromCol('a'), 0);
|
|
39
|
+
t.is(fromCol('A'), 0);
|
|
40
|
+
t.is(fromCol('AA'), 26);
|
|
41
|
+
t.is(fromCol('zz'), 701);
|
|
42
|
+
t.is(fromCol('ZZZ'), 18277);
|
|
43
|
+
t.is(toCol(0), 'A');
|
|
44
|
+
t.is(toCol(26), 'AA');
|
|
45
|
+
t.is(toCol(701), 'ZZ');
|
|
46
|
+
t.is(toCol(18277), 'ZZZ');
|
|
47
|
+
t.is(fromRow('11'), 10);
|
|
48
|
+
t.is(fromRow('1'), 0);
|
|
49
|
+
t.is(toRow(12), '13');
|
|
50
|
+
t.is(toRow(77), '78');
|
|
51
|
+
t.end();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('parse A1 references', t => {
|
|
55
|
+
t.isA1Equal('A1', { range: { top: 0, left: 0, bottom: 0, right: 0 } });
|
|
56
|
+
t.isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } });
|
|
57
|
+
|
|
58
|
+
t.isA1Equal('$A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $left: true } });
|
|
59
|
+
t.isA1Equal('A$1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $top: true } });
|
|
60
|
+
t.isA1Equal('A1:$B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $right: true } });
|
|
61
|
+
t.isA1Equal('A1:B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, $bottom: true } });
|
|
62
|
+
|
|
63
|
+
t.isA1Equal('A:A', { range: { left: 0, right: 0 } });
|
|
64
|
+
t.isA1Equal('C:C', { range: { left: 2, right: 2 } });
|
|
65
|
+
t.isA1Equal('C:$C', { range: { left: 2, right: 2, $right: true } });
|
|
66
|
+
t.isA1Equal('$C:C', { range: { left: 2, right: 2, $left: true } });
|
|
67
|
+
t.isA1Equal('$C:$C', { range: { left: 2, right: 2, $left: true, $right: true } });
|
|
68
|
+
|
|
69
|
+
t.isA1Equal('1:1', { range: { top: 0, bottom: 0 } });
|
|
70
|
+
t.isA1Equal('10:10', { range: { top: 9, bottom: 9 } });
|
|
71
|
+
t.isA1Equal('10:$10', { range: { top: 9, bottom: 9, $bottom: true } });
|
|
72
|
+
t.isA1Equal('$10:10', { range: { top: 9, bottom: 9, $top: true } });
|
|
73
|
+
t.isA1Equal('$10:$10', { range: { top: 9, bottom: 9, $top: true, $bottom: true } });
|
|
74
|
+
|
|
75
|
+
t.isA1Equal('XFD1048576', { range: { top: 1048575, left: 16383, bottom: 1048575, right: 16383 } });
|
|
76
|
+
|
|
77
|
+
t.isA1Equal('Sheet1!A1', {
|
|
78
|
+
context: [ 'Sheet1' ],
|
|
79
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
t.isA1Equal('\'Sheet1\'!A1', {
|
|
83
|
+
context: [ 'Sheet1' ],
|
|
84
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
t.isA1Equal('\'Sheet1\'\'s\'!A1', {
|
|
88
|
+
context: [ 'Sheet1\'s' ],
|
|
89
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
t.isA1Equal('[Workbook.xlsx]Sheet1!A1', {
|
|
93
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
94
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
t.isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
|
|
98
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
99
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
t.isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
|
|
103
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
104
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
t.isA1Equal("='[Workbook.xlsx]Sheet1'!A1", {
|
|
108
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
109
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
t.isA1Equal('[foo bar]Sheet1!A1', {
|
|
113
|
+
context: [ 'foo bar', 'Sheet1' ],
|
|
114
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
t.isA1Equal('[a "b" c]d!A1', {
|
|
118
|
+
context: [ 'a "b" c', 'd' ],
|
|
119
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// unless we know the contexts available, we don't know that this is a sheet
|
|
123
|
+
// or a filename, so we can't reject it:
|
|
124
|
+
t.isA1Equal('0123456789abcdefghijklmnopqrstuvwxyz!A1', {
|
|
125
|
+
context: [ '0123456789abcdefghijklmnopqrstuvwxyz' ],
|
|
126
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
t.isA1Equal('[Workbook.xlsx]!A1', null);
|
|
130
|
+
t.isA1Equal('[Workbook.xlsx]!A1:B2', null);
|
|
131
|
+
t.isA1Equal('[Workbook.xlsx]!A:A', null);
|
|
132
|
+
t.isA1Equal('[Workbook.xlsx]!1:1', null);
|
|
133
|
+
t.isA1Equal('[]Sheet1!A1', null);
|
|
134
|
+
t.isA1Equal('namedrange', { name: 'namedrange' });
|
|
135
|
+
|
|
136
|
+
t.isA1Equal('Workbook.xlsx!namedrange', {
|
|
137
|
+
context: [ 'Workbook.xlsx' ],
|
|
138
|
+
name: 'namedrange'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
t.isA1Equal("'Workbook.xlsx'!namedrange", {
|
|
142
|
+
context: [ 'Workbook.xlsx' ],
|
|
143
|
+
name: 'namedrange'
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
t.isA1Equal('[Workbook.xlsx]!namedrange', null);
|
|
147
|
+
t.isA1Equal('pensioneligibilitypartner1', { name: 'pensioneligibilitypartner1' });
|
|
148
|
+
t.isA1Equal('XFE1048577', { name: 'XFE1048577' });
|
|
149
|
+
|
|
150
|
+
// with named ranges disallowed
|
|
151
|
+
t.isA1Equal('namedrange', null, { allowNamed: false });
|
|
152
|
+
t.isA1Equal('Workbook.xlsx!namedrange', null, { allowNamed: false });
|
|
153
|
+
t.isA1Equal('pensioneligibilitypartner1', null, { allowNamed: false });
|
|
154
|
+
t.isA1Equal('XFE1048577', null, { allowNamed: false });
|
|
155
|
+
|
|
156
|
+
t.end();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('A1 partial ranges', t => {
|
|
160
|
+
const opt = { allowTernary: true };
|
|
161
|
+
// partials are not allowed by defult
|
|
162
|
+
t.isA1Equal('A10:A', null);
|
|
163
|
+
t.isA1Equal('B3:2', null);
|
|
164
|
+
// unbounded bottom:
|
|
165
|
+
t.isA1Equal('A10:A', { range: { top: 9, left: 0, right: 0 } }, opt);
|
|
166
|
+
t.isA1Equal('A:A10', { range: { top: 9, left: 0, right: 0 } }, opt);
|
|
167
|
+
t.isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
168
|
+
t.isA1Equal('A:A$5', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
169
|
+
t.isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
170
|
+
t.isA1Equal('A:$B5', { range: { top: 4, left: 0, right: 1, $right: true } }, opt);
|
|
171
|
+
t.isA1Equal('$B:B3', { range: { top: 2, left: 1, right: 1, $left: true } }, opt);
|
|
172
|
+
t.isA1Equal('$B:C5', { range: { top: 4, left: 1, right: 2, $left: true } }, opt);
|
|
173
|
+
t.isA1Equal('C2:B', { range: { top: 1, left: 1, right: 2 } }, opt);
|
|
174
|
+
t.isA1Equal('C:B2', { range: { top: 1, left: 1, right: 2 } }, opt);
|
|
175
|
+
// unbounded right:
|
|
176
|
+
t.isA1Equal('D1:1', { range: { top: 0, left: 3, bottom: 0 } }, opt);
|
|
177
|
+
t.isA1Equal('1:D2', { range: { top: 0, left: 3, bottom: 1 } }, opt);
|
|
178
|
+
t.isA1Equal('2:$D3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
|
|
179
|
+
t.isA1Equal('$D2:3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
|
|
180
|
+
t.isA1Equal('1:D$1', { range: { top: 0, left: 3, bottom: 0, $bottom: true } }, opt);
|
|
181
|
+
t.isA1Equal('$1:D1', { range: { top: 0, left: 3, bottom: 0, $top: true } }, opt);
|
|
182
|
+
t.isA1Equal('AA$3:4', { range: { top: 2, left: 26, bottom: 3, $top: true } }, opt);
|
|
183
|
+
t.isA1Equal('B3:2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
|
|
184
|
+
t.isA1Equal('3:B2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
|
|
185
|
+
t.end();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('A1 serialization', t => {
|
|
189
|
+
// cell: A1
|
|
190
|
+
t.is(toA1({ top: 9, bottom: 9, left: 2, right: 2 }), 'C10', 'C10');
|
|
191
|
+
t.is(toA1({ top: 9, bottom: 9, left: 2, right: 2, $top: true, $bottom: true }), 'C$10', 'C$10');
|
|
192
|
+
t.is(toA1({ top: 9, bottom: 9, left: 2, right: 2, $left: true, $right: true }), '$C10', '$C10');
|
|
193
|
+
t.is(toA1({ top: 9, bottom: 9, left: 2, right: 2, $top: true, $bottom: true, $left: true, $right: true }), '$C$10', '$C$10');
|
|
194
|
+
// rect: A1:A1
|
|
195
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4 }), 'E3', 'E3');
|
|
196
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4, $right: true }), 'E3:$E3', 'E3:$E3');
|
|
197
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4, $top: true }), 'E$3:E3', 'E$3:E3');
|
|
198
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4, $left: true }), '$E3:E3', '$E3:E3');
|
|
199
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4, $bottom: true }), 'E3:E$3', 'E3:E$3');
|
|
200
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 4, $bottom: true, $right: true }), 'E3:$E$3', 'E3:$E$3');
|
|
201
|
+
t.is(toA1({ top: 2, bottom: 2, left: 4, right: 5 }), 'E3:F3', 'E3:F3');
|
|
202
|
+
t.is(toA1({ top: 2, bottom: 3, left: 4, right: 4 }), 'E3:E4', 'E3:E4');
|
|
203
|
+
t.is(toA1({ top: 2, bottom: 3, left: 4, right: 5 }), 'E3:F4', 'E3:F4');
|
|
204
|
+
// ray: A:A, 1:1
|
|
205
|
+
t.is(toA1({ left: 0, right: 0 }), 'A:A', '1:A');
|
|
206
|
+
t.is(toA1({ top: 0, bottom: MAX_ROWS, left: 0, right: 0 }), 'A:A', 'A:A (2)');
|
|
207
|
+
t.is(toA1({ left: 10, right: 15 }), 'K:P', 'K:P');
|
|
208
|
+
t.is(toA1({ left: 10, right: 15, $left: true }), '$K:P', '$K:P');
|
|
209
|
+
t.is(toA1({ left: 10, right: 15, $right: true }), 'K:$P', 'K:$P');
|
|
210
|
+
t.is(toA1({ left: 10, right: 15, $left: true, $right: true }), '$K:$P', '$K:$P');
|
|
211
|
+
t.is(toA1({ top: 0, bottom: 0 }), '1:1', '1:1');
|
|
212
|
+
t.is(toA1({ top: 0, bottom: 0, left: 0, right: MAX_COLS }), '1:1', '1:1 (2)');
|
|
213
|
+
t.is(toA1({ top: 10, bottom: 15 }), '11:16', '11:16');
|
|
214
|
+
t.is(toA1({ top: 10, bottom: 15, $top: true }), '$11:16', '$11:16');
|
|
215
|
+
t.is(toA1({ top: 10, bottom: 15, $bottom: true }), '11:$16', '11:$16');
|
|
216
|
+
t.is(toA1({ top: 10, bottom: 15, $top: true, $bottom: true }), '$11:$16', '$11:$16');
|
|
217
|
+
// partial: A1:A, A1:1, A:A1, 1:A1
|
|
218
|
+
t.is(toA1({ top: 9, left: 0, right: 0 }), 'A10:A', 'A10:A');
|
|
219
|
+
t.is(toA1({ bottom: 9, left: 0, right: 0 }), 'A10:A', 'A:A10 → A10:A');
|
|
220
|
+
t.is(toA1({ top: 9, left: 0, right: 0, $top: true }), 'A$10:A', 'A$10:A');
|
|
221
|
+
t.is(toA1({ top: 9, left: 0, right: 0, $left: true }), '$A10:A', '$A10:A');
|
|
222
|
+
t.is(toA1({ top: 9, left: 0, right: 0, $right: true }), 'A10:$A', 'A10:$A');
|
|
223
|
+
t.is(toA1({ top: 0, left: 3, bottom: 0 }), 'D1:1', 'D1:1');
|
|
224
|
+
t.is(toA1({ top: 0, right: 3, bottom: 0 }), 'D1:1', '1:D1 → D1:1');
|
|
225
|
+
t.is(toA1({ top: 0, left: 3, bottom: 0, $top: true }), 'D$1:1', 'D$1:1');
|
|
226
|
+
t.is(toA1({ top: 0, left: 3, bottom: 0, $left: true }), '$D1:1', '$D1:1');
|
|
227
|
+
t.is(toA1({ top: 0, left: 3, bottom: 0, $bottom: true }), 'D1:$1', 'D1:$1');
|
|
228
|
+
t.end();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('stringifyA1Ref', t => {
|
|
232
|
+
const rangeA1 = { top: 0, bottom: 0, left: 0, right: 0 };
|
|
233
|
+
const testRef = (ref, expect) => t.is(stringifyA1Ref(ref), expect, expect);
|
|
234
|
+
testRef({ range: rangeA1 }, 'A1');
|
|
235
|
+
testRef({ context: [ 'Sheet1' ], range: rangeA1 }, 'Sheet1!A1');
|
|
236
|
+
testRef({ context: [ 'Sheet 1' ], range: rangeA1 }, "'Sheet 1'!A1");
|
|
237
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 }, '[MyFile.xlsx]Sheet1!A1');
|
|
238
|
+
testRef({ context: [ 'My File.xlsx', 'Sheet1' ], range: rangeA1 }, "'[My File.xlsx]Sheet1'!A1");
|
|
239
|
+
testRef({ context: [ 'MyFile.xlsx' ], range: rangeA1 }, 'MyFile.xlsx!A1');
|
|
240
|
+
testRef({ context: [ 'My File.xlsx' ], range: rangeA1 }, "'My File.xlsx'!A1");
|
|
241
|
+
testRef({ name: 'foo' }, 'foo');
|
|
242
|
+
testRef({ context: [ 'Sheet1' ], name: 'foo' }, 'Sheet1!foo');
|
|
243
|
+
testRef({ context: [ 'Sheet 1' ], name: 'foo' }, "'Sheet 1'!foo");
|
|
244
|
+
testRef({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' }, '[MyFile.xlsx]Sheet1!foo');
|
|
245
|
+
testRef({ context: [ 'My File.xlsx', 'Sheet1' ], name: 'foo' }, "'[My File.xlsx]Sheet1'!foo");
|
|
246
|
+
testRef({ context: [ 'MyFile.xlsx' ], name: 'foo' }, 'MyFile.xlsx!foo');
|
|
247
|
+
testRef({ context: [ 'My File.xlsx' ], name: 'foo' }, "'My File.xlsx'!foo");
|
|
248
|
+
t.end();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('A1 utilities', t => {
|
|
252
|
+
const relA1Range = {
|
|
253
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
254
|
+
$top: false, $left: false, $bottom: false, $right: false
|
|
255
|
+
};
|
|
256
|
+
const absA1Range = {
|
|
257
|
+
top: 0, left: 0, bottom: 0, right: 0,
|
|
258
|
+
$top: true, $left: true, $bottom: true, $right: true
|
|
259
|
+
};
|
|
260
|
+
t.deepEqual(toAbsolute(relA1Range), absA1Range, 'toAbsolute');
|
|
261
|
+
t.deepEqual(toRelative(absA1Range), relA1Range, 'toRelative');
|
|
262
|
+
t.end();
|
|
263
|
+
});
|
|
264
|
+
|
package/lib/addMeta.js
CHANGED
|
@@ -1,24 +1,66 @@
|
|
|
1
|
-
import { RANGE, RANGE_BEAM, UNKNOWN } from './constants.js';
|
|
2
|
-
import { parseA1Ref
|
|
1
|
+
import { RANGE, RANGE_BEAM, RANGE_TERNARY, UNKNOWN } from './constants.js';
|
|
2
|
+
import { parseA1Ref } from './a1.js';
|
|
3
3
|
|
|
4
4
|
function getIDer () {
|
|
5
5
|
let i = 1;
|
|
6
6
|
return () => 'fxg' + (i++);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function sameValue (a, b) {
|
|
10
|
+
if (a == null && b == null) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return a === b;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sameStr (a, b) {
|
|
17
|
+
if (!a && !b) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return String(a).toLowerCase() === String(b).toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isEquivalent (refA, refB) {
|
|
24
|
+
// if named, name must match
|
|
25
|
+
if ((refA.name || refB.name) && refA.name !== refB.name) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// if ranged, range must have the same dimensions (we don't care about $)
|
|
29
|
+
if (refA.range || refB.range) {
|
|
30
|
+
if (
|
|
31
|
+
!sameValue(refA.range.top, refB.range.top) ||
|
|
32
|
+
!sameValue(refA.range.bottom, refB.range.bottom) ||
|
|
33
|
+
!sameValue(refA.range.left, refB.range.left) ||
|
|
34
|
+
!sameValue(refA.range.right, refB.range.right)
|
|
35
|
+
) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// must have same context
|
|
40
|
+
if (
|
|
41
|
+
!sameStr(refA.context[0], refB.context[0]) ||
|
|
42
|
+
!sameStr(refA.context[1], refB.context[1])
|
|
43
|
+
) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
9
49
|
// when context is Sheet1, we should consider Sheet!A1 == A1
|
|
10
50
|
export function addMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
11
51
|
const parenStack = [];
|
|
12
52
|
let arrayStart = null;
|
|
13
|
-
const a1Map = {};
|
|
14
53
|
const uid = getIDer();
|
|
54
|
+
const knownRefs = [];
|
|
55
|
+
|
|
56
|
+
const getCurrDepth = () => parenStack.length + (arrayStart ? 1 : 0);
|
|
15
57
|
|
|
16
58
|
tokens.forEach((token, i) => {
|
|
17
59
|
token.index = i;
|
|
18
|
-
token.depth =
|
|
60
|
+
token.depth = getCurrDepth();
|
|
19
61
|
if (token.value === '(') {
|
|
20
|
-
token.depth = parenStack.length + 1;
|
|
21
62
|
parenStack.push(token);
|
|
63
|
+
token.depth = getCurrDepth();
|
|
22
64
|
}
|
|
23
65
|
else if (token.value === ')') {
|
|
24
66
|
const counter = parenStack.pop();
|
|
@@ -35,6 +77,7 @@ export function addMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
|
35
77
|
else if (token.value === '{') {
|
|
36
78
|
if (!arrayStart) {
|
|
37
79
|
arrayStart = token;
|
|
80
|
+
token.depth = getCurrDepth();
|
|
38
81
|
}
|
|
39
82
|
else {
|
|
40
83
|
token.error = true;
|
|
@@ -44,6 +87,7 @@ export function addMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
|
44
87
|
if (arrayStart) {
|
|
45
88
|
const pairId = uid();
|
|
46
89
|
token.groupId = pairId;
|
|
90
|
+
token.depth = arrayStart.depth;
|
|
47
91
|
arrayStart.groupId = pairId;
|
|
48
92
|
}
|
|
49
93
|
else {
|
|
@@ -51,16 +95,31 @@ export function addMeta (tokens, { sheetName = '', workbookName = '' } = {}) {
|
|
|
51
95
|
}
|
|
52
96
|
arrayStart = null;
|
|
53
97
|
}
|
|
54
|
-
else if (token.type === RANGE || token.type === RANGE_BEAM) {
|
|
55
|
-
const ref = parseA1Ref(token.value, false);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
98
|
+
else if (token.type === RANGE || token.type === RANGE_BEAM || token.type === RANGE_TERNARY) {
|
|
99
|
+
const ref = parseA1Ref(token.value, { allowNamed: false, allowTernary: true });
|
|
100
|
+
if (ref && ref.range) {
|
|
101
|
+
ref.source = token.value;
|
|
102
|
+
if (!ref.context.length) {
|
|
103
|
+
ref.context = [ workbookName, sheetName ];
|
|
104
|
+
}
|
|
105
|
+
else if (ref.context.length === 1) {
|
|
106
|
+
const scope = ref.context[0];
|
|
107
|
+
if (scope === sheetName || scope === workbookName) {
|
|
108
|
+
ref.context = [ workbookName, sheetName ];
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// a single scope on a non-named range is going to be a sheet name
|
|
112
|
+
ref.context = [ workbookName, scope ];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const known = knownRefs.find(d => isEquivalent(d, ref));
|
|
116
|
+
if (known) {
|
|
117
|
+
token.groupId = known.groupId;
|
|
60
118
|
}
|
|
61
119
|
else {
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
ref.groupId = uid();
|
|
121
|
+
token.groupId = ref.groupId;
|
|
122
|
+
knownRefs.push(ref);
|
|
64
123
|
}
|
|
65
124
|
}
|
|
66
125
|
}
|