@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/dist/types.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenType = exports.DecimalRoundingMode = void 0;
4
+ // ============================================================================
5
+ // Decimal Configuration
6
+ // ============================================================================
7
+ var DecimalRoundingMode;
8
+ (function (DecimalRoundingMode) {
9
+ DecimalRoundingMode["CEIL"] = "CEIL";
10
+ DecimalRoundingMode["FLOOR"] = "FLOOR";
11
+ DecimalRoundingMode["DOWN"] = "DOWN";
12
+ DecimalRoundingMode["UP"] = "UP";
13
+ DecimalRoundingMode["HALF_UP"] = "HALF_UP";
14
+ DecimalRoundingMode["HALF_DOWN"] = "HALF_DOWN";
15
+ DecimalRoundingMode["HALF_EVEN"] = "HALF_EVEN";
16
+ DecimalRoundingMode["HALF_ODD"] = "HALF_ODD";
17
+ })(DecimalRoundingMode || (exports.DecimalRoundingMode = DecimalRoundingMode = {}));
18
+ // ============================================================================
19
+ // Token Types
20
+ // ============================================================================
21
+ var TokenType;
22
+ (function (TokenType) {
23
+ // Literals
24
+ TokenType["NUMBER"] = "NUMBER";
25
+ TokenType["STRING"] = "STRING";
26
+ TokenType["BOOLEAN"] = "BOOLEAN";
27
+ TokenType["NULL"] = "NULL";
28
+ // Identifiers and Variables
29
+ TokenType["IDENTIFIER"] = "IDENTIFIER";
30
+ TokenType["VARIABLE"] = "VARIABLE";
31
+ TokenType["CONTEXT_VAR"] = "CONTEXT_VAR";
32
+ // Operators
33
+ TokenType["PLUS"] = "PLUS";
34
+ TokenType["MINUS"] = "MINUS";
35
+ TokenType["MULTIPLY"] = "MULTIPLY";
36
+ TokenType["DIVIDE"] = "DIVIDE";
37
+ TokenType["MODULO"] = "MODULO";
38
+ TokenType["POWER"] = "POWER";
39
+ // Comparison
40
+ TokenType["EQ"] = "EQ";
41
+ TokenType["NEQ"] = "NEQ";
42
+ TokenType["LT"] = "LT";
43
+ TokenType["GT"] = "GT";
44
+ TokenType["LTE"] = "LTE";
45
+ TokenType["GTE"] = "GTE";
46
+ // Logical
47
+ TokenType["AND"] = "AND";
48
+ TokenType["OR"] = "OR";
49
+ TokenType["NOT"] = "NOT";
50
+ // Punctuation
51
+ TokenType["LPAREN"] = "LPAREN";
52
+ TokenType["RPAREN"] = "RPAREN";
53
+ TokenType["LBRACKET"] = "LBRACKET";
54
+ TokenType["RBRACKET"] = "RBRACKET";
55
+ TokenType["COMMA"] = "COMMA";
56
+ TokenType["DOT"] = "DOT";
57
+ TokenType["QUESTION"] = "QUESTION";
58
+ TokenType["COLON"] = "COLON";
59
+ // Special
60
+ TokenType["EOF"] = "EOF";
61
+ })(TokenType || (exports.TokenType = TokenType = {}));
62
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBVUEsK0VBQStFO0FBQy9FLHdCQUF3QjtBQUN4QiwrRUFBK0U7QUFFL0UsSUFBWSxtQkFTWDtBQVRELFdBQVksbUJBQW1CO0lBQzdCLG9DQUFhLENBQUE7SUFDYixzQ0FBZSxDQUFBO0lBQ2Ysb0NBQWEsQ0FBQTtJQUNiLGdDQUFTLENBQUE7SUFDVCwwQ0FBbUIsQ0FBQTtJQUNuQiw4Q0FBdUIsQ0FBQTtJQUN2Qiw4Q0FBdUIsQ0FBQTtJQUN2Qiw0Q0FBcUIsQ0FBQTtBQUN2QixDQUFDLEVBVFcsbUJBQW1CLG1DQUFuQixtQkFBbUIsUUFTOUI7QUEwUUQsK0VBQStFO0FBQy9FLGNBQWM7QUFDZCwrRUFBK0U7QUFFL0UsSUFBWSxTQTZDWDtBQTdDRCxXQUFZLFNBQVM7SUFDbkIsV0FBVztJQUNYLDhCQUFpQixDQUFBO0lBQ2pCLDhCQUFpQixDQUFBO0lBQ2pCLGdDQUFtQixDQUFBO0lBQ25CLDBCQUFhLENBQUE7SUFFYiw0QkFBNEI7SUFDNUIsc0NBQXlCLENBQUE7SUFDekIsa0NBQXFCLENBQUE7SUFDckIsd0NBQTJCLENBQUE7SUFFM0IsWUFBWTtJQUNaLDBCQUFhLENBQUE7SUFDYiw0QkFBZSxDQUFBO0lBQ2Ysa0NBQXFCLENBQUE7SUFDckIsOEJBQWlCLENBQUE7SUFDakIsOEJBQWlCLENBQUE7SUFDakIsNEJBQWUsQ0FBQTtJQUVmLGFBQWE7SUFDYixzQkFBUyxDQUFBO0lBQ1Qsd0JBQVcsQ0FBQTtJQUNYLHNCQUFTLENBQUE7SUFDVCxzQkFBUyxDQUFBO0lBQ1Qsd0JBQVcsQ0FBQTtJQUNYLHdCQUFXLENBQUE7SUFFWCxVQUFVO0lBQ1Ysd0JBQVcsQ0FBQTtJQUNYLHNCQUFTLENBQUE7SUFDVCx3QkFBVyxDQUFBO0lBRVgsY0FBYztJQUNkLDhCQUFpQixDQUFBO0lBQ2pCLDhCQUFpQixDQUFBO0lBQ2pCLGtDQUFxQixDQUFBO0lBQ3JCLGtDQUFxQixDQUFBO0lBQ3JCLDRCQUFlLENBQUE7SUFDZix3QkFBVyxDQUFBO0lBQ1gsa0NBQXFCLENBQUE7SUFDckIsNEJBQWUsQ0FBQTtJQUVmLFVBQVU7SUFDVix3QkFBVyxDQUFBO0FBQ2IsQ0FBQyxFQTdDVyxTQUFTLHlCQUFULFNBQVMsUUE2Q3BCIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgRGVjaW1hbCB9IGZyb20gJ2RlY2ltYWwuanMnO1xuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4vLyBWYWx1ZSBUeXBlc1xuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgdHlwZSBWYWx1ZVR5cGUgPSAnbnVtYmVyJyB8ICdkZWNpbWFsJyB8ICdzdHJpbmcnIHwgJ2Jvb2xlYW4nIHwgJ2FycmF5JyB8ICdvYmplY3QnIHwgJ251bGwnIHwgJ2FueSc7XG5cbmV4cG9ydCB0eXBlIEZvcm11bGFWYWx1ZSA9IERlY2ltYWwgfCBudW1iZXIgfCBzdHJpbmcgfCBib29sZWFuIHwgbnVsbCB8IEZvcm11bGFWYWx1ZVtdIHwgeyBba2V5OiBzdHJpbmddOiBGb3JtdWxhVmFsdWUgfTtcblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gRGVjaW1hbCBDb25maWd1cmF0aW9uXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5cbmV4cG9ydCBlbnVtIERlY2ltYWxSb3VuZGluZ01vZGUge1xuICBDRUlMID0gJ0NFSUwnLFxuICBGTE9PUiA9ICdGTE9PUicsXG4gIERPV04gPSAnRE9XTicsXG4gIFVQID0gJ1VQJyxcbiAgSEFMRl9VUCA9ICdIQUxGX1VQJyxcbiAgSEFMRl9ET1dOID0gJ0hBTEZfRE9XTicsXG4gIEhBTEZfRVZFTiA9ICdIQUxGX0VWRU4nLFxuICBIQUxGX09ERCA9ICdIQUxGX09ERCcsXG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgRGVjaW1hbENvbmZpZyB7XG4gIHByZWNpc2lvbj86IG51bWJlcjtcbiAgcm91bmRpbmdNb2RlPzogRGVjaW1hbFJvdW5kaW5nTW9kZTtcbiAgZGl2aXNpb25TY2FsZT86IG51bWJlcjtcbiAgcHJlc2VydmVUcmFpbGluZ1plcm9zPzogYm9vbGVhbjtcbiAgYXV0b0NvbnZlcnRGbG9hdHM/OiBib29sZWFuO1xuICBtYXhFeHBvbmVudD86IG51bWJlcjtcbiAgbWluRXhwb25lbnQ/OiBudW1iZXI7XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIFJvdW5kaW5nIENvbmZpZ3VyYXRpb25cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IGludGVyZmFjZSBSb3VuZGluZ0NvbmZpZyB7XG4gIG1vZGU6ICdIQUxGX1VQJyB8ICdIQUxGX0RPV04nIHwgJ0ZMT09SJyB8ICdDRUlMJyB8ICdOT05FJztcbiAgcHJlY2lzaW9uOiBudW1iZXI7XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEVycm9yIEJlaGF2aW9yXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5cbmV4cG9ydCBpbnRlcmZhY2UgRXJyb3JCZWhhdmlvciB7XG4gIHR5cGU6ICdUSFJPVycgfCAnTlVMTCcgfCAnWkVSTycgfCAnREVGQVVMVCcgfCAnU0tJUCc7XG4gIGRlZmF1bHRWYWx1ZT86IHVua25vd247XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEZvcm11bGEgRGVmaW5pdGlvblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgaW50ZXJmYWNlIEZvcm11bGFEZWZpbml0aW9uIHtcbiAgaWQ6IHN0cmluZztcbiAgZXhwcmVzc2lvbjogc3RyaW5nO1xuICBkZXBlbmRlbmNpZXM/OiBzdHJpbmdbXTtcbiAgb25FcnJvcj86IEVycm9yQmVoYXZpb3I7XG4gIGRlZmF1bHRWYWx1ZT86IHVua25vd247XG4gIHJvdW5kaW5nPzogUm91bmRpbmdDb25maWc7XG4gIG1ldGFkYXRhPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj47XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEV2YWx1YXRpb24gQ29udGV4dFxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgaW50ZXJmYWNlIEV2YWx1YXRpb25Db250ZXh0IHtcbiAgdmFyaWFibGVzOiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPjtcbiAgY29sbGVjdGlvbnM/OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duW10+O1xuICBleHRyYT86IFJlY29yZDxzdHJpbmcsIHVua25vd24+O1xufVxuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4vLyBFbmdpbmUgQ29uZmlndXJhdGlvblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgaW50ZXJmYWNlIE9wZXJhdG9yRGVmaW5pdGlvbiB7XG4gIHN5bWJvbDogc3RyaW5nO1xuICBwcmVjZWRlbmNlOiBudW1iZXI7XG4gIGFzc29jaWF0aXZpdHk6ICdsZWZ0JyB8ICdyaWdodCc7XG4gIGhhbmRsZXI6IChsZWZ0OiB1bmtub3duLCByaWdodDogdW5rbm93bikgPT4gdW5rbm93bjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBTZWN1cml0eUNvbmZpZyB7XG4gIG1heEV4cHJlc3Npb25MZW5ndGg/OiBudW1iZXI7XG4gIG1heFJlY3Vyc2lvbkRlcHRoPzogbnVtYmVyO1xuICBtYXhJdGVyYXRpb25zPzogbnVtYmVyO1xuICBtYXhFeGVjdXRpb25UaW1lPzogbnVtYmVyO1xuICBhbGxvd2VkRnVuY3Rpb25zPzogc3RyaW5nW107XG4gIGJsb2NrZWRGdW5jdGlvbnM/OiBzdHJpbmdbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBGb3JtdWxhRW5naW5lQ29uZmlnIHtcbiAgZW5hYmxlQ2FjaGU/OiBib29sZWFuO1xuICBtYXhDYWNoZVNpemU/OiBudW1iZXI7XG4gIGRlZmF1bHRFcnJvckJlaGF2aW9yPzogRXJyb3JCZWhhdmlvcjtcbiAgZGVmYXVsdFJvdW5kaW5nPzogUm91bmRpbmdDb25maWc7XG4gIHZhcmlhYmxlUHJlZml4Pzogc3RyaW5nO1xuICBjb250ZXh0UHJlZml4Pzogc3RyaW5nO1xuICBzdHJpY3RNb2RlPzogYm9vbGVhbjtcbiAgb3BlcmF0b3JzPzogT3BlcmF0b3JEZWZpbml0aW9uW107XG4gIGZ1bmN0aW9ucz86IEZ1bmN0aW9uRGVmaW5pdGlvbltdO1xuICBkZWNpbWFsPzogRGVjaW1hbENvbmZpZztcbiAgc2VjdXJpdHk/OiBTZWN1cml0eUNvbmZpZztcbn1cblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gQVNUIE5vZGUgVHlwZXNcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IGludGVyZmFjZSBEZWNpbWFsTGl0ZXJhbCB7XG4gIHR5cGU6ICdEZWNpbWFsTGl0ZXJhbCc7XG4gIHZhbHVlOiBzdHJpbmc7XG4gIHJhdzogc3RyaW5nO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE51bWJlckxpdGVyYWwge1xuICB0eXBlOiAnTnVtYmVyTGl0ZXJhbCc7XG4gIHZhbHVlOiBudW1iZXI7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU3RyaW5nTGl0ZXJhbCB7XG4gIHR5cGU6ICdTdHJpbmdMaXRlcmFsJztcbiAgdmFsdWU6IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBCb29sZWFuTGl0ZXJhbCB7XG4gIHR5cGU6ICdCb29sZWFuTGl0ZXJhbCc7XG4gIHZhbHVlOiBib29sZWFuO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIE51bGxMaXRlcmFsIHtcbiAgdHlwZTogJ051bGxMaXRlcmFsJztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBBcnJheUxpdGVyYWwge1xuICB0eXBlOiAnQXJyYXlMaXRlcmFsJztcbiAgZWxlbWVudHM6IEFTVE5vZGVbXTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBWYXJpYWJsZVJlZmVyZW5jZSB7XG4gIHR5cGU6ICdWYXJpYWJsZVJlZmVyZW5jZSc7XG4gIHByZWZpeDogJyQnIHwgJ0AnO1xuICBuYW1lOiBzdHJpbmc7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgQmluYXJ5T3BlcmF0aW9uIHtcbiAgdHlwZTogJ0JpbmFyeU9wZXJhdGlvbic7XG4gIG9wZXJhdG9yOiBzdHJpbmc7XG4gIGxlZnQ6IEFTVE5vZGU7XG4gIHJpZ2h0OiBBU1ROb2RlO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIFVuYXJ5T3BlcmF0aW9uIHtcbiAgdHlwZTogJ1VuYXJ5T3BlcmF0aW9uJztcbiAgb3BlcmF0b3I6IHN0cmluZztcbiAgb3BlcmFuZDogQVNUTm9kZTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDb25kaXRpb25hbEV4cHJlc3Npb24ge1xuICB0eXBlOiAnQ29uZGl0aW9uYWxFeHByZXNzaW9uJztcbiAgY29uZGl0aW9uOiBBU1ROb2RlO1xuICBjb25zZXF1ZW50OiBBU1ROb2RlO1xuICBhbHRlcm5hdGU6IEFTVE5vZGU7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgRnVuY3Rpb25DYWxsIHtcbiAgdHlwZTogJ0Z1bmN0aW9uQ2FsbCc7XG4gIG5hbWU6IHN0cmluZztcbiAgYXJndW1lbnRzOiBBU1ROb2RlW107XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgTWVtYmVyQWNjZXNzIHtcbiAgdHlwZTogJ01lbWJlckFjY2Vzcyc7XG4gIG9iamVjdDogQVNUTm9kZTtcbiAgcHJvcGVydHk6IHN0cmluZztcbn1cblxuZXhwb3J0IGludGVyZmFjZSBJbmRleEFjY2VzcyB7XG4gIHR5cGU6ICdJbmRleEFjY2Vzcyc7XG4gIG9iamVjdDogQVNUTm9kZTtcbiAgaW5kZXg6IEFTVE5vZGU7XG59XG5cbmV4cG9ydCB0eXBlIEFTVE5vZGUgPVxuICB8IERlY2ltYWxMaXRlcmFsXG4gIHwgTnVtYmVyTGl0ZXJhbFxuICB8IFN0cmluZ0xpdGVyYWxcbiAgfCBCb29sZWFuTGl0ZXJhbFxuICB8IE51bGxMaXRlcmFsXG4gIHwgQXJyYXlMaXRlcmFsXG4gIHwgVmFyaWFibGVSZWZlcmVuY2VcbiAgfCBCaW5hcnlPcGVyYXRpb25cbiAgfCBVbmFyeU9wZXJhdGlvblxuICB8IENvbmRpdGlvbmFsRXhwcmVzc2lvblxuICB8IEZ1bmN0aW9uQ2FsbFxuICB8IE1lbWJlckFjY2Vzc1xuICB8IEluZGV4QWNjZXNzO1xuXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG4vLyBGdW5jdGlvbiBEZWZpbml0aW9uXG4vLyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09XG5cbmV4cG9ydCBpbnRlcmZhY2UgQXJndW1lbnRUeXBlIHtcbiAgbmFtZTogc3RyaW5nO1xuICB0eXBlOiBWYWx1ZVR5cGU7XG4gIHJlcXVpcmVkOiBib29sZWFuO1xuICBkZWZhdWx0PzogdW5rbm93bjtcbn1cblxuZXhwb3J0IHR5cGUgRnVuY3Rpb25JbXBsZW1lbnRhdGlvbiA9IChcbiAgYXJnczogdW5rbm93bltdLFxuICBjb250ZXh0OiBFdmFsdWF0aW9uQ29udGV4dCxcbiAgZW5naW5lOiB1bmtub3duXG4pID0+IHVua25vd247XG5cbmV4cG9ydCBpbnRlcmZhY2UgRnVuY3Rpb25EZWZpbml0aW9uIHtcbiAgbmFtZTogc3RyaW5nO1xuICBtaW5BcmdzOiBudW1iZXI7XG4gIG1heEFyZ3M6IG51bWJlcjtcbiAgYXJnVHlwZXM/OiBBcmd1bWVudFR5cGVbXTtcbiAgcmV0dXJuVHlwZTogVmFsdWVUeXBlO1xuICBpbXBsZW1lbnRhdGlvbjogRnVuY3Rpb25JbXBsZW1lbnRhdGlvbjtcbiAgZGVzY3JpcHRpb24/OiBzdHJpbmc7XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIERlcGVuZGVuY3kgR3JhcGhcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IGludGVyZmFjZSBEZXBlbmRlbmN5R3JhcGgge1xuICBub2RlczogU2V0PHN0cmluZz47XG4gIGVkZ2VzOiBNYXA8c3RyaW5nLCBTZXQ8c3RyaW5nPj47XG4gIGhhc0N5Y2xlcygpOiBib29sZWFuO1xuICBnZXRSb290cygpOiBTZXQ8c3RyaW5nPjtcbiAgZ2V0RGVwZW5kZW50cyhub2RlSWQ6IHN0cmluZyk6IFNldDxzdHJpbmc+O1xuICBnZXREZXBlbmRlbmNpZXMobm9kZUlkOiBzdHJpbmcpOiBTZXQ8c3RyaW5nPjtcbiAgZ2V0VHJhbnNpdGl2ZURlcGVuZGVuY2llcyhub2RlSWQ6IHN0cmluZyk6IFNldDxzdHJpbmc+O1xuICB0b3BvbG9naWNhbFNvcnQoKTogc3RyaW5nW107XG59XG5cbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cbi8vIEV2YWx1YXRpb24gUmVzdWx0c1xuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgaW50ZXJmYWNlIEV2YWx1YXRpb25SZXN1bHQge1xuICB2YWx1ZTogdW5rbm93bjtcbiAgc3VjY2VzczogYm9vbGVhbjtcbiAgZXJyb3I/OiBFcnJvcjtcbiAgZXhlY3V0aW9uVGltZU1zOiBudW1iZXI7XG4gIGFjY2Vzc2VkVmFyaWFibGVzOiBTZXQ8c3RyaW5nPjtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBFdmFsdWF0aW9uUmVzdWx0U2V0IHtcbiAgcmVzdWx0czogTWFwPHN0cmluZywgRXZhbHVhdGlvblJlc3VsdD47XG4gIHN1Y2Nlc3M6IGJvb2xlYW47XG4gIGVycm9yczogRXJyb3JbXTtcbiAgdG90YWxFeGVjdXRpb25UaW1lTXM6IG51bWJlcjtcbiAgZXZhbHVhdGlvbk9yZGVyOiBzdHJpbmdbXTtcbn1cblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gVmFsaWRhdGlvbiBSZXN1bHRcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IGludGVyZmFjZSBWYWxpZGF0aW9uUmVzdWx0IHtcbiAgdmFsaWQ6IGJvb2xlYW47XG4gIGVycm9yczogRXJyb3JbXTtcbiAgd2FybmluZ3M6IHN0cmluZ1tdO1xuICBkZXBlbmRlbmN5R3JhcGg6IERlcGVuZGVuY3lHcmFwaDtcbiAgZXZhbHVhdGlvbk9yZGVyOiBzdHJpbmdbXTtcbn1cblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gQ2FjaGUgU3RhdGlzdGljc1xuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuXG5leHBvcnQgaW50ZXJmYWNlIENhY2hlU3RhdHMge1xuICBzaXplOiBudW1iZXI7XG4gIGhpdHM6IG51bWJlcjtcbiAgbWlzc2VzOiBudW1iZXI7XG4gIGhpdFJhdGU6IG51bWJlcjtcbn1cblxuLy8gPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PVxuLy8gVG9rZW4gVHlwZXNcbi8vID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT1cblxuZXhwb3J0IGVudW0gVG9rZW5UeXBlIHtcbiAgLy8gTGl0ZXJhbHNcbiAgTlVNQkVSID0gJ05VTUJFUicsXG4gIFNUUklORyA9ICdTVFJJTkcnLFxuICBCT09MRUFOID0gJ0JPT0xFQU4nLFxuICBOVUxMID0gJ05VTEwnLFxuXG4gIC8vIElkZW50aWZpZXJzIGFuZCBWYXJpYWJsZXNcbiAgSURFTlRJRklFUiA9ICdJREVOVElGSUVSJyxcbiAgVkFSSUFCTEUgPSAnVkFSSUFCTEUnLFxuICBDT05URVhUX1ZBUiA9ICdDT05URVhUX1ZBUicsXG5cbiAgLy8gT3BlcmF0b3JzXG4gIFBMVVMgPSAnUExVUycsXG4gIE1JTlVTID0gJ01JTlVTJyxcbiAgTVVMVElQTFkgPSAnTVVMVElQTFknLFxuICBESVZJREUgPSAnRElWSURFJyxcbiAgTU9EVUxPID0gJ01PRFVMTycsXG4gIFBPV0VSID0gJ1BPV0VSJyxcblxuICAvLyBDb21wYXJpc29uXG4gIEVRID0gJ0VRJyxcbiAgTkVRID0gJ05FUScsXG4gIExUID0gJ0xUJyxcbiAgR1QgPSAnR1QnLFxuICBMVEUgPSAnTFRFJyxcbiAgR1RFID0gJ0dURScsXG5cbiAgLy8gTG9naWNhbFxuICBBTkQgPSAnQU5EJyxcbiAgT1IgPSAnT1InLFxuICBOT1QgPSAnTk9UJyxcblxuICAvLyBQdW5jdHVhdGlvblxuICBMUEFSRU4gPSAnTFBBUkVOJyxcbiAgUlBBUkVOID0gJ1JQQVJFTicsXG4gIExCUkFDS0VUID0gJ0xCUkFDS0VUJyxcbiAgUkJSQUNLRVQgPSAnUkJSQUNLRVQnLFxuICBDT01NQSA9ICdDT01NQScsXG4gIERPVCA9ICdET1QnLFxuICBRVUVTVElPTiA9ICdRVUVTVElPTicsXG4gIENPTE9OID0gJ0NPTE9OJyxcblxuICAvLyBTcGVjaWFsXG4gIEVPRiA9ICdFT0YnLFxufVxuXG5leHBvcnQgaW50ZXJmYWNlIFRva2VuIHtcbiAgdHlwZTogVG9rZW5UeXBlO1xuICB2YWx1ZTogc3RyaW5nIHwgbnVtYmVyIHwgYm9vbGVhbiB8IG51bGw7XG4gIHBvc2l0aW9uOiBudW1iZXI7XG4gIGxpbmU6IG51bWJlcjtcbiAgY29sdW1uOiBudW1iZXI7XG59XG4iXX0=
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: ['**/*.test.ts'],
6
+ collectCoverageFrom: [
7
+ 'src/**/*.ts',
8
+ '!src/**/*.test.ts',
9
+ '!src/index.ts'
10
+ ],
11
+ coverageThreshold: {
12
+ global: {
13
+ branches: 80,
14
+ functions: 80,
15
+ lines: 80,
16
+ statements: 80
17
+ }
18
+ },
19
+ moduleFileExtensions: ['ts', 'js', 'json'],
20
+ transform: {
21
+ '^.+\\.ts$': 'ts-jest'
22
+ }
23
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@the-trybe/formula-engine",
3
+ "version": "1.0.0",
4
+ "description": "Configuration-driven expression evaluation system with dependency resolution and decimal precision",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch",
11
+ "test:coverage": "jest --coverage",
12
+ "lint": "eslint src --ext .ts",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "keywords": [
16
+ "formula",
17
+ "expression",
18
+ "parser",
19
+ "evaluator",
20
+ "dependency-graph",
21
+ "decimal"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@types/jest": "^29.5.11",
27
+ "@types/node": "^20.10.0",
28
+ "jest": "^29.7.0",
29
+ "ts-jest": "^29.1.1",
30
+ "typescript": "^5.3.0"
31
+ },
32
+ "dependencies": {
33
+ "decimal.js": "^10.4.3"
34
+ }
35
+ }
@@ -0,0 +1,408 @@
1
+ import Decimal from 'decimal.js';
2
+ import { DecimalConfig, DecimalRoundingMode } from './types';
3
+ import { InvalidDecimalError, DecimalDivisionByZeroError } from './errors';
4
+
5
+ // Map our rounding modes to decimal.js rounding modes
6
+ const ROUNDING_MODE_MAP: Record<DecimalRoundingMode, Decimal.Rounding> = {
7
+ [DecimalRoundingMode.CEIL]: Decimal.ROUND_CEIL,
8
+ [DecimalRoundingMode.FLOOR]: Decimal.ROUND_FLOOR,
9
+ [DecimalRoundingMode.DOWN]: Decimal.ROUND_DOWN,
10
+ [DecimalRoundingMode.UP]: Decimal.ROUND_UP,
11
+ [DecimalRoundingMode.HALF_UP]: Decimal.ROUND_HALF_UP,
12
+ [DecimalRoundingMode.HALF_DOWN]: Decimal.ROUND_HALF_DOWN,
13
+ [DecimalRoundingMode.HALF_EVEN]: Decimal.ROUND_HALF_EVEN,
14
+ [DecimalRoundingMode.HALF_ODD]: Decimal.ROUND_HALF_CEIL, // decimal.js doesn't have HALF_ODD, use HALF_CEIL as approximation
15
+ };
16
+
17
+ export type DecimalLike = Decimal | string | number | bigint;
18
+
19
+ export interface DecimalUtilsConfig {
20
+ precision: number;
21
+ roundingMode: DecimalRoundingMode;
22
+ divisionScale: number;
23
+ maxExponent: number;
24
+ minExponent: number;
25
+ }
26
+
27
+ const DEFAULT_CONFIG: DecimalUtilsConfig = {
28
+ precision: 20,
29
+ roundingMode: DecimalRoundingMode.HALF_UP,
30
+ divisionScale: 10,
31
+ maxExponent: 1000,
32
+ minExponent: -1000,
33
+ };
34
+
35
+ export class DecimalUtils {
36
+ private config: DecimalUtilsConfig;
37
+
38
+ constructor(config?: Partial<DecimalConfig>) {
39
+ this.config = {
40
+ ...DEFAULT_CONFIG,
41
+ ...config,
42
+ };
43
+
44
+ // Configure decimal.js globally
45
+ Decimal.set({
46
+ precision: this.config.precision,
47
+ rounding: ROUNDING_MODE_MAP[this.config.roundingMode],
48
+ toExpNeg: this.config.minExponent,
49
+ toExpPos: this.config.maxExponent,
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Create a Decimal from various input types
55
+ */
56
+ from(value: DecimalLike): Decimal {
57
+ if (value instanceof Decimal) {
58
+ return value;
59
+ }
60
+
61
+ try {
62
+ if (typeof value === 'bigint') {
63
+ return new Decimal(value.toString());
64
+ }
65
+ return new Decimal(value);
66
+ } catch {
67
+ throw new InvalidDecimalError(String(value));
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if a value is a Decimal
73
+ */
74
+ isDecimal(value: unknown): value is Decimal {
75
+ return value instanceof Decimal;
76
+ }
77
+
78
+ /**
79
+ * Convert a value to Decimal if it's numeric
80
+ */
81
+ toDecimal(value: unknown): Decimal | unknown {
82
+ if (value instanceof Decimal) {
83
+ return value;
84
+ }
85
+ if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
86
+ return new Decimal(value);
87
+ }
88
+ if (typeof value === 'string') {
89
+ const trimmed = value.trim();
90
+ if (/^-?\d+\.?\d*$/.test(trimmed) || /^-?\d*\.?\d+$/.test(trimmed)) {
91
+ try {
92
+ return new Decimal(trimmed);
93
+ } catch {
94
+ return value;
95
+ }
96
+ }
97
+ }
98
+ if (typeof value === 'bigint') {
99
+ return new Decimal(value.toString());
100
+ }
101
+ return value;
102
+ }
103
+
104
+ /**
105
+ * Addition
106
+ */
107
+ add(a: DecimalLike, b: DecimalLike): Decimal {
108
+ return this.from(a).plus(this.from(b));
109
+ }
110
+
111
+ /**
112
+ * Subtraction
113
+ */
114
+ subtract(a: DecimalLike, b: DecimalLike): Decimal {
115
+ return this.from(a).minus(this.from(b));
116
+ }
117
+
118
+ /**
119
+ * Multiplication
120
+ */
121
+ multiply(a: DecimalLike, b: DecimalLike): Decimal {
122
+ return this.from(a).times(this.from(b));
123
+ }
124
+
125
+ /**
126
+ * Division with scale
127
+ */
128
+ divide(a: DecimalLike, b: DecimalLike, scale?: number, roundingMode?: DecimalRoundingMode): Decimal {
129
+ const divisor = this.from(b);
130
+ if (divisor.isZero()) {
131
+ throw new DecimalDivisionByZeroError();
132
+ }
133
+
134
+ const result = this.from(a).dividedBy(divisor);
135
+
136
+ if (scale !== undefined) {
137
+ const rounding = roundingMode
138
+ ? ROUNDING_MODE_MAP[roundingMode]
139
+ : ROUNDING_MODE_MAP[this.config.roundingMode];
140
+ return result.toDecimalPlaces(scale, rounding);
141
+ }
142
+
143
+ return result.toDecimalPlaces(this.config.divisionScale, ROUNDING_MODE_MAP[this.config.roundingMode]);
144
+ }
145
+
146
+ /**
147
+ * Modulo
148
+ */
149
+ modulo(a: DecimalLike, b: DecimalLike): Decimal {
150
+ const divisor = this.from(b);
151
+ if (divisor.isZero()) {
152
+ throw new DecimalDivisionByZeroError();
153
+ }
154
+ return this.from(a).modulo(divisor);
155
+ }
156
+
157
+ /**
158
+ * Power
159
+ */
160
+ power(base: DecimalLike, exponent: number): Decimal {
161
+ return this.from(base).pow(exponent);
162
+ }
163
+
164
+ /**
165
+ * Negation
166
+ */
167
+ negate(value: DecimalLike): Decimal {
168
+ return this.from(value).negated();
169
+ }
170
+
171
+ /**
172
+ * Absolute value
173
+ */
174
+ abs(value: DecimalLike): Decimal {
175
+ return this.from(value).absoluteValue();
176
+ }
177
+
178
+ /**
179
+ * Round to specified decimal places
180
+ */
181
+ round(value: DecimalLike, scale: number, roundingMode?: DecimalRoundingMode): Decimal {
182
+ const rounding = roundingMode
183
+ ? ROUNDING_MODE_MAP[roundingMode]
184
+ : ROUNDING_MODE_MAP[this.config.roundingMode];
185
+ return this.from(value).toDecimalPlaces(scale, rounding);
186
+ }
187
+
188
+ /**
189
+ * Floor to specified decimal places
190
+ */
191
+ floor(value: DecimalLike, scale: number = 0): Decimal {
192
+ return this.from(value).toDecimalPlaces(scale, Decimal.ROUND_FLOOR);
193
+ }
194
+
195
+ /**
196
+ * Ceiling to specified decimal places
197
+ */
198
+ ceil(value: DecimalLike, scale: number = 0): Decimal {
199
+ return this.from(value).toDecimalPlaces(scale, Decimal.ROUND_CEIL);
200
+ }
201
+
202
+ /**
203
+ * Truncate to specified decimal places
204
+ */
205
+ truncate(value: DecimalLike, scale: number = 0): Decimal {
206
+ return this.from(value).toDecimalPlaces(scale, Decimal.ROUND_DOWN);
207
+ }
208
+
209
+ /**
210
+ * Square root
211
+ */
212
+ sqrt(value: DecimalLike): Decimal {
213
+ return this.from(value).sqrt();
214
+ }
215
+
216
+ /**
217
+ * Natural logarithm
218
+ */
219
+ ln(value: DecimalLike): Decimal {
220
+ return this.from(value).ln();
221
+ }
222
+
223
+ /**
224
+ * Base-10 logarithm
225
+ */
226
+ log10(value: DecimalLike): Decimal {
227
+ return this.from(value).log(10);
228
+ }
229
+
230
+ /**
231
+ * Comparison: returns -1, 0, or 1
232
+ */
233
+ compare(a: DecimalLike, b: DecimalLike): -1 | 0 | 1 {
234
+ const result = this.from(a).comparedTo(this.from(b));
235
+ return result as -1 | 0 | 1;
236
+ }
237
+
238
+ /**
239
+ * Equality check
240
+ */
241
+ equals(a: DecimalLike, b: DecimalLike): boolean {
242
+ return this.from(a).equals(this.from(b));
243
+ }
244
+
245
+ /**
246
+ * Greater than
247
+ */
248
+ greaterThan(a: DecimalLike, b: DecimalLike): boolean {
249
+ return this.from(a).greaterThan(this.from(b));
250
+ }
251
+
252
+ /**
253
+ * Greater than or equal
254
+ */
255
+ greaterThanOrEqual(a: DecimalLike, b: DecimalLike): boolean {
256
+ return this.from(a).greaterThanOrEqualTo(this.from(b));
257
+ }
258
+
259
+ /**
260
+ * Less than
261
+ */
262
+ lessThan(a: DecimalLike, b: DecimalLike): boolean {
263
+ return this.from(a).lessThan(this.from(b));
264
+ }
265
+
266
+ /**
267
+ * Less than or equal
268
+ */
269
+ lessThanOrEqual(a: DecimalLike, b: DecimalLike): boolean {
270
+ return this.from(a).lessThanOrEqualTo(this.from(b));
271
+ }
272
+
273
+ /**
274
+ * Check if zero
275
+ */
276
+ isZero(value: DecimalLike): boolean {
277
+ return this.from(value).isZero();
278
+ }
279
+
280
+ /**
281
+ * Check if positive
282
+ */
283
+ isPositive(value: DecimalLike): boolean {
284
+ return this.from(value).isPositive();
285
+ }
286
+
287
+ /**
288
+ * Check if negative
289
+ */
290
+ isNegative(value: DecimalLike): boolean {
291
+ return this.from(value).isNegative();
292
+ }
293
+
294
+ /**
295
+ * Check if integer
296
+ */
297
+ isInteger(value: DecimalLike): boolean {
298
+ return this.from(value).isInteger();
299
+ }
300
+
301
+ /**
302
+ * Get sign: -1, 0, or 1
303
+ */
304
+ sign(value: DecimalLike): -1 | 0 | 1 {
305
+ const d = this.from(value);
306
+ if (d.isZero()) return 0;
307
+ return d.isNegative() ? -1 : 1;
308
+ }
309
+
310
+ /**
311
+ * Get precision (total significant digits)
312
+ */
313
+ precision(value: DecimalLike): number {
314
+ return this.from(value).precision();
315
+ }
316
+
317
+ /**
318
+ * Get scale (decimal places)
319
+ */
320
+ scale(value: DecimalLike): number {
321
+ return this.from(value).decimalPlaces();
322
+ }
323
+
324
+ /**
325
+ * Convert to JavaScript number (may lose precision)
326
+ */
327
+ toNumber(value: DecimalLike): number {
328
+ return this.from(value).toNumber();
329
+ }
330
+
331
+ /**
332
+ * Convert to string
333
+ */
334
+ toString(value: DecimalLike): string {
335
+ return this.from(value).toString();
336
+ }
337
+
338
+ /**
339
+ * Convert to fixed decimal places string
340
+ */
341
+ toFixed(value: DecimalLike, scale: number): string {
342
+ return this.from(value).toFixed(scale);
343
+ }
344
+
345
+ /**
346
+ * Minimum of values
347
+ */
348
+ min(...values: DecimalLike[]): Decimal {
349
+ if (values.length === 0) {
350
+ throw new Error('min requires at least one argument');
351
+ }
352
+ return Decimal.min(...values.map(v => this.from(v)));
353
+ }
354
+
355
+ /**
356
+ * Maximum of values
357
+ */
358
+ max(...values: DecimalLike[]): Decimal {
359
+ if (values.length === 0) {
360
+ throw new Error('max requires at least one argument');
361
+ }
362
+ return Decimal.max(...values.map(v => this.from(v)));
363
+ }
364
+
365
+ /**
366
+ * Sum of values
367
+ */
368
+ sum(values: DecimalLike[]): Decimal {
369
+ return values.reduce<Decimal>((acc, v) => acc.plus(this.from(v)), new Decimal(0));
370
+ }
371
+
372
+ /**
373
+ * Average of values
374
+ */
375
+ avg(values: DecimalLike[]): Decimal {
376
+ if (values.length === 0) {
377
+ throw new Error('avg requires at least one value');
378
+ }
379
+ return this.sum(values).dividedBy(values.length);
380
+ }
381
+
382
+ /**
383
+ * Product of values
384
+ */
385
+ product(values: DecimalLike[]): Decimal {
386
+ return values.reduce<Decimal>((acc, v) => acc.times(this.from(v)), new Decimal(1));
387
+ }
388
+
389
+ /**
390
+ * Create zero
391
+ */
392
+ zero(): Decimal {
393
+ return new Decimal(0);
394
+ }
395
+
396
+ /**
397
+ * Create one
398
+ */
399
+ one(): Decimal {
400
+ return new Decimal(1);
401
+ }
402
+ }
403
+
404
+ // Export a default instance
405
+ export const decimalUtils = new DecimalUtils();
406
+
407
+ // Re-export Decimal type for convenience
408
+ export { Decimal };
@@ -0,0 +1,117 @@
1
+ import { ASTNode } from './types';
2
+ import { Parser } from './parser';
3
+
4
+ export class DependencyExtractor {
5
+ private parser: Parser;
6
+
7
+ constructor() {
8
+ this.parser = new Parser();
9
+ }
10
+
11
+ /**
12
+ * Extract all variable dependencies from an expression string
13
+ * Returns a set of variable names (without the $ or @ prefix)
14
+ */
15
+ extract(expression: string): Set<string> {
16
+ const ast = this.parser.parse(expression);
17
+ return this.extractFromNode(ast);
18
+ }
19
+
20
+ /**
21
+ * Extract dependencies from an AST node
22
+ */
23
+ extractFromNode(node: ASTNode): Set<string> {
24
+ const dependencies = new Set<string>();
25
+ this.visit(node, dependencies);
26
+ return dependencies;
27
+ }
28
+
29
+ private visit(node: ASTNode, dependencies: Set<string>): void {
30
+ switch (node.type) {
31
+ case 'VariableReference':
32
+ // Only extract $ prefixed variables (not @ context variables)
33
+ // Context variables are considered external and don't form part of the dependency graph
34
+ if (node.prefix === '$') {
35
+ dependencies.add(node.name);
36
+ }
37
+ break;
38
+
39
+ case 'BinaryOperation':
40
+ this.visit(node.left, dependencies);
41
+ this.visit(node.right, dependencies);
42
+ break;
43
+
44
+ case 'UnaryOperation':
45
+ this.visit(node.operand, dependencies);
46
+ break;
47
+
48
+ case 'ConditionalExpression':
49
+ this.visit(node.condition, dependencies);
50
+ this.visit(node.consequent, dependencies);
51
+ this.visit(node.alternate, dependencies);
52
+ break;
53
+
54
+ case 'FunctionCall':
55
+ // Visit all function arguments
56
+ for (const arg of node.arguments) {
57
+ this.visit(arg, dependencies);
58
+ }
59
+ break;
60
+
61
+ case 'MemberAccess':
62
+ // For member access like $product.price, we want to extract 'product'
63
+ // The root variable is what matters for dependency tracking
64
+ this.visitMemberRoot(node.object, dependencies);
65
+ break;
66
+
67
+ case 'IndexAccess':
68
+ // Similar to MemberAccess - extract the root variable
69
+ this.visitMemberRoot(node.object, dependencies);
70
+ // Also visit the index expression as it might contain variables
71
+ this.visit(node.index, dependencies);
72
+ break;
73
+
74
+ case 'ArrayLiteral':
75
+ for (const element of node.elements) {
76
+ this.visit(element, dependencies);
77
+ }
78
+ break;
79
+
80
+ // Literals don't have dependencies
81
+ case 'DecimalLiteral':
82
+ case 'NumberLiteral':
83
+ case 'StringLiteral':
84
+ case 'BooleanLiteral':
85
+ case 'NullLiteral':
86
+ break;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Visit the root of a member/index access chain
92
+ * For $product.price.value, we want to extract 'product'
93
+ */
94
+ private visitMemberRoot(node: ASTNode, dependencies: Set<string>): void {
95
+ switch (node.type) {
96
+ case 'VariableReference':
97
+ if (node.prefix === '$') {
98
+ dependencies.add(node.name);
99
+ }
100
+ break;
101
+
102
+ case 'MemberAccess':
103
+ this.visitMemberRoot(node.object, dependencies);
104
+ break;
105
+
106
+ case 'IndexAccess':
107
+ this.visitMemberRoot(node.object, dependencies);
108
+ this.visit(node.index, dependencies);
109
+ break;
110
+
111
+ default:
112
+ // For other node types (like function calls), visit normally
113
+ this.visit(node, dependencies);
114
+ break;
115
+ }
116
+ }
117
+ }