@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.
- package/.claude/settings.local.json +6 -0
- package/PRD_FORMULA_ENGINE.md +1863 -0
- package/README.md +382 -0
- package/dist/decimal-utils.d.ts +180 -0
- package/dist/decimal-utils.js +355 -0
- package/dist/dependency-extractor.d.ts +20 -0
- package/dist/dependency-extractor.js +103 -0
- package/dist/dependency-graph.d.ts +60 -0
- package/dist/dependency-graph.js +252 -0
- package/dist/errors.d.ts +161 -0
- package/dist/errors.js +260 -0
- package/dist/evaluator.d.ts +51 -0
- package/dist/evaluator.js +494 -0
- package/dist/formula-engine.d.ts +79 -0
- package/dist/formula-engine.js +355 -0
- package/dist/functions.d.ts +3 -0
- package/dist/functions.js +720 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +61 -0
- package/dist/lexer.d.ts +25 -0
- package/dist/lexer.js +357 -0
- package/dist/parser.d.ts +32 -0
- package/dist/parser.js +372 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.js +62 -0
- package/jest.config.js +23 -0
- package/package.json +35 -0
- package/src/decimal-utils.ts +408 -0
- package/src/dependency-extractor.ts +117 -0
- package/src/dependency-graph.test.ts +238 -0
- package/src/dependency-graph.ts +288 -0
- package/src/errors.ts +296 -0
- package/src/evaluator.ts +604 -0
- package/src/formula-engine.test.ts +660 -0
- package/src/formula-engine.ts +430 -0
- package/src/functions.ts +770 -0
- package/src/index.ts +103 -0
- package/src/lexer.test.ts +288 -0
- package/src/lexer.ts +394 -0
- package/src/parser.test.ts +349 -0
- package/src/parser.ts +449 -0
- package/src/types.ts +347 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Parser } from './parser';
|
|
2
|
+
import { UnexpectedTokenError } from './errors';
|
|
3
|
+
|
|
4
|
+
describe('Parser', () => {
|
|
5
|
+
let parser: Parser;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
parser = new Parser();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('Literals', () => {
|
|
12
|
+
it('should parse decimal literals', () => {
|
|
13
|
+
const ast = parser.parse('42');
|
|
14
|
+
|
|
15
|
+
expect(ast.type).toBe('DecimalLiteral');
|
|
16
|
+
expect((ast as any).value).toBe('42');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should parse string literals', () => {
|
|
20
|
+
const ast = parser.parse('"hello"');
|
|
21
|
+
|
|
22
|
+
expect(ast.type).toBe('StringLiteral');
|
|
23
|
+
expect((ast as any).value).toBe('hello');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should parse boolean literals', () => {
|
|
27
|
+
expect(parser.parse('true').type).toBe('BooleanLiteral');
|
|
28
|
+
expect((parser.parse('true') as any).value).toBe(true);
|
|
29
|
+
expect((parser.parse('false') as any).value).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should parse null literal', () => {
|
|
33
|
+
const ast = parser.parse('null');
|
|
34
|
+
|
|
35
|
+
expect(ast.type).toBe('NullLiteral');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should parse array literals', () => {
|
|
39
|
+
const ast = parser.parse('[1, 2, 3]');
|
|
40
|
+
|
|
41
|
+
expect(ast.type).toBe('ArrayLiteral');
|
|
42
|
+
expect((ast as any).elements).toHaveLength(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should parse empty arrays', () => {
|
|
46
|
+
const ast = parser.parse('[]');
|
|
47
|
+
|
|
48
|
+
expect(ast.type).toBe('ArrayLiteral');
|
|
49
|
+
expect((ast as any).elements).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('Variables', () => {
|
|
54
|
+
it('should parse $ variables', () => {
|
|
55
|
+
const ast = parser.parse('$price');
|
|
56
|
+
|
|
57
|
+
expect(ast.type).toBe('VariableReference');
|
|
58
|
+
expect((ast as any).prefix).toBe('$');
|
|
59
|
+
expect((ast as any).name).toBe('price');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should parse @ context variables', () => {
|
|
63
|
+
const ast = parser.parse('@userId');
|
|
64
|
+
|
|
65
|
+
expect(ast.type).toBe('VariableReference');
|
|
66
|
+
expect((ast as any).prefix).toBe('@');
|
|
67
|
+
expect((ast as any).name).toBe('userId');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Binary Operations', () => {
|
|
72
|
+
it('should parse addition', () => {
|
|
73
|
+
const ast = parser.parse('$a + $b');
|
|
74
|
+
|
|
75
|
+
expect(ast.type).toBe('BinaryOperation');
|
|
76
|
+
expect((ast as any).operator).toBe('+');
|
|
77
|
+
expect((ast as any).left.name).toBe('a');
|
|
78
|
+
expect((ast as any).right.name).toBe('b');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should parse subtraction', () => {
|
|
82
|
+
const ast = parser.parse('$a - $b');
|
|
83
|
+
|
|
84
|
+
expect((ast as any).operator).toBe('-');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should parse multiplication', () => {
|
|
88
|
+
const ast = parser.parse('$a * $b');
|
|
89
|
+
|
|
90
|
+
expect((ast as any).operator).toBe('*');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should parse division', () => {
|
|
94
|
+
const ast = parser.parse('$a / $b');
|
|
95
|
+
|
|
96
|
+
expect((ast as any).operator).toBe('/');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should parse modulo', () => {
|
|
100
|
+
const ast = parser.parse('$a % $b');
|
|
101
|
+
|
|
102
|
+
expect((ast as any).operator).toBe('%');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should parse power', () => {
|
|
106
|
+
const ast = parser.parse('$a ^ $b');
|
|
107
|
+
|
|
108
|
+
expect((ast as any).operator).toBe('^');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should respect operator precedence (mul before add)', () => {
|
|
112
|
+
const ast = parser.parse('$a + $b * $c');
|
|
113
|
+
|
|
114
|
+
expect(ast.type).toBe('BinaryOperation');
|
|
115
|
+
expect((ast as any).operator).toBe('+');
|
|
116
|
+
expect((ast as any).right.operator).toBe('*');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should respect operator precedence (power before mul)', () => {
|
|
120
|
+
const ast = parser.parse('$a * $b ^ $c');
|
|
121
|
+
|
|
122
|
+
expect(ast.type).toBe('BinaryOperation');
|
|
123
|
+
expect((ast as any).operator).toBe('*');
|
|
124
|
+
expect((ast as any).right.operator).toBe('^');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle parentheses', () => {
|
|
128
|
+
const ast = parser.parse('($a + $b) * $c');
|
|
129
|
+
|
|
130
|
+
expect((ast as any).operator).toBe('*');
|
|
131
|
+
expect((ast as any).left.operator).toBe('+');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('Comparison Operations', () => {
|
|
136
|
+
it('should parse equality', () => {
|
|
137
|
+
const ast = parser.parse('$a == $b');
|
|
138
|
+
|
|
139
|
+
expect((ast as any).operator).toBe('==');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should parse inequality', () => {
|
|
143
|
+
const ast = parser.parse('$a != $b');
|
|
144
|
+
|
|
145
|
+
expect((ast as any).operator).toBe('!=');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should parse less than', () => {
|
|
149
|
+
const ast = parser.parse('$a < $b');
|
|
150
|
+
|
|
151
|
+
expect((ast as any).operator).toBe('<');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should parse greater than', () => {
|
|
155
|
+
const ast = parser.parse('$a > $b');
|
|
156
|
+
|
|
157
|
+
expect((ast as any).operator).toBe('>');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should parse less than or equal', () => {
|
|
161
|
+
const ast = parser.parse('$a <= $b');
|
|
162
|
+
|
|
163
|
+
expect((ast as any).operator).toBe('<=');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should parse greater than or equal', () => {
|
|
167
|
+
const ast = parser.parse('$a >= $b');
|
|
168
|
+
|
|
169
|
+
expect((ast as any).operator).toBe('>=');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Logical Operations', () => {
|
|
174
|
+
it('should parse AND', () => {
|
|
175
|
+
const ast = parser.parse('$a && $b');
|
|
176
|
+
|
|
177
|
+
expect((ast as any).operator).toBe('&&');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should parse OR', () => {
|
|
181
|
+
const ast = parser.parse('$a || $b');
|
|
182
|
+
|
|
183
|
+
expect((ast as any).operator).toBe('||');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should parse NOT', () => {
|
|
187
|
+
const ast = parser.parse('!$a');
|
|
188
|
+
|
|
189
|
+
expect(ast.type).toBe('UnaryOperation');
|
|
190
|
+
expect((ast as any).operator).toBe('!');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should respect logical operator precedence (AND before OR)', () => {
|
|
194
|
+
const ast = parser.parse('$a || $b && $c');
|
|
195
|
+
|
|
196
|
+
expect((ast as any).operator).toBe('||');
|
|
197
|
+
expect((ast as any).right.operator).toBe('&&');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Unary Operations', () => {
|
|
202
|
+
it('should parse negation', () => {
|
|
203
|
+
const ast = parser.parse('-$a');
|
|
204
|
+
|
|
205
|
+
expect(ast.type).toBe('UnaryOperation');
|
|
206
|
+
expect((ast as any).operator).toBe('-');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should parse double negation', () => {
|
|
210
|
+
const ast = parser.parse('--$a');
|
|
211
|
+
|
|
212
|
+
expect(ast.type).toBe('UnaryOperation');
|
|
213
|
+
expect((ast as any).operand.type).toBe('UnaryOperation');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('Conditional Expressions', () => {
|
|
218
|
+
it('should parse ternary operator', () => {
|
|
219
|
+
const ast = parser.parse('$a > 0 ? $b : $c');
|
|
220
|
+
|
|
221
|
+
expect(ast.type).toBe('ConditionalExpression');
|
|
222
|
+
expect((ast as any).condition.operator).toBe('>');
|
|
223
|
+
expect((ast as any).consequent.name).toBe('b');
|
|
224
|
+
expect((ast as any).alternate.name).toBe('c');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should parse nested ternary', () => {
|
|
228
|
+
const ast = parser.parse('$a ? $b ? $c : $d : $e');
|
|
229
|
+
|
|
230
|
+
expect(ast.type).toBe('ConditionalExpression');
|
|
231
|
+
expect((ast as any).consequent.type).toBe('ConditionalExpression');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('Function Calls', () => {
|
|
236
|
+
it('should parse function with no arguments', () => {
|
|
237
|
+
// Note: Our parser requires variables to have prefix, so we can't call functions with no args easily
|
|
238
|
+
// Let's test with a simple case
|
|
239
|
+
const ast = parser.parse('MAX($a, $b)');
|
|
240
|
+
|
|
241
|
+
expect(ast.type).toBe('FunctionCall');
|
|
242
|
+
expect((ast as any).name).toBe('MAX');
|
|
243
|
+
expect((ast as any).arguments).toHaveLength(2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should parse function with single argument', () => {
|
|
247
|
+
const ast = parser.parse('ABS($x)');
|
|
248
|
+
|
|
249
|
+
expect(ast.type).toBe('FunctionCall');
|
|
250
|
+
expect((ast as any).arguments).toHaveLength(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should parse nested function calls', () => {
|
|
254
|
+
const ast = parser.parse('MAX(MIN($a, $b), $c)');
|
|
255
|
+
|
|
256
|
+
expect(ast.type).toBe('FunctionCall');
|
|
257
|
+
expect((ast as any).arguments[0].type).toBe('FunctionCall');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should uppercase function names', () => {
|
|
261
|
+
const ast = parser.parse('max($a, $b)');
|
|
262
|
+
|
|
263
|
+
expect((ast as any).name).toBe('MAX');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('Member Access', () => {
|
|
268
|
+
it('should parse dot notation', () => {
|
|
269
|
+
const ast = parser.parse('$product.price');
|
|
270
|
+
|
|
271
|
+
expect(ast.type).toBe('MemberAccess');
|
|
272
|
+
expect((ast as any).object.name).toBe('product');
|
|
273
|
+
expect((ast as any).property).toBe('price');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should parse chained dot notation', () => {
|
|
277
|
+
const ast = parser.parse('$customer.address.city');
|
|
278
|
+
|
|
279
|
+
expect(ast.type).toBe('MemberAccess');
|
|
280
|
+
expect((ast as any).object.type).toBe('MemberAccess');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('Index Access', () => {
|
|
285
|
+
it('should parse bracket notation with number', () => {
|
|
286
|
+
const ast = parser.parse('$items[0]');
|
|
287
|
+
|
|
288
|
+
expect(ast.type).toBe('IndexAccess');
|
|
289
|
+
expect((ast as any).object.name).toBe('items');
|
|
290
|
+
expect((ast as any).index.type).toBe('DecimalLiteral');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should parse bracket notation with string', () => {
|
|
294
|
+
const ast = parser.parse('$data["key"]');
|
|
295
|
+
|
|
296
|
+
expect(ast.type).toBe('IndexAccess');
|
|
297
|
+
expect((ast as any).index.type).toBe('StringLiteral');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should parse bracket notation with variable', () => {
|
|
301
|
+
const ast = parser.parse('$items[$index]');
|
|
302
|
+
|
|
303
|
+
expect(ast.type).toBe('IndexAccess');
|
|
304
|
+
expect((ast as any).index.type).toBe('VariableReference');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should parse mixed access', () => {
|
|
308
|
+
const ast = parser.parse('$items[0].price');
|
|
309
|
+
|
|
310
|
+
expect(ast.type).toBe('MemberAccess');
|
|
311
|
+
expect((ast as any).object.type).toBe('IndexAccess');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('Complex Expressions', () => {
|
|
316
|
+
it('should parse invoice calculation', () => {
|
|
317
|
+
const ast = parser.parse('$unitPrice * $quantity * (1 - $discountRate)');
|
|
318
|
+
|
|
319
|
+
expect(ast.type).toBe('BinaryOperation');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should parse conditional with operations', () => {
|
|
323
|
+
const ast = parser.parse('$quantity > 10 ? $unitPrice * 0.9 : $unitPrice');
|
|
324
|
+
|
|
325
|
+
expect(ast.type).toBe('ConditionalExpression');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should parse function in expression', () => {
|
|
329
|
+
const ast = parser.parse('$total + ROUND($tax, 2)');
|
|
330
|
+
|
|
331
|
+
expect(ast.type).toBe('BinaryOperation');
|
|
332
|
+
expect((ast as any).right.type).toBe('FunctionCall');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('Error Handling', () => {
|
|
337
|
+
it('should throw on unexpected token', () => {
|
|
338
|
+
expect(() => parser.parse('$a +')).toThrow(UnexpectedTokenError);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should throw on unmatched parenthesis', () => {
|
|
342
|
+
expect(() => parser.parse('($a + $b')).toThrow(UnexpectedTokenError);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should throw on invalid ternary', () => {
|
|
346
|
+
expect(() => parser.parse('$a ? $b')).toThrow(UnexpectedTokenError);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|