@trebco/treb 28.11.1 → 28.13.2

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 v28.11. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v28.13. 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
@@ -756,6 +756,9 @@ export declare class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
756
756
  /**
757
757
  * Create a macro function.
758
758
  *
759
+ * FIXME: this needs a control for argument separator, like other
760
+ * functions that use formulas (@see SetRange)
761
+ *
759
762
  * @public
760
763
  */
761
764
  DefineFunction(name: string, argument_names?: string | string[], function_def?: string): void;
@@ -1105,6 +1108,11 @@ export interface SerializeOptions {
1105
1108
 
1106
1109
  /** share resources (images, for now) to prevent writing data URIs more than once */
1107
1110
  share_resources?: boolean;
1111
+
1112
+ /**
1113
+ * if a function has an export() handler, call that
1114
+ */
1115
+ export_functions?: boolean;
1108
1116
  }
1109
1117
 
1110
1118
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "28.11.1",
3
+ "version": "28.13.2",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -23,7 +23,7 @@ import type { Cell, ICellAddress, ICellAddress2, UnionValue, EvaluateOptions,
23
23
  ArrayUnion, IArea, CellDataWithAddress, CellValue} from 'treb-base-types';
24
24
  import { Localization, Area, ValueType, IsCellAddress} from 'treb-base-types';
25
25
 
26
- import type { ExpressionUnit, DependencyList, UnitRange, UnitAddress, UnitIdentifier } from 'treb-parser';
26
+ import type { ExpressionUnit, DependencyList, UnitRange, UnitAddress, UnitIdentifier, ParseResult } from 'treb-parser';
27
27
  import { Parser,
28
28
  DecimalMarkType, ArgumentSeparatorType, QuotedSheetNameRegex } from 'treb-parser';
29
29
 
@@ -57,6 +57,8 @@ import { StateLeafVertex } from './dag/state_leaf_vertex';
57
57
  import { CalculationLeafVertex } from './dag/calculation_leaf_vertex';
58
58
  import type { ConnectedElementType } from 'treb-grid';
59
59
 
