@borgar/fx 4.12.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.
Files changed (139) hide show
  1. package/dist/index-BMr6cTgc.d.cts +1444 -0
  2. package/dist/index-BMr6cTgc.d.ts +1444 -0
  3. package/dist/index.cjs +3054 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +2984 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/xlsx/index.cjs +3120 -0
  10. package/dist/xlsx/index.cjs.map +1 -0
  11. package/dist/xlsx/index.d.cts +55 -0
  12. package/dist/xlsx/index.d.ts +55 -0
  13. package/dist/xlsx/index.js +3049 -0
  14. package/dist/xlsx/index.js.map +1 -0
  15. package/docs/API.md +2959 -718
  16. package/docs/AST_format.md +2 -2
  17. package/eslint.config.mjs +40 -0
  18. package/lib/a1.spec.ts +32 -0
  19. package/lib/a1.ts +26 -0
  20. package/lib/addA1RangeBounds.ts +50 -0
  21. package/lib/addTokenMeta.spec.ts +166 -0
  22. package/lib/{addTokenMeta.js → addTokenMeta.ts} +53 -33
  23. package/lib/astTypes.ts +211 -0
  24. package/lib/cloneToken.ts +29 -0
  25. package/lib/{constants.js → constants.ts} +6 -3
  26. package/lib/fixRanges.spec.ts +220 -0
  27. package/lib/fixRanges.ts +260 -0
  28. package/lib/fromCol.spec.ts +15 -0
  29. package/lib/{fromCol.js → fromCol.ts} +1 -1
  30. package/lib/index.spec.ts +119 -0
  31. package/lib/index.ts +76 -0
  32. package/lib/isNodeType.ts +151 -0
  33. package/lib/isType.spec.ts +208 -0
  34. package/lib/{isType.js → isType.ts} +26 -25
  35. package/lib/lexers/advRangeOp.ts +18 -0
  36. package/lib/lexers/canEndRange.ts +25 -0
  37. package/lib/lexers/lexBoolean.ts +55 -0
  38. package/lib/lexers/lexContext.ts +104 -0
  39. package/lib/lexers/lexError.ts +15 -0
  40. package/lib/lexers/lexFunction.ts +37 -0
  41. package/lib/lexers/lexNameFuncCntx.ts +112 -0
  42. package/lib/lexers/lexNamed.ts +60 -0
  43. package/lib/lexers/lexNewLine.ts +12 -0
  44. package/lib/lexers/lexNumber.ts +48 -0
  45. package/lib/lexers/lexOperator.ts +26 -0
  46. package/lib/lexers/lexRange.ts +15 -0
  47. package/lib/lexers/lexRangeA1.ts +134 -0
  48. package/lib/lexers/lexRangeR1C1.ts +146 -0
  49. package/lib/lexers/lexRangeTrim.ts +26 -0
  50. package/lib/lexers/lexRefOp.ts +19 -0
  51. package/lib/lexers/lexString.ts +22 -0
  52. package/lib/lexers/lexStructured.ts +25 -0
  53. package/lib/lexers/lexWhitespace.ts +31 -0
  54. package/lib/lexers/sets.ts +51 -0
  55. package/lib/mergeRefTokens.spec.ts +141 -0
  56. package/lib/{mergeRefTokens.js → mergeRefTokens.ts} +47 -32
  57. package/lib/nodeTypes.ts +54 -0
  58. package/lib/parse.spec.ts +1410 -0
  59. package/lib/{parser.js → parse.ts} +81 -63
  60. package/lib/parseA1Range.spec.ts +233 -0
  61. package/lib/parseA1Range.ts +206 -0
  62. package/lib/parseA1Ref.spec.ts +337 -0
  63. package/lib/parseA1Ref.ts +115 -0
  64. package/lib/parseR1C1Range.ts +191 -0
  65. package/lib/parseR1C1Ref.spec.ts +323 -0
  66. package/lib/parseR1C1Ref.ts +127 -0
  67. package/lib/parseRef.spec.ts +90 -0
  68. package/lib/parseRef.ts +240 -0
  69. package/lib/parseSRange.ts +240 -0
  70. package/lib/parseStructRef.spec.ts +168 -0
  71. package/lib/parseStructRef.ts +76 -0
  72. package/lib/stringifyA1Range.spec.ts +72 -0
  73. package/lib/stringifyA1Range.ts +72 -0
  74. package/lib/stringifyA1Ref.spec.ts +64 -0
  75. package/lib/stringifyA1Ref.ts +59 -0
  76. package/lib/{stringifyPrefix.js → stringifyPrefix.ts} +17 -2
  77. package/lib/stringifyR1C1Range.spec.ts +92 -0
  78. package/lib/stringifyR1C1Range.ts +73 -0
  79. package/lib/stringifyR1C1Ref.spec.ts +63 -0
  80. package/lib/stringifyR1C1Ref.ts +67 -0
  81. package/lib/stringifyStructRef.spec.ts +124 -0
  82. package/lib/stringifyStructRef.ts +113 -0
  83. package/lib/stringifyTokens.ts +15 -0
  84. package/lib/toCol.spec.ts +11 -0
  85. package/lib/{toCol.js → toCol.ts} +4 -4
  86. package/lib/tokenTypes.ts +76 -0
  87. package/lib/tokenize-srefs.spec.ts +429 -0
  88. package/lib/tokenize.spec.ts +2103 -0
  89. package/lib/tokenize.ts +346 -0
  90. package/lib/translate.spec.ts +35 -0
  91. package/lib/translateToA1.spec.ts +247 -0
  92. package/lib/translateToA1.ts +231 -0
  93. package/lib/translateToR1C1.spec.ts +227 -0
  94. package/lib/translateToR1C1.ts +145 -0
  95. package/lib/types.ts +179 -0
  96. package/lib/xlsx/index.spec.ts +27 -0
  97. package/lib/xlsx/index.ts +32 -0
  98. package/package.json +46 -30
  99. package/tsconfig.json +28 -0
  100. package/typedoc-ignore-links.ts +17 -0
  101. package/typedoc.json +41 -0
  102. package/.eslintrc +0 -22
  103. package/dist/fx.d.ts +0 -823
  104. package/dist/fx.js +0 -2
  105. package/dist/package.json +0 -1
  106. package/lib/a1.js +0 -348
  107. package/lib/a1.spec.js +0 -458
  108. package/lib/addTokenMeta.spec.js +0 -153
  109. package/lib/astTypes.js +0 -96
  110. package/lib/extraTypes.js +0 -74
  111. package/lib/fixRanges.js +0 -104
  112. package/lib/fixRanges.spec.js +0 -170
  113. package/lib/fromCol.spec.js +0 -11
  114. package/lib/index.js +0 -134
  115. package/lib/index.spec.js +0 -67
  116. package/lib/isType.spec.js +0 -168
  117. package/lib/lexer-srefs.spec.js +0 -324
  118. package/lib/lexer.js +0 -283
  119. package/lib/lexer.spec.js +0 -1953
  120. package/lib/lexerParts.js +0 -228
  121. package/lib/mergeRefTokens.spec.js +0 -121
  122. package/lib/package.json +0 -1
  123. package/lib/parseRef.js +0 -157
  124. package/lib/parseRef.spec.js +0 -71
  125. package/lib/parseSRange.js +0 -167
  126. package/lib/parseStructRef.js +0 -48
  127. package/lib/parseStructRef.spec.js +0 -164
  128. package/lib/parser.spec.js +0 -1208
  129. package/lib/rc.js +0 -341
  130. package/lib/rc.spec.js +0 -403
  131. package/lib/stringifyStructRef.js +0 -80
  132. package/lib/stringifyStructRef.spec.js +0 -182
  133. package/lib/toCol.spec.js +0 -11
  134. package/lib/translate-toA1.spec.js +0 -214
  135. package/lib/translate-toRC.spec.js +0 -197
  136. package/lib/translate.js +0 -239
  137. package/lib/translate.spec.js +0 -21
  138. package/rollup.config.mjs +0 -22
  139. package/tsd.json +0 -12
