@trebco/treb 28.17.5 → 29.1.4

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 (88) hide show
  1. package/dist/treb-spreadsheet-light.mjs +12 -12
  2. package/dist/treb-spreadsheet.mjs +12 -12
  3. package/dist/treb.d.ts +121 -82
  4. package/eslint.config.js +21 -0
  5. package/package.json +6 -6
  6. package/treb-base-types/src/area.ts +4 -2
  7. package/treb-base-types/src/cell.ts +1 -1
  8. package/treb-base-types/src/cells.ts +16 -7
  9. package/treb-base-types/src/dom-utilities.ts +4 -2
  10. package/treb-base-types/src/import.ts +2 -2
  11. package/treb-base-types/src/rectangle.ts +5 -5
  12. package/treb-base-types/src/text_part.ts +7 -0
  13. package/treb-base-types/src/union.ts +6 -1
  14. package/treb-base-types/src/value-type.ts +1 -1
  15. package/treb-calculator/src/calculator.ts +114 -165
  16. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +1 -2
  17. package/treb-calculator/src/dag/graph.ts +3 -3
  18. package/treb-calculator/src/dag/spreadsheet_vertex.ts +2 -2
  19. package/treb-calculator/src/dag/state_leaf_vertex.ts +2 -4
  20. package/treb-calculator/src/descriptors.ts +28 -2
  21. package/treb-calculator/src/expression-calculator.ts +25 -34
  22. package/treb-calculator/src/function-error.ts +2 -2
  23. package/treb-calculator/src/function-library.ts +16 -0
  24. package/treb-calculator/src/functions/base-functions.ts +185 -211
  25. package/treb-calculator/src/functions/checkbox.ts +0 -1
  26. package/treb-calculator/src/functions/complex-functions.ts +49 -47
  27. package/treb-calculator/src/functions/finance-functions.ts +10 -10
  28. package/treb-calculator/src/functions/function-utilities.ts +26 -0
  29. package/treb-calculator/src/functions/information-functions.ts +21 -41
  30. package/treb-calculator/src/functions/matrix-functions.ts +8 -1
  31. package/treb-calculator/src/functions/sparkline.ts +6 -4
  32. package/treb-calculator/src/functions/statistics-functions.ts +21 -17
  33. package/treb-calculator/src/functions/text-functions.ts +14 -13
  34. package/treb-calculator/src/primitives.ts +48 -37
  35. package/treb-calculator/src/utilities.ts +117 -134
  36. package/treb-charts/src/chart-functions.ts +3 -3
  37. package/treb-charts/src/chart-types.ts +42 -1
  38. package/treb-charts/src/chart-utils.ts +155 -113
  39. package/treb-charts/src/chart.ts +6 -5
  40. package/treb-charts/src/default-chart-renderer.ts +6 -5
  41. package/treb-charts/src/renderer.ts +12 -11
  42. package/treb-charts/src/util.ts +25 -36
  43. package/treb-data-model/package.json +5 -0
  44. package/{treb-grid/src/types → treb-data-model/src}/annotation.ts +2 -2
  45. package/{treb-grid/src/types → treb-data-model/src}/conditional_format.ts +20 -0
  46. package/{treb-grid/src/types → treb-data-model/src}/data_model.ts +231 -133
  47. package/treb-data-model/src/index.ts +45 -0
  48. package/{treb-grid/src/types/named_range.ts → treb-data-model/src/named.ts} +459 -376
  49. package/{treb-grid/src/types → treb-data-model/src}/sheet.ts +13 -5
  50. package/treb-data-model/src/sheet_collection.ts +114 -0
  51. package/{treb-grid/src/types → treb-data-model/src}/sheet_types.ts +6 -3
  52. package/treb-embed/modern.tsconfig.json +1 -0
  53. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +2 -2
  54. package/treb-embed/src/embedded-spreadsheet.ts +125 -270
  55. package/treb-embed/src/selection-state.ts +1 -1
  56. package/treb-embed/src/toolbar-message.ts +1 -1
  57. package/treb-embed/src/types.ts +13 -5
  58. package/treb-export/src/export-worker/export-worker.ts +22 -7
  59. package/treb-export/src/export2.ts +110 -41
  60. package/treb-export/src/import2.ts +6 -5
  61. package/treb-export/src/workbook2.ts +31 -13
  62. package/treb-export/src/xml-utils.ts +5 -1
  63. package/treb-format/src/format.ts +8 -6
  64. package/treb-grid/src/editors/autocomplete.ts +2 -2
  65. package/treb-grid/src/editors/autocomplete_matcher.ts +57 -19
  66. package/treb-grid/src/editors/editor.ts +27 -25
  67. package/treb-grid/src/editors/formula_bar.ts +5 -5
  68. package/treb-grid/src/editors/overlay_editor.ts +1 -2
  69. package/treb-grid/src/index.ts +0 -11
  70. package/treb-grid/src/layout/base_layout.ts +20 -8
  71. package/treb-grid/src/layout/grid_layout.ts +2 -2
  72. package/treb-grid/src/layout/mock-layout.ts +5 -6
  73. package/treb-grid/src/render/selection-renderer.ts +2 -3
  74. package/treb-grid/src/render/tile_renderer.ts +18 -8
  75. package/treb-grid/src/types/grid.ts +96 -67
  76. package/treb-grid/src/types/grid_base.ts +76 -60
  77. package/treb-grid/src/types/grid_command.ts +3 -2
  78. package/treb-grid/src/types/grid_events.ts +12 -6
  79. package/treb-grid/src/types/tab_bar.ts +1 -2
  80. package/treb-parser/src/parser-types.ts +2 -1
  81. package/treb-parser/src/parser.ts +7 -5
  82. package/treb-utils/src/event_source.ts +1 -1
  83. package/treb-utils/src/serialize_html.ts +31 -6
  84. package/.eslintignore +0 -8
  85. package/.eslintrc.cjs +0 -168
  86. package/treb-grid/src/layout/rectangle_cache.ts +0 -86
  87. /package/{treb-grid/src/types → treb-data-model/src}/serialize_options.ts +0 -0
  88. /package/{treb-grid/src/types/grid_selection.ts → treb-data-model/src/sheet_selection.ts} +0 -0