60
+ import { ValueParser } from 'treb-format';
61
+
60
62
  /**
61
63
  * breaking this out so we can use it for export (TODO)
62
64
  *
@@ -241,6 +243,160 @@ export class Calculator extends Graph {
241
243
  }
242
244
 
243
245
  // special functions... need reference to the graph (this)
246
+ // moving countif here so we can reference it in COUNTIFS...
247
+
248
+ const FlattenBooleans = (value: ArrayUnion) => {
249
+ const result: boolean[] = [];
250
+ for (const col of value.value) {
251
+ for (const entry of col) {
252
+ result.push(entry.type === ValueType.boolean && entry.value);
253
+ }
254
+ }
255
+ return result;
256
+ };
257
+
258
+ const CountIfInternal = (range: any, criteria: any): UnionValue => {
259
+
260
+ // do we really need parser/calculator for this? I think
261
+ // we've maybe gone overboard here, could we just use valueparser
262
+ // on the criteria and then calculate normally? I think we might...
263
+ // in any event there are no dynamic dependencies with this
264
+ // function.
265
+
266
+ const data = Utilities.FlattenUnboxed(range);
267
+
268
+ let parse_result: ParseResult|undefined;
269
+ let expression: ExpressionUnit|undefined;
270
+
271
+ // we'll handle operator and operand separately
272
+
273
+ let operator = '=';
274
+
275
+ // handle wildcards first. if we have a wildcard we use a
276
+ // matching function so we can centralize.
277
+
278
+ if (typeof criteria === 'string') {
279
+
280
+ // normalize first, pull out operator
281
+
282
+ criteria = criteria.trim();
283
+ const match = criteria.match(/^([=<>]+)/);
284
+ if (match) {
285
+ operator = match[1];
286
+ criteria = criteria.substring(operator.length);
287
+ }
288
+
289
+ const value_parser_result = ValueParser.TryParse(criteria);
290
+ if (value_parser_result?.type === ValueType.string) {
291
+ criteria = `"${value_parser_result.value}"`;
292
+ }
293
+ else {
294
+ criteria = value_parser_result?.value?.toString() || '';
295
+ }
296
+
297
+ // console.info({operator, criteria});
298
+
299
+ // check for wildcards (this will false-positive on escaped
300
+ // wildcards, which will not break but will waste cycles. we
301
+ // could check. TOOD/FIXME)
302
+
303
+ if (/[?*]/.test(criteria)) {
304
+
305
+ // NOTE: we're not specifying an argument separator when writing
306
+ // functions, because that might break numbers passed as strings.
307
+ // so we write the function based on the current separator.
308
+
309
+ const separator = this.parser.argument_separator;
310
+
311
+ if (operator === '=' || operator === '<>') {
312
+
313
+ parse_result = this.parser.Parse(`=WildcardMatch({}${separator} ${criteria}${separator} ${operator === '<>'})`);
314
+ expression = parse_result.expression;
315
+
316
+ if (parse_result.error || !expression) {
317
+ return ExpressionError();
318
+ }
319
+
320
+ if (expression?.type === 'call' && expression.args[0]?.type === 'array') {
321
+ expression.args[0].values = [data];
322
+ }
323
+
324
+ }
325
+
326
+ }
327
+
328
+ }
329
+ else {
330
+
331
+ // if it's not a string, by definition it doesn't have an
332
+ // operator so use equality (default). it does not need
333
+ // escaping.
334
+
335
+ criteria = (criteria || 0).toString();
336
+
337
+ }
338
+
339
+ if (!parse_result) {
340
+
341
+ parse_result = this.parser.Parse('{}' + operator + criteria);
342
+ expression = parse_result.expression;
343
+
344
+ if (parse_result.error || !expression) {
345
+ return ExpressionError();
346
+ }
347
+ if (expression.type !== 'binary') {
348
+ console.warn('invalid expression [1]', expression);
349
+ return ExpressionError();
350
+ }
351
+ if (expression.left.type !== 'array') {
352
+ console.warn('invalid expression [1]', expression);
353
+ return ExpressionError();
354
+ }
355
+
356
+ // this is only going to work for binary left/right. it won't
357
+ // work if we change this to a function (wildcard match)
358
+
359
+ // this will not happen anymore, we can remove
360
+
361
+ if (expression.right.type === 'identifier') {
362
+
363
+ console.warn('will never happen');
364
+
365
+ expression.right = {
366
+ ...expression.right,
367
+ type: 'literal',
368
+ value: expression.right.name,
369
+ }
370
+ }
371
+
372
+ expression.left.values = [data];
373
+
374
+ }
375
+
376
+ if (!expression) {
377
+ return ValueError();
378
+ }
379
+
380
+ const result = this.CalculateExpression(expression);
381
+ return result;
382
+
383
+ /*
384
+ // console.info({expression, result});
385
+
386
+ if (result.type === ValueType.array) {
387
+ let count = 0;
388
+ for (const column of (result as ArrayUnion).value) {
389
+ for (const cell of column) {
390
+ if (cell.value) { count++; }
391
+ }
392
+ }
393
+ return { type: ValueType.number, value: count };
394
+ }
395
+
396
+ return result; // error?
397
+ */
398
+
399
+ };
244
400
 
