@borgar/fx 4.13.0 → 5.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/dist/index-BMr6cTgc.d.cts +1444 -0
- package/dist/index-BMr6cTgc.d.ts +1444 -0
- package/dist/index.cjs +3054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2984 -0
- package/dist/index.js.map +1 -0
- package/dist/xlsx/index.cjs +3120 -0
- package/dist/xlsx/index.cjs.map +1 -0
- package/dist/xlsx/index.d.cts +55 -0
- package/dist/xlsx/index.d.ts +55 -0
- package/dist/xlsx/index.js +3049 -0
- package/dist/xlsx/index.js.map +1 -0
- package/docs/API.md +2959 -718
- package/docs/AST_format.md +2 -2
- package/eslint.config.mjs +40 -0
- package/lib/a1.spec.ts +32 -0
- package/lib/a1.ts +26 -0
- package/lib/addA1RangeBounds.ts +50 -0
- package/lib/addTokenMeta.spec.ts +166 -0
- package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
- package/lib/astTypes.ts +211 -0
- package/lib/cloneToken.ts +29 -0
- package/lib/{constants.js → constants.ts} +6 -3
- package/lib/fixRanges.spec.ts +220 -0
- package/lib/fixRanges.ts +260 -0
- package/lib/fromCol.spec.ts +15 -0
- package/lib/{fromCol.js → fromCol.ts} +1 -1
- package/lib/index.spec.ts +119 -0
- package/lib/index.ts +76 -0
- package/lib/isNodeType.ts +151 -0
- package/lib/isType.spec.ts +208 -0
- package/lib/{isType.js → isType.ts} +26 -25
- package/lib/lexers/{advRangeOp.js → advRangeOp.ts} +1 -1
- package/lib/lexers/{canEndRange.js → canEndRange.ts} +2 -2
- package/lib/lexers/{lexBoolean.js → lexBoolean.ts} +25 -6
- package/lib/lexers/{lexContext.js → lexContext.ts} +14 -6
- package/lib/lexers/{lexError.js → lexError.ts} +3 -3
- package/lib/lexers/{lexFunction.js → lexFunction.ts} +3 -2
- package/lib/lexers/lexNameFuncCntx.ts +112 -0
- package/lib/lexers/{lexNamed.js → lexNamed.ts} +4 -4
- package/lib/lexers/{lexNewLine.js → lexNewLine.ts} +3 -2
- package/lib/lexers/{lexNumber.js → lexNumber.ts} +4 -3
- package/lib/lexers/{lexOperator.js → lexOperator.ts} +5 -4
- package/lib/lexers/lexRange.ts +15 -0
- package/lib/lexers/{lexRangeA1.js → lexRangeA1.ts} +11 -7
- package/lib/lexers/{lexRangeR1C1.js → lexRangeR1C1.ts} +10 -6
- package/lib/lexers/{lexRangeTrim.js → lexRangeTrim.ts} +3 -2
- package/lib/lexers/{lexRefOp.js → lexRefOp.ts} +4 -3
- package/lib/lexers/{lexString.js → lexString.ts} +3 -3
- package/lib/lexers/{lexStructured.js → lexStructured.ts} +5 -5
- package/lib/lexers/{lexWhitespace.js → lexWhitespace.ts} +3 -2
- package/lib/lexers/sets.ts +51 -0
- package/lib/mergeRefTokens.spec.ts +141 -0
- package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +14 -9
- package/lib/nodeTypes.ts +54 -0
- package/lib/parse.spec.ts +1410 -0
- package/lib/{parser.js → parse.ts} +81 -63
- package/lib/parseA1Range.spec.ts +233 -0
- package/lib/parseA1Range.ts +206 -0
- package/lib/parseA1Ref.spec.ts +337 -0
- package/lib/parseA1Ref.ts +115 -0
- package/lib/parseR1C1Range.ts +191 -0
- package/lib/parseR1C1Ref.spec.ts +323 -0
- package/lib/parseR1C1Ref.ts +127 -0
- package/lib/parseRef.spec.ts +90 -0
- package/lib/parseRef.ts +240 -0
- package/lib/{parseSRange.js → parseSRange.ts} +15 -10
- package/lib/parseStructRef.spec.ts +168 -0
- package/lib/parseStructRef.ts +76 -0
- package/lib/stringifyA1Range.spec.ts +72 -0
- package/lib/stringifyA1Range.ts +72 -0
- package/lib/stringifyA1Ref.spec.ts +64 -0
- package/lib/stringifyA1Ref.ts +59 -0
- package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
- package/lib/stringifyR1C1Range.spec.ts +92 -0
- package/lib/stringifyR1C1Range.ts +73 -0
- package/lib/stringifyR1C1Ref.spec.ts +63 -0
- package/lib/stringifyR1C1Ref.ts +67 -0
- package/lib/stringifyStructRef.spec.ts +124 -0
- package/lib/stringifyStructRef.ts +113 -0
- package/lib/stringifyTokens.ts +15 -0
- package/lib/toCol.spec.ts +11 -0
- package/lib/{toCol.js → toCol.ts} +4 -4
- package/lib/tokenTypes.ts +76 -0
- package/lib/tokenize-srefs.spec.ts +429 -0
- package/lib/tokenize.spec.ts +2103 -0
- package/lib/tokenize.ts +346 -0
- package/lib/translate.spec.ts +35 -0
- package/lib/translateToA1.spec.ts +247 -0
- package/lib/translateToA1.ts +231 -0
- package/lib/translateToR1C1.spec.ts +227 -0
- package/lib/translateToR1C1.ts +145 -0
- package/lib/types.ts +179 -0
- package/lib/xlsx/index.spec.ts +27 -0
- package/lib/xlsx/index.ts +32 -0
- package/package.json +45 -31
- package/tsconfig.json +28 -0
- package/typedoc-ignore-links.ts +17 -0
- package/typedoc.json +41 -0
- package/.eslintrc +0 -22
- package/benchmark/benchmark.js +0 -48
- package/benchmark/formulas.json +0 -15677
- package/dist/fx.d.ts +0 -823
- package/dist/fx.js +0 -2
- package/dist/package.json +0 -1
- package/lib/a1.js +0 -348
- package/lib/a1.spec.js +0 -458
- package/lib/addTokenMeta.spec.js +0 -153
- package/lib/astTypes.js +0 -96
- package/lib/extraTypes.js +0 -74
- package/lib/fixRanges.js +0 -104
- package/lib/fixRanges.spec.js +0 -171
- package/lib/fromCol.spec.js +0 -11
- package/lib/index.js +0 -134
- package/lib/index.spec.js +0 -67
- package/lib/isType.spec.js +0 -168
- package/lib/lexer-srefs.spec.js +0 -324
- package/lib/lexer.js +0 -264
- package/lib/lexer.spec.js +0 -1953
- package/lib/lexers/lexRange.js +0 -8
- package/lib/lexers/sets.js +0 -38
- package/lib/mergeRefTokens.spec.js +0 -121
- package/lib/package.json +0 -1
- package/lib/parseRef.js +0 -157
- package/lib/parseRef.spec.js +0 -71
- package/lib/parseStructRef.js +0 -48
- package/lib/parseStructRef.spec.js +0 -164
- package/lib/parser.spec.js +0 -1208
- package/lib/rc.js +0 -341
- package/lib/rc.spec.js +0 -403
- package/lib/stringifyStructRef.js +0 -80
- package/lib/stringifyStructRef.spec.js +0 -182
- package/lib/toCol.spec.js +0 -11
- package/lib/translate-toA1.spec.js +0 -214
- package/lib/translate-toRC.spec.js +0 -197
- package/lib/translate.js +0 -239
- package/lib/translate.spec.js +0 -21
- package/rollup.config.mjs +0 -22
- package/tsd.json +0 -12
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { MAX_COLS, MAX_ROWS } from './constants.ts';
|
|
2
|
+
import type { RangeA1 } from './types.ts';
|
|
3
|
+
|
|
4
|
+
type TrimString = 'both' | 'head' | 'tail';
|
|
5
|
+
|
|
6
|
+
export function fromRow (rowStr: string): number {
|
|
7
|
+
return +rowStr - 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CHAR_DOLLAR = 36;
|
|
11
|
+
const CHAR_PERIOD = 46;
|
|
12
|
+
const CHAR_COLON = 58;
|
|
13
|
+
const CHAR_A_LC = 97;
|
|
14
|
+
const CHAR_A_UC = 65;
|
|
15
|
+
const CHAR_Z_LC = 122;
|
|
16
|
+
const CHAR_Z_UC = 90;
|
|
17
|
+
const CHAR_0 = 48;
|
|
18
|
+
const CHAR_1 = 49;
|
|
19
|
+
const CHAR_9 = 57;
|
|
20
|
+
|
|
21
|
+
function advRangeOp (str: string, pos: number): [ number, TrimString | '' ] {
|
|
22
|
+
const c0 = str.charCodeAt(pos);
|
|
23
|
+
if (c0 === CHAR_PERIOD) {
|
|
24
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
25
|
+
if (c1 === CHAR_COLON) {
|
|
26
|
+
return str.charCodeAt(pos + 2) === CHAR_PERIOD
|
|
27
|
+
? [ 3, 'both' ]
|
|
28
|
+
: [ 2, 'head' ];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (c0 === CHAR_COLON) {
|
|
32
|
+
const c1 = str.charCodeAt(pos + 1);
|
|
33
|
+
return c1 === CHAR_PERIOD
|
|
34
|
+
? [ 2, 'tail' ]
|
|
35
|
+
: [ 1, '' ];
|
|
36
|
+
}
|
|
37
|
+
return [ 0, '' ];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function advA1Col (str: string, pos: number): [ number, number, boolean ] {
|
|
41
|
+
// [A-Z]{1,3}
|
|
42
|
+
const start = pos;
|
|
43
|
+
const lock = str.charCodeAt(pos) === CHAR_DOLLAR;
|
|
44
|
+
if (lock) { pos++; }
|
|
45
|
+
const stop = pos + 3;
|
|
46
|
+
let col = 0;
|
|
47
|
+
do {
|
|
48
|
+
const c = str.charCodeAt(pos);
|
|
49
|
+
if (c >= CHAR_A_UC && c <= CHAR_Z_UC) {
|
|
50
|
+
col = (26 * col) + c - (CHAR_A_UC - 1);
|
|
51
|
+
pos++;
|
|
52
|
+
}
|
|
53
|
+
else if (c >= CHAR_A_LC && c <= CHAR_Z_LC) {
|
|
54
|
+
col = (26 * col) + c - (CHAR_A_LC - 1);
|
|
55
|
+
pos++;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
while (pos < stop && pos < str.length);
|
|
62
|
+
return (col && col <= MAX_COLS + 1)
|
|
63
|
+
? [ pos - start, col - 1, lock ]
|
|
64
|
+
: [ 0, 0, false ];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function advA1Row (str: string, pos: number): [number, number, boolean] {
|
|
68
|
+
// [1-9][0-9]{0,6}
|
|
69
|
+
const start = pos;
|
|
70
|
+
const lock = str.charCodeAt(pos) === CHAR_DOLLAR;
|
|
71
|
+
if (lock) { pos++; }
|
|
72
|
+
const stop = pos + 7;
|
|
73
|
+
let row = 0;
|
|
74
|
+
let c = str.charCodeAt(pos);
|
|
75
|
+
if (c >= CHAR_1 && c <= CHAR_9) {
|
|
76
|
+
row = (row * 10) + c - CHAR_0;
|
|
77
|
+
pos++;
|
|
78
|
+
do {
|
|
79
|
+
c = str.charCodeAt(pos);
|
|
80
|
+
if (c >= CHAR_0 && c <= CHAR_9) {
|
|
81
|
+
row = (row * 10) + c - CHAR_0;
|
|
82
|
+
pos++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
while (pos < stop && pos < str.length);
|
|
89
|
+
}
|
|
90
|
+
return (row && row <= MAX_ROWS + 1)
|
|
91
|
+
? [ pos - start, row - 1, lock ]
|
|
92
|
+
: [ 0, 0, false ];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function makeRange (
|
|
96
|
+
top: number | null,
|
|
97
|
+
$top: boolean | null,
|
|
98
|
+
left: number | null,
|
|
99
|
+
$left: boolean | null,
|
|
100
|
+
bottom: number | null,
|
|
101
|
+
$bottom: boolean | null,
|
|
102
|
+
right: number | null,
|
|
103
|
+
$right: boolean | null,
|
|
104
|
+
trim: TrimString | ''
|
|
105
|
+
): RangeA1 {
|
|
106
|
+
// flip left/right and top/bottom as needed
|
|
107
|
+
// for partial ranges we perfer the coord on the left-side of the:
|
|
108
|
+
if (right != null && (left == null || (left != null && right < left))) {
|
|
109
|
+
[ left, right, $left, $right ] = [ right, left, $right, $left ];
|
|
110
|
+
}
|
|
111
|
+
if (bottom != null && (top == null || (top != null && bottom < top))) {
|
|
112
|
+
[ top, bottom, $top, $bottom ] = [ bottom, top, $bottom, $top ];
|
|
113
|
+
}
|
|
114
|
+
const range: RangeA1 = { top, left, bottom, right, $top, $left, $bottom, $right };
|
|
115
|
+
if (trim) {
|
|
116
|
+
range.trim = trim;
|
|
117
|
+
}
|
|
118
|
+
return range;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse A1-style range string into a RangeA1 object.
|
|
123
|
+
*
|
|
124
|
+
* @param rangeString A1-style range string.
|
|
125
|
+
* @param [allowTernary] Permit ternary ranges like A2:A or B2:2.
|
|
126
|
+
* @return A reference object.
|
|
127
|
+
*/
|
|
128
|
+
export function parseA1Range (rangeString: string, allowTernary = true): RangeA1 | undefined {
|
|
129
|
+
let p = 0;
|
|
130
|
+
const [ leftChars, left, $left ] = advA1Col(rangeString, p);
|
|
131
|
+
let right = 0;
|
|
132
|
+
let $right = false;
|
|
133
|
+
let bottom = 0;
|
|
134
|
+
let $bottom = false;
|
|
135
|
+
let rightChars: number;
|
|
136
|
+
let bottomChars: number;
|
|
137
|
+
if (leftChars) {
|
|
138
|
+
// TLBR: could be A1:A1
|
|
139
|
+
// TL R: could be A1:A (if allowTernary)
|
|
140
|
+
// TLB : could be A1:1 (if allowTernary)
|
|
141
|
+
// LBR: could be A:A1 (if allowTernary)
|
|
142
|
+
// L R: could be A:A
|
|
143
|
+
p += leftChars;
|
|
144
|
+
const [ topChars, top, $top ] = advA1Row(rangeString, p);
|
|
145
|
+
p += topChars;
|
|
146
|
+
const [ op, trim ] = advRangeOp(rangeString, p);
|
|
147
|
+
if (op) {
|
|
148
|
+
p += op;
|
|
149
|
+
[ rightChars, right, $right ] = advA1Col(rangeString, p);
|
|
150
|
+
p += rightChars;
|
|
151
|
+
[ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p);
|
|
152
|
+
p += bottomChars;
|
|
153
|
+
if (topChars && bottomChars && rightChars) {
|
|
154
|
+
if (p === rangeString.length) {
|
|
155
|
+
return makeRange(top, $top, left, $left, bottom, $bottom, right, $right, trim);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (!topChars && !bottomChars) {
|
|
159
|
+
if (p === rangeString.length) {
|
|
160
|
+
return makeRange(null, false, left, $left, null, false, right, $right, trim);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (allowTernary && (bottomChars || rightChars) && p === rangeString.length) {
|
|
164
|
+
if (!topChars) {
|
|
165
|
+
return makeRange(null, false, left, $left, bottom, $bottom, right, $right, trim);
|
|
166
|
+
}
|
|
167
|
+
else if (!bottomChars) {
|
|
168
|
+
return makeRange(top, $top, left, $left, null, false, right, $right, trim);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
return makeRange(top, $top, left, $left, bottom, $bottom, null, false, trim);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// LT : this is A1
|
|
176
|
+
if (topChars && p === rangeString.length) {
|
|
177
|
+
return makeRange(top, $top, left, $left, top, $top, left, $left, trim);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// T B : could be 1:1
|
|
182
|
+
// T BR: could be 1:A1 (if allowTernary)
|
|
183
|
+
const [ topChars, top, $top ] = advA1Row(rangeString, p);
|
|
184
|
+
if (topChars) {
|
|
185
|
+
p += topChars;
|
|
186
|
+
const [ op, trim ] = advRangeOp(rangeString, p);
|
|
187
|
+
if (op) {
|
|
188
|
+
p += op;
|
|
189
|
+
[ rightChars, right, $right ] = advA1Col(rangeString, p);
|
|
190
|
+
p += rightChars;
|
|
191
|
+
[ bottomChars, bottom, $bottom ] = advA1Row(rangeString, p);
|
|
192
|
+
p += bottomChars;
|
|
193
|
+
if (rightChars && bottomChars && allowTernary) {
|
|
194
|
+
if (p === rangeString.length) {
|
|
195
|
+
return makeRange(top, $top, null, false, bottom, $bottom, right, $right, trim);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (!rightChars && bottomChars) {
|
|
199
|
+
if (p === rangeString.length) {
|
|
200
|
+
return makeRange(top, $top, null, false, bottom, $bottom, null, false, trim);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/* eslint-disable @stylistic/object-property-newline */
|
|
2
|
+
import { describe, test, expect } from 'vitest';
|
|
3
|
+
import { parseA1Ref, parseA1RefXlsx, type OptsParseA1Ref } from './parseA1Ref.ts';
|
|
4
|
+
|
|
5
|
+
type IsA1EqualOptions = OptsParseA1Ref & {
|
|
6
|
+
xlsx?: boolean,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function isA1Equal (expr: string, expected: any, opts?: IsA1EqualOptions) {
|
|
10
|
+
const xlsx = !!(opts?.xlsx);
|
|
11
|
+
if (expected) {
|
|
12
|
+
expected = xlsx
|
|
13
|
+
? { workbookName: '', sheetName: '', ...expected }
|
|
14
|
+
: { context: [], ...expected };
|
|
15
|
+
Object.assign(expected, expected);
|
|
16
|
+
if (expected.range && typeof expected.range === 'object') {
|
|
17
|
+
// mix in some defaults so we don't have to write things out in full
|
|
18
|
+
expected.range = {
|
|
19
|
+
top: null, left: null, bottom: null, right: null,
|
|
20
|
+
$top: false, $left: false, $bottom: false, $right: false,
|
|
21
|
+
...expected.range
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
expect(xlsx ? parseA1RefXlsx(expr, opts) : parseA1Ref(expr, opts)).toEqual(expected);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('parse A1 references', () => {
|
|
29
|
+
test('basic A1 references', () => {
|
|
30
|
+
isA1Equal('A1', { range: { top: 0, left: 0, bottom: 0, right: 0 } });
|
|
31
|
+
isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } });
|
|
32
|
+
|
|
33
|
+
isA1Equal('$A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $left: true } });
|
|
34
|
+
isA1Equal('A$1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $top: true } });
|
|
35
|
+
isA1Equal('A1:$B2', { range: { top: 0, left: 0, bottom: 1, right: 1, $right: true } });
|
|
36
|
+
isA1Equal('A1:B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, $bottom: true } });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('column and row ranges', () => {
|
|
40
|
+
isA1Equal('A:A', { range: { left: 0, right: 0 } });
|
|
41
|
+
isA1Equal('C:C', { range: { left: 2, right: 2 } });
|
|
42
|
+
isA1Equal('C:$C', { range: { left: 2, right: 2, $right: true } });
|
|
43
|
+
isA1Equal('$C:C', { range: { left: 2, right: 2, $left: true } });
|
|
44
|
+
isA1Equal('$C:$C', { range: { left: 2, right: 2, $left: true, $right: true } });
|
|
45
|
+
|
|
46
|
+
isA1Equal('1:1', { range: { top: 0, bottom: 0 } });
|
|
47
|
+
isA1Equal('10:10', { range: { top: 9, bottom: 9 } });
|
|
48
|
+
isA1Equal('10:$10', { range: { top: 9, bottom: 9, $bottom: true } });
|
|
49
|
+
isA1Equal('$10:10', { range: { top: 9, bottom: 9, $top: true } });
|
|
50
|
+
isA1Equal('$10:$10', { range: { top: 9, bottom: 9, $top: true, $bottom: true } });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('maximum ranges', () => {
|
|
54
|
+
isA1Equal('XFD1048576', { range: { top: 1048575, left: 16383, bottom: 1048575, right: 16383 } });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('sheet references', () => {
|
|
58
|
+
isA1Equal('Sheet1!A1', {
|
|
59
|
+
context: [ 'Sheet1' ],
|
|
60
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
isA1Equal('\'Sheet1\'!A1', {
|
|
64
|
+
context: [ 'Sheet1' ],
|
|
65
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
isA1Equal('\'Sheet1\'\'s\'!A1', {
|
|
69
|
+
context: [ 'Sheet1\'s' ],
|
|
70
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('workbook references', () => {
|
|
75
|
+
isA1Equal('[Workbook.xlsx]Sheet1!A1', {
|
|
76
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
77
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
|
|
81
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
82
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
isA1Equal("='[Workbook.xlsx]Sheet1'!A1", {
|
|
86
|
+
context: [ 'Workbook.xlsx', 'Sheet1' ],
|
|
87
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
isA1Equal('[foo bar]Sheet1!A1', {
|
|
91
|
+
context: [ 'foo bar', 'Sheet1' ],
|
|
92
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
isA1Equal('[a "b" c]d!A1', {
|
|
96
|
+
context: [ 'a "b" c', 'd' ],
|
|
97
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('long context names', () => {
|
|
102
|
+
// unless we know the contexts available, we don't know that this is a sheet
|
|
103
|
+
// or a filename, so we can't reject it:
|
|
104
|
+
isA1Equal('0123456789abcdefghijklmnopqrstuvwxyz!A1', {
|
|
105
|
+
context: [ '0123456789abcdefghijklmnopqrstuvwxyz' ],
|
|
106
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('invalid references', () => {
|
|
111
|
+
isA1Equal('[Workbook.xlsx]!A1', undefined);
|
|
112
|
+
isA1Equal('[Workbook.xlsx]!A1:B2', undefined);
|
|
113
|
+
isA1Equal('[Workbook.xlsx]!A:A', undefined);
|
|
114
|
+
isA1Equal('[Workbook.xlsx]!1:1', undefined);
|
|
115
|
+
isA1Equal('[]Sheet1!A1', undefined);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('named ranges', () => {
|
|
119
|
+
isA1Equal('namedrange', { name: 'namedrange' });
|
|
120
|
+
|
|
121
|
+
isA1Equal('Workbook.xlsx!namedrange', {
|
|
122
|
+
context: [ 'Workbook.xlsx' ],
|
|
123
|
+
name: 'namedrange'
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
isA1Equal("'Workbook.xlsx'!namedrange", {
|
|
127
|
+
context: [ 'Workbook.xlsx' ],
|
|
128
|
+
name: 'namedrange'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
isA1Equal('[Workbook.xlsx]!namedrange', undefined);
|
|
132
|
+
isA1Equal('pensioneligibilitypartner1', { name: 'pensioneligibilitypartner1' });
|
|
133
|
+
isA1Equal('XFE1048577', { name: 'XFE1048577' });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('named ranges with allowNamed: false', () => {
|
|
137
|
+
isA1Equal('namedrange', undefined, { allowNamed: false });
|
|
138
|
+
isA1Equal('Workbook.xlsx!namedrange', undefined, { allowNamed: false });
|
|
139
|
+
isA1Equal('pensioneligibilitypartner1', undefined, { allowNamed: false });
|
|
140
|
+
isA1Equal('XFE1048577', undefined, { allowNamed: false });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('parse A1 ranges in XLSX mode', () => {
|
|
145
|
+
const opts = { xlsx: true };
|
|
146
|
+
|
|
147
|
+
test('workbook references', () => {
|
|
148
|
+
isA1Equal('[1]!A1', {
|
|
149
|
+
workbookName: '1',
|
|
150
|
+
sheetName: '',
|
|
151
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
152
|
+
}, opts);
|
|
153
|
+
|
|
154
|
+
isA1Equal('[Workbook.xlsx]!A1', {
|
|
155
|
+
workbookName: 'Workbook.xlsx',
|
|
156
|
+
sheetName: '',
|
|
157
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
158
|
+
}, opts);
|
|
159
|
+
|
|
160
|
+
isA1Equal('[1]Sheet1!A1', {
|
|
161
|
+
workbookName: '1',
|
|
162
|
+
sheetName: 'Sheet1',
|
|
163
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
164
|
+
}, opts);
|
|
165
|
+
|
|
166
|
+
isA1Equal('[Workbook.xlsx]Sheet1!A1', {
|
|
167
|
+
workbookName: 'Workbook.xlsx',
|
|
168
|
+
sheetName: 'Sheet1',
|
|
169
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
170
|
+
}, opts);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('named ranges in workbooks', () => {
|
|
174
|
+
isA1Equal('[4]!name', {
|
|
175
|
+
workbookName: '4',
|
|
176
|
+
sheetName: '',
|
|
177
|
+
name: 'name'
|
|
178
|
+
}, opts);
|
|
179
|
+
|
|
180
|
+
isA1Equal('[Workbook.xlsx]!name', {
|
|
181
|
+
workbookName: 'Workbook.xlsx',
|
|
182
|
+
sheetName: '',
|
|
183
|
+
name: 'name'
|
|
184
|
+
}, opts);
|
|
185
|
+
|
|
186
|
+
isA1Equal('[16]Sheet1!name', {
|
|
187
|
+
workbookName: '16',
|
|
188
|
+
sheetName: 'Sheet1',
|
|
189
|
+
name: 'name'
|
|
190
|
+
}, opts);
|
|
191
|
+
|
|
192
|
+
isA1Equal('[Workbook.xlsx]Sheet1!name', {
|
|
193
|
+
workbookName: 'Workbook.xlsx',
|
|
194
|
+
sheetName: 'Sheet1',
|
|
195
|
+
name: 'name'
|
|
196
|
+
}, opts);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('quoted references', () => {
|
|
200
|
+
isA1Equal("='[1]'!A1", {
|
|
201
|
+
workbookName: '1',
|
|
202
|
+
sheetName: '',
|
|
203
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
204
|
+
}, opts);
|
|
205
|
+
|
|
206
|
+
isA1Equal("='[Workbook.xlsx]'!A1", {
|
|
207
|
+
workbookName: 'Workbook.xlsx',
|
|
208
|
+
sheetName: '',
|
|
209
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
210
|
+
}, opts);
|
|
211
|
+
|
|
212
|
+
isA1Equal("'[1]Sheet1'!A1", {
|
|
213
|
+
workbookName: '1',
|
|
214
|
+
sheetName: 'Sheet1',
|
|
215
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
216
|
+
}, opts);
|
|
217
|
+
|
|
218
|
+
isA1Equal("'[Workbook.xlsx]Sheet1'!A1", {
|
|
219
|
+
workbookName: 'Workbook.xlsx',
|
|
220
|
+
sheetName: 'Sheet1',
|
|
221
|
+
range: { top: 0, left: 0, bottom: 0, right: 0 }
|
|
222
|
+
}, opts);
|
|
223
|
+
|
|
224
|
+
isA1Equal("'[4]'!name", {
|
|
225
|
+
workbookName: '4',
|
|
226
|
+
sheetName: '',
|
|
227
|
+
name: 'name'
|
|
228
|
+
}, opts);
|
|
229
|
+
|
|
230
|
+
isA1Equal("'[Workbook.xlsx]'!name", {
|
|
231
|
+
workbookName: 'Workbook.xlsx',
|
|
232
|
+
sheetName: '',
|
|
233
|
+
name: 'name'
|
|
234
|
+
}, opts);
|
|
235
|
+
|
|
236
|
+
isA1Equal("'[16]Sheet1'!name", {
|
|
237
|
+
workbookName: '16',
|
|
238
|
+
sheetName: 'Sheet1',
|
|
239
|
+
name: 'name'
|
|
240
|
+
}, opts);
|
|
241
|
+
|
|
242
|
+
isA1Equal("'[Workbook.xlsx]Sheet1'!name", {
|
|
243
|
+
workbookName: 'Workbook.xlsx',
|
|
244
|
+
sheetName: 'Sheet1',
|
|
245
|
+
name: 'name'
|
|
246
|
+
}, opts);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('A1 partial ranges', () => {
|
|
251
|
+
const opt = { allowTernary: true };
|
|
252
|
+
|
|
253
|
+
test('partial ranges not allowed by default', () => {
|
|
254
|
+
isA1Equal('A10:A', undefined);
|
|
255
|
+
isA1Equal('B3:2', undefined);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('unbounded bottom ranges', () => {
|
|
259
|
+
isA1Equal('A10:A', { range: { top: 9, left: 0, right: 0 } }, opt);
|
|
260
|
+
isA1Equal('A:A10', { range: { top: 9, left: 0, right: 0 } }, opt);
|
|
261
|
+
isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
262
|
+
isA1Equal('A:A$5', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
263
|
+
isA1Equal('A$5:A', { range: { top: 4, left: 0, right: 0, $top: true } }, opt);
|
|
264
|
+
isA1Equal('A:$B5', { range: { top: 4, left: 0, right: 1, $right: true } }, opt);
|
|
265
|
+
isA1Equal('$B:B3', { range: { top: 2, left: 1, right: 1, $left: true } }, opt);
|
|
266
|
+
isA1Equal('$B:C5', { range: { top: 4, left: 1, right: 2, $left: true } }, opt);
|
|
267
|
+
isA1Equal('C2:B', { range: { top: 1, left: 1, right: 2 } }, opt);
|
|
268
|
+
isA1Equal('C:B2', { range: { top: 1, left: 1, right: 2 } }, opt);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('unbounded right ranges', () => {
|
|
272
|
+
isA1Equal('D1:1', { range: { top: 0, left: 3, bottom: 0 } }, opt);
|
|
273
|
+
isA1Equal('1:D2', { range: { top: 0, left: 3, bottom: 1 } }, opt);
|
|
274
|
+
isA1Equal('2:$D3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
|
|
275
|
+
isA1Equal('$D2:3', { range: { top: 1, left: 3, bottom: 2, $left: true } }, opt);
|
|
276
|
+
isA1Equal('1:D$1', { range: { top: 0, left: 3, bottom: 0, $bottom: true } }, opt);
|
|
277
|
+
isA1Equal('$1:D1', { range: { top: 0, left: 3, bottom: 0, $top: true } }, opt);
|
|
278
|
+
isA1Equal('AA$3:4', { range: { top: 2, left: 26, bottom: 3, $top: true } }, opt);
|
|
279
|
+
isA1Equal('B3:2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
|
|
280
|
+
isA1Equal('3:B2', { range: { top: 1, bottom: 2, left: 1 } }, opt);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('A1 trimmed ranges', () => {
|
|
285
|
+
const locks = { $top: true, $left: true, $bottom: true, $right: true };
|
|
286
|
+
const opts = [ {}, { xlsx: true } ];
|
|
287
|
+
|
|
288
|
+
for (const opt of opts) {
|
|
289
|
+
test(`trimmed ranges with ${opt.xlsx ? 'XLSX' : 'default'} options`, () => {
|
|
290
|
+
isA1Equal('A1:B2', { range: { top: 0, left: 0, bottom: 1, right: 1 } }, opt);
|
|
291
|
+
isA1Equal('A1.:B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head' } }, opt);
|
|
292
|
+
isA1Equal('A1:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail' } }, opt);
|
|
293
|
+
isA1Equal('A1.:.B2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both' } }, opt);
|
|
294
|
+
|
|
295
|
+
isA1Equal('$A$1:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, ...locks } }, opt);
|
|
296
|
+
isA1Equal('$A$1.:$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'head', ...locks } }, opt);
|
|
297
|
+
isA1Equal('$A$1:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'tail', ...locks } }, opt);
|
|
298
|
+
isA1Equal('$A$1.:.$B$2', { range: { top: 0, left: 0, bottom: 1, right: 1, trim: 'both', ...locks } }, opt);
|
|
299
|
+
|
|
300
|
+
isA1Equal('J:J', { range: { top: null, left: 9, bottom: null, right: 9 } }, opt);
|
|
301
|
+
isA1Equal('J.:J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'head' } }, opt);
|
|
302
|
+
isA1Equal('J:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'tail' } }, opt);
|
|
303
|
+
isA1Equal('J.:.J', { range: { top: null, left: 9, bottom: null, right: 9, trim: 'both' } }, opt);
|
|
304
|
+
|
|
305
|
+
isA1Equal('10:10', { range: { top: 9, left: null, bottom: 9, right: null } }, opt);
|
|
306
|
+
isA1Equal('10.:10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'head' } }, opt);
|
|
307
|
+
isA1Equal('10:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'tail' } }, opt);
|
|
308
|
+
isA1Equal('10.:.10', { range: { top: 9, left: null, bottom: 9, right: null, trim: 'both' } }, opt);
|
|
309
|
+
|
|
310
|
+
isA1Equal('J10:J', undefined, { ...opt });
|
|
311
|
+
isA1Equal('J10:10', undefined, { ...opt });
|
|
312
|
+
isA1Equal('J10.:.J', undefined, { ...opt });
|
|
313
|
+
isA1Equal('J10.:.10', undefined, { ...opt });
|
|
314
|
+
isA1Equal('J10:J', { range: { top: 9, left: 9, bottom: null, right: 9 } }, { allowTernary: true, ...opt });
|
|
315
|
+
isA1Equal('J10:10', { range: { top: 9, left: 9, bottom: 9, right: null } }, { allowTernary: true, ...opt });
|
|
316
|
+
isA1Equal('J10.:.J', { range: { top: 9, left: 9, bottom: null, right: 9, trim: 'both' } }, { allowTernary: true, ...opt });
|
|
317
|
+
isA1Equal('J10.:.10', { range: { top: 9, left: 9, bottom: 9, right: null, trim: 'both' } }, { allowTernary: true, ...opt });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('A1 trimmed ranges vs named ranges', () => {
|
|
323
|
+
test('named ranges cannot be trimmed', () => {
|
|
324
|
+
isA1Equal('name1.:.name1', undefined);
|
|
325
|
+
isA1Equal('name1.:.foo', undefined);
|
|
326
|
+
isA1Equal('foo.:.name1', undefined);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('trimmed column references', () => {
|
|
330
|
+
// prior to the intruduction of trimed ranges, the following would have
|
|
331
|
+
// been an expression: NAME:`foo.` OP:`:`, COLUMN:`bar`
|
|
332
|
+
isA1Equal('foo.:bar', { range: { left: 1395, right: 4460, trim: 'head' } });
|
|
333
|
+
// prior to the intruduction of trimed ranges, the following would have
|
|
334
|
+
// been an expression: NAME:`foo.` OP:`:`, CELL:`B2`
|
|
335
|
+
isA1Equal('foo.:B2', { range: { top: 1, left: 1, right: 4460, trim: 'head' } }, { allowTernary: true });
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { parseRefCtx, parseRefXlsx } from './parseRef.ts';
|
|
2
|
+
import { parseA1Range } from './parseA1Range.ts';
|
|
3
|
+
import type { ReferenceA1, ReferenceA1Xlsx, ReferenceName, ReferenceNameXlsx } from './types.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for {@link parseA1Ref}.
|
|
7
|
+
*/
|
|
8
|
+
export type OptsParseA1Ref = {
|
|
9
|
+
/**
|
|
10
|
+
* Enable parsing names as well as ranges.
|
|
11
|
+
* @defaultValue true
|
|
12
|
+
*/
|
|
13
|
+
allowNamed?: boolean,
|
|
14
|
+
/**
|
|
15
|
+
* Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`.
|
|
16
|
+
* These are supported by Google Sheets but not Excel. See: [References.md](./References.md).
|
|
17
|
+
* @defaultValue false
|
|
18
|
+
*/
|
|
19
|
+
allowTernary?: boolean,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a string reference into an object representing it.
|
|
24
|
+
*
|
|
25
|
+
* ```js
|
|
26
|
+
* parseA1Ref('Sheet1!A$1:$B2');
|
|
27
|
+
* // => {
|
|
28
|
+
* // context: [ 'Sheet1' ],
|
|
29
|
+
* // range: {
|
|
30
|
+
* // top: 0,
|
|
31
|
+
* // left: 0,
|
|
32
|
+
* // bottom: 1,
|
|
33
|
+
* // right: 1
|
|
34
|
+
* // $top: true,
|
|
35
|
+
* // $left: false,
|
|
36
|
+
* // $bottom: false,
|
|
37
|
+
* // $right: true
|
|
38
|
+
* // }
|
|
39
|
+
* // }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* For A:A or A1:A style ranges, `null` will be used for any dimensions that the
|
|
43
|
+
* syntax does not specify.
|
|
44
|
+
*
|
|
45
|
+
* @see {@link OptsParseA1Ref}
|
|
46
|
+
* @param refString An A1-style reference string.
|
|
47
|
+
* @param options Options.
|
|
48
|
+
* @returns An object representing a valid reference or `undefined` if it is invalid.
|
|
49
|
+
*/
|
|
50
|
+
export function parseA1Ref (
|
|
51
|
+
refString: string,
|
|
52
|
+
{ allowNamed = true, allowTernary = false }: OptsParseA1Ref = {}
|
|
53
|
+
): ReferenceA1 | ReferenceName | undefined {
|
|
54
|
+
const d = parseRefCtx(refString, { allowNamed, allowTernary, r1c1: false });
|
|
55
|
+
if (d) {
|
|
56
|
+
if (d.name) {
|
|
57
|
+
return { context: d.context, name: d.name };
|
|
58
|
+
}
|
|
59
|
+
else if (d.r0) {
|
|
60
|
+
const range = parseA1Range(d.r1 ? d.r0 + d.operator + d.r1 : d.r0);
|
|
61
|
+
if (range) {
|
|
62
|
+
return { context: d.context, range };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a string reference into an object representing it.
|
|
70
|
+
*
|
|
71
|
+
* ```js
|
|
72
|
+
* parseA1Ref('Sheet1!A$1:$B2');
|
|
73
|
+
* // => {
|
|
74
|
+
* // workbookName: '',
|
|
75
|
+
* // sheetName: 'Sheet1',
|
|
76
|
+
* // range: {
|
|
77
|
+
* // top: 0,
|
|
78
|
+
* // left: 0,
|
|
79
|
+
* // bottom: 1,
|
|
80
|
+
* // right: 1
|
|
81
|
+
* // $top: true,
|
|
82
|
+
* // $left: false,
|
|
83
|
+
* // $bottom: false,
|
|
84
|
+
* // $right: true
|
|
85
|
+
* // }
|
|
86
|
+
* // }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* For A:A or A1:A style ranges, `null` will be used for any dimensions that the
|
|
90
|
+
* syntax does not specify.
|
|
91
|
+
*
|
|
92
|
+
* @see {@link OptsParseA1Ref}
|
|
93
|
+
* @param refString An A1-style reference string.
|
|
94
|
+
* @param options Options.
|
|
95
|
+
* @returns An object representing a valid reference or `undefined` if it is invalid.
|
|
96
|
+
*/
|
|
97
|
+
export function parseA1RefXlsx (
|
|
98
|
+
refString: string,
|
|
99
|
+
{ allowNamed = true, allowTernary = false }: OptsParseA1Ref = {}
|
|
100
|
+
): ReferenceA1Xlsx | ReferenceNameXlsx | undefined {
|
|
101
|
+
const d = parseRefXlsx(refString, { allowNamed, allowTernary, r1c1: false });
|
|
102
|
+
if (d) {
|
|
103
|
+
if (d.name) {
|
|
104
|
+
return { workbookName: d.workbookName, sheetName: d.sheetName, name: d.name };
|
|
105
|
+
}
|
|
106
|
+
else if (d.r0) {
|
|
107
|
+
if (d.r0) {
|
|
108
|
+
const range = parseA1Range(d.r1 ? d.r0 + d.operator + d.r1 : d.r0);
|
|
109
|
+
if (range) {
|
|
110
|
+
return { workbookName: d.workbookName, sheetName: d.sheetName, range };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|