@trebco/treb 29.1.6 → 29.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "29.1.6",
3
+ "version": "29.2.0",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -501,6 +501,9 @@ export class ExpressionCalculator {
501
501
  const func = this.library.Get(outer.name);
502
502
 
503
503
  if (!func) {
504
+
505
+ // console.info('missing function', outer.name);
506
+
504
507
  return () => NameError();
505
508
  }
506
509
 
@@ -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,85 @@ 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
+ if (year % 4 === 0 && (year === 1900 || (year % 100 !== 0))) {
420
+ date.setUTCDate(29);
421
+ }
422
+ else {
423
+ date.setUTCDate(28);
424
+ }
425
+ }
426
+ break;
427
+
428
+ case 0: // jan
429
+ case 2:
430
+ case 4:
431
+ case 6: // july
432
+ case 7: // august
433
+ case 9:
434
+ case 11: // dec
435
+ date.setUTCDate(31);
436
+ break;
437
+
438
+ default:
439
+ date.setUTCDate(30);
440
+ break;
441
+ }
442
+
443
+
444
+
445
+ return { type: ValueType.number, value: UnlotusDate(date.getTime(), false) };
446
+
447
+ }
448
+ },
449
+
332
450
  Now: {
333
451
  description: 'Returns current time',
334
452
  volatile: true,
@@ -429,6 +547,8 @@ export const BaseFunctionLibrary: FunctionMap = {
429
547
  },
430
548
  },
431
549
 