245
401
  this.library.Register({
246
402
 
@@ -403,6 +559,57 @@ export class Calculator extends Graph {
403
559
  },
404
560
  },
405
561
 
562
+ /**
563
+ * anything I said about COUNTIF applies here, but worse.
564
+ * COUNTIFS is an AND operation across separate COUNTIFs.
565
+ * presumably they have to be the same shape.
566
+ */
567
+ CountIfs: {
568
+ arguments: [
569
+ { name: 'range1', },
570
+ { name: 'criteria1', },
571
+ { name: 'range2', },
572
+ { name: 'criteria2', }
573
+ ],
574
+ fn: (...args): UnionValue => {
575
+
576
+ let count = 0;
577
+
578
+ let result = CountIfInternal(args[0], args[1]);
579
+ if (result.type !== ValueType.array) {
580
+ return result; // error
581
+ }
582
+
583
+ const base = FlattenBooleans(result);
584
+
585
+ for (let i = 2; i < args.length; i += 2) {
586
+ if (args[i] && args[i + 1]) {
587
+
588
+ const result = CountIfInternal(args[i], args[i+1]);
589
+ if (result.type !== ValueType.array) {
590
+ return result;
591
+ }
592
+
593
+ const step = FlattenBooleans(result);
594
+ for (const [index, value] of base.entries()) {
595
+ base[index] = value && step[index];
596
+ }
597
+
598
+ }
599
+ }
600
+
601
+ for (const element of base) {
602
+ if (element) { count++; }
603
+ }
604
+
605
+ return {
606
+ type: ValueType.number,
607
+ value: count,
608
+ }
609
+
610
+ },
611
+ },
612
+
406
613
  /**
407
614
  * this function is here so it has access to the parser.
408
615
  * this is crazy expensive. is there a way to reduce cost?
@@ -425,59 +632,7 @@ export class Calculator extends Graph {
425
632
  ],
426
633
  fn: (range, criteria): UnionValue => {
427
634
 
428
- const data = Utilities.FlattenUnboxed(range);
429
-
430
- // console.info({range, data});
431
-
432
- // console.info({range});
433
-
434
- if (typeof criteria !== 'string') {
435
- criteria = '=' + (criteria || 0).toString();
436
- }
437
- else {
438
- criteria = criteria.trim();
439
- if (!/^[=<>]/.test(criteria)) {
440
- criteria = '=' + criteria;
441
- }
442
- }
443
-
444
- // switching to an array. doesn't actually seem to be any
445
- // faster... more appropriate, though.
446
-
447
- const parse_result = this.parser.Parse('{}' + criteria);
448
- const expression = parse_result.expression;
449
-
450
- if (parse_result.error || !expression) {
451
- return ExpressionError();
452
- }
453
- if (expression.type !== 'binary') {
454
- // console.warn('invalid expression [1]', expression);
455
- return ExpressionError();
456
- }
457
- if (expression.left.type !== 'array') {
458
- // console.warn('invalid expression [1]', expression);
459
- return ExpressionError();
460
- }
461
-
462
- expression.left.values = [data];
463
- const result = this.CalculateExpression(expression);
464
-
465
- // console.info({expression, result});
466
-
467
- // this is no longer the case because we're getting
468
- // a boxed result (union)
469
-
470
- /*
471
- if (Array.isArray(result)) {
472
- let count = 0;
473
- for (const column of result) {
474
- for (const cell of column) {
475
- if (cell.value) { count++; }
476
- }
477
- }
478
- return { type: ValueType.number, value: count };
479
- }
480
- */
635
+ const result = CountIfInternal(range, criteria);
481
636
 
482
637
  if (result.type === ValueType.array) {
483
638
  let count = 0;
@@ -489,7 +644,7 @@ export class Calculator extends Graph {
489
644
  return { type: ValueType.number, value: count };
490
645
  }
491
646
 
492
- return result; // error?
647
+ return result; // error
493
648
 
494
649
  },
495
650
  },
