@trebco/treb 28.11.1 → 28.15.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.
Files changed (37) hide show
  1. package/dist/treb-spreadsheet-light.mjs +11 -11
  2. package/dist/treb-spreadsheet.mjs +11 -11
  3. package/dist/treb.d.ts +27 -3
  4. package/package.json +1 -1
  5. package/treb-base-types/src/style.ts +3 -0
  6. package/treb-calculator/src/calculator.ts +235 -68
  7. package/treb-calculator/src/descriptors.ts +5 -0
  8. package/treb-calculator/src/expression-calculator.ts +9 -5
  9. package/treb-calculator/src/functions/base-functions.ts +410 -21
  10. package/treb-calculator/src/functions/text-functions.ts +45 -55
  11. package/treb-calculator/src/primitives.ts +11 -0
  12. package/treb-calculator/src/utilities.ts +55 -0
  13. package/treb-embed/markup/layout.html +15 -10
  14. package/treb-embed/markup/toolbar.html +5 -5
  15. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +38 -2
  16. package/treb-embed/src/embedded-spreadsheet.ts +227 -29
  17. package/treb-embed/src/options.ts +5 -0
  18. package/treb-embed/style/dark-theme.scss +1 -0
  19. package/treb-embed/style/formula-bar.scss +20 -7
  20. package/treb-embed/style/theme-defaults.scss +20 -0
  21. package/treb-export/src/export-worker/export-worker.ts +1 -0
  22. package/treb-export/src/export2.ts +6 -1
  23. package/treb-export/src/import2.ts +76 -6
  24. package/treb-export/src/shared-strings2.ts +1 -1
  25. package/treb-export/src/workbook-style2.ts +89 -52
  26. package/treb-export/src/workbook2.ts +119 -1
  27. package/treb-grid/src/editors/editor.ts +7 -0
  28. package/treb-grid/src/editors/formula_bar.ts +23 -1
  29. package/treb-grid/src/render/tile_renderer.ts +46 -3
  30. package/treb-grid/src/types/annotation.ts +17 -3
  31. package/treb-grid/src/types/grid.ts +28 -9
  32. package/treb-grid/src/types/grid_base.ts +10 -105
  33. package/treb-grid/src/types/grid_options.ts +3 -2
  34. package/treb-grid/src/types/named_range.ts +8 -1
  35. package/treb-grid/src/types/serialize_options.ts +5 -0
  36. package/treb-parser/src/parser-types.ts +27 -4
  37. package/treb-parser/src/parser.ts +74 -36
@@ -88,6 +88,98 @@ const inverse_normal = (q: number): number => {
88
88
 
89
89
  };
90
90
 