@@ -1,376 +1,459 @@
1
- /*
2
- * This file is part of TREB.
3
- *
4
- * TREB is free software: you can redistribute it and/or modify it under the
5
- * terms of the GNU General Public License as published by the Free Software
6
- * Foundation, either version 3 of the License, or (at your option) any
7
- * later version.
8
- *
9
- * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
- * details.
13
- *
14
- * You should have received a copy of the GNU General Public License along
15
- * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
- * Copyright 2022-2024 trebco, llc.
18
- * info@treb.app
19
- *
20
- */
21
-
22
- import { type IArea, Area } from 'treb-base-types';
23
-
24
- /**
25
- * I want to repurpose named ranges (a little) to allow either values or
26
- * arbitrary expressions. this is sort of 1/2 way between named ranges and
27
- * "macro functions".
28
- *
29
- * not sure if we should change named ranges, or create a side path for
30
- * "named expressions".
31
- */
32
-
33
- export class NamedRangeCollection {
34
-
35
- private forward: {[index: string]: Area} = {};
36
- private backward: Array<{name: string; range: Area}> = [];
37
-
38
- /** FIXME: why not an accessor? */
39
- public Count(): number {
40
- return this.backward.length;
41
- }
42
-
43
- /** FIXME: why not just use toJSON? */
44
- public Serialize(): Record<string, IArea> {
45
- return JSON.parse(JSON.stringify(this.Map()));
46
- }
47
-
48
- public Deserialize(data?: Record<string, IArea>): void {
49
- this.Reset();
50
- if (data) {
51
- for (const key of Object.keys(data)) {
52
- this.SetName(key, new Area(data[key].start, data[key].end), false);
53
- }
54
- this.RebuildList();
55
- }
56
- }
57
-
58
- /**
59
- * match an area, optionally a target within a larger area (for selections).
60
- * we don't use the selection directly, as we may need to adjust target for
61
- * merge area.
62
- */
63
- public MatchSelection(area: Area, target?: Area): string|undefined {
64
-
65
- if (!area.start.sheet_id) {
66
- throw new Error('match selection without sheet id');
67
- }
68
-
69
- let label: string|undefined;
70
- for (const entry of this.List()) {
71
- if (entry.range.start.sheet_id === area.start.sheet_id) {
72
- if (entry.range.Equals(area)) {
73
- label = entry.name; // don't break, in case there's a match for target which takes precendence.
74
- }
75
- if (target?.Equals(entry.range)) {
76
- return entry.name;
77
- }
78
- }
79
- }
80
- return label;
81
- }
82
-
83
- /**
84
- * add name. names are case-insensitive. if the name already
85
- * exists, it will be overwritten.
86
- *
87
- * update: returns success (FIXME: proper errors)
88
- */
89
- public SetName(name: string, range: Area, apply = true): boolean {
90
- const validated = this.ValidateNamed(name);
91
- if (!validated) {
92
- console.warn('invalid name');
93
- return false;
94
- }
95
-
96
- // why is this considered invalid here? I've seen it done.
97
- // maybe something we're doing with these ranges doesn't
98
- // collapse them? (...)
99
-
100
- if (range.entire_column || range.entire_row) {
101
- console.info({range});
102
- console.warn(`invalid range`);
103
- return false;
104
- }
105
-
106
- this.forward[validated] = range;
107
- if (apply) {
108
- this.RebuildList();
109
- }
110
- return true;
111
- }
112
-
113
- public SetNames(list: {[index: string]: IArea}): void {
114
- for (const key of Object.keys(list)) {
115
- const area = list[key];
116
- this.SetName(key, new Area(area.start, area.end), false);
117
- }
118
- this.RebuildList();
119
- }
120
-
121
- public ClearName(name: string, apply = true): void {
122
- delete this.forward[name];
123
- if (apply) {
124
- this.RebuildList();
125
- }
126
- }
127
-
128
- /**
129
- * if we delete a sheet, remove ranges in that sheet
130
- * @param sheet_id
131
- */
132
- public RemoveRangesForSheet(sheet_id: number, apply = true) {
133
-
134
- const temp: {[index: string]: Area} = {};
135
- const list = this.List();
136
-
137
- for (const entry of list) {
138
- if (entry.range.start.sheet_id !== sheet_id) {
139
- temp[entry.name] = entry.range;
140
- }
141
- }
142
-
143
- this.forward = temp;
144
-
145
- if (apply) {
146
- this.RebuildList();
147
- }
148
- }
149
-
150
- public Reset(): void {
151
- this.forward = {};
152
- this.backward = [];
153
- }
154
-
155
- public Get(name: string) {
156
- return this.forward[name.toUpperCase()];
157
- }
158
-
159
- /** FIXME: accessor */
160
- public Map() {
161
- return this.forward;
162
- }
163
-
164
- /** FIXME: accessor */
165
- public List() {
166
- return this.backward;
167
- }
168
-
169
- /**
170
- * named range rules:
171
- *
172
- * - legal characters are alphanumeric, underscore and dot.
173
- * - must start with letter or underscore (not a number or dot).
174
- * - cannot look like a spreadsheet address, which is 1-3 letters followed by numbers.
175
- *
176
- * returns a normalized name (just caps, atm)
177
- */
178
- public ValidateNamed(name: string): string|false {
179
- name = name.trim();
180
- if (!name.length) return false;
181
- if (/^[A-Za-z]{1,3}\d+$/.test(name)) return false;
182
- if (/[^A-Za-z\d_.]/.test(name)) return false;
183
- if (/^[^A-Za-z_]/.test(name)) return false;
184
- return name.toUpperCase();
185
- }
186
-
187
- // was in sheet
188
-
189
-
190
- /**
191
- * fix named range references after row/column insert/delete
192
- */
193
- public PatchNamedRanges(sheet_id: number, before_column: number, column_count: number, before_row: number, row_count: number) {
194
-
195
- const copy = this.List().slice(0);
196
-
197
- for (const entry of copy) {
198
-
199
- const key = entry.name;
200
- const range = entry.range;
201
-
202
- if (range.start.sheet_id !== sheet_id) {
203
- console.info('skipping name', key);
204
- continue;
205
- }
206
-
207
- if (column_count && before_column <= range.end.column) {
208
-
209
- /*
210
- // (1) we are before the insert point, not affected
211
-
212
- if (before_column > range.end.column) {
213
- continue;
214
- }
215
- */
216
-
217
- if (column_count > 0) {
218
-
219
- // (2) it's an insert and we are past the insert point:
220
- // increment [start] and [end] by [count]
221
-
222
- if (before_column <= range.start.column) {
223
- range.Shift(0, column_count);
224
- }
225
-
226
- // (3) it's an insert and we contain the insert point:
227
- // increment [end] by [count]
228
-
229
- else if (before_column > range.start.column && before_column <= range.end.column) {
230
- range.ConsumeAddress({row: range.end.row, column: range.end.column + column_count});
231
- }
232
-
233
- else {
234
- console.warn(`PNR X case 1`, before_column, column_count, JSON.stringify(range));
235
- }
236
-
237
- }
238
- else if (column_count < 0) {
239
-
240
- // (4) it's a delete and we are past the delete point (before+count):
241
- // decrement [start] and [end] by [count]
242
-
243
- if (before_column - column_count <= range.start.column) {
244
- range.Shift(0, column_count);
245
- }
246
-
247
- // (5) it's a delete and contains the entire range
248
-
249
- else if (before_column <= range.start.column && before_column - column_count > range.end.column) {
250
- this.ClearName(key, false);
251
- }
252
-
253
- // (6) it's a delete and contains part of the range. clip the range.
254
-
255
- else if (before_column <= range.start.column) {
256
- const last_column = before_column - column_count - 1;
257
- this.SetName(key, new Area({
258
- row: range.start.row, column: last_column + 1 + column_count, sheet_id }, {
259
- row: range.end.row, column: range.end.column + column_count }), false);
260
- }
261
-
262
- else if (before_column <= range.end.column) {
263
- const last_column = before_column - column_count - 1;
264
-
265
- if (last_column >= range.end.column) {
266
- this.SetName(key, new Area({
267
- row: range.start.row, column: range.start.column, sheet_id }, {
268
- row: range.end.row, column: before_column - 1 }), false);
269
- }
270
- else {
271
- this.SetName(key, new Area({
272
- row: range.start.row, column: range.start.column, sheet_id }, {
273
- row: range.end.row, column: range.start.column + range.columns + column_count - 1}), false);
274
- }
275
-
276
- }
277
-
278
- else {
279
- console.warn(`PNR X case 2`, before_column, column_count, JSON.stringify(range));
280
- }
281
-
282
- }
283
- }
284
-
285
-
286
- if (row_count && before_row <= range.end.row) {
287
-
288
- /*
289
- // (1) we are before the insert point, not affected
290
-
291
- if (before_row > range.end.row) {
292
- continue;
293
- }
294
- */
295
-
296
- if (row_count > 0) {
297
-
298
- // (2) it's an insert and we are past the insert point:
299
- // increment [start] and [end] by [count]
300
-
301
- if (before_row <= range.start.row) {
302
- range.Shift(row_count, 0);
303
- }
304
-
305
- // (3) it's an insert and we contain the insert point:
306
- // increment [end] by [count]
307
-
308
- else if (before_row > range.start.row && before_row <= range.end.row) {
309
- range.ConsumeAddress({row: range.end.row + row_count, column: range.end.column});
310
- }
311
-
312
- else {
313
- console.warn(`PNR X case 3`, before_row, row_count, JSON.stringify(range));
314
- }
315
-
316
- }
317
- else if (row_count < 0) {
318
-
319
- // (4) it's a delete and we are past the delete point (before+count):
320
- // decrement [start] and [end] by [count]
321
-
322
- if (before_row - row_count <= range.start.row) {
323
- range.Shift(row_count, 0);
324
- }
325
-
326
- // (5) it's a delete and contains the entire range
327
-
328
- else if (before_row <= range.start.row && before_row - row_count > range.end.row) {
329
- this.ClearName(key, false);
330
- }
331
-
332
- // (6) it's a delete and contains part of the range. clip the range.
333
-
334
- else if (before_row <= range.start.row) {
335
- const last_row = before_row - row_count - 1;
336
- this.SetName(key, new Area({
337
- column: range.start.column, row: last_row + 1 + row_count, sheet_id }, {
338
- column: range.end.column, row: range.end.row + row_count }), false);
339
- }
340
-
341
- else if (before_row <= range.end.row) {
342
- const last_row = before_row - row_count - 1;
343
- if (last_row >= range.end.row) {
344
- this.SetName(key, new Area({
345
- column: range.start.column, row: range.start.row, sheet_id }, {
346
- column: range.end.column, row: before_row - 1 }), false);
347
- }
348
- else {
349
- this.SetName(key, new Area({
350
- column: range.start.column, row: range.start.row, sheet_id }, {
351
- column: range.end.column, row: range.start.row + range.rows + row_count - 1 }), false);
352
- }
353
-
354
- }
355
-
356
- else {
357
- console.warn(`PNR X case 4`, before_row, row_count, JSON.stringify(range));
358
- }
359
-
360
- }
361
- }
362
-
363
- }
364
-
365
- this.RebuildList();
366
-
367
- }
368
-
369
- public RebuildList(): void {
370
- this.backward = [];
371
- for (const key of Object.keys(this.forward)) {
372
- this.backward.push({ name: key, range: this.forward[key] });
373
- }
374
- }
375
-
376
- }
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2024 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ import { Area } from 'treb-base-types';
23
+ import type { SerializedArea, IArea } from 'treb-base-types';
24
+ import type { ExpressionUnit } from 'treb-parser';
25
+
26
+ export interface NamedExpression {
27
+ type: 'expression';
28
+ expression: ExpressionUnit;
29
+ }
30
+
31
+ export interface NamedRange {
32
+ type: 'range';
33
+ area: Area;
34
+ }
35
+
36
+ export type Named = (NamedExpression | NamedRange) & {
37
+ name: string; // canonical name
38
+ scope?: number; // scope to sheet by ID
39
+ };
40
+
41
+ /**
42
+ * serialized type
43
+ *
44
+ * @privateRemarks
45
+ *
46
+ * for the external type we switch on the presence of the area
47
+ * or the expression. area uses a type that includes sheet names
48
+ * (IArea should allow that?). expression here is a string.
49
+ *
50
+ * we could theoretically switch the internal type the same way
51
+ * and drop the string keys. something to think about.
52
+ *
53
+ * when serialized, scope is either the sheet name or nothing
54
+ * (implicit global scope).
55
+ */
56
+ export interface SerializedNamed {
57
+ name: string;
58
+ area?: SerializedArea;
59
+ expression?: string;
60
+ scope?: string;
61
+ }
62
+
63
+ /**
64
+ * this is a type we're using in imports. it consolidates the
65
+ * two types. we should maybe switch as well, at least for
66
+ * serialized representation? something to think about.
67
+ */
68
+ export interface CompositeNamed {
69
+
70
+ name: string;
71
+
72
+ /**
73
+ * could be a address/range or a function expression. we'll distinguish
74
+ * when we parse it.
75
+ */
76
+ expression: string;
77
+
78
+ /** resolved sheet name */
79
+ scope?: string;
80
+
81
+ }
82
+
83
+ /**
84
+ * this is a replacement for the name manager, which handles
85
+ * operations relating to named ranges.
86
+ */
87
+ export class NamedRangeManager {
88
+
89
+ /**
90
+ * this map is stored with normalized names. normalized names
91
+ * here means we call `toLowerCase`. the objects themselves
92
+ * contain canonical names.
93
+ *
94
+ * ...we've always had a map for this, for fast lookups. but
95
+ * with scoping, we can't necessarily look up by name. let's try
96
+ * using scope:name keys. that way we can search for scope:name
97
+ * and then name and return the first match, if any.
98
+ *
99
+ */
100
+ protected named: Map<string, Named> = new Map();
101
+
102
+ public get list() {
103
+ return this.named.values();
104
+ }
105
+
106
+ /** shorthand for setting named expression */
107
+ public SetNamedExpression(name: string, expression: ExpressionUnit, scope?: number) {
108
+ return this.SetName({
109
+ type: 'expression',
110
+ name,
111
+ expression,
112
+ scope,
113
+ });
114
+ }
115
+
116
+ /** shorthand for setting named range */
117
+ public SetNamedRange(name: string, area: IArea, scope?: number) {
118
+ return this.SetName({
119
+ type: 'range',
120
+ name,
121
+ area: new Area(area.start, area.end),
122
+ scope,
123
+ });
124
+ }
125
+
126
+ /**
127
+ * add name. names are case-insensitive. if the name already
128
+ * exists, it will be overwritten.
129
+ *
130
+ * update: returns success (FIXME: proper errors)
131
+ */
132
+ private SetName(named: Named): boolean {
133
+
134
+ const name = named.name;
135
+ // console.info('set name', named.name, {named});
136
+
137
+ const validated = this.ValidateNamed(name);
138
+ if (!validated) {
139
+ console.warn('invalid name');
140
+ return false;
141
+ }
142
+
143
+ if (named.type === 'range') {
144
+
145
+ // why is this considered invalid here? I've seen it done.
146
+ // maybe something we're doing with these ranges doesn't
147
+ // collapse them? (...)
148
+
149
+ if (named.area.entire_column || named.area.entire_row) {
150
+ console.info({named});
151
+ console.warn(`invalid range`);
152
+ return false;
153
+ }
154
+
155
+ }
156
+
157
+ // this.named.set(name.toLowerCase(), named);
158
+ this.named.set(this.ScopedName(name, named.scope), named);
159
+
160
+ return true;
161
+ }
162
+
163
+ private ScopedName(name: string, scope?: number) {
164
+ if (typeof scope === 'number') {
165
+ return scope + ':' + name.toLowerCase();
166
+ }
167
+ return name.toLowerCase();
168
+ }
169
+
170
+ public ClearName(name: string, scope?: number): void {
171
+
172
+ if (typeof scope === 'number') {
173
+ this.named.delete(this.ScopedName(name, scope));
174
+ }
175
+ else {
176
+ this.named.delete(name.toLowerCase());
177
+ }
178
+ }
179
+
180
+ /**
181
+ * if we delete a sheet, remove ranges in that sheet. also remove
182
+ * anything that's scoped to the sheet.
183
+ */
184
+ public RemoveRangesForSheet(sheet_id: number) {
185
+
186
+ const remove: string[] = [];
187
+ for (const [name, entry] of this.named) {
188
+ if (entry.type === 'range' && entry.area.start.sheet_id === sheet_id) {
189
+ remove.push(name);
190
+ }
191
+ else if (entry.scope === sheet_id) {
192
+ remove.push(name);
193
+ }
194
+ }
195
+
196
+ for (const name of remove) {
197
+ this.named.delete(name)
198
+ }
199
+
200
+ }
201
+
202
+ public Reset() {
203
+ this.named.clear();
204
+ }
205
+
206
+ /**
207
+ * requiring scope to help propgogate changes. we check the scoped
208
+ * version first; if that's not found, we default to the global version.
209
+ * that implies that if there are both, we'll prefer the scoped name.
210
+ *
211
+ * now possible to require scope, for qualified scoped names
212
+ */
213
+ public Get_(name: string, scope: number, require_scope = false) {
214
+
215
+ if (require_scope) {
216
+ return this.named.get(this.ScopedName(name, scope));
217
+ }
218
+
219
+ return this.named.get(this.ScopedName(name, scope)) || this.named.get(name.toLowerCase());
220
+ }
221
+
222
+ /**
223
+ * named range rules:
224
+ *
225
+ * - legal characters are alphanumeric, underscore and dot.
226
+ * - must start with letter or underscore (not a number or dot).
227
+ * - cannot look like a spreadsheet address, which is 1-3 letters followed by numbers.
228
+ *
229
+ * returns a normalized name (just caps, atm)
230
+ */
231
+ public ValidateNamed(name: string): string|false {
232
+ name = name.trim();
233
+ if (!name.length) return false;
234
+ if (/^[A-Za-z]{1,3}\d+$/.test(name)) return false;
235
+ if (/[^A-Za-z\d_.]/.test(name)) return false;
236
+ if (/^[^A-Za-z_]/.test(name)) return false;
237
+ return name.toUpperCase();
238
+ }
239
+
240
+ /**
241
+ * match an area, optionally a target within a larger area (for selections).
242
+ * we don't use the selection directly, as we may need to adjust target for
243
+ * merge area. returns the name only if the area is an exact match.
244
+ */
245
+ public MatchSelection(area: Area, target?: Area): string|undefined {
246
+
247
+ if (!area.start.sheet_id) {
248
+ throw new Error('match selection without sheet id');
249
+ }
250
+
251
+ let label: string|undefined;
252
+
253
+ for (const entry of this.named.values()) {
254
+ if (entry.type === 'range') {
255
+ if (entry.area.start.sheet_id === area.start.sheet_id) {
256
+ if (area.Equals(entry.area)) {
257
+ label = entry.name; // don't break, in case there's a match for target which takes precendence.
258
+ }
259
+ if (target?.Equals(entry.area)) {
260
+ return entry.name;
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ return label;
267
+
268
+ }
269
+
270
+
271
+ /**
272
+ * fix named range references after row/column insert/delete
273
+ *
274
+ * surely there's overlap between this function and what we do in
275
+ * grid when columns are added/removed. can we consolidate? (FIXME/TODO)
276
+ *
277
+ */
278
+ public PatchNamedRanges(sheet_id: number, before_column: number, column_count: number, before_row: number, row_count: number) {
279
+
280
+ const copy = [...this.list];
281
+
282
+ for (const entry of copy) {
283
+
284
+ if (entry.type === 'expression') {
285
+ continue;
286
+ }
287
+
288
+ const key = entry.name;
289
+ const range = entry.area;
290
+
291
+ if (range.start.sheet_id !== sheet_id) {
292
+ console.info('skipping name', key);
293
+ continue;
294
+ }
295
+
296
+ if (column_count && before_column <= range.end.column) {
297
+
298
+ /*
299
+ // (1) we are before the insert point, not affected
300
+
301
+ if (before_column > range.end.column) {
302
+ continue;
303
+ }
304
+ */
305
+
306
+ if (column_count > 0) {
307
+
308
+ // (2) it's an insert and we are past the insert point:
309
+ // increment [start] and [end] by [count]
310
+
311
+ if (before_column <= range.start.column) {
312
+ range.Shift(0, column_count);
313
+ }
314
+
315
+ // (3) it's an insert and we contain the insert point:
316
+ // increment [end] by [count]
317
+
318
+ else if (before_column > range.start.column && before_column <= range.end.column) {
319
+ range.ConsumeAddress({row: range.end.row, column: range.end.column + column_count});
320
+ }
321
+
322
+ else {
323
+ console.warn(`PNR X case 1`, before_column, column_count, JSON.stringify(range));
324
+ }
325
+
326
+ }
327
+ else if (column_count < 0) {
328
+
329
+ // (4) it's a delete and we are past the delete point (before+count):
330
+ // decrement [start] and [end] by [count]
331
+
332
+ if (before_column - column_count <= range.start.column) {
333
+ range.Shift(0, column_count);
334
+ }
335
+
336
+ // (5) it's a delete and contains the entire range
337
+
338
+ else if (before_column <= range.start.column && before_column - column_count > range.end.column) {
339
+ this.ClearName(key);
340
+ }
341
+
342
+ // (6) it's a delete and contains part of the range. clip the range.
343
+
344
+ else if (before_column <= range.start.column) {
345
+ const last_column = before_column - column_count - 1;
346
+ this.SetName({
347
+ type: 'range', area: new Area({
348
+ row: range.start.row, column: last_column + 1 + column_count, sheet_id }, {
349
+ row: range.end.row, column: range.end.column + column_count }), name: key, });
350
+ }
351
+
352
+ else if (before_column <= range.end.column) {
353
+ const last_column = before_column - column_count - 1;
354
+
355
+ if (last_column >= range.end.column) {
356
+ this.SetName({ type: 'range', area: new Area({
357
+ row: range.start.row, column: range.start.column, sheet_id }, {
358
+ row: range.end.row, column: before_column - 1 }), name: key });
359
+ }
360
+ else {
361
+ this.SetName({ type: 'range', name: key, area: new Area({
362
+ row: range.start.row, column: range.start.column, sheet_id }, {
363
+ row: range.end.row, column: range.start.column + range.columns + column_count - 1})});
364
+ }
365
+
366
+ }
367
+
368
+ else {
369
+ console.warn(`PNR X case 2`, before_column, column_count, JSON.stringify(range));
370
+ }
371
+
372
+ }
373
+ }
374
+
375
+
376
+ if (row_count && before_row <= range.end.row) {
377
+
378
+ /*
379
+ // (1) we are before the insert point, not affected
380
+
381
+ if (before_row > range.end.row) {
382
+ continue;
383
+ }
384
+ */
385
+
386
+ if (row_count > 0) {
387
+
388
+ // (2) it's an insert and we are past the insert point:
389
+ // increment [start] and [end] by [count]
390
+
391
+ if (before_row <= range.start.row) {
392
+ range.Shift(row_count, 0);
393
+ }
394
+
395
+ // (3) it's an insert and we contain the insert point:
396
+ // increment [end] by [count]
397
+
398
+ else if (before_row > range.start.row && before_row <= range.end.row) {
399
+ range.ConsumeAddress({row: range.end.row + row_count, column: range.end.column});
400
+ }
401
+
402
+ else {
403
+ console.warn(`PNR X case 3`, before_row, row_count, JSON.stringify(range));
404
+ }
405
+
406
+ }
407
+ else if (row_count < 0) {
408
+
409
+ // (4) it's a delete and we are past the delete point (before+count):
410
+ // decrement [start] and [end] by [count]
411
+
412
+ if (before_row - row_count <= range.start.row) {
413
+ range.Shift(row_count, 0);
414
+ }
415
+
416
+ // (5) it's a delete and contains the entire range
417
+
418
+ else if (before_row <= range.start.row && before_row - row_count > range.end.row) {
419
+ this.ClearName(key);
420
+ }
421
+
422
+ // (6) it's a delete and contains part of the range. clip the range.
423
+
424
+ else if (before_row <= range.start.row) {
425
+ const last_row = before_row - row_count - 1;
426
+ this.SetNamedRange(key, new Area({
427
+ column: range.start.column, row: last_row + 1 + row_count, sheet_id }, {
428
+ column: range.end.column, row: range.end.row + row_count }));
429
+ }
430
+
431
+ else if (before_row <= range.end.row) {
432
+ const last_row = before_row - row_count - 1;
433
+ if (last_row >= range.end.row) {
434
+ this.SetNamedRange(key, new Area({
435
+ column: range.start.column, row: range.start.row, sheet_id }, {
436
+ column: range.end.column, row: before_row - 1 }));
437
+ }
438
+ else {
439
+ this.SetNamedRange(key, new Area({
440
+ column: range.start.column, row: range.start.row, sheet_id }, {
441
+ column: range.end.column, row: range.start.row + range.rows + row_count - 1 }));
442
+ }
443
+
444
+ }
445
+
446
+ else {
447
+ console.warn(`PNR X case 4`, before_row, row_count, JSON.stringify(range));
448
+ }
449
+
450
+ }
451
+ }
452
+
453
+ }
454
+
455
+ }
456
+
457
+ }
458
+
459
+