@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-spreadsheet-light.mjs +9 -9
- package/dist/treb-spreadsheet.mjs +9 -9
- package/dist/treb.d.ts +1 -1
- package/package.json +1 -1
- package/treb-calculator/src/expression-calculator.ts +3 -0
- package/treb-calculator/src/functions/base-functions.ts +120 -0
- package/treb-data-model/src/named.ts +19 -3
- package/treb-export/src/import2.ts +18 -13
- package/treb-format/src/format.ts +11 -9
- package/treb-format/src/value_parser.ts +1 -1
- package/treb-grid/src/types/grid.ts +1 -69
- package/treb-parser/src/parser.ts +6 -0
package/dist/treb.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
351
|
+
{
|
|
352
|
+
let style = {};
|
|
352
353
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
|
@@ -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 && (
|