550
+ // ---------------------------------------------------------------------------
551
+
432
552
  IfError: {
433
553
  description: 'Returns the original value, or the alternate value if the original value contains an error',
434
554
  arguments: [{ name: 'original value', allow_error: true, boxed: true }, { name: 'alternate value' }],
@@ -136,10 +136,12 @@ export class NamedRangeManager {
136
136
 
137
137
  const validated = this.ValidateNamed(name);
138
138
  if (!validated) {
139
- console.warn('invalid name');
139
+ console.warn('invalid name', {name});
140
140
  return false;
141
141
  }
142
142
 
143
+ /*
144
+
143
145
  if (named.type === 'range') {
144
146
 
145
147
  // why is this considered invalid here? I've seen it done.
@@ -148,12 +150,14 @@ export class NamedRangeManager {
148
150
 
149
151
  if (named.area.entire_column || named.area.entire_row) {
150
152
  console.info({named});
151
- console.warn(`invalid range`);
153
+ console.warn('invalid range');
152
154
  return false;
153
155
  }
154
156
 
155
157
  }
156
158
 
159
+ */
160
+
157
161
  // this.named.set(name.toLowerCase(), named);
158
162
  this.named.set(this.ScopedName(name, named.scope), named);
159
163
 
@@ -226,14 +230,26 @@ export class NamedRangeManager {
226
230
  * - must start with letter or underscore (not a number or dot).
227
231
  * - cannot look like a spreadsheet address, which is 1-3 letters followed by numbers.
228
232
  *
233
+ * - apparently questuon marks are legal, but not in first position. atm
234
+ * our parser will reject.
235
+ *
229
236
  * returns a normalized name (just caps, atm)
230
237
  */
231
238
  public ValidateNamed(name: string): string|false {
232
239
  name = name.trim();
240
+
241
+ // can't be empty
233
242
  if (!name.length) return false;
243
+
244
+ // can't look like a spreadsheet address
234
245
  if (/^[A-Za-z]{1,3}\d+$/.test(name)) return false;
235
- if (/[^A-Za-z\d_.]/.test(name)) return false;
246
+
247
+ // can only contain legal characters
248
+ if (/[^A-Za-z\d_.?]/.test(name)) return false;
249
+
250
+ // must start with ascii letter or underscore
236
251
  if (/^[^A-Za-z_]/.test(name)) return false;
252
+
237
253
  return name.toUpperCase();
238
254
  }
239
255
 
@@ -348,21 +348,23 @@ export class Importer {
348
348
  case 'duplicateValues':
349
349
  case 'uniqueValues':
350
350
 
351
- let style = {};
351
+ {
352
+ let style = {};
352
353
 
353
- if (rule.a$.dxfId) {
354
- const index = Number(rule.a$.dxfId);
355
- if (!isNaN(index)) {
356
- style = this.workbook?.style_cache.dxf_styles[index] || {};
354
+ if (rule.a$.dxfId) {
355
+ const index = Number(rule.a$.dxfId);
356
+ if (!isNaN(index)) {
357
+ style = this.workbook?.style_cache.dxf_styles[index] || {};
358
+ }
357
359
  }
358
- }
359
360
 
360
- return {
361
- type: 'duplicate-values',
362
- area,
363
- style,
364
- unique: (rule.a$.type === 'uniqueValues'),
365
- };
361
+ return {
362
+ type: 'duplicate-values',
363
+ area,
364
+ style,
365
+ unique: (rule.a$.type === 'uniqueValues'),
366
+ };
367
+ }
366
368
 
367
369
  case 'cellIs':
368
370
  if (rule.a$.operator && rule.formula) {
@@ -392,7 +394,10 @@ export class Importer {
392
394
  }
393
395
  break;
394
396
 
397
+ case 'containsErrors':
398
+ case 'notContainsErrors':
395
399
  case 'expression':
400
+
396
401
  if (rule.formula) {
397
402
 
398
403
  if (typeof rule.formula !== 'string') {
@@ -436,7 +441,7 @@ export class Importer {
436
441
  const stops: GradientStop[] = [];
437
442
  for (const [index, entry] of rule.colorScale.cfvo.entries()) {
438
443
  let value = 0;
439
- let color: Color = {};
444
+ const color: Color = {};
440
445
 
441
446
  const color_element = rule.colorScale.color[index];
442
447
  if (color_element.a$.rgb) {
@@ -70,15 +70,17 @@ export const UnlotusDate = (value: number, local = true): number => {
70
70
  if (local) {
71
71
 
72
72
  const local_date = new Date(value);
73
- const utc_date = new Date();
74
-
75
- utc_date.setUTCMilliseconds(local_date.getUTCMilliseconds());
76
- utc_date.setUTCSeconds(local_date.getUTCSeconds());
77
- utc_date.setUTCMinutes(local_date.getUTCMinutes());
78
- utc_date.setUTCHours(local_date.getHours());
79
- utc_date.setUTCDate(local_date.getDate());
80
- utc_date.setUTCMonth(local_date.getMonth());
81
- utc_date.setUTCFullYear(local_date.getFullYear());
73
+ const utc_date = new Date(
74
+ local_date.getFullYear(),
75
+ local_date.getMonth(),
76
+ local_date.getDate(),
77
+ local_date.getHours(),
78
+ local_date.getMinutes(),
79
+ local_date.getSeconds(),
80
+ local_date.getMilliseconds(),
81
+ );
82
+
83
+ // console.info("Converting local", utc_date.toUTCString());
82
84
 
83
85
  value = utc_date.getTime();
84
86
 
@@ -322,7 +322,7 @@ class ValueParserType {
322
322
  }
323
323
 
324
324
  return {
325
- value: UnlotusDate(date),
325
+ value: UnlotusDate(date, true),
326
326
  type: ValueType.number,
327
327
  hints,
328
328
  };
@@ -970,79 +970,11 @@ export class Grid extends GridBase {
970
970
  this.model.sheets.UpdateIndexes();
971
971
 
972
972
  this.model.named.Reset();
973
- // this.model.named_ranges.Reset();
974
- // this.model.named_expressions.clear();
975
- // console.info({IDX: import_data.named});
976
973
 
977
974
  if (import_data.named) {
978
975
  this.model.UnserializeComposite(import_data.named, this.active_sheet);
979
976
  }
980
977
 
981
- /*
982
- if (import_data.names) {
983
-
984
- console.info("IDN", { names: import_data.names })
985
-
986
- for (const name of Object.keys(import_data.names)) {
987
-
988
- const validated = this.model.named.ValidateNamed(name);
989
-
990
- if (!validated) {
991
- console.warn(`invalid name: ${name}`, import_data.names[name]);
992
- continue;
993
- }
994
-
995
- const label = import_data.names[name];
996
- if (typeof label === 'number') {
997
- // console.info('dropping (temp)', name, label);
998
- }
999
- else {
1000
- const parse_result = this.parser.Parse(label);
1001
-
1002
- if (parse_result.expression) {
1003
- if (parse_result.expression.type === 'range') {
1004
- const sheet_id = name_map[parse_result.expression.start.sheet || ''];
1005
- if (sheet_id) {
1006
- parse_result.expression.start.sheet_id = sheet_id;
1007
- this.model.named.SetNamedRange(validated, new Area(parse_result.expression.start, parse_result.expression.end));
1008
- }
1009
- }
1010
- else if (parse_result.expression.type === 'address') {
1011
- const sheet_id = name_map[parse_result.expression.sheet || ''];
1012
- if (sheet_id) {
1013
- parse_result.expression.sheet_id = sheet_id;
1014
- this.model.named.SetNamedRange(validated, new Area(parse_result.expression));
1015
- }
1016
- }
1017
- else {
1018
-
1019
- // we need to map sheet names to sheet IDs in named
1020
- // expressions, if they have any addresses/references.
1021
-
1022
- const expr = parse_result.expression;
1023
- this.parser.Walk(expr, unit => {
1024
- if (unit.type === 'address' || unit.type === 'range') {
1025
- if (unit.type === 'range') {
1026
- unit = unit.start;
1027
- }
1028
- if (!unit.sheet_id) {
1029
- unit.sheet_id = name_map[unit.sheet || ''] || 0; // default is bad here -- do we have an active sheet yet?
1030
- }
1031
- return false;
1032
- }
1033
- return true;
1034
- });
1035
-
1036
- this.model.named.SetNamedExpression(validated, expr);
1037
-
1038
- }
1039
- }
1040
- }
1041
- }
1042
- // this.model.named_ranges.RebuildList();
1043
- }
1044
- */
1045
-
1046
978
  // FIXME: do we need to rebuild autocomplete here (A: yes)
1047
979
  // ...
1048
980
 
@@ -1597,7 +1529,7 @@ export class Grid extends GridBase {
1597
1529
  */
1598
1530
  public SetName(name: string, range?: ICellAddress | Area, expression?: string, scope?: number, overwrite = false): void {
1599
1531
 
1600
- // console.info('setname', name, range, expression, scope, overwrite);
1532
+ // console.info('setname', { name, range, expression, scope, overwrite });
1601
1533
 
1602
1534
  // validate/translate name first
1603
1535
 
@@ -89,6 +89,7 @@ const CLOSE_BRACE = 0x7d;
89
89
  const OPEN_SQUARE_BRACKET = 0x5b;
90
90
  const CLOSE_SQUARE_BRACKET = 0x5d;
91
91
 
92
+ const QUESTION_MARK = 0x3f;
92
93
  const EXCLAMATION_MARK = 0x21;
93
94
  // const COLON = 0x3a; // became an operator
94
95
  const SEMICOLON = 0x3b;
@@ -2100,6 +2101,11 @@ export class Parser {
2100
2101
 
2101
2102
  // I think that's all the rules for structured references.
2102
2103
 
2104
+ // testing question marks, which are legal in defined names
2105
+ // (but I think not in table names or column names)
2106
+
2107
+ || (char === QUESTION_MARK && square_bracket === 0)
2108
+
2103
2109
  /*
2104
2110
 
2105
2111
  || (this.flags.r1c1 && (