@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.
- package/dist/treb-spreadsheet-light.mjs +11 -11
- package/dist/treb-spreadsheet.mjs +11 -11
- package/dist/treb.d.ts +27 -3
- package/package.json +1 -1
- package/treb-base-types/src/style.ts +3 -0
- package/treb-calculator/src/calculator.ts +235 -68
- package/treb-calculator/src/descriptors.ts +5 -0
- package/treb-calculator/src/expression-calculator.ts +9 -5
- package/treb-calculator/src/functions/base-functions.ts +410 -21
- package/treb-calculator/src/functions/text-functions.ts +45 -55
- package/treb-calculator/src/primitives.ts +11 -0
- package/treb-calculator/src/utilities.ts +55 -0
- package/treb-embed/markup/layout.html +15 -10
- package/treb-embed/markup/toolbar.html +5 -5
- package/treb-embed/src/custom-element/spreadsheet-constructor.ts +38 -2
- package/treb-embed/src/embedded-spreadsheet.ts +227 -29
- package/treb-embed/src/options.ts +5 -0
- package/treb-embed/style/dark-theme.scss +1 -0
- package/treb-embed/style/formula-bar.scss +20 -7
- package/treb-embed/style/theme-defaults.scss +20 -0
- package/treb-export/src/export-worker/export-worker.ts +1 -0
- package/treb-export/src/export2.ts +6 -1
- package/treb-export/src/import2.ts +76 -6
- package/treb-export/src/shared-strings2.ts +1 -1
- package/treb-export/src/workbook-style2.ts +89 -52
- package/treb-export/src/workbook2.ts +119 -1
- package/treb-grid/src/editors/editor.ts +7 -0
- package/treb-grid/src/editors/formula_bar.ts +23 -1
- package/treb-grid/src/render/tile_renderer.ts +46 -3
- package/treb-grid/src/types/annotation.ts +17 -3
- package/treb-grid/src/types/grid.ts +28 -9
- package/treb-grid/src/types/grid_base.ts +10 -105
- package/treb-grid/src/types/grid_options.ts +3 -2
- package/treb-grid/src/types/named_range.ts +8 -1
- package/treb-grid/src/types/serialize_options.ts +5 -0
- package/treb-parser/src/parser-types.ts +27 -4
- 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
|
-
*
|
|
744
|
-
*
|
|
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
|
-
|
|
747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
959
|
+
// maybe reverse...
|
|
752
960
|
|
|
753
|
-
|
|
754
|
-
|
|
961
|
+
if (search_mode < 0) {
|
|
962
|
+
lookup_array.reverse();
|
|
963
|
+
return_array.reverse();
|
|
964
|
+
}
|
|
755
965
|
|
|
756
|
-
|
|
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
|
-
|
|
973
|
+
const values = return_array[index];
|
|
759
974
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
+
};
|