@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
package/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # Formula Engine
2
+
3
+ A configuration-driven expression evaluation system with automatic dependency resolution and arbitrary-precision decimal arithmetic.
4
+
5
+ ## Features
6
+
7
+ - **Expression Parsing**: Parse mathematical and logical expressions into an AST
8
+ - **Automatic Dependency Resolution**: Extracts dependencies from expressions and evaluates formulas in correct order
9
+ - **Circular Dependency Detection**: Fails fast with helpful error messages when cycles are detected
10
+ - **Decimal Precision**: Uses arbitrary-precision arithmetic to avoid floating-point errors (e.g., `0.1 + 0.2 = 0.3`)
11
+ - **40+ Built-in Functions**: Math, string, logical, aggregation, and type functions
12
+ - **Custom Functions**: Register your own functions
13
+ - **Caching**: AST and dependency caching for improved performance
14
+ - **Type Safety**: Full TypeScript support with comprehensive type definitions
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { FormulaEngine } from 'formula-engine';
26
+
27
+ const engine = new FormulaEngine();
28
+
29
+ // Evaluate a simple expression
30
+ const result = engine.evaluate('$price * $quantity', {
31
+ variables: { price: 19.99, quantity: 3 }
32
+ });
33
+
34
+ console.log(result.value.toString()); // "59.97"
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Single Expression Evaluation
40
+
41
+ ```typescript
42
+ const engine = new FormulaEngine();
43
+
44
+ // Arithmetic
45
+ engine.evaluate('$a + $b * 2', { variables: { a: 10, b: 5 } });
46
+ // Result: 20
47
+
48
+ // Comparison
49
+ engine.evaluate('$score >= 90', { variables: { score: 85 } });
50
+ // Result: false
51
+
52
+ // Conditional (ternary)
53
+ engine.evaluate('$quantity > 10 ? $price * 0.9 : $price', {
54
+ variables: { quantity: 15, price: 100 }
55
+ });
56
+ // Result: 90
57
+
58
+ // Function calls
59
+ engine.evaluate('ROUND($price * 1.19, 2)', { variables: { price: 99.99 } });
60
+ // Result: 118.99
61
+ ```
62
+
63
+ ### Batch Evaluation with Dependencies
64
+
65
+ The engine automatically determines the correct evaluation order:
66
+
67
+ ```typescript
68
+ const formulas = [
69
+ { id: 'gross', expression: '$unitPrice * $quantity' },
70
+ { id: 'discount', expression: '$gross * $discountRate' },
71
+ { id: 'net', expression: '$gross - $discount' },
72
+ { id: 'tax', expression: '$net * $taxRate' },
73
+ { id: 'total', expression: '$net + $tax' },
74
+ ];
75
+
76
+ const context = {
77
+ variables: {
78
+ unitPrice: 100,
79
+ quantity: 5,
80
+ discountRate: 0.1,
81
+ taxRate: 0.2,
82
+ }
83
+ };
84
+
85
+ const results = engine.evaluateAll(formulas, context);
86
+
87
+ // Evaluation order: gross → discount → net → tax → total
88
+ console.log(results.results.get('total')?.value.toString()); // "540"
89
+ ```
90
+
91
+ ### Decimal Precision
92
+
93
+ JavaScript floating-point math has precision issues:
94
+
95
+ ```javascript
96
+ // Native JavaScript
97
+ 0.1 + 0.2 // 0.30000000000000004 ❌
98
+
99
+ // Formula Engine
100
+ engine.evaluate('0.1 + 0.2', { variables: {} });
101
+ // Result: "0.3" ✓
102
+ ```
103
+
104
+ ### Context Variables
105
+
106
+ Use `$` for local variables and `@` for context variables:
107
+
108
+ ```typescript
109
+ engine.evaluate('$price * (1 + @taxRate)', {
110
+ variables: { price: 100 },
111
+ extra: { taxRate: 0.19 }
112
+ });
113
+ // Result: 119
114
+ ```
115
+
116
+ ### Member and Index Access
117
+
118
+ ```typescript
119
+ // Dot notation
120
+ engine.evaluate('$product.price * $product.quantity', {
121
+ variables: {
122
+ product: { price: 25, quantity: 4 }
123
+ }
124
+ });
125
+
126
+ // Bracket notation
127
+ engine.evaluate('$items[0].name', {
128
+ variables: {
129
+ items: [{ name: 'Widget' }, { name: 'Gadget' }]
130
+ }
131
+ });
132
+ ```
133
+
134
+ ### Array Functions
135
+
136
+ ```typescript
137
+ // SUM with expression
138
+ engine.evaluate('SUM($items, $it.price * $it.qty)', {
139
+ variables: {
140
+ items: [
141
+ { price: 10, qty: 2 },
142
+ { price: 20, qty: 1 },
143
+ ]
144
+ }
145
+ });
146
+ // Result: 40
147
+
148
+ // FILTER
149
+ engine.evaluate('FILTER($numbers, $it > 5)', {
150
+ variables: { numbers: [1, 3, 7, 9, 2] }
151
+ });
152
+ // Result: [7, 9]
153
+
154
+ // MAP
155
+ engine.evaluate('MAP($prices, $it * 1.1)', {
156
+ variables: { prices: [100, 200, 300] }
157
+ });
158
+ // Result: [110, 220, 330]
159
+ ```
160
+
161
+ ### Custom Functions
162
+
163
+ ```typescript
164
+ engine.registerFunction({
165
+ name: 'DISCOUNT_TIER',
166
+ minArgs: 2,
167
+ maxArgs: 2,
168
+ returnType: 'decimal',
169
+ implementation: (args) => {
170
+ const [amount, tiers] = args;
171
+ const tier = tiers
172
+ .filter(t => amount >= t.threshold)
173
+ .sort((a, b) => b.threshold - a.threshold)[0];
174
+ return tier ? amount * tier.rate : 0;
175
+ }
176
+ });
177
+
178
+ engine.evaluate('DISCOUNT_TIER($total, @tiers)', {
179
+ variables: { total: 150 },
180
+ extra: {
181
+ tiers: [
182
+ { threshold: 0, rate: 0 },
183
+ { threshold: 100, rate: 0.05 },
184
+ { threshold: 200, rate: 0.10 },
185
+ ]
186
+ }
187
+ });
188
+ // Result: 7.5 (150 * 0.05)
189
+ ```
190
+
191
+ ### Validation
192
+
193
+ Validate formulas before evaluation:
194
+
195
+ ```typescript
196
+ const formulas = [
197
+ { id: 'a', expression: '$b + 1' },
198
+ { id: 'b', expression: '$a + 1' }, // Circular!
199
+ ];
200
+
201
+ const validation = engine.validate(formulas);
202
+
203
+ if (!validation.valid) {
204
+ console.log(validation.errors);
205
+ // CircularDependencyError: Circular dependency detected: a → b → a
206
+ }
207
+ ```
208
+
209
+ ## Built-in Functions
210
+
211
+ ### Math Functions
212
+
213
+ | Function | Description | Example |
214
+ |----------|-------------|---------|
215
+ | `ABS(x)` | Absolute value | `ABS(-5)` → `5` |
216
+ | `ROUND(x, p?)` | Round to precision | `ROUND(3.456, 2)` → `3.46` |
217
+ | `FLOOR(x, p?)` | Round down | `FLOOR(3.9)` → `3` |
218
+ | `CEIL(x, p?)` | Round up | `CEIL(3.1)` → `4` |
219
+ | `TRUNCATE(x, p?)` | Truncate decimals | `TRUNCATE(3.999, 2)` → `3.99` |
220
+ | `MIN(a, b, ...)` | Minimum value | `MIN(5, 3, 8)` → `3` |
221
+ | `MAX(a, b, ...)` | Maximum value | `MAX(5, 3, 8)` → `8` |
222
+ | `POW(x, y)` | Power | `POW(2, 3)` → `8` |
223
+ | `SQRT(x)` | Square root | `SQRT(16)` → `4` |
224
+ | `LOG(x)` | Natural logarithm | `LOG(10)` → `2.302...` |
225
+ | `LOG10(x)` | Base-10 logarithm | `LOG10(100)` → `2` |
226
+ | `SIGN(x)` | Sign (-1, 0, 1) | `SIGN(-5)` → `-1` |
227
+
228
+ ### String Functions
229
+
230
+ | Function | Description | Example |
231
+ |----------|-------------|---------|
232
+ | `LEN(s)` | String length | `LEN("hello")` → `5` |
233
+ | `UPPER(s)` | Uppercase | `UPPER("hello")` → `"HELLO"` |
234
+ | `LOWER(s)` | Lowercase | `LOWER("HELLO")` → `"hello"` |
235
+ | `TRIM(s)` | Trim whitespace | `TRIM(" hi ")` → `"hi"` |
236
+ | `CONCAT(a, b, ...)` | Concatenate | `CONCAT("a", "b")` → `"ab"` |
237
+ | `SUBSTR(s, start, len?)` | Substring | `SUBSTR("hello", 1, 3)` → `"ell"` |
238
+ | `REPLACE(s, find, rep)` | Replace all | `REPLACE("aaa", "a", "b")` → `"bbb"` |
239
+ | `CONTAINS(s, sub)` | Contains check | `CONTAINS("hello", "ell")` → `true` |
240
+ | `STARTSWITH(s, pre)` | Starts with | `STARTSWITH("hello", "he")` → `true` |
241
+ | `ENDSWITH(s, suf)` | Ends with | `ENDSWITH("hello", "lo")` → `true` |
242
+
243
+ ### Logical Functions
244
+
245
+ | Function | Description | Example |
246
+ |----------|-------------|---------|
247
+ | `IF(cond, t, f)` | Conditional | `IF(true, "yes", "no")` → `"yes"` |
248
+ | `COALESCE(a, b, ...)` | First non-null | `COALESCE(null, 5)` → `5` |
249
+ | `ISNULL(x)` | Check null | `ISNULL(null)` → `true` |
250
+ | `ISEMPTY(x)` | Check empty | `ISEMPTY([])` → `true` |
251
+ | `DEFAULT(x, d)` | Default if null | `DEFAULT(null, 0)` → `0` |
252
+ | `AND(a, b, ...)` | Logical AND | `AND(true, false)` → `false` |
253
+ | `OR(a, b, ...)` | Logical OR | `OR(true, false)` → `true` |
254
+ | `NOT(x)` | Logical NOT | `NOT(true)` → `false` |
255
+
256
+ ### Aggregation Functions
257
+
258
+ | Function | Description | Example |
259
+ |----------|-------------|---------|
260
+ | `SUM(arr)` | Sum of array | `SUM([1, 2, 3])` → `6` |
261
+ | `SUM(arr, expr)` | Sum with expression | `SUM($items, $it.price)` |
262
+ | `AVG(arr)` | Average | `AVG([10, 20, 30])` → `20` |
263
+ | `COUNT(arr)` | Count elements | `COUNT([1, 2, 3])` → `3` |
264
+ | `PRODUCT(arr)` | Product of values | `PRODUCT([2, 3, 4])` → `24` |
265
+ | `FILTER(arr, cond)` | Filter array | `FILTER($arr, $it > 5)` |
266
+ | `MAP(arr, expr)` | Transform array | `MAP($arr, $it * 2)` |
267
+ | `FIRST(arr)` | First element | `FIRST([1, 2, 3])` → `1` |
268
+ | `LAST(arr)` | Last element | `LAST([1, 2, 3])` → `3` |
269
+
270
+ ### Type Functions
271
+
272
+ | Function | Description | Example |
273
+ |----------|-------------|---------|
274
+ | `NUMBER(x)` | Convert to number | `NUMBER("42")` → `42` |
275
+ | `STRING(x)` | Convert to string | `STRING(42)` → `"42"` |
276
+ | `BOOLEAN(x)` | Convert to boolean | `BOOLEAN(1)` → `true` |
277
+ | `TYPEOF(x)` | Get type name | `TYPEOF(42)` → `"decimal"` |
278
+
279
+ ## Configuration
280
+
281
+ ```typescript
282
+ const engine = new FormulaEngine({
283
+ // Enable expression caching (default: true)
284
+ enableCache: true,
285
+
286
+ // Maximum cache size (default: 1000)
287
+ maxCacheSize: 1000,
288
+
289
+ // Strict mode - fail on undefined variables (default: true)
290
+ strictMode: true,
291
+
292
+ // Decimal configuration
293
+ decimal: {
294
+ precision: 20, // Significant digits
295
+ roundingMode: 'HALF_UP', // Rounding mode
296
+ divisionScale: 10, // Decimal places for division
297
+ },
298
+
299
+ // Security limits
300
+ security: {
301
+ maxExpressionLength: 10000,
302
+ maxRecursionDepth: 100,
303
+ maxIterations: 10000,
304
+ },
305
+ });
306
+ ```
307
+
308
+ ## Error Handling
309
+
310
+ ```typescript
311
+ const result = engine.evaluate('$a / $b', {
312
+ variables: { a: 10, b: 0 }
313
+ });
314
+
315
+ if (!result.success) {
316
+ console.log(result.error); // DivisionByZeroError
317
+ }
318
+ ```
319
+
320
+ ### Error Types
321
+
322
+ - **Parse Errors**: `SyntaxError`, `UnexpectedTokenError`, `UnterminatedStringError`
323
+ - **Validation Errors**: `CircularDependencyError`, `UndefinedVariableError`, `UndefinedFunctionError`
324
+ - **Evaluation Errors**: `DivisionByZeroError`, `TypeMismatchError`, `ArgumentCountError`
325
+
326
+ ## API Reference
327
+
328
+ ### FormulaEngine
329
+
330
+ ```typescript
331
+ class FormulaEngine {
332
+ constructor(config?: FormulaEngineConfig);
333
+
334
+ // Parse expression to AST
335
+ parse(expression: string): ASTNode;
336
+
337
+ // Extract dependencies from expression
338
+ extractDependencies(expression: string): Set<string>;
339
+
340
+ // Build dependency graph
341
+ buildDependencyGraph(formulas: FormulaDefinition[]): DependencyGraph;
342
+
343
+ // Get evaluation order
344
+ getEvaluationOrder(formulas: FormulaDefinition[]): string[];
345
+
346
+ // Validate formulas
347
+ validate(formulas: FormulaDefinition[]): ValidationResult;
348
+
349
+ // Evaluate single expression
350
+ evaluate(expression: string, context: EvaluationContext): EvaluationResult;
351
+
352
+ // Evaluate all formulas in order
353
+ evaluateAll(formulas: FormulaDefinition[], context: EvaluationContext): EvaluationResultSet;
354
+
355
+ // Register custom function
356
+ registerFunction(definition: FunctionDefinition): void;
357
+
358
+ // Cache management
359
+ clearCache(): void;
360
+ getCacheStats(): CacheStats;
361
+ }
362
+ ```
363
+
364
+ ## Development
365
+
366
+ ```bash
367
+ # Run tests
368
+ npm test
369
+
370
+ # Run tests with coverage
371
+ npm run test:coverage
372
+
373
+ # Build
374
+ npm run build
375
+
376
+ # Lint
377
+ npm run lint
378
+ ```
379
+
380
+ ## License
381
+
382
+ MIT
@@ -0,0 +1,180 @@
1
+ import Decimal from 'decimal.js';
2
+ import { DecimalConfig, DecimalRoundingMode } from './types';
3
+ export type DecimalLike = Decimal | string | number | bigint;
4
+ export interface DecimalUtilsConfig {
5
+ precision: number;
6
+ roundingMode: DecimalRoundingMode;
7
+ divisionScale: number;
8
+ maxExponent: number;
9
+ minExponent: number;
10
+ }
11
+ export declare class DecimalUtils {
12
+ private config;
13
+ constructor(config?: Partial<DecimalConfig>);
14
+ /**
15
+ * Create a Decimal from various input types
16
+ */
17
+ from(value: DecimalLike): Decimal;
18
+ /**
19
+ * Check if a value is a Decimal
20
+ */
21
+ isDecimal(value: unknown): value is Decimal;
22
+ /**
23
+ * Convert a value to Decimal if it's numeric
24
+ */
25
+ toDecimal(value: unknown): Decimal | unknown;
26
+ /**
27
+ * Addition
28
+ */
29
+ add(a: DecimalLike, b: DecimalLike): Decimal;
30
+ /**
31
+ * Subtraction
32
+ */
33
+ subtract(a: DecimalLike, b: DecimalLike): Decimal;
34
+ /**
35
+ * Multiplication
36
+ */
37
+ multiply(a: DecimalLike, b: DecimalLike): Decimal;
38
+ /**
39
+ * Division with scale
40
+ */
41
+ divide(a: DecimalLike, b: DecimalLike, scale?: number, roundingMode?: DecimalRoundingMode): Decimal;
42
+ /**
43
+ * Modulo
44
+ */
45
+ modulo(a: DecimalLike, b: DecimalLike): Decimal;
46
+ /**
47
+ * Power
48
+ */
49
+ power(base: DecimalLike, exponent: number): Decimal;
50
+ /**
51
+ * Negation
52
+ */
53
+ negate(value: DecimalLike): Decimal;
54
+ /**
55
+ * Absolute value
56
+ */
57
+ abs(value: DecimalLike): Decimal;
58
+ /**
59
+ * Round to specified decimal places
60
+ */
61
+ round(value: DecimalLike, scale: number, roundingMode?: DecimalRoundingMode): Decimal;
62
+ /**
63
+ * Floor to specified decimal places
64
+ */
65
+ floor(value: DecimalLike, scale?: number): Decimal;
66
+ /**
67
+ * Ceiling to specified decimal places
68
+ */
69
+ ceil(value: DecimalLike, scale?: number): Decimal;
70
+ /**
71
+ * Truncate to specified decimal places
72
+ */
73
+ truncate(value: DecimalLike, scale?: number): Decimal;
74
+ /**
75
+ * Square root
76
+ */
77
+ sqrt(value: DecimalLike): Decimal;
78
+ /**
79
+ * Natural logarithm
80
+ */
81
+ ln(value: DecimalLike): Decimal;
82
+ /**
83
+ * Base-10 logarithm
84
+ */
85
+ log10(value: DecimalLike): Decimal;
86
+ /**
87
+ * Comparison: returns -1, 0, or 1
88
+ */
89
+ compare(a: DecimalLike, b: DecimalLike): -1 | 0 | 1;
90
+ /**
91
+ * Equality check
92
+ */
93
+ equals(a: DecimalLike, b: DecimalLike): boolean;
94
+ /**
95
+ * Greater than
96
+ */
97
+ greaterThan(a: DecimalLike, b: DecimalLike): boolean;
98
+ /**
99
+ * Greater than or equal
100
+ */
101
+ greaterThanOrEqual(a: DecimalLike, b: DecimalLike): boolean;
102
+ /**
103
+ * Less than
104
+ */
105
+ lessThan(a: DecimalLike, b: DecimalLike): boolean;
106
+ /**
107
+ * Less than or equal
108
+ */
109
+ lessThanOrEqual(a: DecimalLike, b: DecimalLike): boolean;
110
+ /**
111
+ * Check if zero
112
+ */
113
+ isZero(value: DecimalLike): boolean;
114
+ /**
115
+ * Check if positive
116
+ */
117
+ isPositive(value: DecimalLike): boolean;
118
+ /**
119
+ * Check if negative
120
+ */
121
+ isNegative(value: DecimalLike): boolean;
122
+ /**
123
+ * Check if integer
124
+ */
125
+ isInteger(value: DecimalLike): boolean;
126
+ /**
127
+ * Get sign: -1, 0, or 1
128
+ */
129
+ sign(value: DecimalLike): -1 | 0 | 1;
130
+ /**
131
+ * Get precision (total significant digits)
132
+ */
133
+ precision(value: DecimalLike): number;
134
+ /**
135
+ * Get scale (decimal places)
136
+ */
137
+ scale(value: DecimalLike): number;
138
+ /**
139
+ * Convert to JavaScript number (may lose precision)
140
+ */
141
+ toNumber(value: DecimalLike): number;
142
+ /**
143
+ * Convert to string
144
+ */
145
+ toString(value: DecimalLike): string;
146
+ /**
147
+ * Convert to fixed decimal places string
148
+ */
149
+ toFixed(value: DecimalLike, scale: number): string;
150
+ /**
151
+ * Minimum of values
152
+ */
153
+ min(...values: DecimalLike[]): Decimal;
154
+ /**
155
+ * Maximum of values
156
+ */
157
+ max(...values: DecimalLike[]): Decimal;
158
+ /**
159
+ * Sum of values
160
+ */
161
+ sum(values: DecimalLike[]): Decimal;
162
+ /**
163
+ * Average of values
164
+ */
165
+ avg(values: DecimalLike[]): Decimal;
166
+ /**
167
+ * Product of values
168
+ */
169
+ product(values: DecimalLike[]): Decimal;
170
+ /**
171
+ * Create zero
172
+ */
173
+ zero(): Decimal;
174
+ /**
175
+ * Create one
176
+ */
177
+ one(): Decimal;
178
+ }
179
+ export declare const decimalUtils: DecimalUtils;
180
+ export { Decimal };