@trebco/treb 29.1.5 → 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.5",
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
  };
@@ -594,16 +594,11 @@ export class TileRenderer {
594
594
 
595
595
  const scale = this.layout.dpr;
596
596
 
597
- // const render_list: Array<{row: number, column: number, cell: Cell}> = [];
598
-
599
- // this.last_font = undefined;
600
597
  context.setTransform(scale, 0, 0, scale, 0, 0);
601
598
 
602
599
  let left = 0;
603
600
  let top = 0;
604
601
 
605
- // console.info('r', tile.first_cell);
606
-
607
602
  for (let column = tile.first_cell.column; column <= tile.last_cell.column; column++) {
608
603
  const width = this.layout.ColumnWidth(column);
609
604
  if (!width) continue;
@@ -1126,7 +1121,6 @@ export class TileRenderer {
1126
1121
  // (moved to sheet, using numpad naming)
1127
1122
 
1128
1123
  const numpad = this.view.active_sheet.SurroundingStyle(address, this.theme.table);
1129
-
1130
1124
 
1131
1125
  // --- start with fills ----------------------------------------------------
1132
1126
 
@@ -1253,7 +1247,7 @@ export class TileRenderer {
1253
1247
  // paint right border?
1254
1248
 
1255
1249
  if (numpad[6].border_left) {
1256
- context.fillStyle = ThemeColor2(this.theme, numpad[4].border_left_fill, 1);
1250
+ context.fillStyle = ThemeColor2(this.theme, numpad[6].border_left_fill, 1);
1257
1251
  context.fillRect(left + width - 1, top - 1, 1, height + 1);
1258
1252
  }
1259
1253
 
@@ -970,80 +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
-
976
- console.info({IDX: import_data.named});
977
973
 
978
974
  if (import_data.named) {
979
975
  this.model.UnserializeComposite(import_data.named, this.active_sheet);
980
976
  }
981
977
 
982
- /*
983
- if (import_data.names) {
984
-
985
- console.info("IDN", { names: import_data.names })
986
-
987
- for (const name of Object.keys(import_data.names)) {
988
-
989
- const validated = this.model.named.ValidateNamed(name);
990
-
991
- if (!validated) {
992
- console.warn(`invalid name: ${name}`, import_data.names[name]);
993
- continue;
994
- }
995
-
996
- const label = import_data.names[name];
997
- if (typeof label === 'number') {
998
- // console.info('dropping (temp)', name, label);
999
- }
1000
- else {
1001
- const parse_result = this.parser.Parse(label);
1002
-
1003
- if (parse_result.expression) {
1004
- if (parse_result.expression.type === 'range') {
1005
- const sheet_id = name_map[parse_result.expression.start.sheet || ''];
1006
- if (sheet_id) {
1007
- parse_result.expression.start.sheet_id = sheet_id;
1008
- this.model.named.SetNamedRange(validated, new Area(parse_result.expression.start, parse_result.expression.end));
1009
- }
1010
- }
1011
- else if (parse_result.expression.type === 'address') {
1012
- const sheet_id = name_map[parse_result.expression.sheet || ''];
1013
- if (sheet_id) {
1014
- parse_result.expression.sheet_id = sheet_id;
1015
- this.model.named.SetNamedRange(validated, new Area(parse_result.expression));
1016
- }
1017
- }
1018
- else {
1019
-
1020
- // we need to map sheet names to sheet IDs in named
1021
- // expressions, if they have any addresses/references.
1022
-
1023
- const expr = parse_result.expression;
1024
- this.parser.Walk(expr, unit => {
1025
- if (unit.type === 'address' || unit.type === 'range') {
1026
- if (unit.type === 'range') {
1027
- unit = unit.start;
1028
- }
1029
- if (!unit.sheet_id) {
1030
- unit.sheet_id = name_map[unit.sheet || ''] || 0; // default is bad here -- do we have an active sheet yet?
1031
- }
1032
- return false;
1033
- }
1034
- return true;
1035
- });
1036
-
1037
- this.model.named.SetNamedExpression(validated, expr);
1038
-
1039
- }
1040
- }
1041
- }
1042
- }
1043
- // this.model.named_ranges.RebuildList();
1044
- }
1045
- */
1046
-
1047
978
  // FIXME: do we need to rebuild autocomplete here (A: yes)
1048
979
  // ...
1049
980
 
@@ -1598,7 +1529,7 @@ export class Grid extends GridBase {
1598
1529
  */
1599
1530
  public SetName(name: string, range?: ICellAddress | Area, expression?: string, scope?: number, overwrite = false): void {
1600
1531
 
1601
- // console.info('setname', name, range, expression, scope, overwrite);
1532
+ // console.info('setname', { name, range, expression, scope, overwrite });
1602
1533
 
1603
1534
  // validate/translate name first
1604
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 && (