@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
package/src/errors.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { ValueType } from './types';
|
|
2
|
+
|
|
3
|
+
export type ErrorCategory = 'PARSE' | 'VALIDATION' | 'EVALUATION' | 'CONFIGURATION';
|
|
4
|
+
|
|
5
|
+
export abstract class FormulaEngineError extends Error {
|
|
6
|
+
abstract readonly code: string;
|
|
7
|
+
abstract readonly category: ErrorCategory;
|
|
8
|
+
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = this.constructor.name;
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Parse Errors
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export class SyntaxError extends FormulaEngineError {
|
|
21
|
+
readonly code = 'PARSE_SYNTAX_ERROR';
|
|
22
|
+
readonly category: ErrorCategory = 'PARSE';
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
public position: number,
|
|
27
|
+
public line: number,
|
|
28
|
+
public column: number,
|
|
29
|
+
public expression: string
|
|
30
|
+
) {
|
|
31
|
+
super(`${message} at line ${line}, column ${column}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class UnexpectedTokenError extends FormulaEngineError {
|
|
36
|
+
readonly code = 'PARSE_UNEXPECTED_TOKEN';
|
|
37
|
+
readonly category: ErrorCategory = 'PARSE';
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
public token: string,
|
|
41
|
+
public expected: string[],
|
|
42
|
+
public position: number
|
|
43
|
+
) {
|
|
44
|
+
super(`Unexpected token '${token}', expected one of: ${expected.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class UnterminatedStringError extends FormulaEngineError {
|
|
49
|
+
readonly code = 'PARSE_UNTERMINATED_STRING';
|
|
50
|
+
readonly category: ErrorCategory = 'PARSE';
|
|
51
|
+
|
|
52
|
+
constructor(position: number) {
|
|
53
|
+
super(`Unterminated string starting at position ${position}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class InvalidNumberError extends FormulaEngineError {
|
|
58
|
+
readonly code = 'PARSE_INVALID_NUMBER';
|
|
59
|
+
readonly category: ErrorCategory = 'PARSE';
|
|
60
|
+
|
|
61
|
+
constructor(value: string, position: number) {
|
|
62
|
+
super(`Invalid number '${value}' at position ${position}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Validation Errors
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
export class CircularDependencyError extends FormulaEngineError {
|
|
71
|
+
readonly code = 'VALIDATION_CIRCULAR_DEPENDENCY';
|
|
72
|
+
readonly category: ErrorCategory = 'VALIDATION';
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
public cycle: string[],
|
|
76
|
+
public involvedFormulas: string[]
|
|
77
|
+
) {
|
|
78
|
+
super(`Circular dependency detected: ${cycle.join(' → ')}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class UndefinedVariableError extends FormulaEngineError {
|
|
83
|
+
readonly code = 'VALIDATION_UNDEFINED_VARIABLE';
|
|
84
|
+
readonly category: ErrorCategory = 'VALIDATION';
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
public variableName: string,
|
|
88
|
+
public expression: string
|
|
89
|
+
) {
|
|
90
|
+
super(`Undefined variable: ${variableName}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class UndefinedFunctionError extends FormulaEngineError {
|
|
95
|
+
readonly code = 'VALIDATION_UNDEFINED_FUNCTION';
|
|
96
|
+
readonly category: ErrorCategory = 'VALIDATION';
|
|
97
|
+
|
|
98
|
+
constructor(public functionName: string) {
|
|
99
|
+
super(`Undefined function: ${functionName}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class DuplicateFormulaError extends FormulaEngineError {
|
|
104
|
+
readonly code = 'VALIDATION_DUPLICATE_FORMULA';
|
|
105
|
+
readonly category: ErrorCategory = 'VALIDATION';
|
|
106
|
+
|
|
107
|
+
constructor(public formulaId: string) {
|
|
108
|
+
super(`Duplicate formula ID: ${formulaId}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Evaluation Errors
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
export class DivisionByZeroError extends FormulaEngineError {
|
|
117
|
+
readonly code = 'EVAL_DIVISION_BY_ZERO';
|
|
118
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
super('Division by zero');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class TypeMismatchError extends FormulaEngineError {
|
|
126
|
+
readonly code = 'EVAL_TYPE_MISMATCH';
|
|
127
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
128
|
+
|
|
129
|
+
constructor(
|
|
130
|
+
public expected: ValueType | ValueType[] | string,
|
|
131
|
+
public actual: ValueType | string,
|
|
132
|
+
public context: string
|
|
133
|
+
) {
|
|
134
|
+
const expectedStr = Array.isArray(expected) ? expected.join(' or ') : expected;
|
|
135
|
+
super(`Type mismatch: expected ${expectedStr}, got ${actual} in ${context}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class ArgumentCountError extends FormulaEngineError {
|
|
140
|
+
readonly code = 'EVAL_ARGUMENT_COUNT';
|
|
141
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
142
|
+
|
|
143
|
+
constructor(
|
|
144
|
+
public functionName: string,
|
|
145
|
+
public expected: { min: number; max: number },
|
|
146
|
+
public actual: number
|
|
147
|
+
) {
|
|
148
|
+
const expectedStr = expected.min === expected.max
|
|
149
|
+
? `${expected.min}`
|
|
150
|
+
: expected.max === -1
|
|
151
|
+
? `at least ${expected.min}`
|
|
152
|
+
: `${expected.min}-${expected.max}`;
|
|
153
|
+
super(`Function ${functionName} expects ${expectedStr} arguments, got ${actual}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class InvalidOperationError extends FormulaEngineError {
|
|
158
|
+
readonly code = 'EVAL_INVALID_OPERATION';
|
|
159
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
160
|
+
|
|
161
|
+
constructor(
|
|
162
|
+
public operator: string,
|
|
163
|
+
public operandTypes: string[]
|
|
164
|
+
) {
|
|
165
|
+
super(`Cannot apply operator '${operator}' to types: ${operandTypes.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class PropertyAccessError extends FormulaEngineError {
|
|
170
|
+
readonly code = 'EVAL_PROPERTY_ACCESS';
|
|
171
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
172
|
+
|
|
173
|
+
constructor(
|
|
174
|
+
public property: string,
|
|
175
|
+
public objectType: string
|
|
176
|
+
) {
|
|
177
|
+
super(`Cannot access property '${property}' on ${objectType}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class IndexAccessError extends FormulaEngineError {
|
|
182
|
+
readonly code = 'EVAL_INDEX_ACCESS';
|
|
183
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
184
|
+
|
|
185
|
+
constructor(
|
|
186
|
+
public index: unknown,
|
|
187
|
+
public objectType: string
|
|
188
|
+
) {
|
|
189
|
+
super(`Cannot use index '${index}' on ${objectType}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Decimal Errors
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
export class DecimalError extends FormulaEngineError {
|
|
198
|
+
readonly code: string = 'DECIMAL_ERROR';
|
|
199
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
200
|
+
|
|
201
|
+
constructor(message: string) {
|
|
202
|
+
super(message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class DecimalOverflowError extends DecimalError {
|
|
207
|
+
override readonly code: string = 'DECIMAL_OVERFLOW';
|
|
208
|
+
|
|
209
|
+
constructor(public value: string, public maxExponent: number) {
|
|
210
|
+
super(`Decimal overflow: exponent exceeds ${maxExponent}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class DecimalUnderflowError extends DecimalError {
|
|
215
|
+
override readonly code: string = 'DECIMAL_UNDERFLOW';
|
|
216
|
+
|
|
217
|
+
constructor(public value: string, public minExponent: number) {
|
|
218
|
+
super(`Decimal underflow: exponent below ${minExponent}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export class DecimalDivisionByZeroError extends DecimalError {
|
|
223
|
+
override readonly code: string = 'DECIMAL_DIVISION_BY_ZERO';
|
|
224
|
+
|
|
225
|
+
constructor() {
|
|
226
|
+
super('Division by zero');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class InvalidDecimalError extends DecimalError {
|
|
231
|
+
override readonly code: string = 'INVALID_DECIMAL';
|
|
232
|
+
|
|
233
|
+
constructor(public input: string) {
|
|
234
|
+
super(`Invalid decimal value: "${input}"`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Configuration Errors
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
export class ConfigurationError extends FormulaEngineError {
|
|
243
|
+
readonly code = 'CONFIGURATION_ERROR';
|
|
244
|
+
readonly category: ErrorCategory = 'CONFIGURATION';
|
|
245
|
+
|
|
246
|
+
constructor(message: string) {
|
|
247
|
+
super(message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================================
|
|
252
|
+
// Security Errors
|
|
253
|
+
// ============================================================================
|
|
254
|
+
|
|
255
|
+
export class SecurityError extends FormulaEngineError {
|
|
256
|
+
readonly code: string = 'SECURITY_ERROR';
|
|
257
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
258
|
+
|
|
259
|
+
constructor(message: string) {
|
|
260
|
+
super(message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export class MaxIterationsError extends SecurityError {
|
|
265
|
+
override readonly code: string = 'MAX_ITERATIONS_EXCEEDED';
|
|
266
|
+
|
|
267
|
+
constructor(limit: number) {
|
|
268
|
+
super(`Maximum iterations exceeded: ${limit}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class MaxRecursionError extends SecurityError {
|
|
273
|
+
override readonly code: string = 'MAX_RECURSION_EXCEEDED';
|
|
274
|
+
|
|
275
|
+
constructor(limit: number) {
|
|
276
|
+
super(`Maximum recursion depth exceeded: ${limit}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export class MaxExpressionLengthError extends SecurityError {
|
|
281
|
+
override readonly code: string = 'MAX_EXPRESSION_LENGTH_EXCEEDED';
|
|
282
|
+
|
|
283
|
+
constructor(length: number, limit: number) {
|
|
284
|
+
super(`Expression length ${length} exceeds maximum ${limit}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Concrete class for general errors
|
|
289
|
+
export class GeneralFormulaError extends FormulaEngineError {
|
|
290
|
+
readonly code: string = 'GENERAL_ERROR';
|
|
291
|
+
readonly category: ErrorCategory = 'EVALUATION';
|
|
292
|
+
|
|
293
|
+
constructor(message: string) {
|
|
294
|
+
super(message);
|
|
295
|
+
}
|
|
296
|
+
}
|