@trebco/treb 29.1.6 → 29.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/treb.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v29.1. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v29.3. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
@@ -1602,19 +1602,23 @@ export interface SerializedNamedExpression {
1602
1602
  }
1603
1603
 
1604
1604
  /**
1605
- * serialized type
1605
+ * serialized type is a composite of expression/range. we determine
1606
+ * what it is when parsing the expression. this simplifies passing these
1607
+ * things around.
1608
+ *
1609
+ * (named expressions and ranges they have slightly different behavior,
1610
+ * which is why we have a distinction at all).
1611
+ *
1606
1612
  */
1607
1613
  export interface SerializedNamed {
1608
1614
  name: string;
1609
- area?: SerializedArea;
1610
- expression?: string;
1615
+
1616
+ /** expression or address/area */
1617
+ expression: string;
1618
+
1619
+ /** scope is a sheet name (not ID) */
1611
1620
  scope?: string;
1612
1621
  }
1613
- export type SerializedArea = IArea & {
1614
- start: ICellAddress & {
1615
- sheet: string;
1616
- };
1617
- };
1618
1622
  export interface SerializedSheet {
1619
1623
 
1620
1624
  /** cell data */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "29.1.6",
3
+ "version": "29.3.1",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -102,6 +102,9 @@ export interface Dimensions {
102
102
  * start/end value for row/column/both, so watch out on loops. the
103
103
  * sheet class has a method for reducing infinite ranges to actual
104
104
  * populated ranges.
105
+ *
106
+ * infinitiy is turning into a headache because it doesn't serialize
107
+ * to json properly. should we switch to a flag, or -1, or something?
105
108
  */
106
109
  export class Area implements IArea {
107
110
 
@@ -166,6 +169,10 @@ export class Area implements IArea {
166
169
 
167
170
  public static CellAddressToLabel(address: ICellAddress, sheet_id = false): string {
168
171
 
172
+ if (address.row === Infinity || address.column === Infinity) {
173
+ throw new Error('this is going to break something');
174
+ }
175
+
169
176
  const prefix = sheet_id ? `${address.sheet_id || 0}!` : '';
170
177
 
171
178
  return prefix
@@ -1930,12 +1930,15 @@ export class Calculator extends Graph {
1930
1930
  }
1931
1931
  }
1932
1932
 
1933
+ /*
1933
1934
  if (area.count > 1) {
1934
1935
  range = Area.CellAddressToLabel(area.start) + ':' + Area.CellAddressToLabel(area.end);
1935
1936
  }
1936
1937
  else {
1937
1938
  range = Area.CellAddressToLabel(area.start);
1938
1939
  }
1940
+ */
1941
+ range = area.spreadsheet_label;
1939
1942
 
1940
1943
  if (!qualified) {
1941
1944
  return range;
@@ -2514,21 +2517,20 @@ export class Calculator extends Graph {
2514
2517
  const unit = dependencies.ranges[key];
2515
2518
  const range = new Area(unit.start, unit.end);
2516
2519
 
2517
- for (const address of range) {
2518
- this.AddLeafVertexEdge(address, vertex);
2520
+ if (range.entire_column || range.entire_row || range.count > 1) {
2521
+ // this.AddLeafVertexEdge(range.start, vertex);
2522
+ this.AddLeafVertexArrayEdge(range, vertex);
2523
+ }
2524
+ else {
2525
+ this.AddLeafVertexEdge(range.start, vertex);
2519
2526
  }
2520
-
2521
- /*
2522
- range.Iterate((address: ICellAddress) => {
2523
- this.AddLeafVertexEdge(address, vertex);
2524
- });
2525
- */
2526
2527
 
2527
2528
  /*
2528
2529
  for (const address of range) {
2529
2530
  this.AddLeafVertexEdge(address, vertex);
2530
2531
  }
2531
2532
  */
2533
+
2532
2534
  }
2533
2535
 
2534
2536
  for (const key of Object.keys(dependencies.addresses)){
@@ -101,11 +101,9 @@ export class ArrayVertex extends SpreadsheetVertexBase {
101
101
  * returns a list of arrays that contain this address
102
102
  */
103
103
  public static GetContainingArrays(address: ICellAddress2): ArrayVertex[] {
104
- // console.info('av2 get arrays:', address.row, address.column);
105
104
  const list: ArrayVertex[] = [];
106
105
  for (const entry of this.list) {
107
106
  if ((entry.area.start.sheet_id === address.sheet_id) && entry.area.Contains(address)) {
108
- // console.info("match", entry.area.spreadsheet_label);
109
107
  list.push(entry);
110
108
  }
111
109
  }
@@ -613,9 +613,91 @@ export abstract class Graph implements GraphCallbacks {
613
613
  /**
614
614
  * new array vertices
615
615
  */
616
- public AddArrayEdge(u: Area, v: ICellAddress): void {
616
+ protected CompositeAddArrayEdge(u: Area, vertex: Vertex): void {
617
+
618
+ if (!u.start.sheet_id) {
619
+ throw new Error('AddArrayEdge called without sheet ID');
620
+ }
621
+
622
+ // create or use existing
623
+ const [array_vertex, created] = ArrayVertex.GetVertex(u);
624
+
625
+ // add an edge
626
+ vertex.DependsOn(array_vertex);
627
+
628
+ // force a check on next calculation pass
629
+ this.loop_check_required = true;
630
+
631
+ if (!created) {
632
+ // console.info('reusing, so not adding edges');
633
+ return;
634
+ }
635
+
636
+ // now add edges from/to nodes THAT ALREADY EXIST
637
+
638
+ // range can't span sheets, so we only need one set to look up
639
+
640
+ const map = this.vertices[u.start.sheet_id];
641
+
642
+ // this might happen on create, we can let it go because the
643
+ // references will be added when the relevant sheet is added
644
+
645
+ if (!map) {
646
+ return;
647
+ }
648
+
649
+ // ...
650
+
651
+ if (u.entire_row) {
652
+ // console.group('entire row(s)')
653
+ for (let column = 0; column < map.length; column++) {
654
+ if (map[column]) {
655
+ for (let row = u.start.row; row <= u.end.row; row++ ) {
656
+ const vertex = map[column][row];
657
+ if (vertex) {
658
+ // console.info('add', column, row);
659
+ array_vertex.DependsOn(vertex);
660
+ }
661
+ }
662
+ }
663
+ }
664
+ // console.groupEnd();
665
+ }
666
+ else if (u.entire_column) {
667
+ // console.group('entire column(s)');
668
+ for (let column = u.start.column; column <= u.end.column; column++) {
669
+ if(map[column]) {
670
+ for (const vertex of map[column]) {
671
+ if (vertex?.address) {
672
+ // console.info('add', vertex.address);
673
+ array_vertex.DependsOn(vertex);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ // console.groupEnd();
679
+ }
680
+ else {
681
+ for (let row = u.start.row; row <= u.end.row; row++) {
682
+ for (let column = u.start.column; column <= u.end.column; column++) {
683
+ const vertex = map[column][row];
684
+ if (vertex) {
685
+ array_vertex.DependsOn(vertex);
686
+ }
687
+ }
688
+ }
689
+ }
617
690
 
618
- // console.info('add array edge', u, v);
691
+ }
692
+
693
+ public AddLeafVertexArrayEdge(u: Area, vertex: LeafVertex) {
694
+ this.CompositeAddArrayEdge(u, vertex);
695
+ }
696
+
697
+ /**
698
+ * new array vertices
699
+ */
700
+ public AddArrayEdge(u: Area, v: ICellAddress): void {
619
701
 
620
702
  if (!u.start.sheet_id) {
621
703
  throw new Error('AddArrayEdge called without sheet ID');
@@ -624,6 +706,9 @@ export abstract class Graph implements GraphCallbacks {
624
706
  // this should have already been added...
625
707
  const v_v = this.GetVertex(v, true);
626
708
 
709
+ this.CompositeAddArrayEdge(u, v_v);
710
+
711
+ /*
627
712
  // create or use existing
628
713
  const [array_vertex, created] = ArrayVertex.GetVertex(u);
629
714
 
@@ -692,6 +777,7 @@ export abstract class Graph implements GraphCallbacks {
692
777
  }
693
778
  }
694
779
  }
780
+ */
695
781
 
696
782
  }
697
783
 
@@ -226,71 +226,13 @@ export class SpreadsheetVertex extends SpreadsheetVertexBase {
226
226
  // handle it properly regardless.
227
227
 
228
228
  const single = (this.result.type === ValueType.array) ? this.result.value[0][0] : this.result;
229
- /*
230
- if (single.type === ValueType.object) {
231
- this.reference.SetCalculationError('OBJECT');
232
- }
233
- else */
234
229
 
235
230
  // we are using object type in the returned value for sparklines...
236
231
  // so we can't drop it here. we could change rendering though. or
237
232
  // whitelist types. or blacklist types. or something.
238
233
 
239
- {
240
- this.reference.SetCalculatedValue(single.value as CellValue, single.type);
241
- }
242
-
243
- /*
244
- console.info("T2", t2);
245
-
246
- // because we let sloppy data filter through, it's possible
247
- // that we get some random stuff at this point. generally this
248
- // shoudl not happen but if you use (e.g.) one of the chart
249
- // functions in a spreadsheet cell, you'll get a 1d array.
250
-
251
- // so we need to validate that what we have is either a UnionValue
252
- // or a 2d UnionValue[][] array.
253
-
254
- // don't know the performance cost of this.
255
-
256
- // FIXME: don't let sloppy data through.
257
-
258
- let test: any = this.result;
259
-
260
- if (Array.isArray(test)) {
261
- if (test[0] && Array.isArray(test[0])) {
262
- test = test[0][0];
263
- }
264
- else {
265
- // console.warn('error 1');
266
- test = undefined;
267
- }
268
- }
269
-
270
- if (!test || typeof (test as any).type === undefined) {
271
- // console.warn('error 2/3');
272
- this.reference.SetCalculationError('UNK');
273
- }
274
- else {
275
- this.reference.SetCalculatedValue((test as UnionValue).value, (test as UnionValue).type);
276
- }
277
- */
278
-
279
- /*
280
- const single = Array.isArray(this.result) ? this.result[0][0] : this.result;
281
-
282
- // error is implicit
283
- this.reference.SetCalculatedValue(single.value, single.type);
284
- */
234
+ this.reference.SetCalculatedValue(single.value as CellValue, single.type);
285
235
 
286
- /*
287
- if (typeof this.result === 'object' && this.result.error) {
288
- this.reference.SetCalculationError(this.result.error);
289
- }
290
- else {
291
- this.reference.SetCalculatedValue(this.result);
292
- }
293
- */
294
236
  }
295
237
 
296
238
  }
@@ -318,7 +260,6 @@ export class SpreadsheetVertex extends SpreadsheetVertexBase {
318
260
  //
319
261
 
320
262
  for (const edge of this.edges_out as Set<SpreadsheetVertexBase>){
321
- // (edge as SpreadsheetVertex).Calculate(graph);
322
263
  if (edge.dirty) {
323
264
  graph.calculation_list.push(edge);
324
265
  }
@@ -501,6 +501,11 @@ export class ExpressionCalculator {
501
501
  const func = this.library.Get(outer.name);
502
502
 
503
503
  if (!func) {
504
+
505
+ if (process.env.NODE_ENV !== 'production') {
506
+ console.info('(dev) missing function', outer.name);
507
+ }
508
+
504
509
  return () => NameError();
505
510
  }
506
511
 
@@ -89,6 +89,45 @@ const inverse_normal = (q: number): number => {
89
89
 
90
90
  };
91
91
 
92
+ const edate_calc = (start: number, months: number) => {
93
+
94
+ let date = new Date(LotusDate(start));
95
+ let month = date.getUTCMonth() + months;
96
+ let year = date.getUTCFullYear();
97
+
98
+ // if we don't ensure the time we'll wind up hitting boundary cases
99
+
100
+ date.setUTCHours(12);
101
+ date.setUTCMinutes(0);
102
+ date.setUTCSeconds(0);
103
+ date.setUTCMilliseconds(0);
104
+
105
+ while (month < 0) {
106
+ month += 12;
107
+ year--;
108
+ }
109
+ while (month > 11) {
110
+ month -= 12;
111
+ year++;
112
+ }
113
+
114
+ date.setUTCMonth(month);
115
+ date.setUTCFullYear(year);
116
+
117
+ // if this rolls over the month, then we need to go back to the
118
+ // last valid day of the month. so jan 31 + 1 month needs to equal
119
+ // feb 28 (feb 29 in leap year).
120
+
121
+ const check_month = date.getUTCMonth();
122
+ if (check_month !== month) {
123
+ const days = date.getUTCDate();
124
+ date = new Date(date.getTime() - (days * 86400 * 1000));
125
+ }
126
+
127
+ return date;
128
+
129
+ };
130
+
92
131
  const zlookup_arguments = [
93
132
  {
94
133
  name: "Lookup value",
@@ -329,6 +368,90 @@ export const BaseFunctionLibrary: FunctionMap = {
329
368
  },
330
369
  },
331
370
 
371
+ // --- FIXME: break out date functions? --------------------------------------
372
+
373
+ EDate: {
374
+ arguments: [
375
+ {
376
+ name: 'Start date',
377
+ unroll: true,
378
+ },
379
+ {
380
+ name: 'Months',
381
+ unroll: true,
382
+ },
383
+ ],
384
+ fn: (start: number, months = 0) => {
385
+ if (typeof start !== 'number' || typeof months !== 'number') {
386
+ return ArgumentError();
387
+ }
388
+ const date = edate_calc(start, months);
389
+ return { type: ValueType.number, value: UnlotusDate(date.getTime(), false) };
390
+ }
391
+ },
392
+
393
+ EOMonth: {
394
+ arguments: [
395
+ {
396
+ name: 'Start date',
397
+ unroll: true,
398
+ },
399
+ {
400
+ name: 'Months',
401
+ unroll: true,
402
+ },
403
+ ],
404
+ fn: (start: number, months = 0) => {
405
+
406
+ // this is the same as edate, except it advances to the end of the
407
+ // month. so jan 15, 2023 plus one month -> feb 28, 2023 (last day).
408
+
409
+ if (typeof start !== 'number' || typeof months !== 'number') {
410
+ return ArgumentError();
411
+ }
412
+ const date = edate_calc(start, months);
413
+
414
+ const month = date.getUTCMonth();
415
+ switch (month) {
416
+ case 1: // feb, special
417
+ {
418
+ const year = date.getUTCFullYear();
419
+
420
+ // it's a leap year if it is divisible by 4, unless it's also
421
+ // divisible by 100 AND NOT divisible by 400. 1900 is grand-
422
+ // fathered in via an error in Lotus.
423
+
424
+ if (year % 4 === 0 && (year === 1900 || (year % 400 === 0) || (year % 100 !== 0))) {
425
+ date.setUTCDate(29);
426
+ }
427
+ else {
428
+ date.setUTCDate(28);
429
+ }
430
+ }
431
+ break;
432
+
433
+ case 0: // jan
434
+ case 2:
435
+ case 4:
436
+ case 6: // july
437
+ case 7: // august
438
+ case 9:
439
+ case 11: // dec
440
+ date.setUTCDate(31);
441
+ break;
442
+
443
+ default:
444
+ date.setUTCDate(30);
445
+ break;
446
+ }
447
+
448
+
449
+
450
+ return { type: ValueType.number, value: UnlotusDate(date.getTime(), false) };
451
+
452
+ }
453
+ },
454
+
332
455
  Now: {
333
456
  description: 'Returns current time',
334
457
  volatile: true,
@@ -429,6 +552,8 @@ export const BaseFunctionLibrary: FunctionMap = {
429
552
  },
430
553
  },
431
554
 
555
+ // ---------------------------------------------------------------------------
556
+
432
557
  IfError: {
433
558
  description: 'Returns the original value, or the alternate value if the original value contains an error',
434
559
  arguments: [{ name: 'original value', allow_error: true, boxed: true }, { name: 'alternate value' }],
@@ -25,7 +25,7 @@ import type { IArea, ICellAddress, Table, CellStyle } from 'treb-base-types';
25
25
  import type { SerializedSheet } from './sheet_types';
26
26
  import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex, DecimalMarkType, ArgumentSeparatorType } from 'treb-parser';
27
27
  import { Area, IsCellAddress, Style } from 'treb-base-types';
28
- import type { CompositeNamed, SerializedNamed } from './named';
28
+ import type { SerializedNamed } from './named';
29
29
  import { NamedRangeManager } from './named';
30
30
 
31
31
  export interface ConnectedElementType {
@@ -132,36 +132,72 @@ export class DataModel {
132
132
  * need to check if something is a range -- if so, it will just be
133
133
  * a single address or range. in that case store it as a named range.
134
134
  */
135
- public UnserializeComposite(names: CompositeNamed[], active_sheet?: Sheet) {
135
+ public UnserializeNames(names: SerializedNamed[], active_sheet?: Sheet) {
136
136
 
137
137
  this.parser.Save();
138
138
  this.parser.SetLocaleSettings(DecimalMarkType.Period, ArgumentSeparatorType.Comma);
139
139
 
140
- const sorted = names.map(named => {
140
+ //const sorted = names.map(named => {
141
+ for (const named of names) {
141
142
  const parse_result = this.parser.Parse(named.expression);
142
143
  if (parse_result.expression) {
144
+
145
+ const scope = (typeof named.scope === 'string') ? this.sheets.ID(named.scope) : undefined;
146
+
143
147
  if (parse_result.expression.type === 'address' || parse_result.expression.type === 'range') {
144
- return {
145
- ...named,
146
- expression: undefined,
147
- area: parse_result.expression.type === 'address' ? {
148
- start: parse_result.expression,
149
- end: parse_result.expression,
150
- } : parse_result.expression,
151
- } as SerializedNamed;
148
+
149
+ const [start, end] = parse_result.expression.type === 'range' ?
150
+ [ parse_result.expression.start, parse_result.expression.end, ] :
151
+ [ parse_result.expression, parse_result.expression ];
152
+
153
+ if (start.sheet) {
154
+ const area = new Area({...start, sheet_id: this.sheets.ID(start.sheet), }, end);
155
+
156
+ if (area.start.sheet_id) {
157
+ this.named.SetNamedRange(named.name, area, scope);
158
+ }
159
+ else {
160
+ console.warn("missing sheet ID?", start);
161
+ }
162
+
163
+ }
164
+ else {
165
+ console.warn("missing sheet name?", start);
166
+ }
167
+
152
168
  }
153
- return {
154
- ...named
155
- } as SerializedNamed;
169
+ else {
170
+ this.parser.Walk(parse_result.expression, unit => {
171
+ if (unit.type === 'address' || unit.type === 'range') {
172
+ if (unit.type === 'range') {
173
+ unit = unit.start;
174
+ }
175
+ if (!unit.sheet_id) {
176
+ if (unit.sheet) {
177
+ unit.sheet_id = this.sheets.ID(unit.sheet);
178
+ }
179
+ }
180
+ if (!unit.sheet_id) {
181
+ unit.sheet_id = active_sheet?.id;
182
+ }
183
+ return false; // don't continue in ranges
184
+ }
185
+ return true;
186
+ });
187
+
188
+ this.named.SetNamedExpression(named.name, parse_result.expression, scope);
189
+ }
190
+
156
191
  }
157
192
  return undefined;
158
- }).filter((test): test is SerializedNamed => !!test);
193
+ } // ).filter((test): test is SerializedNamed => !!test);
159
194
 
160
195
  this.parser.Restore();
161
- this.UnserializeNames(sorted, active_sheet);
196
+ // this.UnserializeNames(sorted, active_sheet);
162
197
 
163
198
  }
164
199
 
200
+ /*
165
201
  public UnserializeNames(names: SerializedNamed[], active_sheet?: Sheet) {
166
202
 
167
203
  for (const entry of names) {
@@ -218,6 +254,7 @@ export class DataModel {
218
254
  }
219
255
 
220
256
  }
257
+ */
221
258
 
222
259
  /**
223
260
  * serialize names. ranges are easy, but make sure there's a sheet name
@@ -231,7 +268,7 @@ export class DataModel {
231
268
 
232
269
  const named: SerializedNamed = {
233
270
  name: entry.name,
234
- // scope: entry.scope,
271
+ expression: '',
235
272
  };
236
273
 
237
274
  if (entry.scope) {
@@ -278,11 +315,10 @@ export class DataModel {
278
315
 
279
316
  }
280
317
  else {
281
- named.area = {
318
+
319
+ const area = {
282
320
  start: {
283
321
  ...entry.area.start,
284
- sheet: this.sheets.Name(entry.area.start.sheet_id || 0) || '',
285
- sheet_id: undefined, // in favor of name
286
322
  absolute_column: true,
287
323
  absolute_row: true,
288
324
  },
@@ -292,6 +328,9 @@ export class DataModel {
292
328
  absolute_row: true,
293
329
  },
294
330
  };
331
+
332
+ named.expression = this.AddressToLabel(area);
333
+
295
334
  }
296
335
 
297
336
  list.push(named);
@@ -30,7 +30,7 @@ export type {
30
30
  SerializedMacroFunction,
31
31
  } from './data_model';
32
32
 
33
- export type { SerializedNamed, CompositeNamed } from './named';
33
+ export type { SerializedNamed } from './named';
34
34
 
35
35
  export { Sheet } from './sheet';
36
36
  export type { SerializedSheet, FreezePane, LegacySerializedSheet } from './sheet_types';