91
+ const zlookup_arguments = [
92
+ {
93
+ name: "Lookup value",
94
+ },
95
+ {
96
+ name: "Table",
97
+ },
98
+ {
99
+ name: "Result index",
100
+ },
101
+ {
102
+ name: "Inexact",
103
+ default: true,
104
+ },
105
+ ];
106
+
107
+ /**
108
+ * unified VLOOKUP/HLOOKUP. ordinarily we'd call it XLOOKUP but that's taken.
109
+ * FIXME: can't use use that function for this?
110
+ */
111
+ const ZLookup = (value: any, table: any[][], col: number, inexact = true, transpose = false): UnionValue => {
112
+
113
+ if (transpose) {
114
+ table = Utils.TransposeArray(table);
115
+ }
116
+
117
+ col = Math.max(0, col - 1);
118
+
119
+ // inexact is the default. this assumes that the data is sorted,
120
+ // either numerically or alphabetically. it returns the closest
121
+ // value without going over -- meaning walk the list, and when
122
+ // you're over return the _previous_ item. except if there's an
123
+ // exact match, I guess, in that case return the exact match.
124
+
125
+ // FIXME: there's a hint in the docs for XLOOKUP that this might
126
+ // be using a binary search. not sure why, but that might be
127
+ // correct.
128
+
129
+ if (inexact) {
130
+
131
+ let result: any = table[col][0];
132
+
133
+ if (typeof value === 'number') {
134
+
135
+ let compare = Number(table[0][0]);
136
+ if (isNaN(compare) || compare > value) {
137
+ return NAError();
138
+ }
139
+
140
+ for (let i = 1; i < table[0].length; i++) {
141
+ compare = Number(table[0][i]);
142
+ if (isNaN(compare) || compare > value) {
143
+ break;
144
+ }
145
+ result = table[col][i];
146
+
147
+ }
148
+
149
+ }
150
+ else {
151
+
152
+ value = value.toLowerCase(); // ?
153
+ let compare: string = (table[0][0] || '').toString().toLowerCase();
154
+ if (compare.localeCompare(value) > 0) {
155
+ return NAError();
156
+ }
157
+
158
+ for (let i = 1; i < table[0].length; i++) {
159
+ compare = (table[0][i] || '').toString().toLowerCase();
160
+ if (compare.localeCompare(value) > 0) {
161
+ break;
162
+ }
163
+ result = table[col][i];
164
+
165
+ }
166
+
167
+ }
168
+
169
+ return Box(result);
170
+
171
+ }
172
+ else {
173
+ for (let i = 0; i < table[0].length; i++) {
174
+ if (table[0][i] == value) { // ==
175
+ return Box(table[col][i]);
176
+ }
177
+ }
178
+ return NAError();
179
+ }
180
+
181
+ };
182
+
91
183
  /**
92
184
  * alternate functions. these are used (atm) only for changing complex
93
185
  * behavior.
@@ -241,6 +333,51 @@ export const BaseFunctionLibrary: FunctionMap = {
241
333
  },
242
334
  },
243
335
 
336
+ YearFrac: {
337
+ description: 'Returns the fraction of a year between two dates',
338
+ arguments: [
339
+ { name: 'Start', },
340
+ { name: 'End', },
341
+ { name: 'Basis', default: 0 },
342
+ ],
343
+ fn: (start: number, end: number, basis: number): UnionValue => {
344
+
345
+ // is this in the spec? should it not be negative here? (...)
346
+
347
+ if (end < start) {
348
+ const temp = start;
349
+ start = end;
350
+ end = temp;
351
+ }
352
+
353
+ const delta = Math.max(0, end - start);
354
+ let divisor = 360;
355
+
356
+ if (basis && basis < 0 || basis > 4) {
357
+ return ArgumentError();
358
+ }
359
+
360
+ // console.info({start, end, basis, delta});
361
+
362
+ switch (basis) {
363
+ case 1:
364
+ break;
365
+ case 2:
366
+ break;
367
+ case 3:
368
+ divisor = 365;
369
+ break;
370
+ }
371
+
372
+ return {
373
+ type: ValueType.number,
374
+ value: delta / divisor,
375
+ };
376
+
377
+ return NAError();
378
+ },
379
+ },
380
+
244
381
  Date: {
245
382
  description: 'Constructs a Lotus date from parts',
246
383
  arguments: [
@@ -299,6 +436,25 @@ export const BaseFunctionLibrary: FunctionMap = {
299
436
  },
300
437
  },
301
438
 
439
+ IsNA: {
440
+ description: 'Checks if another cell contains a #NA error',
441
+ arguments: [{ name: 'reference', allow_error: true, boxed: true }],
442
+ fn: (...args: UnionValue[]): UnionValue => {
443
+
444
+ const values = Utils.FlattenBoxed(args);
445
+ for (const value of values) {
446
+ if (value.type === ValueType.error) {
447
+ if (value.value === 'N/A') {
448
+ return { type: ValueType.boolean, value: true };
449
+ }
450
+ }
451
+ }
452
+
453
+ return { type: ValueType.boolean, value: false };
454
+
455
+ },
456
+ },
457
+
302
458
  IsError: {
303
459
  description: 'Checks if another cell contains an error',
304
460
  arguments: [{ name: 'reference', allow_error: true, boxed: true }],
@@ -739,42 +895,275 @@ export const BaseFunctionLibrary: FunctionMap = {
739
895
  },
740
896
  */
741
897
 
