@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/lib/a1.js CHANGED
@@ -1,27 +1,35 @@
1
- import { MAX_ROWS, MAX_COLS, tokenHandlersRefsA1 } from './constants.js';
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 (colStr) {
5
- const c = (colStr || '').toUpperCase();
6
- let d = 0;
7
- let i = 0;
8
- for (; i !== c.length; ++i) {
9
- const chr = c.charCodeAt(i);
10
- if (chr >= 65 && chr <= 90) { // omits any non A-Z character
11
- d = 26 * d + chr - 64;
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
- return d - 1;
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
- let n = left;
19
- let c = '';
20
- while (n >= 0) {
21
- c = String.fromCharCode(n % 26 + 65) + c;
22
- n = Math.floor(n / 26) - 1;
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 === MAX_ROWS) {
50
- return toAbs($left) + toCol(left) + ':' + toAbs($right) + toCol(right);
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 === MAX_COLS) {
54
- return toAbs($top) + toRow(top) + ':' + toAbs($bottom) + toRow(bottom);
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 != null && bottom != null && (right !== left || bottom !== top)) {
58
- return toAbs($left) + toCol(left) + toAbs($top) + toRow(top) + ':' + toAbs($right) + toCol(right) + toAbs($bottom) + toRow(bottom);
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 toAbs($left) + toCol(left) + toAbs($top) + toRow(top);
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 m;
66
- let top = 0;
67
- let left = 0;
68
- let bottom = MAX_ROWS;
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
- // A:A
75
- if ((m = /^(\$?)([A-Z]{1,3}):(\$?)([A-Z]{1,3})$/.exec(rangeStr))) {
76
- const a = fromCol(m[2]);
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
- // 1:1
87
- else if ((m = /^(\$?)([1-9]\d{0,6}):(\$?)([1-9]\d{0,6})$/.exec(rangeStr))) {
88
- const a = fromRow(m[2]);
89
- const b = fromRow(m[4]);
90
- top = Math.min(a, b);
91
- bottom = Math.max(a, b);
92
- $top = !!m[a <= b ? 1 : 3];
93
- $bottom = !!m[a <= b ? 3 : 1];
94
- $left = true;
95
- $right = true;
96
- return { top, left, bottom, right, $top, $left, $bottom, $right };
97
- }
98
- // A1 | A1:B2
99
- else {
100
- const [ part1, part2 ] = rangeStr.split(':');
101
- if ((m = /^(\$?)([A-Z]{1,3})(\$?)([1-9]\d{0,6})$/i.exec(part1))) {
102
- left = fromCol(m[2]);
103
- top = fromRow(m[4]);
104
- $left = !!m[1];
105
- $top = !!m[3];
106
- if (part2 && (m = /^(\$?)([A-Z]{1,3})(\$?)([1-9]\d{0,6})$/i.exec(part2))) {
107
- right = fromCol(m[2]);
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
- return null;
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, allow_named = true) {
132
- const d = parseRef(ref, allow_named, tokenHandlersRefsA1);
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 ? fromA1(d.r0 + ':' + d.r1) : fromA1(d.r0);
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, toRelative, toA1 } from './a1.js';
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 = parenStack.length;
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
- const a1 = ref && (`[${ref.workbookName || workbookName}]${ref.sheetName || sheetName}!${ref.range ? toA1(toRelative(ref.range)) : ref.name}`).toLowerCase();
57
- if (a1) {
58
- if (a1 in a1Map) {
59
- token.groupId = a1Map[a1];
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
- token.groupId = uid();
63
- a1Map[a1] = token.groupId;
120
+ ref.groupId = uid();
121
+ token.groupId = ref.groupId;
122
+ knownRefs.push(ref);
64
123
  }
65
124
  }
66
125
  }