@the-trybe/formula-engine 1.0.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 (43) hide show
  1. package/.claude/settings.local.json +6 -0
  2. package/PRD_FORMULA_ENGINE.md +1863 -0
  3. package/README.md +382 -0
  4. package/dist/decimal-utils.d.ts +180 -0
  5. package/dist/decimal-utils.js +355 -0
  6. package/dist/dependency-extractor.d.ts +20 -0
  7. package/dist/dependency-extractor.js +103 -0
  8. package/dist/dependency-graph.d.ts +60 -0
  9. package/dist/dependency-graph.js +252 -0
  10. package/dist/errors.d.ts +161 -0
  11. package/dist/errors.js +260 -0
  12. package/dist/evaluator.d.ts +51 -0
  13. package/dist/evaluator.js +494 -0
  14. package/dist/formula-engine.d.ts +79 -0
  15. package/dist/formula-engine.js +355 -0
  16. package/dist/functions.d.ts +3 -0
  17. package/dist/functions.js +720 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +61 -0
  20. package/dist/lexer.d.ts +25 -0
  21. package/dist/lexer.js +357 -0
  22. package/dist/parser.d.ts +32 -0
  23. package/dist/parser.js +372 -0
  24. package/dist/types.d.ts +228 -0
  25. package/dist/types.js +62 -0
  26. package/jest.config.js +23 -0
  27. package/package.json +35 -0
  28. package/src/decimal-utils.ts +408 -0
  29. package/src/dependency-extractor.ts +117 -0
  30. package/src/dependency-graph.test.ts +238 -0
  31. package/src/dependency-graph.ts +288 -0
  32. package/src/errors.ts +296 -0
  33. package/src/evaluator.ts +604 -0
  34. package/src/formula-engine.test.ts +660 -0
  35. package/src/formula-engine.ts +430 -0
  36. package/src/functions.ts +770 -0
  37. package/src/index.ts +103 -0
  38. package/src/lexer.test.ts +288 -0
  39. package/src/lexer.ts +394 -0
  40. package/src/parser.test.ts +349 -0
  41. package/src/parser.ts +449 -0
  42. package/src/types.ts +347 -0
  43. package/tsconfig.json +29 -0
