@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-spreadsheet-light.mjs +11 -11
- package/dist/treb-spreadsheet.mjs +11 -11
- package/dist/treb.d.ts +9 -1
- package/package.json +1 -1
- package/treb-calculator/src/calculator.ts +235 -68
- package/treb-calculator/src/descriptors.ts +5 -0
- package/treb-calculator/src/functions/base-functions.ts +119 -7
- 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/src/embedded-spreadsheet.ts +161 -27
- package/treb-embed/style/dark-theme.scss +1 -0
- package/treb-export/src/export-worker/export-worker.ts +1 -0
- package/treb-export/src/import2.ts +9 -3
- package/treb-export/src/workbook-style2.ts +78 -14
- package/treb-grid/src/editors/editor.ts +7 -0
- package/treb-grid/src/types/grid.ts +20 -9
- package/treb-grid/src/types/grid_base.ts +10 -105
- 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
package/dist/treb.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! API v28.
|
|
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
|
@@ -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
|
|
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.
|
|
1154
|
-
|
|
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.
|
|
1158
|
-
|
|
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.
|
|
1390
|
-
|
|
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.
|
|
1394
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
759
860
|
|
|
760
|
-
|
|
761
|
-
|
|
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 =
|
|
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
|
|