@@ -0,0 +1,168 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { parseStructRef, parseStructRefXlsx } from './parseStructRef.ts';
3
+
4
+ function isSREqual (expr: string, expected: any, opts?: any) {
5
+ if (expected) {
6
+ expected = opts?.xlsx
7
+ ? {
8
+ workbookName: '',
9
+ sheetName: '',
10
+ table: '',
11
+ columns: [],
12
+ sections: [],
13
+ ...expected
14
+ }
15
+ : {
16
+ context: [],
17
+ table: '',
18
+ columns: [],
19
+ sections: [],
20
+ ...expected
21
+ };
22
+ }
23
+ expect(opts?.xlsx ? parseStructRefXlsx(expr) : parseStructRef(expr)).toEqual(expected);
24
+ }
25
+
26
+ describe('parse structured references', () => {
27
+ test('basic table and column references', () => {
28
+ isSREqual('table[col]', {
29
+ table: 'table',
30
+ columns: [ 'col' ]
31
+ });
32
+
33
+ isSREqual('table[]', {
34
+ table: 'table'
35
+ });
36
+ });
37
+
38
+ test('section references', () => {
39
+ isSREqual('[#All]', {
40
+ sections: [ 'all' ]
41
+ });
42
+ });
43
+
44
+ test('column name references', () => {
45
+ isSREqual('[column name]', {
46
+ columns: [ 'column name' ]
47
+ });
48
+
49
+ isSREqual('[[my column]]', {
50
+ columns: [ 'my column' ]
51
+ });
52
+ });
53
+
54
+ test('invalid references', () => {
55
+ isSREqual('[column name]!foo', undefined);
56
+ isSREqual('[foo]bar', undefined);
57
+ });
58
+
59
+ test('column range references', () => {
60
+ isSREqual('[[my column]:otherColumn]', {
61
+ columns: [ 'my column', 'otherColumn' ]
62
+ });
63
+
64
+ isSREqual('[ [my column]:otherColumn ]', {
65
+ columns: [ 'my column', 'otherColumn ' ]
66
+ });
67
+
68
+ isSREqual('[ [my column]: otherColumn ]', {
69
+ columns: [ 'my column', ' otherColumn ' ]
70
+ });
71
+ });
72
+
73
+ test('this row references', () => {
74
+ isSREqual('[ @[ my column ]: otherColumn ]', {
75
+ columns: [ ' my column ', ' otherColumn ' ],
76
+ sections: [ 'this row' ]
77
+ });
78
+ });
79
+
80
+ test('section and column combinations', () => {
81
+ isSREqual('[[#Data], [my column]:otherColumn]', {
82
+ columns: [ 'my column', 'otherColumn' ],
83
+ sections: [ 'data' ]
84
+ });
85
+
86
+ isSREqual('[ [#Data], [my column]:[\'@foo] ]', {
87
+ columns: [ 'my column', '@foo' ],
88
+ sections: [ 'data' ]
89
+ });
90
+ });
91
+
92
+ test('context-qualified references', () => {
93
+ isSREqual('workbook.xlsx!tableName[ [#Data], [my column]:[\'@foo] ]', {
94
+ columns: [ 'my column', '@foo' ],
95
+ sections: [ 'data' ],
96
+ table: 'tableName',
97
+ context: [ 'workbook.xlsx' ]
98
+ });
99
+
100
+ isSREqual("'Sheet'!Table[Column]", {
101
+ columns: [ 'Column' ],
102
+ table: 'Table',
103
+ context: [ 'Sheet' ]
104
+ });
105
+
106
+ isSREqual("Sheet1!Table1[foo '[bar']]", {
107
+ columns: [ 'foo [bar]' ],
108
+ table: 'Table1',
109
+ context: [ 'Sheet1' ]
110
+ });
111
+
112
+ isSREqual('[myworkbook.xlsx]Sheet1!TMP8w0habhr[#All]', {
113
+ columns: [],
114
+ table: 'TMP8w0habhr',
115
+ context: [ 'myworkbook.xlsx', 'Sheet1' ],
116
+ sections: [ 'all' ]
117
+ });
118
+ });
119
+
120
+ test('duplicate section handling', () => {
121
+ isSREqual('[[#Data],[#data],[#Data],[#Data],[#Totals],[#Totals],[#Totals],foo]', {
122
+ columns: [ 'foo' ],
123
+ sections: [ 'data', 'totals' ]
124
+ });
125
+ });
126
+ });
127
+
128
+ describe('structured references parse in xlsx mode', () => {
129
+ test('workbook-only references', () => {
130
+ isSREqual('[Workbook.xlsx]!Table[#Data]', {
131
+ workbookName: 'Workbook.xlsx',
132
+ table: 'Table',
133
+ sections: [ 'data' ]
134
+ }, { xlsx: true });
135
+ });
136
+
137
+ test('workbook and sheet references', () => {
138
+ isSREqual('[Workbook.xlsx]Sheet1!Table[#Data]', {
139
+ workbookName: 'Workbook.xlsx',
140
+ sheetName: 'Sheet1',
141
+ table: 'Table',
142
+ sections: [ 'data' ]
143
+ }, { xlsx: true });
144
+ });
145
+
146
+ test('sheet-only references', () => {
147
+ isSREqual('Sheet1!Table[#Data]', {
148
+ sheetName: 'Sheet1',
149
+ table: 'Table',
150
+ sections: [ 'data' ]
151
+ }, { xlsx: true });
152
+ });
153
+ });
154
+
155
+ describe('longform parse (in xlsx mode)', () => {
156
+ test('thisRow option should have no effect when parsing', () => {
157
+ const expectedResult = {
158
+ table: 'Table2',
159
+ columns: [ 'col1' ],
160
+ sections: [ 'this row' ]
161
+ };
162
+
163
+ isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: true, thisRow: true });
164
+ isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: true, thisRow: false });
165
+ isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: false, thisRow: true });
166
+ isSREqual('Table2[[#This Row],[col1]]', expectedResult, { xlsx: false, thisRow: false });
167
+ });
168
+ });
@@ -0,0 +1,76 @@
1
+ import type { ReferenceStruct, ReferenceStructXlsx } from './types.ts';
2
+ import { parseRefCtx, parseRefXlsx } from './parseRef.ts';
3
+ import { parseSRange } from './parseSRange.ts';
4
+
5
+ /**
6
+ * Parse a structured reference string into an object representing it.
7
+ *
8
+ * ```js
9
+ * parseStructRef('workbook.xlsx!tableName[[#Data],[Column1]:[Column2]]');
10
+ * // => {
11
+ * // context: [ 'workbook.xlsx' ],
12
+ * // sections: [ 'data' ],
13
+ * // columns: [ 'my column', '@foo' ],
14
+ * // table: 'tableName',
15
+ * // }
16
+ * ```
17
+ *
18
+ * For A:A or A1:A style ranges, `null` will be used for any dimensions that the
19
+ * syntax does not specify:
20
+ *
21
+ * See [References.md](./References.md).
22
+ *
23
+ * @param ref A structured reference string
24
+ * @returns An object representing a valid reference or `undefined` if it is invalid.
25
+ */
26
+ export function parseStructRef (ref: string): ReferenceStruct | undefined {
27
+ const r = parseRefCtx(ref);
28
+ if (r && r.struct) {
29
+ const structData = parseSRange(r.struct);
30
+ if (structData && structData.length === r.struct.length) {
31
+ return {
32
+ context: r.context,
33
+ table: r.name,
34
+ columns: structData.columns,
35
+ sections: structData.sections
36
+ };
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Parse a structured reference string into an object representing it.
43
+ *
44
+ * ```js
45
+ * parseStructRef('[workbook.xlsx]!tableName[[#Data],[Column1]:[Column2]]');
46
+ * // => {
47
+ * // workbookName: 'workbook.xlsx',
48
+ * // sections: [ 'data' ],
49
+ * // columns: [ 'my column', '@foo' ],
50
+ * // table: 'tableName',
51
+ * // }
52
+ * ```
53
+ *
54
+ * For A:A or A1:A style ranges, `null` will be used for any dimensions that the
55
+ * syntax does not specify:
56
+ *
57
+ * See [References.md](./References.md).
58
+ *
59
+ * @param ref A structured reference string
60
+ * @returns An object representing a valid reference or null if it is invalid.
61
+ */
62
+ export function parseStructRefXlsx (ref: string): ReferenceStructXlsx | undefined {
63
+ const r = parseRefXlsx(ref);
64
+ if (r && r.struct) {
65
+ const structData = parseSRange(r.struct);
66
+ if (structData && structData.length === r.struct.length) {
67
+ return {
68
+ workbookName: r.workbookName,
69
+ sheetName: r.sheetName,
70
+ table: r.name,
71
+ columns: structData.columns,
72
+ sections: structData.sections
73
+ };
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { stringifyA1Range } from './stringifyA1Range.ts';
3
+ import { MAX_COLS, MAX_ROWS } from './constants.ts';
4
+
5
+ describe('stringifyA1Range', () => {
6
+ test('cell references: A1', () => {
7
+ expect(stringifyA1Range({ top: 9, bottom: 9, left: 2, right: 2 })).toBe('C10');
8
+ expect(stringifyA1Range({ top: 9, bottom: 9, left: 2, right: 2, $top: true, $bottom: true })).toBe('C$10');
9
+ expect(stringifyA1Range({ top: 9, bottom: 9, left: 2, right: 2, $left: true, $right: true })).toBe('$C10');
10
+ expect(stringifyA1Range({ top: 9, bottom: 9, left: 2, right: 2, $top: true, $bottom: true, $left: true, $right: true })).toBe('$C$10');
11
+ });
12
+
13
+ test('rectangle references: A1:B2', () => {
14
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4 })).toBe('E3');
15
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, $right: true })).toBe('E3:$E3');
16
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, $top: true })).toBe('E$3:E3');
17
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, $left: true })).toBe('$E3:E3');
18
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, $bottom: true })).toBe('E3:E$3');
19
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, $bottom: true, $right: true })).toBe('E3:$E$3');
20
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 5 })).toBe('E3:F3');
21
+ expect(stringifyA1Range({ top: 2, bottom: 3, left: 4, right: 4 })).toBe('E3:E4');
22
+ expect(stringifyA1Range({ top: 2, bottom: 3, left: 4, right: 5 })).toBe('E3:F4');
23
+ });
24
+
25
+ test('beam references: A:A 1:1', () => {
26
+ expect(stringifyA1Range({ left: 0, right: 0 })).toBe('A:A');
27
+ expect(stringifyA1Range({ top: 0, bottom: MAX_ROWS, left: 0, right: 0 })).toBe('A:A');
28
+ expect(stringifyA1Range({ left: 10, right: 15 })).toBe('K:P');
29
+ expect(stringifyA1Range({ left: 10, right: 15, $left: true })).toBe('$K:P');
30
+ expect(stringifyA1Range({ left: 10, right: 15, $right: true })).toBe('K:$P');
31
+ expect(stringifyA1Range({ left: 10, right: 15, $left: true, $right: true })).toBe('$K:$P');
32
+ expect(stringifyA1Range({ top: 0, bottom: 0 })).toBe('1:1');
33
+ expect(stringifyA1Range({ top: 0, bottom: 0, left: 0, right: MAX_COLS })).toBe('1:1');
34
+ expect(stringifyA1Range({ top: 10, bottom: 15 })).toBe('11:16');
35
+ expect(stringifyA1Range({ top: 10, bottom: 15, $top: true })).toBe('$11:16');
36
+ expect(stringifyA1Range({ top: 10, bottom: 15, $bottom: true })).toBe('11:$16');
37
+ expect(stringifyA1Range({ top: 10, bottom: 15, $top: true, $bottom: true })).toBe('$11:$16');
38
+ });
39
+
40
+ test('partial references: B1:C B2:3', () => {
41
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0 })).toBe('A10:A');
42
+ expect(stringifyA1Range({ bottom: 9, left: 0, right: 0 })).toBe('A10:A');
43
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, $top: true })).toBe('A$10:A');
44
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, $left: true })).toBe('$A10:A');
45
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, $right: true })).toBe('A10:$A');
46
+ expect(stringifyA1Range({ top: 0, left: 3, bottom: 0 })).toBe('D1:1');
47
+ expect(stringifyA1Range({ top: 0, right: 3, bottom: 0 })).toBe('D1:1');
48
+ expect(stringifyA1Range({ top: 0, left: 3, bottom: 0, $top: true })).toBe('D$1:1');
49
+ expect(stringifyA1Range({ top: 0, left: 3, bottom: 0, $left: true })).toBe('$D1:1');
50
+ expect(stringifyA1Range({ top: 0, left: 3, bottom: 0, $left: true })).toBe('$D1:1');
51
+ });
52
+
53
+ test('edge cases', () => {
54
+ // allow skipping right/bottom for cells
55
+ expect(stringifyA1Range({ top: 0, left: 0 })).toBe('A1');
56
+ // clamp the range at min/max dimensions
57
+ expect(stringifyA1Range({ top: -10, bottom: -5, left: -10, right: -5 })).toBe('A1');
58
+ expect(stringifyA1Range({ top: 15e5, bottom: 15e5, left: 20000, right: 20000 })).toBe('XFD1048576');
59
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 2.5, right: 2.5 })).toBe('C3');
60
+ expect(stringifyA1Range({ top: 1.5, bottom: 2.5, left: 4.5, right: 8.5 })).toBe('E2:I3');
61
+ });
62
+
63
+ test('trimming', () => {
64
+ expect(stringifyA1Range({ top: 2, bottom: 2, left: 4, right: 4, trim: 'both' })).toBe('E3');
65
+ expect(stringifyA1Range({ top: 2, bottom: 3, left: 4, right: 6, trim: 'both' })).toBe('E3.:.G4');
66
+ expect(stringifyA1Range({ top: 2, bottom: 3, trim: 'both' })).toBe('3.:.4');
67
+ expect(stringifyA1Range({ left: 4, right: 6, trim: 'both' })).toBe('E.:.G');
68
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, trim: 'tail' })).toBe('A10:.A');
69
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, trim: 'head' })).toBe('A10.:A');
70
+ expect(stringifyA1Range({ top: 9, left: 0, right: 0, trim: 'both' })).toBe('A10.:.A');
71
+ });
72
+ });
@@ -0,0 +1,72 @@
1
+ import { MAX_ROWS, MAX_COLS } from './constants.ts';
2
+ import { toCol } from './toCol.ts';
3
+ import type { RangeA1 } from './types.ts';
4
+ import { rangeOperator } from './a1.ts';
5
+
6
+ const clamp = (min: number, val: number, max: number) => Math.min(Math.max(val, min), max);
7
+ const toColStr = (c: number, a: boolean) => (a ? '$' : '') + toCol(c);
8
+ const toRowStr = (r: number, a: boolean) => (a ? '$' : '') + toRow(r);
9
+ const toRow = (top: number): string => String(top + 1);
10
+
11
+ /**
12
+ * Stringify a range object into A1 syntax.
13
+ * @param range A range object
14
+ * @returns An A1-style string represenation of a range
15
+ * @internal
16
+ */
17
+ export function stringifyA1Range (range: Partial<RangeA1>): string {
18
+ // eslint-disable-next-line prefer-const
19
+ let { top, left, bottom, right, trim } = range;
20
+ const { $left, $right, $top, $bottom } = range;
21
+ const noLeft = left == null;
22
+ const noRight = right == null;
23
+ const noTop = top == null;
24
+ const noBottom = bottom == null;
25
+ // allow skipping right and bottom to define a cell
26
+ top = clamp(0, top | 0, MAX_ROWS);
27
+ left = clamp(0, left | 0, MAX_COLS);
28
+ if (!noLeft && !noTop && noRight && noBottom) {
29
+ bottom = top;
30
+ right = left;
31
+ }
32
+ else {
33
+ bottom = clamp(0, bottom | 0, MAX_ROWS);
34
+ right = clamp(0, right | 0, MAX_COLS);
35
+ }
36
+ const op = rangeOperator(trim);
37
+ // A:A
38
+ const allRows = top === 0 && bottom >= MAX_ROWS;
39
+ const haveAbsCol = ($left && !noLeft) || ($right && !noRight);
40
+ if ((allRows && !noLeft && !noRight && (!haveAbsCol || left === right)) || (noTop && noBottom)) {
41
+ return toColStr(left, $left) + op + toColStr(right, $right);
42
+ }
43
+ // 1:1
44
+ const allCols = left === 0 && right >= MAX_COLS;
45
+ const haveAbsRow = ($top && !noTop) || ($bottom && !noBottom);
46
+ if ((allCols && !noTop && !noBottom && (!haveAbsRow || top === bottom)) || (noLeft && noRight)) {
47
+ return toRowStr(top, $top) + op + toRowStr(bottom, $bottom);
48
+ }
49
+ // A1:1
50
+ if (!noLeft && !noTop && !noRight && noBottom) {
51
+ return toColStr(left, $left) + toRowStr(top, $top) + op + toColStr(right, $right);
52
+ }
53
+ // A:A1 => A1:1
54
+ if (!noLeft && noTop && !noRight && !noBottom) {
55
+ return toColStr(left, $left) + toRowStr(bottom, $bottom) + op + toColStr(right, $right);
56
+ }
57
+ // A1:A
58
+ if (!noLeft && !noTop && noRight && !noBottom) {
59
+ return toColStr(left, $left) + toRowStr(top, $top) + op + toRowStr(bottom, $bottom);
60
+ }
61
+ // A:A1 => A1:A
62
+ if (noLeft && !noTop && !noRight && !noBottom) {
63
+ return toColStr(right, $right) + toRowStr(top, $top) + op + toRowStr(bottom, $bottom);
64
+ }
65
+ // A1:A1
66
+ if (right !== left || bottom !== top || $right !== $left || $bottom !== $top) {
67
+ return toColStr(left, $left) + toRowStr(top, $top) + op +
68
+ toColStr(right, $right) + toRowStr(bottom, $bottom);
69
+ }
70
+ // A1
71
+ return toColStr(left, $left) + toRowStr(top, $top);
72
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { stringifyA1Ref, stringifyA1RefXlsx } from './stringifyA1Ref.ts';
3
+
4
+ describe('stringifyA1Ref', () => {
5
+ const rangeA1 = { top: 0, bottom: 0, left: 0, right: 0 };
6
+
7
+ test('basic stringification', () => {
8
+ expect(stringifyA1Ref({ range: rangeA1 })).toBe('A1');
9
+ expect(stringifyA1Ref({ context: [ 'Sheet1' ], range: rangeA1 })).toBe('Sheet1!A1');
10
+ expect(stringifyA1Ref({ context: [ 'Sheet 1' ], range: rangeA1 })).toBe("'Sheet 1'!A1");
11
+ expect(stringifyA1Ref({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 })).toBe('[MyFile.xlsx]Sheet1!A1');
12
+ expect(stringifyA1Ref({ context: [ 'My File.xlsx', 'Sheet1' ], range: rangeA1 })).toBe("'[My File.xlsx]Sheet1'!A1");
13
+ expect(stringifyA1Ref({ context: [ 'MyFile.xlsx' ], range: rangeA1 })).toBe('MyFile.xlsx!A1');
14
+ expect(stringifyA1Ref({ context: [ 'My File.xlsx' ], range: rangeA1 })).toBe("'My File.xlsx'!A1");
15
+ });
16
+
17
+ test('named ranges', () => {
18
+ expect(stringifyA1Ref({ name: 'foo' })).toBe('foo');
19
+ expect(stringifyA1Ref({ context: [ 'Sheet1' ], name: 'foo' })).toBe('Sheet1!foo');
20
+ expect(stringifyA1Ref({ context: [ 'Sheet 1' ], name: 'foo' })).toBe("'Sheet 1'!foo");
21
+ expect(stringifyA1Ref({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' })).toBe('[MyFile.xlsx]Sheet1!foo');
22
+ expect(stringifyA1Ref({ context: [ 'My File.xlsx', 'Sheet1' ], name: 'foo' })).toBe("'[My File.xlsx]Sheet1'!foo");
23
+ expect(stringifyA1Ref({ context: [ 'MyFile.xlsx' ], name: 'foo' })).toBe('MyFile.xlsx!foo');
24
+ expect(stringifyA1Ref({ context: [ 'My File.xlsx' ], name: 'foo' })).toBe("'My File.xlsx'!foo");
25
+ });
26
+
27
+ test('ignore workbookName/sheetName in non-XLSX mode', () => {
28
+ // @ts-expect-error -- testing invalid input
29
+ expect(stringifyA1Ref({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 })).toBe('A1');
30
+ // @ts-expect-error -- testing invalid input
31
+ expect(stringifyA1Ref({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' })).toBe('foo');
32
+ });
33
+ });
34
+
35
+ describe('stringifyA1Ref in XLSX mode', () => {
36
+ const rangeA1 = { top: 0, bottom: 0, left: 0, right: 0 };
37
+
38
+ test('basic stringification', () => {
39
+ expect(stringifyA1RefXlsx({ range: rangeA1 })).toBe('A1');
40
+ expect(stringifyA1RefXlsx({ sheetName: 'Sheet1', range: rangeA1 })).toBe('Sheet1!A1');
41
+ expect(stringifyA1RefXlsx({ sheetName: 'Sheet 1', range: rangeA1 })).toBe("'Sheet 1'!A1");
42
+ expect(stringifyA1RefXlsx({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', range: rangeA1 })).toBe('[MyFile.xlsx]Sheet1!A1');
43
+ expect(stringifyA1RefXlsx({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', range: rangeA1 })).toBe("'[My File.xlsx]Sheet1'!A1");
44
+ expect(stringifyA1RefXlsx({ workbookName: 'MyFile.xlsx', range: rangeA1 })).toBe('[MyFile.xlsx]!A1');
45
+ expect(stringifyA1RefXlsx({ workbookName: 'My File.xlsx', range: rangeA1 })).toBe("'[My File.xlsx]'!A1");
46
+ });
47
+
48
+ test('named ranges', () => {
49
+ expect(stringifyA1RefXlsx({ name: 'foo' })).toBe('foo');
50
+ expect(stringifyA1RefXlsx({ sheetName: 'Sheet1', name: 'foo' })).toBe('Sheet1!foo');
51
+ expect(stringifyA1RefXlsx({ sheetName: 'Sheet 1', name: 'foo' })).toBe("'Sheet 1'!foo");
52
+ expect(stringifyA1RefXlsx({ workbookName: 'MyFile.xlsx', sheetName: 'Sheet1', name: 'foo' })).toBe('[MyFile.xlsx]Sheet1!foo');
53
+ expect(stringifyA1RefXlsx({ workbookName: 'My File.xlsx', sheetName: 'Sheet1', name: 'foo' })).toBe("'[My File.xlsx]Sheet1'!foo");
54
+ expect(stringifyA1RefXlsx({ workbookName: 'MyFile.xlsx', name: 'foo' })).toBe('[MyFile.xlsx]!foo');
55
+ expect(stringifyA1RefXlsx({ workbookName: 'My File.xlsx', name: 'foo' })).toBe("'[My File.xlsx]'!foo");
56
+ });
57
+
58
+ test('ignore context in XLSX mode', () => {
59
+ // @ts-expect-error -- testing invalid input
60
+ expect(stringifyA1RefXlsx({ context: [ 'MyFile.xlsx', 'Sheet1' ], range: rangeA1 })).toBe('A1');
61
+ // @ts-expect-error -- testing invalid input
62
+ expect(stringifyA1RefXlsx({ context: [ 'MyFile.xlsx', 'Sheet1' ], name: 'foo' })).toBe('foo');
63
+ });
64
+ });
@@ -0,0 +1,59 @@
1
+ import { stringifyPrefix, stringifyPrefixXlsx } from './stringifyPrefix.ts';
2
+ import type { ReferenceA1, ReferenceA1Xlsx, ReferenceName, ReferenceNameXlsx } from './types.ts';
3
+ import { stringifyA1Range } from './stringifyA1Range.ts';
4
+
5
+ /**
6
+ * Get an A1-style string representation of a reference object.
7
+ *
8
+ * ```js
9
+ * stringifyA1Ref({
10
+ * context: [ 'Sheet1' ],
11
+ * range: {
12
+ * top: 0,
13
+ * left: 0,
14
+ * bottom: 1,
15
+ * right: 1,
16
+ * $top: true,
17
+ * $left: false,
18
+ * $bottom: false,
19
+ * $right: true
20
+ * }
21
+ * });
22
+ * // => 'Sheet1!A$1:$B2'
23
+ * ```
24
+ *
25
+ * @param refObject A reference object.
26
+ * @returns The reference in A1-style string format.
27
+ */
28
+ export function stringifyA1Ref (refObject: ReferenceA1 | ReferenceName): string {
29
+ const prefix = stringifyPrefix(refObject);
30
+ return prefix + ('name' in refObject ? refObject.name : stringifyA1Range(refObject.range));
31
+ }
32
+
33
+ /**
34
+ * Get an A1-style string representation of a reference object.
35
+ *
36
+ * ```js
37
+ * stringifyA1Ref({
38
+ * sheetName: 'Sheet1',
39
+ * range: {
40
+ * top: 0,
41
+ * left: 0,
42
+ * bottom: 1,
43
+ * right: 1,
44
+ * $top: true,
45
+ * $left: false,
46
+ * $bottom: false,
47
+ * $right: true
48
+ * }
49
+ * });
50
+ * // => 'Sheet1!A$1:$B2'
51
+ * ```
52
+ *
53
+ * @param refObject A reference object.
54
+ * @returns The reference in A1-style string format.
55
+ */
56
+ export function stringifyA1RefXlsx (refObject: ReferenceA1Xlsx | ReferenceNameXlsx): string {
57
+ const prefix = stringifyPrefixXlsx(refObject);
58
+ return prefix + ('name' in refObject ? refObject.name : stringifyA1Range(refObject.range));
59
+ }
@@ -1,6 +1,19 @@
1
+ import type {
2
+ ReferenceA1,
3
+ ReferenceStruct,
4
+ ReferenceR1C1,
5
+ ReferenceA1Xlsx,
6
+ ReferenceStructXlsx,
7
+ ReferenceR1C1Xlsx,
8
+ ReferenceName,
9
+ ReferenceNameXlsx
10
+ } from './types.ts';
11
+
1
12
  const reBannedChars = /[^0-9A-Za-z._¡¤§¨ª\u00ad¯-\uffff]/;
2
13
 
3
- export function stringifyPrefix (ref) {
14
+ export function stringifyPrefix (
15
+ ref: ReferenceA1 | ReferenceName | ReferenceStruct | ReferenceR1C1
16
+ ): string {
4
17
  let pre = '';
5
18
  let quote = 0;
6
19
  let nth = 0;
@@ -20,7 +33,9 @@ export function stringifyPrefix (ref) {
20
33
  return pre ? pre + '!' : pre;
21
34
  }
22
35
 
23
- export function stringifyPrefixAlt (ref) {
36
+ export function stringifyPrefixXlsx (
37
+ ref: ReferenceA1Xlsx | ReferenceNameXlsx | ReferenceStructXlsx | ReferenceR1C1Xlsx
38
+ ): string {
24
39
  let pre = '';
25
40
  let quote = 0;
26
41
  const { workbookName, sheetName } = ref;
@@ -0,0 +1,92 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { MAX_COLS, MAX_ROWS } from './constants.ts';
3
+ import { stringifyR1C1Range } from './stringifyR1C1Range.ts';
4
+
5
+ function isR1C1Rendered (range: any, expected: string) {
6
+ expect(stringifyR1C1Range(range)).toBe(expected);
7
+ }
8
+
9
+ describe('R1C1 serialization', () => {
10
+ test('ray serialization', () => {
11
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: MAX_COLS }, 'R');
12
+ isR1C1Rendered({ r0: 0, r1: 0 }, 'R');
13
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: MAX_COLS, $r0: true, $r1: true }, 'R1');
14
+ isR1C1Rendered({ r0: 0, r1: 0, $r0: true, $r1: true }, 'R1');
15
+ isR1C1Rendered({ r0: 1, c0: 0, r1: 1, c1: MAX_COLS }, 'R[1]');
16
+ isR1C1Rendered({ r0: 1, r1: 1 }, 'R[1]');
17
+
18
+ isR1C1Rendered({ r0: 0, c0: 0, r1: MAX_ROWS, c1: 0 }, 'C');
19
+ isR1C1Rendered({ c0: 0, c1: 0 }, 'C');
20
+ isR1C1Rendered({ r0: 0, c0: 0, r1: MAX_ROWS, c1: 0, $c0: true, $c1: true }, 'C1');
21
+ isR1C1Rendered({ c0: 0, c1: 0, $c0: true, $c1: true }, 'C1');
22
+ isR1C1Rendered({ r0: 0, c0: 1, r1: MAX_ROWS, c1: 1 }, 'C[1]');
23
+ isR1C1Rendered({ c0: 1, c1: 1 }, 'C[1]');
24
+ });
25
+
26
+ test('rectangle serialization', () => {
27
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: 0 }, 'RC');
28
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 0, c1: 0, $c0: true, $c1: true, $r0: true, $r1: true }, 'R1C1');
29
+ isR1C1Rendered({ r0: 9, c0: 7, r1: 9, c1: 7, $c0: true, $c1: true, $r0: true, $r1: true }, 'R10C8');
30
+ isR1C1Rendered({ r0: 2, c0: 0, r1: 2, c1: 0 }, 'R[2]C');
31
+ isR1C1Rendered({ r0: -2, c0: 0, r1: -2, c1: 0 }, 'R[-2]C');
32
+ isR1C1Rendered({ r0: 0, c0: 3, r1: 0, c1: 3 }, 'RC[3]');
33
+ isR1C1Rendered({ r0: 0, c0: -3, r1: 0, c1: -3 }, 'RC[-3]');
34
+ isR1C1Rendered({ r0: 2, c0: 4, r1: 2, c1: 4 }, 'R[2]C[4]');
35
+ isR1C1Rendered({ r0: -2, c0: -4, r1: -2, c1: -4 }, 'R[-2]C[-4]');
36
+ isR1C1Rendered({ r0: 9, c0: 8, r1: 9, c1: 8, $c0: true, $c1: true }, 'R[9]C9');
37
+ isR1C1Rendered({ r0: 8, c0: 9, r1: 8, c1: 9, $r0: true, $r1: true }, 'R9C[9]');
38
+ });
39
+
40
+ test('range serialization', () => {
41
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 0, c1: 0, $c1: true, $r1: true }, 'R[1]C[1]:R1C1');
42
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 1, c1: 1, $c0: true, $c1: true, $r0: true, $r1: true }, 'R1C1:R2C2');
43
+ isR1C1Rendered({ c0: 0, c1: 2, $c0: true, $c1: true }, 'C1:C3');
44
+ isR1C1Rendered({ r0: 1, r1: 2, $r1: true }, 'R[1]:R3');
45
+ isR1C1Rendered({ r0: 1, c0: 0, r1: 0, c1: -1, $c0: true, $r1: true }, 'R[1]C1:R1C[-1]');
46
+ });
47
+
48
+ test('partial serialization', () => {
49
+ isR1C1Rendered({ r0: -5, c0: -2, c1: -2 }, 'R[-5]C[-2]:C[-2]');
50
+ isR1C1Rendered({ r0: -5, c0: -3, r1: -5 }, 'R[-5]C[-3]:R[-5]');
51
+ isR1C1Rendered({ r0: -6, c0: 0, c1: 0, $c0: true, $c1: true }, 'R[-6]C1:C1');
52
+ isR1C1Rendered({ r0: -6, c0: 0, r1: -6, $c0: true, $c1: true }, 'R[-6]C1:R[-6]');
53
+ isR1C1Rendered({ r0: 0, c0: -2, c1: -2, $r0: true, $r1: true }, 'R1C[-2]:C[-2]');
54
+ isR1C1Rendered({ r0: 0, c0: -3, r1: 0, $r0: true, $r1: true }, 'R1C[-3]:R1');
55
+ isR1C1Rendered({ r0: 0, c0: 0, c1: 0, $r0: true, $c0: true, $r1: true, $c1: true }, 'R1C1:C1');
56
+ isR1C1Rendered({ r0: 0, c0: 0, r1: 0, $r0: true, $c0: true, $r1: true, $c1: true }, 'R1C1:R1');
57
+ isR1C1Rendered({ r0: -5, c0: 10, r1: 4 }, 'R[-5]C[10]:R[4]');
58
+ isR1C1Rendered({ r0: -6, c0: 15, r1: 3, $c0: true, $c1: true }, 'R[-6]C16:R[3]');
59
+ isR1C1Rendered({ r0: 0, c0: 10, r1: 9, $r0: true, $r1: true }, 'R1C[10]:R10');
60
+ isR1C1Rendered({ r0: 0, c0: 15, r1: 9, $r0: true, $c0: true, $r1: true, $c1: true }, 'R1C16:R10');
61
+ });
62
+
63
+ test('edge cases', () => {
64
+ // allow skipping right/bottom for cells
65
+ isR1C1Rendered({ r0: -5, c0: -2 }, 'R[-5]C[-2]');
66
+
67
+ // clamp the range at min/max dimensions
68
+ const abs = { $r0: true, $c0: true, $r1: true, $c1: true };
69
+ isR1C1Rendered({ r0: 1, c0: -20000, r1: 1, c1: 20000, ...abs }, 'R2');
70
+ isR1C1Rendered({ r0: -15e5, c0: 1, r1: 15e5, c1: 1, ...abs }, 'C2');
71
+ isR1C1Rendered({ r0: -5, c0: -2, r1: -8, c1: -7, ...abs }, 'R1C1');
72
+ isR1C1Rendered({ r0: 0, c0: -20000, r1: 0, c1: 20000 }, 'RC[-16383]:RC[16383]');
73
+ isR1C1Rendered({ r0: -15e5, c0: 0, r1: 15e5, c1: 0 }, 'R[-1048575]C:R[1048575]C');
74
+ isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5, ...abs }, 'R1C1');
75
+ isR1C1Rendered({ r0: 0.5, c0: 0.5, r1: 0.5, c1: 0.5 }, 'RC');
76
+ });
77
+
78
+ test('trimming', () => {
79
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2 }, 'R[1]C[1]:R[2]C[2]');
80
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'head' }, 'R[1]C[1].:R[2]C[2]');
81
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'tail' }, 'R[1]C[1]:.R[2]C[2]');
82
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 2, c1: 2, trim: 'both' }, 'R[1]C[1].:.R[2]C[2]');
83
+ isR1C1Rendered({ r0: 1, c0: 1, r1: 1, c1: 1, trim: 'both' }, 'R[1]C[1]');
84
+ isR1C1Rendered({ r0: 1, r1: 1 }, 'R[1]');
85
+ isR1C1Rendered({ r0: 1, r1: 1, trim: 'head' }, 'R[1].:R[1]');
86
+ isR1C1Rendered({ r0: 1, r1: 1, trim: 'both' }, 'R[1].:.R[1]');
87
+ isR1C1Rendered({ c0: 1, c1: 1 }, 'C[1]');
88
+ isR1C1Rendered({ c0: 1, c1: 1, trim: 'tail' }, 'C[1]:.C[1]');
89
+ isR1C1Rendered({ c0: 1, c1: 1, trim: 'both' }, 'C[1].:.C[1]');
90
+ isR1C1Rendered({ r0: -5, c0: -2, c1: -2, trim: 'both' }, 'R[-5]C[-2].:.C[-2]');
91
+ });
92
+ });