@@ -0,0 +1,770 @@
1
+ import { FunctionDefinition, EvaluationContext } from './types';
2
+ import { DecimalUtils, Decimal } from './decimal-utils';
3
+ import { ArgumentCountError, TypeMismatchError } from './errors';
4
+
5
+ type FnImpl = FunctionDefinition['implementation'];
6
+
7
+ // Helper to check if value is numeric (Decimal or number)
8
+ function isNumeric(value: unknown): value is Decimal | number {
9
+ return value instanceof Decimal || typeof value === 'number';
10
+ }
11
+
12
+ // Helper to convert to Decimal
13
+ function toDecimal(value: unknown, utils: DecimalUtils): Decimal {
14
+ if (value instanceof Decimal) return value;
15
+ if (typeof value === 'number') return utils.from(value);
16
+ if (typeof value === 'string') return utils.from(value);
17
+ throw new TypeMismatchError('number', typeof value, 'numeric conversion');
18
+ }
19
+
20
+ // Helper to convert value to number (for functions that return native numbers)
21
+ function toNumber(value: unknown): number {
22
+ if (value instanceof Decimal) return value.toNumber();
23
+ if (typeof value === 'number') return value;
24
+ if (typeof value === 'string') return parseFloat(value);
25
+ throw new TypeMismatchError('number', typeof value, 'numeric conversion');
26
+ }
27
+
28
+ export function createBuiltInFunctions(decimalUtils: DecimalUtils): Map<string, FunctionDefinition> {
29
+ const functions = new Map<string, FunctionDefinition>();
30
+
31
+ // ============================================================================
32
+ // Math Functions
33
+ // ============================================================================
34
+
35
+ const ABS: FunctionDefinition = {
36
+ name: 'ABS',
37
+ minArgs: 1,
38
+ maxArgs: 1,
39
+ returnType: 'decimal',
40
+ description: 'Absolute value',
41
+ implementation: (args) => {
42
+ return decimalUtils.abs(toDecimal(args[0], decimalUtils));
43
+ },
44
+ };
45
+
46
+ const ROUND: FunctionDefinition = {
47
+ name: 'ROUND',
48
+ minArgs: 1,
49
+ maxArgs: 3,
50
+ returnType: 'decimal',
51
+ description: 'Round to precision',
52
+ implementation: (args) => {
53
+ const value = toDecimal(args[0], decimalUtils);
54
+ const precision = args.length > 1 ? toNumber(args[1]) : 0;
55
+ const mode = args.length > 2 ? String(args[2]) : undefined;
56
+ return decimalUtils.round(value, precision, mode as any);
57
+ },
58
+ };
59
+
60
+ const FLOOR: FunctionDefinition = {
61
+ name: 'FLOOR',
62
+ minArgs: 1,
63
+ maxArgs: 2,
64
+ returnType: 'decimal',
65
+ description: 'Round down',
66
+ implementation: (args) => {
67
+ const value = toDecimal(args[0], decimalUtils);
68
+ const scale = args.length > 1 ? toNumber(args[1]) : 0;
69
+ return decimalUtils.floor(value, scale);
70
+ },
71
+ };
72
+
73
+ const CEIL: FunctionDefinition = {
74
+ name: 'CEIL',
75
+ minArgs: 1,
76
+ maxArgs: 2,
77
+ returnType: 'decimal',
78
+ description: 'Round up',
79
+ implementation: (args) => {
80
+ const value = toDecimal(args[0], decimalUtils);
81
+ const scale = args.length > 1 ? toNumber(args[1]) : 0;
82
+ return decimalUtils.ceil(value, scale);
83
+ },
84
+ };
85
+
86
+ const TRUNCATE: FunctionDefinition = {
87
+ name: 'TRUNCATE',
88
+ minArgs: 1,
89
+ maxArgs: 2,
90
+ returnType: 'decimal',
91
+ description: 'Truncate to precision',
92
+ implementation: (args) => {
93
+ const value = toDecimal(args[0], decimalUtils);
94
+ const scale = args.length > 1 ? toNumber(args[1]) : 0;
95
+ return decimalUtils.truncate(value, scale);
96
+ },
97
+ };
98
+
99
+ const MIN: FunctionDefinition = {
100
+ name: 'MIN',
101
+ minArgs: 1,
102
+ maxArgs: -1,
103
+ returnType: 'decimal',
104
+ description: 'Minimum value',
105
+ implementation: (args) => {
106
+ if (args.length === 1 && Array.isArray(args[0])) {
107
+ return decimalUtils.min(...args[0].map(v => toDecimal(v, decimalUtils)));
108
+ }
109
+ return decimalUtils.min(...args.map(v => toDecimal(v, decimalUtils)));
110
+ },
111
+ };
112
+
113
+ const MAX: FunctionDefinition = {
114
+ name: 'MAX',
115
+ minArgs: 1,
116
+ maxArgs: -1,
117
+ returnType: 'decimal',
118
+ description: 'Maximum value',
119
+ implementation: (args) => {
120
+ if (args.length === 1 && Array.isArray(args[0])) {
121
+ return decimalUtils.max(...args[0].map(v => toDecimal(v, decimalUtils)));
122
+ }
123
+ return decimalUtils.max(...args.map(v => toDecimal(v, decimalUtils)));
124
+ },
125
+ };
126
+
127
+ const POW: FunctionDefinition = {
128
+ name: 'POW',
129
+ minArgs: 2,
130
+ maxArgs: 2,
131
+ returnType: 'decimal',
132
+ description: 'Power',
133
+ implementation: (args) => {
134
+ const base = toDecimal(args[0], decimalUtils);
135
+ const exp = toNumber(args[1]);
136
+ return decimalUtils.power(base, exp);
137
+ },
138
+ };
139
+
140
+ const SQRT: FunctionDefinition = {
141
+ name: 'SQRT',
142
+ minArgs: 1,
143
+ maxArgs: 1,
144
+ returnType: 'decimal',
145
+ description: 'Square root',
146
+ implementation: (args) => {
147
+ return decimalUtils.sqrt(toDecimal(args[0], decimalUtils));
148
+ },
149
+ };
150
+
151
+ const LOG: FunctionDefinition = {
152
+ name: 'LOG',
153
+ minArgs: 1,
154
+ maxArgs: 1,
155
+ returnType: 'decimal',
156
+ description: 'Natural logarithm',
157
+ implementation: (args) => {
158
+ return decimalUtils.ln(toDecimal(args[0], decimalUtils));
159
+ },
160
+ };
161
+
162
+ const LOG10: FunctionDefinition = {
163
+ name: 'LOG10',
164
+ minArgs: 1,
165
+ maxArgs: 1,
166
+ returnType: 'decimal',
167
+ description: 'Base-10 logarithm',
168
+ implementation: (args) => {
169
+ return decimalUtils.log10(toDecimal(args[0], decimalUtils));
170
+ },
171
+ };
172
+
173
+ const SIGN: FunctionDefinition = {
174
+ name: 'SIGN',
175
+ minArgs: 1,
176
+ maxArgs: 1,
177
+ returnType: 'number',
178
+ description: 'Sign of number (-1, 0, or 1)',
179
+ implementation: (args) => {
180
+ return decimalUtils.sign(toDecimal(args[0], decimalUtils));
181
+ },
182
+ };
183
+
184
+ const DECIMAL: FunctionDefinition = {
185
+ name: 'DECIMAL',
186
+ minArgs: 1,
187
+ maxArgs: 2,
188
+ returnType: 'decimal',
189
+ description: 'Convert to Decimal',
190
+ implementation: (args) => {
191
+ const value = toDecimal(args[0], decimalUtils);
192
+ if (args.length > 1) {
193
+ const scale = toNumber(args[1]);
194
+ return decimalUtils.round(value, scale);
195
+ }
196
+ return value;
197
+ },
198
+ };
199
+
200
+ const SCALE: FunctionDefinition = {
201
+ name: 'SCALE',
202
+ minArgs: 1,
203
+ maxArgs: 1,
204
+ returnType: 'number',
205
+ description: 'Get scale (decimal places)',
206
+ implementation: (args) => {
207
+ return decimalUtils.scale(toDecimal(args[0], decimalUtils));
208
+ },
209
+ };
210
+
211
+ const PRECISION: FunctionDefinition = {
212
+ name: 'PRECISION',
213
+ minArgs: 1,
214
+ maxArgs: 1,
215
+ returnType: 'number',
216
+ description: 'Get precision (significant digits)',
217
+ implementation: (args) => {
218
+ return decimalUtils.precision(toDecimal(args[0], decimalUtils));
219
+ },
220
+ };
221
+
222
+ const DIVIDE: FunctionDefinition = {
223
+ name: 'DIVIDE',
224
+ minArgs: 2,
225
+ maxArgs: 4,
226
+ returnType: 'decimal',
227
+ description: 'Division with scale and rounding',
228
+ implementation: (args) => {
229
+ const a = toDecimal(args[0], decimalUtils);
230
+ const b = toDecimal(args[1], decimalUtils);
231
+ const scale = args.length > 2 ? toNumber(args[2]) : undefined;
232
+ const mode = args.length > 3 ? String(args[3]) : undefined;
233
+ return decimalUtils.divide(a, b, scale, mode as any);
234
+ },
235
+ };
236
+
237
+ // ============================================================================
238
+ // Aggregation Functions
239
+ // ============================================================================
240
+
241
+ const SUM: FunctionDefinition = {
242
+ name: 'SUM',
243
+ minArgs: 1,
244
+ maxArgs: 2,
245
+ returnType: 'decimal',
246
+ description: 'Sum of array values',
247
+ implementation: (args, context, engine) => {
248
+ const arr = args[0];
249
+ if (!Array.isArray(arr)) {
250
+ throw new TypeMismatchError('array', typeof arr, 'SUM');
251
+ }
252
+
253
+ if (args.length === 1) {
254
+ // Simple sum
255
+ return decimalUtils.sum(arr.map(v => toDecimal(v, decimalUtils)));
256
+ }
257
+
258
+ // Sum with expression - args[1] should be evaluated for each item
259
+ // This is handled specially by the evaluator
260
+ throw new Error('SUM with expression must be handled by evaluator');
261
+ },
262
+ };
263
+
264
+ const AVG: FunctionDefinition = {
265
+ name: 'AVG',
266
+ minArgs: 1,
267
+ maxArgs: 1,
268
+ returnType: 'decimal',
269
+ description: 'Average of array values',
270
+ implementation: (args) => {
271
+ const arr = args[0];
272
+ if (!Array.isArray(arr)) {
273
+ throw new TypeMismatchError('array', typeof arr, 'AVG');
274
+ }
275
+ return decimalUtils.avg(arr.map(v => toDecimal(v, decimalUtils)));
276
+ },
277
+ };
278
+
279
+ const COUNT: FunctionDefinition = {
280
+ name: 'COUNT',
281
+ minArgs: 1,
282
+ maxArgs: 1,
283
+ returnType: 'number',
284
+ description: 'Count of array elements',
285
+ implementation: (args) => {
286
+ const arr = args[0];
287
+ if (!Array.isArray(arr)) {
288
+ throw new TypeMismatchError('array', typeof arr, 'COUNT');
289
+ }
290
+ return arr.length;
291
+ },
292
+ };
293
+
294
+ const PRODUCT: FunctionDefinition = {
295
+ name: 'PRODUCT',
296
+ minArgs: 1,
297
+ maxArgs: 1,
298
+ returnType: 'decimal',
299
+ description: 'Product of array values',
300
+ implementation: (args) => {
301
+ const arr = args[0];
302
+ if (!Array.isArray(arr)) {
303
+ throw new TypeMismatchError('array', typeof arr, 'PRODUCT');
304
+ }
305
+ return decimalUtils.product(arr.map(v => toDecimal(v, decimalUtils)));
306
+ },
307
+ };
308
+
309
+ const FILTER: FunctionDefinition = {
310
+ name: 'FILTER',
311
+ minArgs: 2,
312
+ maxArgs: 2,
313
+ returnType: 'array',
314
+ description: 'Filter array by condition',
315
+ implementation: () => {
316
+ // This must be handled by the evaluator to evaluate the condition expression
317
+ throw new Error('FILTER must be handled by evaluator');
318
+ },
319
+ };
320
+
321
+ const MAP: FunctionDefinition = {
322
+ name: 'MAP',
323
+ minArgs: 2,
324
+ maxArgs: 2,
325
+ returnType: 'array',
326
+ description: 'Transform array elements',
327
+ implementation: () => {
328
+ // This must be handled by the evaluator to evaluate the transform expression
329
+ throw new Error('MAP must be handled by evaluator');
330
+ },
331
+ };
332
+
333
+ // ============================================================================
334
+ // String Functions
335
+ // ============================================================================
336
+
337
+ const LEN: FunctionDefinition = {
338
+ name: 'LEN',
339
+ minArgs: 1,
340
+ maxArgs: 1,
341
+ returnType: 'number',
342
+ description: 'String length',
343
+ implementation: (args) => {
344
+ const str = String(args[0]);
345
+ return str.length;
346
+ },
347
+ };
348
+
349
+ const UPPER: FunctionDefinition = {
350
+ name: 'UPPER',
351
+ minArgs: 1,
352
+ maxArgs: 1,
353
+ returnType: 'string',
354
+ description: 'Uppercase string',
355
+ implementation: (args) => {
356
+ return String(args[0]).toUpperCase();
357
+ },
358
+ };
359
+
360
+ const LOWER: FunctionDefinition = {
361
+ name: 'LOWER',
362
+ minArgs: 1,
363
+ maxArgs: 1,
364
+ returnType: 'string',
365
+ description: 'Lowercase string',
366
+ implementation: (args) => {
367
+ return String(args[0]).toLowerCase();
368
+ },
369
+ };
370
+
371
+ const TRIM: FunctionDefinition = {
372
+ name: 'TRIM',
373
+ minArgs: 1,
374
+ maxArgs: 1,
375
+ returnType: 'string',
376
+ description: 'Trim whitespace',
377
+ implementation: (args) => {
378
+ return String(args[0]).trim();
379
+ },
380
+ };
381
+
382
+ const CONCAT: FunctionDefinition = {
383
+ name: 'CONCAT',
384
+ minArgs: 1,
385
+ maxArgs: -1,
386
+ returnType: 'string',
387
+ description: 'Concatenate strings',
388
+ implementation: (args) => {
389
+ return args.map(a => String(a)).join('');
390
+ },
391
+ };
392
+
393
+ const SUBSTR: FunctionDefinition = {
394
+ name: 'SUBSTR',
395
+ minArgs: 2,
396
+ maxArgs: 3,
397
+ returnType: 'string',
398
+ description: 'Substring',
399
+ implementation: (args) => {
400
+ const str = String(args[0]);
401
+ const start = toNumber(args[1]);
402
+ const len = args.length > 2 ? toNumber(args[2]) : undefined;
403
+ return len !== undefined ? str.substr(start, len) : str.substr(start);
404
+ },
405
+ };
406
+
407
+ const REPLACE: FunctionDefinition = {
408
+ name: 'REPLACE',
409
+ minArgs: 3,
410
+ maxArgs: 3,
411
+ returnType: 'string',
412
+ description: 'Replace substring',
413
+ implementation: (args) => {
414
+ const str = String(args[0]);
415
+ const search = String(args[1]);
416
+ const replacement = String(args[2]);
417
+ return str.split(search).join(replacement);
418
+ },
419
+ };
420
+
421
+ const CONTAINS: FunctionDefinition = {
422
+ name: 'CONTAINS',
423
+ minArgs: 2,
424
+ maxArgs: 2,
425
+ returnType: 'boolean',
426
+ description: 'Check if string contains substring',
427
+ implementation: (args) => {
428
+ const str = String(args[0]);
429
+ const search = String(args[1]);
430
+ return str.includes(search);
431
+ },
432
+ };
433
+
434
+ const STARTSWITH: FunctionDefinition = {
435
+ name: 'STARTSWITH',
436
+ minArgs: 2,
437
+ maxArgs: 2,
438
+ returnType: 'boolean',
439
+ description: 'Check if string starts with prefix',
440
+ implementation: (args) => {
441
+ return String(args[0]).startsWith(String(args[1]));
442
+ },
443
+ };
444
+
445
+ const ENDSWITH: FunctionDefinition = {
446
+ name: 'ENDSWITH',
447
+ minArgs: 2,
448
+ maxArgs: 2,
449
+ returnType: 'boolean',
450
+ description: 'Check if string ends with suffix',
451
+ implementation: (args) => {
452
+ return String(args[0]).endsWith(String(args[1]));
453
+ },
454
+ };
455
+
456
+ // ============================================================================
457
+ // Logical Functions
458
+ // ============================================================================
459
+
460
+ const IF: FunctionDefinition = {
461
+ name: 'IF',
462
+ minArgs: 3,
463
+ maxArgs: 3,
464
+ returnType: 'any',
465
+ description: 'Conditional expression',
466
+ implementation: (args) => {
467
+ const condition = args[0];
468
+ return condition ? args[1] : args[2];
469
+ },
470
+ };
471
+
472
+ const COALESCE: FunctionDefinition = {
473
+ name: 'COALESCE',
474
+ minArgs: 1,
475
+ maxArgs: -1,
476
+ returnType: 'any',
477
+ description: 'First non-null value',
478
+ implementation: (args) => {
479
+ for (const arg of args) {
480
+ if (arg !== null && arg !== undefined) {
481
+ return arg;
482
+ }
483
+ }
484
+ return null;
485
+ },
486
+ };
487
+
488
+ const ISNULL: FunctionDefinition = {
489
+ name: 'ISNULL',
490
+ minArgs: 1,
491
+ maxArgs: 1,
492
+ returnType: 'boolean',
493
+ description: 'Check if null',
494
+ implementation: (args) => {
495
+ return args[0] === null || args[0] === undefined;
496
+ },
497
+ };
498
+
499
+ const ISEMPTY: FunctionDefinition = {
500
+ name: 'ISEMPTY',
501
+ minArgs: 1,
502
+ maxArgs: 1,
503
+ returnType: 'boolean',
504
+ description: 'Check if empty',
505
+ implementation: (args) => {
506
+ const val = args[0];
507
+ if (val === null || val === undefined) return true;
508
+ if (typeof val === 'string') return val.length === 0;
509
+ if (Array.isArray(val)) return val.length === 0;
510
+ if (typeof val === 'object') return Object.keys(val).length === 0;
511
+ return false;
512
+ },
513
+ };
514
+
515
+ const DEFAULT: FunctionDefinition = {
516
+ name: 'DEFAULT',
517
+ minArgs: 2,
518
+ maxArgs: 2,
519
+ returnType: 'any',
520
+ description: 'Default value if null',
521
+ implementation: (args) => {
522
+ return args[0] !== null && args[0] !== undefined ? args[0] : args[1];
523
+ },
524
+ };
525
+
526
+ const AND: FunctionDefinition = {
527
+ name: 'AND',
528
+ minArgs: 2,
529
+ maxArgs: -1,
530
+ returnType: 'boolean',
531
+ description: 'Logical AND of all arguments',
532
+ implementation: (args) => {
533
+ return args.every(a => Boolean(a));
534
+ },
535
+ };
536
+
537
+ const OR: FunctionDefinition = {
538
+ name: 'OR',
539
+ minArgs: 2,
540
+ maxArgs: -1,
541
+ returnType: 'boolean',
542
+ description: 'Logical OR of all arguments',
543
+ implementation: (args) => {
544
+ return args.some(a => Boolean(a));
545
+ },
546
+ };
547
+
548
+ const NOT: FunctionDefinition = {
549
+ name: 'NOT',
550
+ minArgs: 1,
551
+ maxArgs: 1,
552
+ returnType: 'boolean',
553
+ description: 'Logical NOT',
554
+ implementation: (args) => {
555
+ return !Boolean(args[0]);
556
+ },
557
+ };
558
+
559
+ // ============================================================================
560
+ // Type Functions
561
+ // ============================================================================
562
+
563
+ const NUMBER: FunctionDefinition = {
564
+ name: 'NUMBER',
565
+ minArgs: 1,
566
+ maxArgs: 1,
567
+ returnType: 'decimal',
568
+ description: 'Convert to number',
569
+ implementation: (args) => {
570
+ return toDecimal(args[0], decimalUtils);
571
+ },
572
+ };
573
+
574
+ const STRING: FunctionDefinition = {
575
+ name: 'STRING',
576
+ minArgs: 1,
577
+ maxArgs: 1,
578
+ returnType: 'string',
579
+ description: 'Convert to string',
580
+ implementation: (args) => {
581
+ const val = args[0];
582
+ if (val instanceof Decimal) {
583
+ return val.toString();
584
+ }
585
+ return String(val);
586
+ },
587
+ };
588
+
589
+ const BOOLEAN: FunctionDefinition = {
590
+ name: 'BOOLEAN',
591
+ minArgs: 1,
592
+ maxArgs: 1,
593
+ returnType: 'boolean',
594
+ description: 'Convert to boolean',
595
+ implementation: (args) => {
596
+ const val = args[0];
597
+ if (typeof val === 'string') {
598
+ return val.toLowerCase() === 'true' || val === '1';
599
+ }
600
+ if (val instanceof Decimal) {
601
+ return !val.isZero();
602
+ }
603
+ return Boolean(val);
604
+ },
605
+ };
606
+
607
+ const TYPEOF: FunctionDefinition = {
608
+ name: 'TYPEOF',
609
+ minArgs: 1,
610
+ maxArgs: 1,
611
+ returnType: 'string',
612
+ description: 'Get type name',
613
+ implementation: (args) => {
614
+ const val = args[0];
615
+ if (val === null) return 'null';
616
+ if (val instanceof Decimal) return 'decimal';
617
+ if (Array.isArray(val)) return 'array';
618
+ return typeof val;
619
+ },
620
+ };
621
+
622
+ // ============================================================================
623
+ // Array Functions
624
+ // ============================================================================
625
+
626
+ const FIRST: FunctionDefinition = {
627
+ name: 'FIRST',
628
+ minArgs: 1,
629
+ maxArgs: 1,
630
+ returnType: 'any',
631
+ description: 'First element of array',
632
+ implementation: (args) => {
633
+ const arr = args[0];
634
+ if (!Array.isArray(arr)) {
635
+ throw new TypeMismatchError('array', typeof arr, 'FIRST');
636
+ }
637
+ return arr.length > 0 ? arr[0] : null;
638
+ },
639
+ };
640
+
641
+ const LAST: FunctionDefinition = {
642
+ name: 'LAST',
643
+ minArgs: 1,
644
+ maxArgs: 1,
645
+ returnType: 'any',
646
+ description: 'Last element of array',
647
+ implementation: (args) => {
648
+ const arr = args[0];
649
+ if (!Array.isArray(arr)) {
650
+ throw new TypeMismatchError('array', typeof arr, 'LAST');
651
+ }
652
+ return arr.length > 0 ? arr[arr.length - 1] : null;
653
+ },
654
+ };
655
+
656
+ const REVERSE: FunctionDefinition = {
657
+ name: 'REVERSE',
658
+ minArgs: 1,
659
+ maxArgs: 1,
660
+ returnType: 'array',
661
+ description: 'Reverse array',
662
+ implementation: (args) => {
663
+ const arr = args[0];
664
+ if (!Array.isArray(arr)) {
665
+ throw new TypeMismatchError('array', typeof arr, 'REVERSE');
666
+ }
667
+ return [...arr].reverse();
668
+ },
669
+ };
670
+
671
+ const SLICE: FunctionDefinition = {
672
+ name: 'SLICE',
673
+ minArgs: 2,
674
+ maxArgs: 3,
675
+ returnType: 'array',
676
+ description: 'Slice array',
677
+ implementation: (args) => {
678
+ const arr = args[0];
679
+ if (!Array.isArray(arr)) {
680
+ throw new TypeMismatchError('array', typeof arr, 'SLICE');
681
+ }
682
+ const start = toNumber(args[1]);
683
+ const end = args.length > 2 ? toNumber(args[2]) : undefined;
684
+ return arr.slice(start, end);
685
+ },
686
+ };
687
+
688
+ const INCLUDES: FunctionDefinition = {
689
+ name: 'INCLUDES',
690
+ minArgs: 2,
691
+ maxArgs: 2,
692
+ returnType: 'boolean',
693
+ description: 'Check if array includes value',
694
+ implementation: (args) => {
695
+ const arr = args[0];
696
+ if (!Array.isArray(arr)) {
697
+ throw new TypeMismatchError('array', typeof arr, 'INCLUDES');
698
+ }
699
+ const value = args[1];
700
+ return arr.some(item => {
701
+ if (item instanceof Decimal && value instanceof Decimal) {
702
+ return item.equals(value);
703
+ }
704
+ return item === value;
705
+ });
706
+ },
707
+ };
708
+
709
+ const INDEXOF: FunctionDefinition = {
710
+ name: 'INDEXOF',
711
+ minArgs: 2,
712
+ maxArgs: 2,
713
+ returnType: 'number',
714
+ description: 'Find index of value in array',
715
+ implementation: (args) => {
716
+ const arr = args[0];
717
+ if (!Array.isArray(arr)) {
718
+ throw new TypeMismatchError('array', typeof arr, 'INDEXOF');
719
+ }
720
+ const value = args[1];
721
+ for (let i = 0; i < arr.length; i++) {
722
+ const item = arr[i];
723
+ if (item instanceof Decimal && value instanceof Decimal) {
724
+ if (item.equals(value)) return i;
725
+ } else if (item === value) {
726
+ return i;
727
+ }
728
+ }
729
+ return -1;
730
+ },
731
+ };
732
+
733
+ const FLATTEN: FunctionDefinition = {
734
+ name: 'FLATTEN',
735
+ minArgs: 1,
736
+ maxArgs: 2,
737
+ returnType: 'array',
738
+ description: 'Flatten nested array',
739
+ implementation: (args) => {
740
+ const arr = args[0];
741
+ if (!Array.isArray(arr)) {
742
+ throw new TypeMismatchError('array', typeof arr, 'FLATTEN');
743
+ }
744
+ const depth = args.length > 1 ? toNumber(args[1]) : 1;
745
+ return arr.flat(depth);
746
+ },
747
+ };
748
+
749
+ // Register all functions
750
+ const allFunctions = [
751
+ // Math
752
+ ABS, ROUND, FLOOR, CEIL, TRUNCATE, MIN, MAX, POW, SQRT, LOG, LOG10, SIGN, DECIMAL, SCALE, PRECISION, DIVIDE,
753
+ // Aggregation
754
+ SUM, AVG, COUNT, PRODUCT, FILTER, MAP,
755
+ // String
756
+ LEN, UPPER, LOWER, TRIM, CONCAT, SUBSTR, REPLACE, CONTAINS, STARTSWITH, ENDSWITH,
757
+ // Logical
758
+ IF, COALESCE, ISNULL, ISEMPTY, DEFAULT, AND, OR, NOT,
759
+ // Type
760
+ NUMBER, STRING, BOOLEAN, TYPEOF,
761
+ // Array
762
+ FIRST, LAST, REVERSE, SLICE, INCLUDES, INDEXOF, FLATTEN,
763
+ ];
764
+
765
+ for (const fn of allFunctions) {
766
+ functions.set(fn.name, fn);
767
+ }
768
+
769
+ return functions;
770
+ }