@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,191 @@
1
+ /*
2
+ ** RC notation works differently from A1 in that we can't merge static
3
+ ** references joined by `:`. Merging can only work between references
4
+ ** that are relative/absolute on the same axes, so:
5
+ ** - R1C1:R2C2 will work,
6
+ ** - R[1]C1:R[2]C2 will also work, but
7
+ ** - R[1]C[1]:R2C2 doesn't have a direct rectangle represention without context.
8
+ */
9
+ import type { RangeR1C1 } from './types.ts';
10
+
11
+ function trimDirection (head: boolean, tail: boolean): 'both' | 'head' | 'tail' | undefined {
12
+ if (head && tail) {
13
+ return 'both';
14
+ }
15
+ if (head) {
16
+ return 'head';
17
+ }
18
+ if (tail) {
19
+ return 'tail';
20
+ }
21
+ }
22
+
23
+ function parseR1C1Part (ref: string): [number, number, boolean, boolean] {
24
+ let r0 = null;
25
+ let c0 = null;
26
+ let $r0 = null;
27
+ let $c0 = null;
28
+ // R part
29
+ const rm = /^R(?:\[([+-]?\d+)\]|(\d+))?/.exec(ref);
30
+ if (rm) {
31
+ if (rm[1]) {
32
+ r0 = parseInt(rm[1], 10);
33
+ $r0 = false;
34
+ }
35
+ else if (rm[2]) {
36
+ r0 = parseInt(rm[2], 10) - 1;
37
+ $r0 = true;
38
+ }
39
+ else {
40
+ r0 = 0;
41
+ $r0 = false;
42
+ }
43
+ ref = ref.slice(rm[0].length);
44
+ }
45
+ // C part
46
+ const cm = /^C(?:\[([+-]?\d+)\]|(\d+))?/.exec(ref);
47
+ if (cm) {
48
+ if (cm[1]) {
49
+ c0 = parseInt(cm[1], 10);
50
+ $c0 = false;
51
+ }
52
+ else if (cm[2]) {
53
+ c0 = parseInt(cm[2], 10) - 1;
54
+ $c0 = true;
55
+ }
56
+ else {
57
+ c0 = 0;
58
+ $c0 = false;
59
+ }
60
+ ref = ref.slice(cm[0].length);
61
+ }
62
+ // must have at least one part (and nothing more)
63
+ if ((!rm && !cm) || ref.length) {
64
+ return null;
65
+ }
66
+ return [ r0, c0, $r0, $c0 ];
67
+ }
68
+
69
+ /**
70
+ * Parse R1C1-style range string into a RangeR1C1 object.
71
+ *
72
+ * @param rangeString R1C1-style range string.
73
+ * @return A reference object.
74
+ */
75
+ export function parseR1C1Range (rangeString: string): RangeR1C1 | null {
76
+ let final: RangeR1C1 | null = undefined;
77
+ const [ part1, op, part2, overflow ] = rangeString.split(/(\.?:\.?)/);
78
+ if (overflow) {
79
+ return null;
80
+ }
81
+ const range = parseR1C1Part(part1);
82
+ // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
83
+ const trim = trimDirection(!!op && op[0] === '.', !!op && op[op.length - 1] === '.');
84
+ if (range) {
85
+ const [ r0, c0, $r0, $c0 ] = range;
86
+ if (part2) {
87
+ const extendTo = parseR1C1Part(part2);
88
+ if (extendTo) {
89
+ final = {};
90
+ const [ r1, c1, $r1, $c1 ] = extendTo;
91
+ // rows
92
+ if (r0 != null && r1 != null) {
93
+ final.r0 = $r0 === $r1 ? Math.min(r0, r1) : r0;
94
+ final.$r0 = $r0;
95
+ final.r1 = $r0 === $r1 ? Math.max(r0, r1) : r1;
96
+ final.$r1 = $r1;
97
+ }
98
+ else if (r0 != null && r1 == null) {
99
+ // partial RC:C
100
+ final.r0 = r0;
101
+ final.$r0 = $r0;
102
+ final.r1 = null;
103
+ final.$r1 = $r0;
104
+ }
105
+ else if (r0 == null && r1 != null) {
106
+ // partial C:RC
107
+ final.r0 = r1;
108
+ final.$r0 = $r1;
109
+ final.r1 = null;
110
+ final.$r1 = $r1;
111
+ }
112
+ else if (r0 == null && r1 == null) {
113
+ // C:C
114
+ final.r0 = null;
115
+ final.$r0 = false;
116
+ final.r1 = null;
117
+ final.$r1 = false;
118
+ }
119
+ // columns
120
+ if (c0 != null && c1 != null) {
121
+ final.c0 = $c0 === $c1 ? Math.min(c0, c1) : c0;
122
+ final.$c0 = $c0;
123
+ final.c1 = $c0 === $c1 ? Math.max(c0, c1) : c1;
124
+ final.$c1 = $c1;
125
+ }
126
+ else if (c0 != null && c1 == null) {
127
+ final.c0 = c0;
128
+ final.$c0 = $c0;
129
+ final.c1 = null;
130
+ final.$c1 = $c0;
131
+ }
132
+ else if (c0 == null && c1 != null) {
133
+ final.c0 = c1;
134
+ final.$c0 = $c1;
135
+ final.c1 = null;
136
+ final.$c1 = $c1;
137
+ }
138
+ else if (c0 == null && c1 == null) {
139
+ final.c0 = null;
140
+ final.$c0 = false;
141
+ final.c1 = null;
142
+ final.$c1 = false;
143
+ }
144
+ }
145
+ else {
146
+ return null;
147
+ }
148
+ }
149
+ // range only - no second part
150
+ else if (r0 != null && c0 == null) {
151
+ final = {
152
+ r0: r0,
153
+ c0: null,
154
+ r1: r0,
155
+ c1: null,
156
+ $r0: $r0,
157
+ $c0: false,
158
+ $r1: $r0,
159
+ $c1: false
160
+ };
161
+ }
162
+ else if (r0 == null && c0 != null) {
163
+ final = {
164
+ r0: null,
165
+ c0: c0,
166
+ r1: null,
167
+ c1: c0,
168
+ $r0: false,
169
+ $c0: $c0,
170
+ $r1: false,
171
+ $c1: $c0
172
+ };
173
+ }
174
+ else {
175
+ final = {
176
+ r0: r0 || 0,
177
+ c0: c0 || 0,
178
+ r1: r0 || 0,
179
+ c1: c0 || 0,
180
+ $r0: $r0 || false,
181
+ $c0: $c0 || false,
182
+ $r1: $r0 || false,
183
+ $c1: $c0 || false
184
+ };
185
+ }
186
+ }
187
+ if (final && trim) {
188
+ final.trim = trim;
189
+ }
190
+ return final;
191
+ }
@@ -0,0 +1,323 @@
1
+ /* eslint-disable @stylistic/object-property-newline */
2
+ import { describe, test, expect } from 'vitest';
3
+ import { parseR1C1Ref, parseR1C1RefXlsx } from './parseR1C1Ref.ts';
4
+
5
+ type OptsIsRCEqual = {
6
+ allowNamed?: boolean;
7
+ allowTernary?: boolean;
8
+ xlsx?: boolean;
9
+ };
10
+
11
+ function isRCEqual (expr: string, expected: any, opts?: OptsIsRCEqual) {
12
+ const xlsx = !!opts?.xlsx;
13
+ if (expected) {
14
+ expected = xlsx
15
+ ? { workbookName: '', sheetName: '', ...expected }
16
+ : { context: [], ...expected };
17
+ if (expected.range && typeof expected.range === 'object') {
18
+ // mix in some defaults so we don't have to write things out in full
19
+ expected.range = {
20
+ r0: null, c0: null, r1: null, c1: null,
21
+ $r0: false, $c0: false, $r1: false, $c1: false,
22
+ ...expected.range
23
+ };
24
+ }
25
+ }
26
+ expect(xlsx ? parseR1C1RefXlsx(expr, opts) : parseR1C1Ref(expr, opts)).toEqual(expected);
27
+ }
28
+
29
+ describe('parse single R1C1 references', () => {
30
+ test('current row references', () => {
31
+ isRCEqual('R', { range: { r0: 0, r1: 0 } });
32
+ isRCEqual('R[0]', { range: { r0: 0, r1: 0 } });
33
+ });
34
+
35
+ test('fixed row references', () => {
36
+ isRCEqual('R0', { name: 'R0' });
37
+ isRCEqual('R1', { range: { r0: 0, r1: 0, $r0: true, $r1: true } });
38
+ isRCEqual('R10', { range: { r0: 9, r1: 9, $r0: true, $r1: true } });
39
+ });
40
+
41
+ test('relative row references', () => {
42
+ isRCEqual('R[1]', { range: { r0: 1, r1: 1 } });
43
+ isRCEqual('R[-1]', { range: { r0: -1, r1: -1 } });
44
+ });
45
+
46
+ test('current column references', () => {
47
+ isRCEqual('C', { range: { c0: 0, c1: 0 } });
48
+ isRCEqual('C[0]', { range: { c0: 0, c1: 0 } });
49
+ });
50
+
51
+ test('fixed column references', () => {
52
+ isRCEqual('C0', { name: 'C0' });
53
+ isRCEqual('C1', { range: { c0: 0, c1: 0, $c0: true, $c1: true } });
54
+ isRCEqual('C10', { range: { c0: 9, c1: 9, $c0: true, $c1: true } });
55
+ });
56
+
57
+ test('relative column references', () => {
58
+ isRCEqual('C[1]', { range: { c0: 1, c1: 1 } });
59
+ isRCEqual('C[-1]', { range: { c0: -1, c1: -1 } });
60
+ });
61
+
62
+ test('cell references', () => {
63
+ isRCEqual('RC', { range: { r0: 0, c0: 0, r1: 0, c1: 0 } });
64
+ isRCEqual('R0C0', { name: 'R0C0' });
65
+ isRCEqual('R1C1', { range: { r0: 0, c0: 0, r1: 0, c1: 0, $c0: true, $c1: true, $r0: true, $r1: true } });
66
+ isRCEqual('R10C8', { range: { r0: 9, c0: 7, r1: 9, c1: 7, $c0: true, $c1: true, $r0: true, $r1: true } });
67
+ isRCEqual('R-10C-8', undefined);
68
+ });
69
+
70
+ test('relative cell parts', () => {
71
+ isRCEqual('R[2]C', { range: { r0: 2, c0: 0, r1: 2, c1: 0 } });
72
+ isRCEqual('R[-2]C', { range: { r0: -2, c0: 0, r1: -2, c1: 0 } });
73
+ isRCEqual('RC[3]', { range: { r0: 0, c0: 3, r1: 0, c1: 3 } });
74
+ isRCEqual('RC[-3]', { range: { r0: 0, c0: -3, r1: 0, c1: -3 } });
75
+ isRCEqual('R[2]C[4]', { range: { r0: 2, c0: 4, r1: 2, c1: 4 } });
76
+ isRCEqual('R[-2]C[-4]', { range: { r0: -2, c0: -4, r1: -2, c1: -4 } });
77
+ });
78
+
79
+ test('mixed fixed and relative references', () => {
80
+ isRCEqual('R[9]C9', { range: { r0: 9, c0: 8, r1: 9, c1: 8, $c0: true, $c1: true } });
81
+ isRCEqual('R9C[9]', { range: { r0: 8, c0: 9, r1: 8, c1: 9, $r0: true, $r1: true } });
82
+ });
83
+
84
+ test('out of bounds references', () => {
85
+ isRCEqual('R1048577', { name: 'R1048577' });
86
+ isRCEqual('R[1048576]', undefined);
87
+ isRCEqual('C16385', { name: 'C16385' });
88
+ isRCEqual('C[16384]', undefined);
89
+ });
90
+ });
91
+
92
+ describe('R1C1 partial ranges', () => {
93
+ const opts = { allowTernary: true };
94
+
95
+ test('partials are not allowed by default', () => {
96
+ isRCEqual('R[-5]C[-2]:C[-2]', undefined);
97
+ isRCEqual('R1:R1C1', undefined);
98
+ });
99
+
100
+ test('beam type partials', () => {
101
+ isRCEqual('R[-5]C[-2]:C[-2]', { range: { r0: -5, c0: -2, c1: -2 } }, opts);
102
+ isRCEqual('C[-2]:R[-5]C[-2]', { range: { r0: -5, c0: -2, c1: -2 } }, opts);
103
+ isRCEqual('R[-5]C[-3]:R[-5]', { range: { r0: -5, c0: -3, r1: -5 } }, opts);
104
+ isRCEqual('R[-5]:R[-5]C[-3]', { range: { r0: -5, c0: -3, r1: -5 } }, opts);
105
+ isRCEqual('R[-6]C1:C1', { range: { r0: -6, c0: 0, c1: 0, $c0: true, $c1: true } }, opts);
106
+ isRCEqual('C1:R[-6]C1', { range: { r0: -6, c0: 0, c1: 0, $c0: true, $c1: true } }, opts);
107
+ isRCEqual('R[-6]C1:R[-6]', { range: { r0: -6, c0: 0, r1: -6, $c0: true, $c1: true } }, opts);
108
+ isRCEqual('R[-6]:R[-6]C1', { range: { r0: -6, c0: 0, r1: -6, $c0: true, $c1: true } }, opts);
109
+ isRCEqual('R1C[-2]:C[-2]', { range: { r0: 0, c0: -2, c1: -2, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
110
+ isRCEqual('C[-2]:R1C[-2]', { range: { r0: 0, c0: -2, c1: -2, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
111
+ isRCEqual('R1C[-3]:R1', { range: { r0: 0, c0: -3, r1: 0, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
112
+ isRCEqual('R1:R1C[-3]', { range: { r0: 0, c0: -3, r1: 0, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
113
+ isRCEqual('R1C1:C1', { range: { r0: 0, c0: 0, c1: 0, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
114
+ isRCEqual('C1:R1C1', { range: { r0: 0, c0: 0, c1: 0, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
115
+ isRCEqual('R1C1:R1', { range: { r0: 0, c0: 0, r1: 0, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
116
+ isRCEqual('R1:R1C1', { range: { r0: 0, c0: 0, r1: 0, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
117
+ });
118
+
119
+ test('range type partials', () => {
120
+ isRCEqual('R[-5]C[10]:R[4]', { range: { r0: -5, c0: 10, r1: 4 } }, opts);
121
+ isRCEqual('R[4]:R[-5]C[10]', { range: { r0: -5, c0: 10, r1: 4 } }, opts);
122
+ isRCEqual('R[-6]C16:R[3]', { range: { r0: -6, c0: 15, r1: 3, $c0: true, $c1: true } }, opts);
123
+ isRCEqual('R[3]:R[-6]C16', { range: { r0: -6, c0: 15, r1: 3, $c0: true, $c1: true } }, opts);
124
+ isRCEqual('R1C[10]:R10', { range: { r0: 0, c0: 10, r1: 9, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
125
+ isRCEqual('R10:R1C[10]', { range: { r0: 0, c0: 10, r1: 9, $r0: true, $c0: false, $r1: true, $c1: false } }, opts);
126
+ isRCEqual('R1C16:R10', { range: { r0: 0, c0: 15, r1: 9, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
127
+ isRCEqual('R10:R1C16', { range: { r0: 0, c0: 15, r1: 9, $r0: true, $c0: true, $r1: true, $c1: true } }, opts);
128
+ });
129
+ });
130
+
131
+ describe('parse joined R1C1 references', () => {
132
+ test('equivalent mirrored references', () => {
133
+ isRCEqual('R:R', { range: { r0: 0, r1: 0 } });
134
+ isRCEqual('R[0]:R[0]', { range: { r0: 0, r1: 0 } });
135
+ isRCEqual('R1:R1', { range: { r0: 0, r1: 0, $r0: true, $r1: true } });
136
+ isRCEqual('R10:R10', { range: { r0: 9, r1: 9, $r0: true, $r1: true } });
137
+ isRCEqual('R[1]:R[1]', { range: { r0: 1, r1: 1 } });
138
+ isRCEqual('R[-1]:R[-1]', { range: { r0: -1, r1: -1 } });
139
+
140
+ isRCEqual('C:C', { range: { c0: 0, c1: 0 } });
141
+ isRCEqual('C[0]:C[0]', { range: { c0: 0, c1: 0 } });
142
+ isRCEqual('C1:C1', { range: { c0: 0, c1: 0, $c0: true, $c1: true } });
143
+ isRCEqual('C10:C10', { range: { c0: 9, c1: 9, $c0: true, $c1: true } });
144
+ isRCEqual('C[1]:C[1]', { range: { c0: 1, c1: 1 } });
145
+ isRCEqual('C[-1]:C[-1]', { range: { c0: -1, c1: -1 } });
146
+ });
147
+
148
+ test('invalid range combinations', () => {
149
+ isRCEqual('R0:R0', undefined);
150
+ isRCEqual('C0:C0', undefined);
151
+ });
152
+
153
+ test('complex range references', () => {
154
+ isRCEqual('R[9]C9:R[9]C9', { range: { r0: 9, c0: 8, r1: 9, c1: 8, $c0: true, $c1: true } });
155
+ isRCEqual('R9C[9]:R9C[9]', { range: { r0: 8, c0: 9, r1: 8, c1: 9, $r0: true, $r1: true } });
156
+ isRCEqual('R[1]C[1]:R1C1', { range: { r0: 1, c0: 1, r1: 0, c1: 0, $c1: true, $r1: true } });
157
+ isRCEqual('R1C1:R2C2', { range: { r0: 0, c0: 0, r1: 1, c1: 1, $c0: true, $c1: true, $r0: true, $r1: true } });
158
+ isRCEqual('R2C2:R1C1', { range: { r0: 0, c0: 0, r1: 1, c1: 1, $c0: true, $c1: true, $r0: true, $r1: true } });
159
+ });
160
+
161
+ test('single dimension ranges', () => {
162
+ isRCEqual('C1:C3', { range: { c0: 0, c1: 2, $c0: true, $c1: true } });
163
+ isRCEqual('R[1]:R3', { range: { r0: 1, r1: 2, $r1: true } });
164
+ isRCEqual('R[1]C1:R1C[-1]', { range: { r0: 1, c0: 0, r1: 0, c1: -1, $c0: true, $r1: true, $c1: false } });
165
+ });
166
+
167
+ test('invalid mixed references', () => {
168
+ isRCEqual('R:C', undefined);
169
+ isRCEqual('R:RC', undefined);
170
+ isRCEqual('RC:R', undefined);
171
+ isRCEqual('RC:C', undefined);
172
+ isRCEqual('C:R', undefined);
173
+ isRCEqual('C:RC', undefined);
174
+ });
175
+ });
176
+
177
+ describe('parse R1C1 ranges in XLSX mode', () => {
178
+ const opts = { xlsx: true };
179
+ const rcRange = { r0: 0, c0: 0, r1: 0, c1: 0 };
180
+
181
+ test('workbook-only references', () => {
182
+ isRCEqual('[1]!RC', {
183
+ workbookName: '1',
184
+ sheetName: '',
185
+ range: rcRange
186
+ }, opts);
187
+
188
+ isRCEqual('[Workbook.xlsx]!RC', {
189
+ workbookName: 'Workbook.xlsx',
190
+ sheetName: '',
191
+ range: rcRange
192
+ }, opts);
193
+ });
194
+
195
+ test('workbook and sheet references', () => {
196
+ isRCEqual('[1]Sheet1!RC', {
197
+ workbookName: '1',
198
+ sheetName: 'Sheet1',
199
+ range: rcRange
200
+ }, opts);
201
+
202
+ isRCEqual('[Workbook.xlsx]Sheet1!RC', {
203
+ workbookName: 'Workbook.xlsx',
204
+ sheetName: 'Sheet1',
205
+ range: rcRange
206
+ }, opts);
207
+ });
208
+
209
+ test('named references', () => {
210
+ isRCEqual('[4]!name', {
211
+ workbookName: '4',
212
+ sheetName: '',
213
+ name: 'name'
214
+ }, opts);
215
+
216
+ isRCEqual('[Workbook.xlsx]!name', {
217
+ workbookName: 'Workbook.xlsx',
218
+ sheetName: '',
219
+ name: 'name'
220
+ }, opts);
221
+
222
+ isRCEqual('[16]Sheet1!name', {
223
+ workbookName: '16',
224
+ sheetName: 'Sheet1',
225
+ name: 'name'
226
+ }, opts);
227
+
228
+ isRCEqual('[Workbook.xlsx]Sheet1!name', {
229
+ workbookName: 'Workbook.xlsx',
230
+ sheetName: 'Sheet1',
231
+ name: 'name'
232
+ }, opts);
233
+ });
234
+
235
+ test('quoted references', () => {
236
+ isRCEqual("='[1]'!RC", {
237
+ workbookName: '1',
238
+ sheetName: '',
239
+ range: rcRange
240
+ }, opts);
241
+
242
+ isRCEqual("='[Workbook.xlsx]'!RC", {
243
+ workbookName: 'Workbook.xlsx',
244
+ sheetName: '',
245
+ range: rcRange
246
+ }, opts);
247
+
248
+ isRCEqual("'[1]Sheet1'!RC", {
249
+ workbookName: '1',
250
+ sheetName: 'Sheet1',
251
+ range: rcRange
252
+ }, opts);
253
+
254
+ isRCEqual("'[Workbook.xlsx]Sheet1'!RC", {
255
+ workbookName: 'Workbook.xlsx',
256
+ sheetName: 'Sheet1',
257
+ range: rcRange
258
+ }, opts);
259
+
260
+ isRCEqual("'[4]'!name", {
261
+ workbookName: '4',
262
+ sheetName: '',
263
+ name: 'name'
264
+ }, opts);
265
+
266
+ isRCEqual("'[Workbook.xlsx]'!name", {
267
+ workbookName: 'Workbook.xlsx',
268
+ sheetName: '',
269
+ name: 'name'
270
+ }, opts);
271
+
272
+ isRCEqual("'[16]Sheet1'!name", {
273
+ workbookName: '16',
274
+ sheetName: 'Sheet1',
275
+ name: 'name'
276
+ }, opts);
277
+
278
+ isRCEqual("'[Workbook.xlsx]Sheet1'!name", {
279
+ workbookName: 'Workbook.xlsx',
280
+ sheetName: 'Sheet1',
281
+ name: 'name'
282
+ }, opts);
283
+ });
284
+ });
285
+
286
+ describe('R1C1 trimmed ranges', () => {
287
+ const locks = { $r0: true, $r1: true, $c0: true, $c1: true };
288
+ const opts = [ {}, { xlsx: true } ];
289
+
290
+ for (const opt of opts) {
291
+ test(`trimmed ranges with ${opt.xlsx ? 'XLSX' : 'default'} options`, () => {
292
+ isRCEqual('R[1]C[1]:R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2 } }, opt);
293
+ isRCEqual('R[1]C[1].:R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'head' } }, opt);
294
+ isRCEqual('R[1]C[1]:.R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'tail' } }, opt);
295
+ isRCEqual('R[1]C[1].:.R[2]C[2]', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'both' } }, opt);
296
+
297
+ isRCEqual('R2C2:R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, ...locks } }, opt);
298
+ isRCEqual('R2C2.:R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'head', ...locks } }, opt);
299
+ isRCEqual('R2C2:.R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'tail', ...locks } }, opt);
300
+ isRCEqual('R2C2.:.R3C3', { range: { r0: 1, r1: 2, c0: 1, c1: 2, trim: 'both', ...locks } }, opt);
301
+
302
+ isRCEqual('C[1]:C[2]', { range: { c0: 1, c1: 2 } }, opt);
303
+ isRCEqual('C[1].:C[2]', { range: { c0: 1, c1: 2, trim: 'head' } }, opt);
304
+ isRCEqual('C[1]:.C[2]', { range: { c0: 1, c1: 2, trim: 'tail' } }, opt);
305
+ isRCEqual('C[1].:.C[2]', { range: { c0: 1, c1: 2, trim: 'both' } }, opt);
306
+
307
+ isRCEqual('R[10]:R[10]', { range: { r0: 10, r1: 10 } }, opt);
308
+ isRCEqual('R[10].:R[10]', { range: { r0: 10, r1: 10, trim: 'head' } }, opt);
309
+ isRCEqual('R[10]:.R[10]', { range: { r0: 10, r1: 10, trim: 'tail' } }, opt);
310
+ isRCEqual('R[10].:.R[10]', { range: { r0: 10, r1: 10, trim: 'both' } }, opt);
311
+
312
+ isRCEqual('R[2]C[2]:R[4]', undefined, { ...opt });
313
+ isRCEqual('R[2]C[2]:C[4]', undefined, { ...opt });
314
+ isRCEqual('R[2]C[2].:.R[4]', undefined, { ...opt });
315
+ isRCEqual('R[2]C[2].:.C[4]', undefined, { ...opt });
316
+
317
+ isRCEqual('R[2]C[2]:R[4]', { range: { r0: 2, r1: 4, c0: 2 } }, { allowTernary: true, ...opt });
318
+ isRCEqual('R[2]C[2]:C[4]', { range: { r0: 2, c0: 2, c1: 4 } }, { allowTernary: true, ...opt });
319
+ isRCEqual('R[2]C[2].:.R[4]', { range: { r0: 2, r1: 4, c0: 2, trim: 'both' } }, { allowTernary: true, ...opt });
320
+ isRCEqual('R[2]C[2].:.C[4]', { range: { r0: 2, c0: 2, c1: 4, trim: 'both' } }, { allowTernary: true, ...opt });
321
+ });
322
+ }
323
+ });
@@ -0,0 +1,127 @@
1
+ /*
2
+ ** RC notation works differently from A1 in that we can't merge static
3
+ ** references joined by `:`. Merging can only work between references
4
+ ** that are relative/absolute on the same axes, so:
5
+ ** - R1C1:R2C2 will work,
6
+ ** - R[1]C1:R[2]C2 will also work, but
7
+ ** - R[1]C[1]:R2C2 doesn't have a direct rectangle represention without context.
8
+ */
9
+ import type { ReferenceName, ReferenceNameXlsx, ReferenceR1C1, ReferenceR1C1Xlsx } from './types.ts';
10
+ import { parseR1C1Range } from './parseR1C1Range.ts';
11
+ import { parseRefCtx, parseRefXlsx } from './parseRef.ts';
12
+
13
+ /**
14
+ * Options for {@link parseR1C1Ref}.
15
+ */
16
+ export type OptsParseR1C1Ref = {
17
+ /**
18
+ * Enable parsing names as well as ranges.
19
+ * @defaultValue true
20
+ */
21
+ allowNamed?: boolean;
22
+ /**
23
+ * Enables the recognition of ternary ranges in the style of `A1:A` or `A1:1`.
24
+ * These are supported by Google Sheets but not Excel.
25
+ * See: [References.md](./References.md).
26
+ * @defaultValue false
27
+ */
28
+ allowTernary?: boolean;
29
+ };
30
+
31
+ /**
32
+ * Parse a string reference into an object representing it.
33
+ *
34
+ * ```js
35
+ * parseR1C1Ref('Sheet1!R[9]C9:R[9]C9');
36
+ * // => {
37
+ * // context: [ 'Sheet1' ],
38
+ * // range: {
39
+ * // r0: 9,
40
+ * // c0: 8,
41
+ * // r1: 9,
42
+ * // c1: 8,
43
+ * // $c0: true,
44
+ * // $c1: true
45
+ * // $r0: false,
46
+ * // $r1: false
47
+ * // }
48
+ * // }
49
+ * ```
50
+ *
51
+ * @see {@link OptsParseR1C1Ref}
52
+ * @param refString An R1C1-style reference string.
53
+ * @param [options] Options.
54
+ * @returns An object representing a valid reference or `undefined` if it is invalid.
55
+ */
56
+ export function parseR1C1Ref (
57
+ refString: string,
58
+ options: OptsParseR1C1Ref = {}
59
+ ): ReferenceR1C1 | ReferenceName | undefined {
60
+ const {
61
+ allowNamed = true,
62
+ allowTernary = false
63
+ } = options;
64
+ const d = parseRefCtx(refString, { allowNamed, allowTernary, r1c1: true });
65
+ if (d) {
66
+ if (d.name) {
67
+ return { context: d.context, name: d.name } as ReferenceName;
68
+ }
69
+ else if (d.r0) {
70
+ const range = d.r1
71
+ ? parseR1C1Range(d.r0 + d.operator + d.r1)
72
+ : parseR1C1Range(d.r0);
73
+ if (range) {
74
+ return { context: d.context, range } as ReferenceR1C1;
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Parse a string reference into an object representing it.
82
+ *
83
+ * ```js
84
+ * parseR1C1Ref('Sheet1!R[9]C9:R[9]C9');
85
+ * // => {
86
+ * // context: [ 'Sheet1' ],
87
+ * // range: {
88
+ * // r0: 9,
89
+ * // c0: 8,
90
+ * // r1: 9,
91
+ * // c1: 8,
92
+ * // $c0: true,
93
+ * // $c1: true
94
+ * // $r0: false,
95
+ * // $r1: false
96
+ * // }
97
+ * // }
98
+ * ```
99
+ *
100
+ * @see {@link OptsParseR1C1Ref}
101
+ * @param refString An R1C1-style reference string.
102
+ * @param [options] Options.
103
+ * @returns An object representing a valid reference or `undefined` if it is invalid.
104
+ */
105
+ export function parseR1C1RefXlsx (
106
+ refString: string,
107
+ options: OptsParseR1C1Ref = {}
108
+ ): ReferenceR1C1Xlsx | ReferenceNameXlsx | undefined {
109
+ const {
110
+ allowNamed = true,
111
+ allowTernary = false
112
+ } = options;
113
+ const d = parseRefXlsx(refString, { allowNamed, allowTernary, r1c1: true });
114
+ if (d) {
115
+ if (d.name && allowNamed) {
116
+ return { workbookName: d.workbookName, sheetName: d.sheetName, name: d.name } as ReferenceNameXlsx;
117
+ }
118
+ else if (d.r0) {
119
+ const range = d.r1
120
+ ? parseR1C1Range(d.r0 + d.operator + d.r1)
121
+ : parseR1C1Range(d.r0);
122
+ if (range) {
123
+ return { workbookName: (d).workbookName, sheetName: (d).sheetName, range } as ReferenceR1C1Xlsx;
124
+ }
125
+ }
126
+ }
127
+ }