742
- /**
743
- * FIXME: does not implement inexact matching (what's the algo for
744
- * that, anyway? nearest? price is right style? what about ties?)
898
+ /*
899
+ * unsaid anywhere (that I can locate) aboud XLOOKUP is that lookup
900
+ * array must be one-dimensional. it can be either a row or a column,
901
+ * but one dimension must be one. that simplifies things quite a bit.
902
+ *
903
+ * there's a note in the docs about binary search over the data --
904
+ * that might explain how inexact VLOOKUP works as well. seems an odd
905
+ * choice but maybe back in the day it made sense
745
906
  */
746
- VLookup: {
747
- fn: (value: any, table: any[][], col: number, inexact = true): UnionValue => {
907
+ XLOOKUP: {
908
+ arguments: [
909
+ { name: 'Lookup value', },
910
+ { name: 'Lookup array', },
911
+ { name: 'Return array', },
912
+ { name: 'Not found', boxed: true },
913
+ { name: 'Match mode', default: 0, },
914
+ { name: 'Search mode', default: 1, },
915
+ ],
916
+ xlfn: true,
917
+ fn: (
918
+ lookup_value: any,
919
+ lookup_array: any[][],
920
+ return_array: any[][],
921
+ not_found?: UnionValue,
922
+ match_mode = 0,
923
+ search_mode = 1,
924
+ ): UnionValue => {
925
+
926
+ // FIXME: we could I suppose be more graceful about single values
927
+ // if passed instead of arrays
928
+
929
+ if (!Array.isArray(lookup_array)) {
930
+ console.info("lookup is not an array");
931
+ return ValueError();
932
+ }
933
+
934
+ const first = lookup_array[0];
935
+ if (!Array.isArray(first)) {
936
+ console.info("lookip is not a 2d array");
937
+ return ValueError();
938
+ }
939
+
940
+ if (lookup_array.length !== 1 && first.length !== 1) {
941
+ console.info("lookup array has invalid dimensions");
942
+ return ValueError();
943
+ }
944
+
945
+ // FIXME: is it required that the return array be (at least) the
946
+ // same size? we can return undefineds, but maybe we should error
748
947
 
749
- col = Math.max(0, col - 1);
948
+ if (!Array.isArray(return_array)) {
949
+ console.info("return array is not an array");
950
+ return ValueError();
951
+ }
952
+
953
+ let transpose = (lookup_array.length === 1);
954
+ if (transpose) {
955
+ lookup_array = Utils.TransposeArray(lookup_array);
956
+ return_array = Utils.TransposeArray(return_array);
957
+ }
750
958
 
751
- if (inexact) {
959
+ // maybe reverse...
752
960
 
753
- let min = Math.abs(value - table[0][0]);
754
- let result: any = table[col][0];
961
+ if (search_mode < 0) {
962
+ lookup_array.reverse();
963
+ return_array.reverse();
964
+ }
755
965
 
756
- for (let i = 1; i < table[0].length; i++) {
966
+ //
967
+ // return value at index, transpose if necessary, and return
968
+ // an array. we might prefer to return a scalar if there's only
969
+ // one value, not sure what's the intended behavior
970
+ //
971
+ const ReturnIndex = (index: number): UnionValue => {
757
972
 
758
- const abs = Math.abs(table[0][i] - value);
973
+ const values = return_array[index];
759
974
 
760
- if (abs < min) { // implies first match
761
- min = abs;
762
- result = table[col][i];
763
- }
975
+ if (!values) {
976
+ return { type: ValueType.undefined };
977
+ }
978
+
979
+ if (!Array.isArray(values)) {
980
+ return Box(values);
981
+ }
982
+
983
+ let boxes = [values.map(value => Box(value))];
984
+
985
+ if (transpose) {
986
+ boxes = Utils.TransposeArray(boxes);
987
+ }
988
+
989
+ return {
990
+ type: ValueType.array,
991
+ value: boxes,
764
992
  }
765
993
 
766
- return Box(result);
994
+ };
995
+
996
+ // if value is not a string, then we can ignore wildcards.
997
+ // in that case convert to exact match.
767
998
 
999
+ if (match_mode === 2 && typeof lookup_value !== 'string') {
1000
+ match_mode = 0;
768
1001
  }
769
- else {
770
- for (let i = 1; i < table[0].length; i++) {
771
- if (table[0][i] == value) { // ==
772
- return table[col][i];
1002
+
1003
+ // what does inexact matching mean in this case if the lookup
1004
+ // value is a string or boolean? (...)
1005
+
1006
+ if ((match_mode === 1 || match_mode === -1) && typeof lookup_value === 'number') {
1007
+
1008
+ let min_delta = 0;
1009
+ let index = -1;
1010
+
1011
+ for (let i = 0; i < lookup_array.length; i++) {
1012
+ const value = lookup_array[i][0];
1013
+
1014
+
1015
+ if (typeof value === 'number') {
1016
+
1017
+ // check for exact match first, just in case
1018
+ if (value === lookup_value) {
1019
+ return ReturnIndex(i);
1020
+ }
1021
+
1022
+ const delta = Math.abs(value - lookup_value);
1023
+
1024
+ if ((match_mode === 1 && value > lookup_value) || (match_mode === -1 && value < lookup_value)){
1025
+ if (index < 0 || delta < min_delta) {
1026
+ min_delta = delta;
1027
+ index = i;
1028
+ }
1029
+ }
1030
+
773
1031
  }
774
1032
  }
775
- return NAError();
1033
+
1034
+ if (index >= 0) {
1035
+ return ReturnIndex(index);
1036
+ }
1037
+
1038
+ }
1039
+
1040
+ switch (match_mode) {
1041
+
1042
+ case 2:
1043
+ {
1044
+ // wildcard string match. we only handle strings for
1045
+ // this case (see above).
1046
+
1047
+ const pattern = Utils.ParseWildcards(lookup_value);
1048
+ const regex = new RegExp('^' + pattern + '$', 'i'); //.exec(lookup_value);
1049
+
1050
+ for (let i = 0; i < lookup_array.length; i++) {
1051
+ let value = lookup_array[i][0];
1052
+ if (typeof value === 'string' && regex.exec(value)) {
1053
+ return ReturnIndex(i);
1054
+ }
1055
+ }
1056
+
1057
+ }
1058
+ break;
1059
+
1060
+ case 0:
1061
+ if (typeof lookup_value === 'string') {
1062
+ lookup_value = lookup_value.toLowerCase();
1063
+ }
1064
+ for (let i = 0; i < lookup_array.length; i++) {
1065
+ let value = lookup_array[i][0];
1066
+
1067
+ if (typeof value === 'string') {
1068
+ value = value.toLowerCase();
1069
+ }
1070
+ if (value === lookup_value) {
1071
+ return ReturnIndex(i);
1072
+ }
1073
+ }
1074
+
1075
+ break;
776
1076
  }
777
1077
 
1078
+ /*
1079
+ const flat_lookup = Utils.FlattenUnboxed(lookup_array);
1080
+ const flat_return = Utils.FlattenUnboxed(return_array);
1081
+
1082
+ // maybe reverse...
1083
+
1084
+ if (search_mode < 0) {
1085
+ flat_lookup.reverse();
1086
+ flat_return.reverse();
1087
+ }
1088
+
1089
+ // if value is not a string, then we can ignore wildcards.
1090
+ // in that case convert to exact match.
1091
+
1092
+ if (match_mode === 2 && typeof lookup_value !== 'string') {
1093
+ match_mode = 0;
1094
+ }
1095
+
1096
+ switch (match_mode) {
1097
+ case 2:
1098
+
1099
+ {
1100
+
1101
+ // wildcard string match. we only handle strings
1102
+ // for wildcard matching (handled above).
1103
+
1104
+ const pattern = Utils.ParseWildcards(lookup_value);
1105
+ const regex = new RegExp('^' + pattern + '$', 'i'); //.exec(lookup_value);
1106
+
1107
+ for (let i = 0; i < flat_lookup.length; i++) {
1108
+ let value = flat_lookup[i];
1109
+ if (typeof value === 'string' && regex.exec(value)) {
1110
+ return Box(flat_return[i]);
1111
+ }
1112
+ }
1113
+
1114
+
1115
+ }
1116
+
1117
+ break;
1118
+
1119
+ case 0:
1120
+
1121
+ // return exact match or NA/default. in this case
1122
+ // "exact" means icase (but not wildcard)
1123
+
1124
+ if (typeof lookup_value === 'string') {
1125
+ lookup_value = lookup_value.toLowerCase();
1126
+ }
1127
+ for (let i = 0; i < flat_lookup.length; i++) {
1128
+ let value = flat_lookup[i];
1129
+ if (typeof value === 'string') {
1130
+ value = value.toLowerCase();
1131
+ }
1132
+ if (value === lookup_value) {
1133
+ return Box(flat_return[i]);
1134
+ }
1135
+ }
1136
+
1137
+ break;
1138
+ }
1139
+ */
1140
+
1141
+ // FIXME: if we're expecting to return an array maybe we should
1142
+ // pack it up as an array? if it's not already an array? (...)
1143
+
1144
+ return (not_found && not_found.type !== ValueType.undefined) ? not_found : NAError();
1145
+
1146
+ },
1147
+ },
1148
+
1149
+ /**
1150
+ * copied from HLOOKUP, fix that one first
1151
+ */
1152
+ HLookup: {
1153
+ arguments: [...zlookup_arguments],
1154
+ fn: (value: any, table: any[][], col: number, inexact = true): UnionValue => {
1155
+ return ZLookup(value, table, col, inexact, true);
1156
+ },
1157
+ },
1158
+
1159
+ /**
1160
+ * FIXME: does not implement inexact matching (what's the algo for
1161
+ * that, anyway? nearest? price is right style? what about ties?)
1162
+ */
1163
+ VLookup: {
1164
+ arguments: [...zlookup_arguments],
1165
+ fn: (value: any, table: any[][], col: number, inexact = true): UnionValue => {
1166
+ return ZLookup(value, table, col, inexact, false);
778
1167
  },
779
1168
  },
780
1169
 
@@ -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
 
@@ -309,6 +309,17 @@ export const Equals = (a: UnionValue, b: UnionValue): UnionValue => {
309
309
 
310
310
  }
311
311
 
312
+ // this is standard (icase) string equality. we might also need
313
+ // to handle wildcard string matching, although it's not the
314
+ // default case for = operators.
315
+
316
+ if (a.type === ValueType.string && b.type === ValueType.string) {
317
+ return {
318
+ type: ValueType.boolean,
319
+ value: a.value.toLowerCase() === b.value.toLowerCase(),
320
+ };
321
+ }
322
+
312
323
  return { type: ValueType.boolean, value: a.value == b.value }; // note ==
313
324
  };
314
325
 
@@ -302,3 +302,58 @@ export const ApplyAsArray2 = (base: (a: any, b: any, ...rest: any[]) => UnionVal
302
302
  }
303
303
  };
304
304
 
305
+
306
+ /**
307
+ * parse a string with wildcards into a regex pattern
308
+ *
309
+ * from
310
+ * https://exceljet.net/glossary/wildcard
311
+ *
312
+ * Excel has 3 wildcards you can use in your formulas:
313
+ *
314
+ * Asterisk (*) - zero or more characters
315
+ * Question mark (?) - any one character
316
+ * Tilde (~) - escape for literal character (~*) a literal question mark (~?), or a literal tilde (~~)
317
+ *
318
+ * they're pretty liberal with escaping, nothing is an error, just roll with it
319
+ *
320
+ */
321
+ export const ParseWildcards = (text: string): string => {
322
+
323
+ const result: string[] = [];
324
+ const length = text.length;
325
+
326
+ const escaped_chars = '[\\^$.|?*+()';
327
+
328
+ for (let i = 0; i < length; i++) {
329
+ let char = text[i];
330
+ switch (char) {
331
+
332
+ case '*':
333
+ result.push('.', '*');
334
+ break;
335
+
336
+ case '?':
337
+ result.push('.');
338
+ break;
339
+
340
+ case '~':
341
+ char = text[++i] || '';
342
+
343
+ // eslint-disable-next-line no-fallthrough
344
+ default:
345
+ for (let j = 0; j < escaped_chars.length; j++) {
346
+ if (char === escaped_chars[j]) {
347
+ result.push('\\');
348
+ break;
349
+ }
350
+ }
351
+ result.push(char);
352
+ break;
353
+
354
+ }
355
+ }
356
+
357
+ return result.join('');
358
+
359
+ };