@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/src/parser.ts ADDED
@@ -0,0 +1,449 @@
1
+ import { Lexer } from './lexer';
2
+ import { Token, TokenType, ASTNode } from './types';
3
+ import { SyntaxError, UnexpectedTokenError } from './errors';
4
+
5
+ // Operator precedence levels (higher = tighter binding)
6
+ const PRECEDENCE = {
7
+ LOWEST: 1,
8
+ TERNARY: 2, // ? :
9
+ OR: 3, // || OR
10
+ AND: 4, // && AND
11
+ EQUALITY: 5, // == !=
12
+ COMPARISON: 6, // < > <= >=
13
+ TERM: 7, // + -
14
+ FACTOR: 8, // * / %
15
+ POWER: 9, // ^
16
+ UNARY: 10, // - ! NOT
17
+ CALL: 11, // function calls
18
+ MEMBER: 12, // . []
19
+ };
20
+
21
+ export class Parser {
22
+ private tokens: Token[] = [];
23
+ private current: number = 0;
24
+ private expression: string = '';
25
+
26
+ parse(expression: string): ASTNode {
27
+ this.expression = expression;
28
+ const lexer = new Lexer(expression);
29
+ this.tokens = lexer.tokenize();
30
+ this.current = 0;
31
+
32
+ const ast = this.parseExpression(PRECEDENCE.LOWEST);
33
+
34
+ if (!this.isAtEnd()) {
35
+ const token = this.peek();
36
+ throw new UnexpectedTokenError(
37
+ String(token.value),
38
+ ['end of expression'],
39
+ token.position
40
+ );
41
+ }
42
+
43
+ return ast;
44
+ }
45
+
46
+ private parseExpression(precedence: number): ASTNode {
47
+ let left = this.parsePrefixExpression();
48
+
49
+ while (!this.isAtEnd() && precedence < this.getPrecedence()) {
50
+ left = this.parseInfixExpression(left);
51
+ }
52
+
53
+ return left;
54
+ }
55
+
56
+ private parsePrefixExpression(): ASTNode {
57
+ const token = this.peek();
58
+
59
+ switch (token.type) {
60
+ case TokenType.NUMBER:
61
+ return this.parseNumber();
62
+ case TokenType.STRING:
63
+ return this.parseString();
64
+ case TokenType.BOOLEAN:
65
+ return this.parseBoolean();
66
+ case TokenType.NULL:
67
+ return this.parseNull();
68
+ case TokenType.VARIABLE:
69
+ return this.parseVariable();
70
+ case TokenType.CONTEXT_VAR:
71
+ return this.parseContextVariable();
72
+ case TokenType.IDENTIFIER:
73
+ return this.parseIdentifierOrFunctionCall();
74
+ case TokenType.LPAREN:
75
+ return this.parseGroupedExpression();
76
+ case TokenType.LBRACKET:
77
+ return this.parseArrayLiteral();
78
+ case TokenType.MINUS:
79
+ case TokenType.NOT:
80
+ return this.parseUnaryExpression();
81
+ default:
82
+ throw new UnexpectedTokenError(
83
+ String(token.value),
84
+ ['number', 'string', 'boolean', 'null', 'variable', 'identifier', '(', '[', '-', '!'],
85
+ token.position
86
+ );
87
+ }
88
+ }
89
+
90
+ private parseInfixExpression(left: ASTNode): ASTNode {
91
+ const token = this.peek();
92
+
93
+ switch (token.type) {
94
+ case TokenType.PLUS:
95
+ case TokenType.MINUS:
96
+ case TokenType.MULTIPLY:
97
+ case TokenType.DIVIDE:
98
+ case TokenType.MODULO:
99
+ case TokenType.POWER:
100
+ case TokenType.EQ:
101
+ case TokenType.NEQ:
102
+ case TokenType.LT:
103
+ case TokenType.GT:
104
+ case TokenType.LTE:
105
+ case TokenType.GTE:
106
+ case TokenType.AND:
107
+ case TokenType.OR:
108
+ return this.parseBinaryExpression(left);
109
+ case TokenType.QUESTION:
110
+ return this.parseTernaryExpression(left);
111
+ case TokenType.DOT:
112
+ return this.parseMemberAccess(left);
113
+ case TokenType.LBRACKET:
114
+ return this.parseIndexAccess(left);
115
+ case TokenType.LPAREN:
116
+ // This handles the case where we have an identifier followed by (
117
+ // But actually this should be handled in parseIdentifierOrFunctionCall
118
+ return left;
119
+ default:
120
+ return left;
121
+ }
122
+ }
123
+
124
+ private parseNumber(): ASTNode {
125
+ const token = this.advance();
126
+ const value = token.value;
127
+
128
+ if (typeof value === 'number') {
129
+ // It's a float
130
+ return {
131
+ type: 'NumberLiteral',
132
+ value: value,
133
+ };
134
+ } else {
135
+ // It's a decimal (stored as string)
136
+ return {
137
+ type: 'DecimalLiteral',
138
+ value: String(value),
139
+ raw: String(value),
140
+ };
141
+ }
142
+ }
143
+
144
+ private parseString(): ASTNode {
145
+ const token = this.advance();
146
+ return {
147
+ type: 'StringLiteral',
148
+ value: String(token.value),
149
+ };
150
+ }
151
+
152
+ private parseBoolean(): ASTNode {
153
+ const token = this.advance();
154
+ return {
155
+ type: 'BooleanLiteral',
156
+ value: token.value === true,
157
+ };
158
+ }
159
+
160
+ private parseNull(): ASTNode {
161
+ this.advance();
162
+ return {
163
+ type: 'NullLiteral',
164
+ };
165
+ }
166
+
167
+ private parseVariable(): ASTNode {
168
+ const token = this.advance();
169
+ return {
170
+ type: 'VariableReference',
171
+ prefix: '$',
172
+ name: String(token.value),
173
+ };
174
+ }
175
+
176
+ private parseContextVariable(): ASTNode {
177
+ const token = this.advance();
178
+ return {
179
+ type: 'VariableReference',
180
+ prefix: '@',
181
+ name: String(token.value),
182
+ };
183
+ }
184
+
185
+ private parseIdentifierOrFunctionCall(): ASTNode {
186
+ const token = this.advance();
187
+ const name = String(token.value);
188
+
189
+ // Check if it's a function call
190
+ if (this.peek().type === TokenType.LPAREN) {
191
+ return this.parseFunctionCall(name);
192
+ }
193
+
194
+ // Otherwise, check for keywords AND/OR/NOT used as standalone identifiers
195
+ const upperName = name.toUpperCase();
196
+ if (upperName === 'AND' || upperName === 'OR') {
197
+ // These should be handled as operators, but if we reach here,
198
+ // it means they were used in an invalid context
199
+ throw new SyntaxError(
200
+ `'${name}' cannot be used as an identifier`,
201
+ token.position,
202
+ token.line,
203
+ token.column,
204
+ this.expression
205
+ );
206
+ }
207
+
208
+ // It's a bare identifier - could be a variable without prefix
209
+ // For now, treat it as an error - require explicit prefix
210
+ throw new SyntaxError(
211
+ `Unknown identifier '${name}'. Variables must be prefixed with $ or @`,
212
+ token.position,
213
+ token.line,
214
+ token.column,
215
+ this.expression
216
+ );
217
+ }
218
+
219
+ private parseFunctionCall(name: string): ASTNode {
220
+ this.advance(); // consume '('
221
+ const args: ASTNode[] = [];
222
+
223
+ if (this.peek().type !== TokenType.RPAREN) {
224
+ do {
225
+ if (this.peek().type === TokenType.COMMA) {
226
+ this.advance();
227
+ }
228
+ args.push(this.parseExpression(PRECEDENCE.LOWEST));
229
+ } while (this.peek().type === TokenType.COMMA);
230
+ }
231
+
232
+ this.expect(TokenType.RPAREN, ')');
233
+
234
+ return {
235
+ type: 'FunctionCall',
236
+ name: name.toUpperCase(), // Functions are case-insensitive
237
+ arguments: args,
238
+ };
239
+ }
240
+
241
+ private parseGroupedExpression(): ASTNode {
242
+ this.advance(); // consume '('
243
+ const expr = this.parseExpression(PRECEDENCE.LOWEST);
244
+ this.expect(TokenType.RPAREN, ')');
245
+ return expr;
246
+ }
247
+
248
+ private parseArrayLiteral(): ASTNode {
249
+ this.advance(); // consume '['
250
+ const elements: ASTNode[] = [];
251
+
252
+ if (this.peek().type !== TokenType.RBRACKET) {
253
+ do {
254
+ if (this.peek().type === TokenType.COMMA) {
255
+ this.advance();
256
+ }
257
+ elements.push(this.parseExpression(PRECEDENCE.LOWEST));
258
+ } while (this.peek().type === TokenType.COMMA);
259
+ }
260
+
261
+ this.expect(TokenType.RBRACKET, ']');
262
+
263
+ return {
264
+ type: 'ArrayLiteral',
265
+ elements,
266
+ };
267
+ }
268
+
269
+ private parseUnaryExpression(): ASTNode {
270
+ const token = this.advance();
271
+ const operator = this.getOperatorSymbol(token.type);
272
+ const operand = this.parseExpression(PRECEDENCE.UNARY);
273
+
274
+ return {
275
+ type: 'UnaryOperation',
276
+ operator,
277
+ operand,
278
+ };
279
+ }
280
+
281
+ private parseBinaryExpression(left: ASTNode): ASTNode {
282
+ const token = this.advance();
283
+ const operator = this.getOperatorSymbol(token.type);
284
+ const precedence = this.getTokenPrecedence(token.type);
285
+
286
+ // Right associativity for power operator
287
+ const nextPrecedence = token.type === TokenType.POWER ? precedence - 1 : precedence;
288
+ const right = this.parseExpression(nextPrecedence);
289
+
290
+ return {
291
+ type: 'BinaryOperation',
292
+ operator,
293
+ left,
294
+ right,
295
+ };
296
+ }
297
+
298
+ private parseTernaryExpression(condition: ASTNode): ASTNode {
299
+ this.advance(); // consume '?'
300
+ const consequent = this.parseExpression(PRECEDENCE.LOWEST);
301
+ this.expect(TokenType.COLON, ':');
302
+ const alternate = this.parseExpression(PRECEDENCE.TERNARY - 1);
303
+
304
+ return {
305
+ type: 'ConditionalExpression',
306
+ condition,
307
+ consequent,
308
+ alternate,
309
+ };
310
+ }
311
+
312
+ private parseMemberAccess(object: ASTNode): ASTNode {
313
+ this.advance(); // consume '.'
314
+ const token = this.peek();
315
+
316
+ if (token.type !== TokenType.IDENTIFIER && token.type !== TokenType.VARIABLE) {
317
+ throw new UnexpectedTokenError(
318
+ String(token.value),
319
+ ['identifier'],
320
+ token.position
321
+ );
322
+ }
323
+
324
+ this.advance();
325
+ const property = String(token.value);
326
+
327
+ const node: ASTNode = {
328
+ type: 'MemberAccess',
329
+ object,
330
+ property,
331
+ };
332
+
333
+ // Check for chained access
334
+ if (this.peek().type === TokenType.DOT) {
335
+ return this.parseMemberAccess(node);
336
+ }
337
+ if (this.peek().type === TokenType.LBRACKET) {
338
+ return this.parseIndexAccess(node);
339
+ }
340
+
341
+ return node;
342
+ }
343
+
344
+ private parseIndexAccess(object: ASTNode): ASTNode {
345
+ this.advance(); // consume '['
346
+ const index = this.parseExpression(PRECEDENCE.LOWEST);
347
+ this.expect(TokenType.RBRACKET, ']');
348
+
349
+ const node: ASTNode = {
350
+ type: 'IndexAccess',
351
+ object,
352
+ index,
353
+ };
354
+
355
+ // Check for chained access
356
+ if (this.peek().type === TokenType.DOT) {
357
+ return this.parseMemberAccess(node);
358
+ }
359
+ if (this.peek().type === TokenType.LBRACKET) {
360
+ return this.parseIndexAccess(node);
361
+ }
362
+
363
+ return node;
364
+ }
365
+
366
+ private getPrecedence(): number {
367
+ return this.getTokenPrecedence(this.peek().type);
368
+ }
369
+
370
+ private getTokenPrecedence(type: TokenType): number {
371
+ switch (type) {
372
+ case TokenType.OR:
373
+ return PRECEDENCE.OR;
374
+ case TokenType.AND:
375
+ return PRECEDENCE.AND;
376
+ case TokenType.EQ:
377
+ case TokenType.NEQ:
378
+ return PRECEDENCE.EQUALITY;
379
+ case TokenType.LT:
380
+ case TokenType.GT:
381
+ case TokenType.LTE:
382
+ case TokenType.GTE:
383
+ return PRECEDENCE.COMPARISON;
384
+ case TokenType.PLUS:
385
+ case TokenType.MINUS:
386
+ return PRECEDENCE.TERM;
387
+ case TokenType.MULTIPLY:
388
+ case TokenType.DIVIDE:
389
+ case TokenType.MODULO:
390
+ return PRECEDENCE.FACTOR;
391
+ case TokenType.POWER:
392
+ return PRECEDENCE.POWER;
393
+ case TokenType.DOT:
394
+ case TokenType.LBRACKET:
395
+ return PRECEDENCE.MEMBER;
396
+ case TokenType.QUESTION:
397
+ return PRECEDENCE.TERNARY;
398
+ default:
399
+ return PRECEDENCE.LOWEST;
400
+ }
401
+ }
402
+
403
+ private getOperatorSymbol(type: TokenType): string {
404
+ switch (type) {
405
+ case TokenType.PLUS: return '+';
406
+ case TokenType.MINUS: return '-';
407
+ case TokenType.MULTIPLY: return '*';
408
+ case TokenType.DIVIDE: return '/';
409
+ case TokenType.MODULO: return '%';
410
+ case TokenType.POWER: return '^';
411
+ case TokenType.EQ: return '==';
412
+ case TokenType.NEQ: return '!=';
413
+ case TokenType.LT: return '<';
414
+ case TokenType.GT: return '>';
415
+ case TokenType.LTE: return '<=';
416
+ case TokenType.GTE: return '>=';
417
+ case TokenType.AND: return '&&';
418
+ case TokenType.OR: return '||';
419
+ case TokenType.NOT: return '!';
420
+ default: return '';
421
+ }
422
+ }
423
+
424
+ private peek(): Token {
425
+ return this.tokens[this.current];
426
+ }
427
+
428
+ private advance(): Token {
429
+ if (!this.isAtEnd()) {
430
+ this.current++;
431
+ }
432
+ return this.tokens[this.current - 1];
433
+ }
434
+
435
+ private isAtEnd(): boolean {
436
+ return this.peek().type === TokenType.EOF;
437
+ }
438
+
439
+ private expect(type: TokenType, expected: string): Token {
440
+ if (this.peek().type === type) {
441
+ return this.advance();
442
+ }
443
+ throw new UnexpectedTokenError(
444
+ String(this.peek().value),
445
+ [expected],
446
+ this.peek().position
447
+ );
448
+ }
449
+ }