@@ -1150,12 +1305,16 @@ export class Calculator extends Graph {
1150
1305
  // don't assume default, always set
1151
1306
 
1152
1307
  if (Localization.decimal_separator === ',') {
1153
- this.parser.decimal_mark = DecimalMarkType.Comma;
1154
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1308
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1309
+
1310
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
1311
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1155
1312
  }
1156
1313
  else {
1157
- this.parser.decimal_mark = DecimalMarkType.Period;
1158
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
1314
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
1315
+
1316
+ // this.parser.decimal_mark = DecimalMarkType.Period;
1317
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1159
1318
  }
1160
1319
 
1161
1320
  // this.expression_calculator.UpdateLocale();
@@ -1381,17 +1540,23 @@ export class Calculator extends Graph {
1381
1540
  /** moved from embedded sheet */
1382
1541
  public Evaluate(expression: string, active_sheet?: Sheet, options: EvaluateOptions = {}, raw_result = false) {
1383
1542
 
1384
- const current = this.parser.argument_separator;
1385
- const r1c1_state = this.parser.flags.r1c1;
1543
+ // const current = this.parser.argument_separator;
1544
+ // const r1c1_state = this.parser.flags.r1c1;
1545
+
1546
+ this.parser.Save();
1386
1547
 
1387
1548
  if (options.argument_separator) {
1388
1549
  if (options.argument_separator === ',') {
1389
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
1390
- this.parser.decimal_mark = DecimalMarkType.Period;
1550
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
1551
+
1552
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1553
+ // this.parser.decimal_mark = DecimalMarkType.Period;
1391
1554
  }
1392
1555
  else {
1393
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1394
- this.parser.decimal_mark = DecimalMarkType.Comma;
1556
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1557
+
1558
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1559
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
1395
1560
  }
1396
1561
  }
1397
1562
 
@@ -1403,9 +1568,11 @@ export class Calculator extends Graph {
1403
1568
 
1404
1569
  // reset
1405
1570
 
1406
- this.parser.argument_separator = current;
1407
- this.parser.decimal_mark = (current === ArgumentSeparatorType.Comma) ? DecimalMarkType.Period : DecimalMarkType.Comma;
1408
- this.parser.flags.r1c1 = r1c1_state;
1571
+ // this.parser.argument_separator = current;
1572
+ // this.parser.decimal_mark = (current === ArgumentSeparatorType.Comma) ? DecimalMarkType.Period : DecimalMarkType.Comma;
1573
+ // this.parser.flags.r1c1 = r1c1_state;
1574
+
1575
+ this.parser.Restore();
1409
1576
 
1410
1577
  // OK
1411
1578
 
@@ -149,6 +149,11 @@ export interface CompositeFunctionDescriptor {
149
149
  */
150
150
  return_type?: ReturnType;
151
151
 
152
+ /**
153
+ * @internal
154
+ */
155
+ export?: (...args: any[]) => string;
156
+
152
157
  }
153
158
 
154
159
  export interface FunctionMap {
@@ -241,6 +241,51 @@ export const BaseFunctionLibrary: FunctionMap = {
241
241
  },
242
242
  },
243
243
 
244
+ YearFrac: {
245
+ description: 'Returns the fraction of a year between two dates',
246
+ arguments: [
247
+ { name: 'Start', },
248
+ { name: 'End', },
249
+ { name: 'Basis', default: 0 },
250
+ ],
251
+ fn: (start: number, end: number, basis: number): UnionValue => {
252
+
253
+ // is this in the spec? should it not be negative here? (...)
254
+
255
+ if (end < start) {
256
+ const temp = start;
257
+ start = end;
258
+ end = temp;
259
+ }
260
+
261
+ const delta = Math.max(0, end - start);
262
+ let divisor = 360;
263
+
264
+ if (basis && basis < 0 || basis > 4) {
265
+ return ArgumentError();
266
+ }
267
+
268
+ // console.info({start, end, basis, delta});
269
+
270
+ switch (basis) {
271
+ case 1:
272
+ break;
273
+ case 2:
274
+ break;
275
+ case 3:
276
+ divisor = 365;
277
+ break;
278
+ }
279
+
280
+ return {
281
+ type: ValueType.number,
282
+ value: delta / divisor,
283
+ };
284
+
285
+ return NAError();
286
+ },
287
+ },
288
+
244
289
  Date: {
245
290
  description: 'Constructs a Lotus date from parts',
246
291
  arguments: [
@@ -299,6 +344,25 @@ export const BaseFunctionLibrary: FunctionMap = {
299
344
  },
300
345
  },
301
346
 
347
+ IsNA: {
348
+ description: 'Checks if another cell contains a #NA error',
349
+ arguments: [{ name: 'reference', allow_error: true, boxed: true }],
350
+ fn: (...args: UnionValue[]): UnionValue => {
351
+
352
+ const values = Utils.FlattenBoxed(args);
353
+ for (const value of values) {
354
+ if (value.type === ValueType.error) {
355
+ if (value.value === 'N/A') {
356
+ return { type: ValueType.boolean, value: true };
357
+ }
358
+ }
359
+ }
360
+
361
+ return { type: ValueType.boolean, value: false };
362
+
363
+ },
364
+ },
365
+
302
366
  IsError: {
303
367
  description: 'Checks if another cell contains an error',
304
368
  arguments: [{ name: 'reference', allow_error: true, boxed: true }],
@@ -744,32 +808,80 @@ export const BaseFunctionLibrary: FunctionMap = {
744
808
  * that, anyway? nearest? price is right style? what about ties?)
745
809
  */
746
810
  VLookup: {
811
+
812
+ arguments: [
813
+ {
814
+ name: "Lookup value",
815
+ },
816
+ {
817
+ name: "Table",
818
+ },
819
+ {
820
+ name: "Result index",
821
+ },
822
+ {
823
+ name: "Inexact",
824
+ default: true,
825
+ },
826
+ ],
827
+
747
828
  fn: (value: any, table: any[][], col: number, inexact = true): UnionValue => {
748
829
 
749
830
  col = Math.max(0, col - 1);
750
831
 
832
+ // inexact is the default. this assumes that the data is sorted,
833
+ // either numerically or alphabetically. it returns the closest
834
+ // value without going over -- meaning walk the list, and when
835
+ // you're over return the _previous_ item. except if there's an
836
+ // exact match, I guess, in that case return the exact match.
837
+
751
838
  if (inexact) {
752
839
 
753
- let min = Math.abs(value - table[0][0]);
754
840
  let result: any = table[col][0];
755
841
 
756
- for (let i = 1; i < table[0].length; i++) {
842
+ if (typeof value === 'number') {
843
+
844
+ let compare = Number(table[0][0]);
845
+ if (isNaN(compare) || compare > value) {
846
+ return NAError();
847
+ }
848
+
849
+ for (let i = 1; i < table[0].length; i++) {
850
+ compare = Number(table[0][i]);
851
+ if (isNaN(compare) || compare > value) {
852
+ break;
853
+ }
854
+ result = table[col][i];
757
855
 
758
- const abs = Math.abs(table[0][i] - value);
856
+ }
857
+
858
+ }
859
+ else {
759
860
 
760
- if (abs < min) { // implies first match
761
- min = abs;
861
+ value = value.toLowerCase(); // ?
862
+ let compare: string = (table[0][0] || '').toString().toLowerCase();
863
+ if (compare.localeCompare(value) > 0) {
864
+ return NAError();
865
+ }
866
+
867
+ for (let i = 1; i < table[0].length; i++) {
868
+ compare = (table[0][i] || '').toString().toLowerCase();
869
+ if (compare.localeCompare(value) > 0) {
870
+ break;
871
+ }
762
872
  result = table[col][i];
873
+
763
874
  }
875
+
764
876
  }
765
877
 
766
878
  return Box(result);
767
879
 
768
880
  }
769
881
  else {
770
- for (let i = 1; i < table[0].length; i++) {
882
+ for (let i = 0; i < table[0].length; i++) {
771
883
  if (table[0][i] == value) { // ==
772
- return table[col][i];
884
+ return Box(table[col][i]);
773
885
  }
774
886
  }
775
887
  return NAError();
@@ -26,60 +26,6 @@ import { Localization, ValueType } from 'treb-base-types';
26
26
  import * as Utils from '../utilities';
27
27
  import { ArgumentError, ValueError } from '../function-error';
28
28
 
29
- /**
30
- * parse a string with wildcards into a regex pattern
31
- *
32
- * from
33
- * https://exceljet.net/glossary/wildcard
34
- *
35
- * Excel has 3 wildcards you can use in your formulas:
36
- *
37
- * Asterisk (*) - zero or more characters
38
- * Question mark (?) - any one character
39
- * Tilde (~) - escape for literal character (~*) a literal question mark (~?), or a literal tilde (~~)
40
- *
41
- * they're pretty liberal with escaping, nothing is an error, just roll with it
42
- *
43
- */
44
- const ParseWildcards = (text: string): string => {
45
-
46
- const result: string[] = [];
47
- const length = text.length;
48
-
49
- const escaped_chars = '[\\^$.|?*+()';
50
-
51
- for (let i = 0; i < length; i++) {
52
- let char = text[i];
53
- switch (char) {
54
-
55
- case '*':
56
- result.push('.', '*');
57
- break;
58
-
59
- case '?':
60
- result.push('.');
61
- break;
62
-
63
- case '~':
64
- char = text[++i] || '';
65
-
66
- // eslint-disable-next-line no-fallthrough
67
- default:
68
- for (let j = 0; j < escaped_chars.length; j++) {
69
- if (char === escaped_chars[j]) {
70
- result.push('\\');
71
- break;
72
- }
73
- }
74
- result.push(char);
75
- break;
76
-
77
- }
78
- }
79
-
80
- return result.join('');
81
-
82
- };
83
29
 
84
30
  export const TextFunctionLibrary: FunctionMap = {
85
31
 
@@ -129,6 +75,50 @@ export const TextFunctionLibrary: FunctionMap = {
129
75
  category: ['text'],
130
76
  },
131
77
 
78
+ WildcardMatch: {
79
+ visibility: 'internal',
80
+ arguments: [
81
+ { name: 'text', },
82
+ { name: 'text', },
83
+
84
+ // the invert parameter is optional, defaults to false. we add this
85
+ // so we can invert wirhout requiring an extra function call.
86
+
87
+ { name: 'invert' },
88
+ ],
89
+ fn: Utils.ApplyAsArray2((a: any, b: any, invert = false) => {
90
+
91
+ if (typeof a === 'string' && typeof b === 'string') {
92
+ const pattern = Utils.ParseWildcards(b);
93
+ const match = new RegExp('^' + pattern + '$', 'i').exec(a);
94
+
95
+ return {
96
+ type: ValueType.boolean,
97
+ value: invert ? !match : !!match,
98
+ };
99
+ }
100
+
101
+ return {
102
+ type: ValueType.boolean,
103
+ value: (a === b || a?.toString() === b?.toString()),
104
+ }
105
+ }),
106
+ },
107
+
108
+ Exact: {
109
+ arguments: [
110
+ { name: 'text', boxed: true, },
111
+ { name: 'text', boxed: true, },
112
+ ],
113
+ category: ['text'],
114
+ fn: Utils.ApplyAsArray2((a: UnionValue, b: UnionValue): UnionValue => {
115
+ return {
116
+ type: ValueType.boolean,
117
+ value: (a?.value?.toString()) === (b?.value?.toString()),
118
+ };
119
+ }),
120
+ },
121
+
132
122
  Left: {
133
123
  arguments: [
134
124
  { name: 'string' },
@@ -225,7 +215,7 @@ export const TextFunctionLibrary: FunctionMap = {
225
215
  // can we get by with regexes? should we have some sort of cache
226
216
  // for common patterns?
227
217
 
228
- const pattern = ParseWildcards(needle);
218
+ const pattern = Utils.ParseWildcards(needle);
229
219
  // console.info('n', needle, 'p', pattern);
230
220
  const match = new RegExp(pattern, 'i').exec(haystack.substr(start - 1));
231
221