@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,1863 @@
|
|
|
1
|
+
# PRD: Formula Engine - Configuration-Driven Expression Evaluation System
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
|
|
5
|
+
**Feature Name:** Formula Engine - Generic Expression Evaluation System
|
|
6
|
+
**Version:** 1.0
|
|
7
|
+
**Date:** November 27, 2025
|
|
8
|
+
**Author:** Engineering Team
|
|
9
|
+
**Status:** Draft
|
|
10
|
+
|
|
11
|
+
### Problem Statement
|
|
12
|
+
|
|
13
|
+
Applications frequently need to evaluate mathematical and logical expressions based on dynamic configurations. Current approaches typically:
|
|
14
|
+
|
|
15
|
+
1. **Hardcode formulas** in application code, requiring deployments for changes
|
|
16
|
+
2. **Lack dependency awareness**, leading to incorrect calculation order
|
|
17
|
+
3. **Provide no validation**, causing runtime errors from invalid expressions
|
|
18
|
+
4. **Couple tightly to business domains**, preventing reuse across contexts
|
|
19
|
+
|
|
20
|
+
### Proposed Solution
|
|
21
|
+
|
|
22
|
+
Create a **generic Formula Engine** that:
|
|
23
|
+
|
|
24
|
+
- Parses and evaluates expressions defined in configuration
|
|
25
|
+
- Automatically extracts variable dependencies from expressions
|
|
26
|
+
- Builds a dependency graph and determines correct evaluation order via topological sort
|
|
27
|
+
- Detects circular dependencies at configuration time
|
|
28
|
+
- Provides a clean, domain-agnostic API that accepts any context data
|
|
29
|
+
|
|
30
|
+
The engine is **completely independent of business logic** - it knows nothing about invoices, products, taxes, or any specific domain. All business concepts are passed as configuration.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Goals & Objectives
|
|
35
|
+
|
|
36
|
+
### Primary Goals
|
|
37
|
+
|
|
38
|
+
1. **Complete Domain Independence:** Engine has zero knowledge of business concepts
|
|
39
|
+
2. **Configuration-Driven:** All formulas, variables, and behaviors defined externally
|
|
40
|
+
3. **Automatic Dependency Resolution:** Parse expressions to build dependency graphs
|
|
41
|
+
4. **Correct Evaluation Order:** Topological sort ensures dependencies calculated first
|
|
42
|
+
5. **Circular Dependency Detection:** Fail fast on invalid configurations
|
|
43
|
+
6. **Type Safety:** Full TypeScript support with generic types
|
|
44
|
+
7. **Extensibility:** Plugin system for custom functions and operators
|
|
45
|
+
8. **Decimal Precision:** Arbitrary-precision decimal arithmetic to avoid floating-point errors
|
|
46
|
+
|
|
47
|
+
### Success Metrics
|
|
48
|
+
|
|
49
|
+
- Zero business logic in the formula engine codebase
|
|
50
|
+
- Support for 50+ concurrent formula evaluations per second
|
|
51
|
+
- Sub-millisecond evaluation time for typical expression sets
|
|
52
|
+
- 100% detection rate for circular dependencies
|
|
53
|
+
- Comprehensive error messages for debugging
|
|
54
|
+
- Zero floating-point precision errors in financial calculations
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Scope
|
|
59
|
+
|
|
60
|
+
### In Scope
|
|
61
|
+
|
|
62
|
+
- Expression parsing and AST generation
|
|
63
|
+
- Dependency extraction from expressions
|
|
64
|
+
- Dependency graph construction
|
|
65
|
+
- Topological sorting for evaluation order
|
|
66
|
+
- Expression evaluation with context
|
|
67
|
+
- Built-in operators and functions
|
|
68
|
+
- Custom function registration
|
|
69
|
+
- Error handling and validation
|
|
70
|
+
- Caching of parsed expressions
|
|
71
|
+
- Arbitrary-precision decimal arithmetic
|
|
72
|
+
|
|
73
|
+
### Out of Scope
|
|
74
|
+
|
|
75
|
+
- Persistence of formulas (handled by consuming applications)
|
|
76
|
+
- UI for formula editing
|
|
77
|
+
- Domain-specific validations
|
|
78
|
+
- Async/remote data fetching within formulas
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Detailed Requirements
|
|
83
|
+
|
|
84
|
+
### 1. Core Architecture
|
|
85
|
+
|
|
86
|
+
#### 1.1 High-Level Architecture
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
90
|
+
│ Formula Engine │
|
|
91
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
92
|
+
│ │
|
|
93
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
94
|
+
│ │ Parser │───▶│ Dependency │───▶│ Topological │ │
|
|
95
|
+
│ │ │ │ Extractor │ │ Sorter │ │
|
|
96
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
97
|
+
│ │ │ │ │
|
|
98
|
+
│ ▼ ▼ ▼ │
|
|
99
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
100
|
+
│ │ AST │ │ Dependency │ │ Evaluation │ │
|
|
101
|
+
│ │ Cache │ │ Graph │ │ Order │ │
|
|
102
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
103
|
+
│ │ │
|
|
104
|
+
│ ▼ │
|
|
105
|
+
│ ┌──────────────┐ │
|
|
106
|
+
│ │ Evaluator │ │
|
|
107
|
+
│ │ │ │
|
|
108
|
+
│ └──────────────┘ │
|
|
109
|
+
│ │ │
|
|
110
|
+
│ ▼ │
|
|
111
|
+
│ ┌──────────────┐ │
|
|
112
|
+
│ │ Results │ │
|
|
113
|
+
│ │ │ │
|
|
114
|
+
│ └──────────────┘ │
|
|
115
|
+
│ │
|
|
116
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### 1.2 Core Components
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
/**
|
|
123
|
+
* Main Formula Engine class - completely domain-agnostic
|
|
124
|
+
*/
|
|
125
|
+
class FormulaEngine {
|
|
126
|
+
// Configuration
|
|
127
|
+
private config: FormulaEngineConfig;
|
|
128
|
+
|
|
129
|
+
// Caches
|
|
130
|
+
private astCache: Map<string, ASTNode>;
|
|
131
|
+
private dependencyCache: Map<string, Set<string>>;
|
|
132
|
+
|
|
133
|
+
// Registered functions
|
|
134
|
+
private functions: Map<string, FormulaFunction>;
|
|
135
|
+
|
|
136
|
+
// Methods
|
|
137
|
+
parse(expression: string): ASTNode;
|
|
138
|
+
extractDependencies(expression: string): Set<string>;
|
|
139
|
+
buildDependencyGraph(formulas: FormulaDefinition[]): DependencyGraph;
|
|
140
|
+
getEvaluationOrder(formulas: FormulaDefinition[]): string[];
|
|
141
|
+
evaluate(expression: string, context: EvaluationContext): EvaluationResult;
|
|
142
|
+
evaluateAll(formulas: FormulaDefinition[], context: EvaluationContext): Map<string, EvaluationResult>;
|
|
143
|
+
|
|
144
|
+
// Extension points
|
|
145
|
+
registerFunction(name: string, fn: FormulaFunction): void;
|
|
146
|
+
registerOperator(operator: string, handler: OperatorHandler): void;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 2. Configuration Interfaces
|
|
153
|
+
|
|
154
|
+
#### 2.1 Formula Definition
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
/**
|
|
158
|
+
* A single formula definition - the basic unit of configuration
|
|
159
|
+
* The engine knows nothing about what these formulas represent
|
|
160
|
+
*/
|
|
161
|
+
interface FormulaDefinition {
|
|
162
|
+
/** Unique identifier for this formula */
|
|
163
|
+
id: string;
|
|
164
|
+
|
|
165
|
+
/** The expression to evaluate */
|
|
166
|
+
expression: string;
|
|
167
|
+
|
|
168
|
+
/** Optional: Explicit dependencies (auto-extracted if not provided) */
|
|
169
|
+
dependencies?: string[];
|
|
170
|
+
|
|
171
|
+
/** Error handling behavior */
|
|
172
|
+
onError?: ErrorBehavior;
|
|
173
|
+
|
|
174
|
+
/** Default value when error occurs (if onError = 'DEFAULT') */
|
|
175
|
+
defaultValue?: unknown;
|
|
176
|
+
|
|
177
|
+
/** Rounding configuration (for numeric results) */
|
|
178
|
+
rounding?: RoundingConfig;
|
|
179
|
+
|
|
180
|
+
/** Optional metadata (passed through, not used by engine) */
|
|
181
|
+
metadata?: Record<string, unknown>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface ErrorBehavior {
|
|
185
|
+
type: 'THROW' | 'NULL' | 'ZERO' | 'DEFAULT' | 'SKIP';
|
|
186
|
+
defaultValue?: unknown;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface RoundingConfig {
|
|
190
|
+
mode: 'HALF_UP' | 'HALF_DOWN' | 'FLOOR' | 'CEIL' | 'NONE';
|
|
191
|
+
precision: number;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### 2.2 Evaluation Context
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
/**
|
|
199
|
+
* Context provided to the engine for evaluation
|
|
200
|
+
* Can contain any data structure - engine is agnostic
|
|
201
|
+
*/
|
|
202
|
+
interface EvaluationContext {
|
|
203
|
+
/** Variables available for reference in expressions */
|
|
204
|
+
variables: Record<string, unknown>;
|
|
205
|
+
|
|
206
|
+
/** Optional: Arrays for aggregation functions */
|
|
207
|
+
collections?: Record<string, unknown[]>;
|
|
208
|
+
|
|
209
|
+
/** Optional: Additional context passed to custom functions */
|
|
210
|
+
extra?: Record<string, unknown>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Example context (domain-specific, NOT part of engine):
|
|
214
|
+
const invoiceContext: EvaluationContext = {
|
|
215
|
+
variables: {
|
|
216
|
+
unitPrice: 100,
|
|
217
|
+
quantity: 5,
|
|
218
|
+
discountRate: 0.1,
|
|
219
|
+
vatRate: 0.19,
|
|
220
|
+
},
|
|
221
|
+
collections: {
|
|
222
|
+
lineItems: [
|
|
223
|
+
{ price: 100, qty: 2 },
|
|
224
|
+
{ price: 200, qty: 1 },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### 2.3 Engine Configuration
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
/**
|
|
234
|
+
* Configuration for the Formula Engine itself
|
|
235
|
+
*/
|
|
236
|
+
interface FormulaEngineConfig {
|
|
237
|
+
/** Enable expression caching (default: true) */
|
|
238
|
+
enableCache?: boolean;
|
|
239
|
+
|
|
240
|
+
/** Maximum cache size (default: 1000) */
|
|
241
|
+
maxCacheSize?: number;
|
|
242
|
+
|
|
243
|
+
/** Default error behavior (default: THROW) */
|
|
244
|
+
defaultErrorBehavior?: ErrorBehavior;
|
|
245
|
+
|
|
246
|
+
/** Default rounding config */
|
|
247
|
+
defaultRounding?: RoundingConfig;
|
|
248
|
+
|
|
249
|
+
/** Variable reference prefix (default: '$') */
|
|
250
|
+
variablePrefix?: string;
|
|
251
|
+
|
|
252
|
+
/** Context variable prefix (default: '@') */
|
|
253
|
+
contextPrefix?: string;
|
|
254
|
+
|
|
255
|
+
/** Enable strict mode - fail on undefined variables (default: true) */
|
|
256
|
+
strictMode?: boolean;
|
|
257
|
+
|
|
258
|
+
/** Custom operators */
|
|
259
|
+
operators?: OperatorDefinition[];
|
|
260
|
+
|
|
261
|
+
/** Custom functions */
|
|
262
|
+
functions?: FunctionDefinition[];
|
|
263
|
+
|
|
264
|
+
/** Decimal arithmetic configuration */
|
|
265
|
+
decimal?: DecimalConfig;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Configuration for decimal arithmetic
|
|
270
|
+
*/
|
|
271
|
+
interface DecimalConfig {
|
|
272
|
+
/** Default precision for decimal operations (default: 20) */
|
|
273
|
+
precision?: number;
|
|
274
|
+
|
|
275
|
+
/** Default rounding mode (default: HALF_UP) */
|
|
276
|
+
roundingMode?: DecimalRoundingMode;
|
|
277
|
+
|
|
278
|
+
/** Scale for division results (default: 10) */
|
|
279
|
+
divisionScale?: number;
|
|
280
|
+
|
|
281
|
+
/** Preserve trailing zeros in output (default: false) */
|
|
282
|
+
preserveTrailingZeros?: boolean;
|
|
283
|
+
|
|
284
|
+
/** Auto-convert JS numbers to Decimal (default: true) */
|
|
285
|
+
autoConvertFloats?: boolean;
|
|
286
|
+
|
|
287
|
+
/** Maximum exponent (default: 1000) */
|
|
288
|
+
maxExponent?: number;
|
|
289
|
+
|
|
290
|
+
/** Minimum exponent (default: -1000) */
|
|
291
|
+
minExponent?: number;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### 3. Expression Language Specification
|
|
298
|
+
|
|
299
|
+
#### 3.1 Syntax Overview
|
|
300
|
+
|
|
301
|
+
The formula language is a simple, safe expression language designed for configuration-driven calculations.
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
Expression := Term (('+' | '-') Term)*
|
|
305
|
+
Term := Factor (('*' | '/' | '%') Factor)*
|
|
306
|
+
Factor := Base ('^' Base)?
|
|
307
|
+
Base := Unary | Primary
|
|
308
|
+
Unary := ('-' | '!' | 'NOT') Base
|
|
309
|
+
Primary := Number | String | Boolean | Variable | FunctionCall | '(' Expression ')'
|
|
310
|
+
Variable := '$' Identifier | '@' Identifier
|
|
311
|
+
FunctionCall := Identifier '(' Arguments? ')'
|
|
312
|
+
Arguments := Expression (',' Expression)*
|
|
313
|
+
Identifier := [a-zA-Z_][a-zA-Z0-9_]*
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### 3.2 Data Types
|
|
317
|
+
|
|
318
|
+
| Type | Examples | Description |
|
|
319
|
+
|------|----------|-------------|
|
|
320
|
+
| Decimal | `42`, `3.14`, `-100.50`, `0.001` | Arbitrary-precision decimal (default for numbers) |
|
|
321
|
+
| Number | `42f`, `3.14f`, `1e6` | 64-bit floating point (explicit, use `f` suffix) |
|
|
322
|
+
| String | `"hello"`, `'world'` | UTF-8 strings |
|
|
323
|
+
| Boolean | `true`, `false` | Boolean values |
|
|
324
|
+
| Null | `null` | Null/undefined value |
|
|
325
|
+
| Array | `[1, 2, 3]` | Array literals |
|
|
326
|
+
|
|
327
|
+
**Important:** By default, all numeric literals are parsed as `Decimal` for precision. Use the `f` suffix (e.g., `3.14f`) to explicitly use floating-point when performance is preferred over precision.
|
|
328
|
+
|
|
329
|
+
#### 3.3 Operators
|
|
330
|
+
|
|
331
|
+
**Arithmetic Operators:**
|
|
332
|
+
|
|
333
|
+
| Operator | Name | Example | Result |
|
|
334
|
+
|----------|------|---------|--------|
|
|
335
|
+
| `+` | Addition | `5 + 3` | `8` |
|
|
336
|
+
| `-` | Subtraction | `5 - 3` | `2` |
|
|
337
|
+
| `*` | Multiplication | `5 * 3` | `15` |
|
|
338
|
+
| `/` | Division | `6 / 3` | `2` |
|
|
339
|
+
| `%` | Modulo | `7 % 3` | `1` |
|
|
340
|
+
| `^` | Power | `2 ^ 3` | `8` |
|
|
341
|
+
| `-` (unary) | Negation | `-5` | `-5` |
|
|
342
|
+
|
|
343
|
+
**Comparison Operators:**
|
|
344
|
+
|
|
345
|
+
| Operator | Name | Example | Result |
|
|
346
|
+
|----------|------|---------|--------|
|
|
347
|
+
| `==` | Equal | `5 == 5` | `true` |
|
|
348
|
+
| `!=` | Not equal | `5 != 3` | `true` |
|
|
349
|
+
| `>` | Greater than | `5 > 3` | `true` |
|
|
350
|
+
| `<` | Less than | `5 < 3` | `false` |
|
|
351
|
+
| `>=` | Greater or equal | `5 >= 5` | `true` |
|
|
352
|
+
| `<=` | Less or equal | `5 <= 3` | `false` |
|
|
353
|
+
|
|
354
|
+
**Logical Operators:**
|
|
355
|
+
|
|
356
|
+
| Operator | Name | Example | Result |
|
|
357
|
+
|----------|------|---------|--------|
|
|
358
|
+
| `&&` / `AND` | Logical AND | `true && false` | `false` |
|
|
359
|
+
| `\|\|` / `OR` | Logical OR | `true \|\| false` | `true` |
|
|
360
|
+
| `!` / `NOT` | Logical NOT | `!true` | `false` |
|
|
361
|
+
|
|
362
|
+
**Conditional Operator:**
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
condition ? valueIfTrue : valueIfFalse
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Example: `$quantity > 10 ? $unitPrice * 0.9 : $unitPrice`
|
|
369
|
+
|
|
370
|
+
#### 3.4 Variable References
|
|
371
|
+
|
|
372
|
+
**Local Variables (`$` prefix):**
|
|
373
|
+
- Reference values in `context.variables`
|
|
374
|
+
- Example: `$unitPrice`, `$quantity`, `$taxRate`
|
|
375
|
+
|
|
376
|
+
**Context Variables (`@` prefix):**
|
|
377
|
+
- Reference values in `context.extra`
|
|
378
|
+
- Example: `@currentDate`, `@userId`, `@locale`
|
|
379
|
+
|
|
380
|
+
**Nested Access:**
|
|
381
|
+
- Dot notation: `$product.price`, `$customer.address.city`
|
|
382
|
+
- Bracket notation: `$items[0]`, `$data["key"]`
|
|
383
|
+
|
|
384
|
+
#### 3.5 Built-in Functions
|
|
385
|
+
|
|
386
|
+
**Math Functions:**
|
|
387
|
+
|
|
388
|
+
| Function | Description | Example |
|
|
389
|
+
|----------|-------------|---------|
|
|
390
|
+
| `ABS(x)` | Absolute value | `ABS(-5)` → `5` |
|
|
391
|
+
| `ROUND(x, p?)` | Round to precision | `ROUND(3.456, 2)` → `3.46` |
|
|
392
|
+
| `FLOOR(x)` | Round down | `FLOOR(3.9)` → `3` |
|
|
393
|
+
| `CEIL(x)` | Round up | `CEIL(3.1)` → `4` |
|
|
394
|
+
| `MIN(a, b, ...)` | Minimum value | `MIN(5, 3, 8)` → `3` |
|
|
395
|
+
| `MAX(a, b, ...)` | Maximum value | `MAX(5, 3, 8)` → `8` |
|
|
396
|
+
| `POW(x, y)` | Power | `POW(2, 3)` → `8` |
|
|
397
|
+
| `SQRT(x)` | Square root | `SQRT(16)` → `4` |
|
|
398
|
+
| `LOG(x)` | Natural logarithm | `LOG(10)` → `2.302...` |
|
|
399
|
+
| `LOG10(x)` | Base-10 logarithm | `LOG10(100)` → `2` |
|
|
400
|
+
|
|
401
|
+
**Aggregation Functions:**
|
|
402
|
+
|
|
403
|
+
| Function | Description | Example |
|
|
404
|
+
|----------|-------------|---------|
|
|
405
|
+
| `SUM(arr)` | Sum of array | `SUM($items)` |
|
|
406
|
+
| `SUM(arr, expr)` | Sum with expression | `SUM($items, $it.price * $it.qty)` |
|
|
407
|
+
| `AVG(arr)` | Average | `AVG($scores)` |
|
|
408
|
+
| `COUNT(arr)` | Count elements | `COUNT($items)` |
|
|
409
|
+
| `PRODUCT(arr)` | Product of values | `PRODUCT($multipliers)` |
|
|
410
|
+
| `FILTER(arr, cond)` | Filter array | `FILTER($items, $it.active)` |
|
|
411
|
+
| `MAP(arr, expr)` | Transform array | `MAP($items, $it.price)` |
|
|
412
|
+
|
|
413
|
+
**String Functions:**
|
|
414
|
+
|
|
415
|
+
| Function | Description | Example |
|
|
416
|
+
|----------|-------------|---------|
|
|
417
|
+
| `LEN(s)` | String length | `LEN("hello")` → `5` |
|
|
418
|
+
| `UPPER(s)` | Uppercase | `UPPER("hello")` → `"HELLO"` |
|
|
419
|
+
| `LOWER(s)` | Lowercase | `LOWER("HELLO")` → `"hello"` |
|
|
420
|
+
| `TRIM(s)` | Trim whitespace | `TRIM(" hi ")` → `"hi"` |
|
|
421
|
+
| `CONCAT(a, b, ...)` | Concatenate | `CONCAT("a", "b")` → `"ab"` |
|
|
422
|
+
| `SUBSTR(s, start, len?)` | Substring | `SUBSTR("hello", 1, 3)` → `"ell"` |
|
|
423
|
+
|
|
424
|
+
**Logical Functions:**
|
|
425
|
+
|
|
426
|
+
| Function | Description | Example |
|
|
427
|
+
|----------|-------------|---------|
|
|
428
|
+
| `IF(cond, t, f)` | Conditional | `IF($x > 5, "big", "small")` |
|
|
429
|
+
| `COALESCE(a, b, ...)` | First non-null | `COALESCE($a, $b, 0)` |
|
|
430
|
+
| `ISNULL(x)` | Check null | `ISNULL($value)` |
|
|
431
|
+
| `ISEMPTY(x)` | Check empty | `ISEMPTY($array)` |
|
|
432
|
+
| `DEFAULT(x, d)` | Default if null | `DEFAULT($x, 0)` |
|
|
433
|
+
|
|
434
|
+
**Type Functions:**
|
|
435
|
+
|
|
436
|
+
| Function | Description | Example |
|
|
437
|
+
|----------|-------------|---------|
|
|
438
|
+
| `NUMBER(x)` | Convert to number | `NUMBER("42")` → `42` |
|
|
439
|
+
| `STRING(x)` | Convert to string | `STRING(42)` → `"42"` |
|
|
440
|
+
| `BOOLEAN(x)` | Convert to boolean | `BOOLEAN(1)` → `true` |
|
|
441
|
+
| `TYPEOF(x)` | Get type name | `TYPEOF(42)` → `"number"` |
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
### 4. Decimal Arithmetic System
|
|
446
|
+
|
|
447
|
+
#### 4.1 Why Decimal?
|
|
448
|
+
|
|
449
|
+
JavaScript's native `Number` type uses IEEE 754 floating-point representation, which cannot accurately represent many decimal fractions:
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
// Floating-point precision problems:
|
|
453
|
+
0.1 + 0.2 // 0.30000000000000004 (wrong!)
|
|
454
|
+
0.1 * 0.1 // 0.010000000000000002 (wrong!)
|
|
455
|
+
1000.10 - 1000.00 // 0.09999999999990905 (wrong!)
|
|
456
|
+
19.99 * 100 // 1998.9999999999998 (wrong!)
|
|
457
|
+
|
|
458
|
+
// With Decimal:
|
|
459
|
+
Decimal("0.1") + Decimal("0.2") // 0.3 (correct!)
|
|
460
|
+
Decimal("0.1") * Decimal("0.1") // 0.01 (correct!)
|
|
461
|
+
Decimal("1000.10") - Decimal("1000.00") // 0.10 (correct!)
|
|
462
|
+
Decimal("19.99") * Decimal("100") // 1999.00 (correct!)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
For financial calculations, tax computations, and any domain requiring exact decimal representation, floating-point errors are unacceptable.
|
|
466
|
+
|
|
467
|
+
#### 4.2 Decimal Configuration
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
interface DecimalConfig {
|
|
471
|
+
/**
|
|
472
|
+
* Default precision for decimal operations (significant digits)
|
|
473
|
+
* Default: 20
|
|
474
|
+
*/
|
|
475
|
+
precision: number;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Default rounding mode for decimal operations
|
|
479
|
+
* Default: HALF_UP (banker's rounding alternative: HALF_EVEN)
|
|
480
|
+
*/
|
|
481
|
+
roundingMode: DecimalRoundingMode;
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Scale (decimal places) for division results
|
|
485
|
+
* Default: 10
|
|
486
|
+
*/
|
|
487
|
+
divisionScale: number;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Whether to preserve trailing zeros in display
|
|
491
|
+
* Default: false
|
|
492
|
+
*/
|
|
493
|
+
preserveTrailingZeros: boolean;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Maximum exponent allowed (for overflow protection)
|
|
497
|
+
* Default: 1000
|
|
498
|
+
*/
|
|
499
|
+
maxExponent: number;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Minimum exponent allowed (for underflow protection)
|
|
503
|
+
* Default: -1000
|
|
504
|
+
*/
|
|
505
|
+
minExponent: number;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Automatically convert floats to decimals in context
|
|
509
|
+
* Default: true
|
|
510
|
+
*/
|
|
511
|
+
autoConvertFloats: boolean;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
enum DecimalRoundingMode {
|
|
515
|
+
/** Round towards positive infinity */
|
|
516
|
+
CEIL = 'CEIL',
|
|
517
|
+
|
|
518
|
+
/** Round towards negative infinity */
|
|
519
|
+
FLOOR = 'FLOOR',
|
|
520
|
+
|
|
521
|
+
/** Round towards zero (truncate) */
|
|
522
|
+
DOWN = 'DOWN',
|
|
523
|
+
|
|
524
|
+
/** Round away from zero */
|
|
525
|
+
UP = 'UP',
|
|
526
|
+
|
|
527
|
+
/** Round to nearest, ties go away from zero (standard rounding) */
|
|
528
|
+
HALF_UP = 'HALF_UP',
|
|
529
|
+
|
|
530
|
+
/** Round to nearest, ties go towards zero */
|
|
531
|
+
HALF_DOWN = 'HALF_DOWN',
|
|
532
|
+
|
|
533
|
+
/** Round to nearest, ties go to even (banker's rounding) */
|
|
534
|
+
HALF_EVEN = 'HALF_EVEN',
|
|
535
|
+
|
|
536
|
+
/** Round to nearest, ties go to odd */
|
|
537
|
+
HALF_ODD = 'HALF_ODD',
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
#### 4.3 Decimal Type Interface
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
/**
|
|
545
|
+
* Immutable arbitrary-precision decimal number
|
|
546
|
+
* All operations return new Decimal instances
|
|
547
|
+
*/
|
|
548
|
+
interface Decimal {
|
|
549
|
+
// Properties
|
|
550
|
+
readonly value: string; // String representation
|
|
551
|
+
readonly precision: number; // Total significant digits
|
|
552
|
+
readonly scale: number; // Digits after decimal point
|
|
553
|
+
readonly sign: -1 | 0 | 1; // Sign of the number
|
|
554
|
+
readonly isZero: boolean;
|
|
555
|
+
readonly isPositive: boolean;
|
|
556
|
+
readonly isNegative: boolean;
|
|
557
|
+
readonly isInteger: boolean;
|
|
558
|
+
|
|
559
|
+
// Arithmetic operations (return new Decimal)
|
|
560
|
+
add(other: DecimalLike): Decimal;
|
|
561
|
+
subtract(other: DecimalLike): Decimal;
|
|
562
|
+
multiply(other: DecimalLike): Decimal;
|
|
563
|
+
divide(other: DecimalLike, scale?: number, roundingMode?: DecimalRoundingMode): Decimal;
|
|
564
|
+
modulo(other: DecimalLike): Decimal;
|
|
565
|
+
power(exponent: number): Decimal;
|
|
566
|
+
negate(): Decimal;
|
|
567
|
+
abs(): Decimal;
|
|
568
|
+
|
|
569
|
+
// Comparison operations
|
|
570
|
+
compareTo(other: DecimalLike): -1 | 0 | 1;
|
|
571
|
+
equals(other: DecimalLike): boolean;
|
|
572
|
+
greaterThan(other: DecimalLike): boolean;
|
|
573
|
+
greaterThanOrEqual(other: DecimalLike): boolean;
|
|
574
|
+
lessThan(other: DecimalLike): boolean;
|
|
575
|
+
lessThanOrEqual(other: DecimalLike): boolean;
|
|
576
|
+
|
|
577
|
+
// Rounding operations
|
|
578
|
+
round(scale: number, mode?: DecimalRoundingMode): Decimal;
|
|
579
|
+
floor(scale?: number): Decimal;
|
|
580
|
+
ceil(scale?: number): Decimal;
|
|
581
|
+
truncate(scale?: number): Decimal;
|
|
582
|
+
|
|
583
|
+
// Conversion
|
|
584
|
+
toNumber(): number; // Convert to JS number (may lose precision)
|
|
585
|
+
toString(): string; // String representation
|
|
586
|
+
toFixed(scale: number): string; // Fixed decimal places
|
|
587
|
+
toExponential(fractionDigits?: number): string;
|
|
588
|
+
toJSON(): string; // For JSON serialization
|
|
589
|
+
|
|
590
|
+
// Static factory methods
|
|
591
|
+
static from(value: DecimalLike): Decimal;
|
|
592
|
+
static fromNumber(value: number): Decimal;
|
|
593
|
+
static fromString(value: string): Decimal;
|
|
594
|
+
static zero(): Decimal;
|
|
595
|
+
static one(): Decimal;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Types that can be converted to Decimal */
|
|
599
|
+
type DecimalLike = Decimal | string | number | bigint;
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
#### 4.4 Decimal Literals in Expressions
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// All numeric literals are Decimal by default
|
|
606
|
+
"100" // Decimal(100)
|
|
607
|
+
"3.14159" // Decimal(3.14159)
|
|
608
|
+
"-0.001" // Decimal(-0.001)
|
|
609
|
+
"1234567890.123456789" // Decimal with full precision
|
|
610
|
+
|
|
611
|
+
// Explicit float suffix for performance-critical non-financial math
|
|
612
|
+
"3.14159f" // JavaScript number (float)
|
|
613
|
+
"1e6" // Scientific notation → float
|
|
614
|
+
"1e6d" // Scientific notation → Decimal
|
|
615
|
+
|
|
616
|
+
// Decimal arithmetic in expressions
|
|
617
|
+
"$price * $quantity" // Decimal * Decimal → Decimal
|
|
618
|
+
"$price * 1.19" // Decimal * Decimal → Decimal
|
|
619
|
+
"ROUND($total, 2)" // Round Decimal to 2 places
|
|
620
|
+
"$amount / 3" // Decimal division with configured scale
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### 4.5 Decimal Functions
|
|
624
|
+
|
|
625
|
+
| Function | Description | Example | Result |
|
|
626
|
+
|----------|-------------|---------|--------|
|
|
627
|
+
| `DECIMAL(x)` | Convert to Decimal | `DECIMAL("123.45")` | `Decimal(123.45)` |
|
|
628
|
+
| `DECIMAL(x, scale)` | Convert with scale | `DECIMAL(10, 2)` | `Decimal(10.00)` |
|
|
629
|
+
| `ROUND(x, scale)` | Round to scale | `ROUND(3.456, 2)` | `Decimal(3.46)` |
|
|
630
|
+
| `ROUND(x, scale, mode)` | Round with mode | `ROUND(2.5, 0, "HALF_EVEN")` | `Decimal(2)` |
|
|
631
|
+
| `FLOOR(x, scale?)` | Round down | `FLOOR(3.9, 0)` | `Decimal(3)` |
|
|
632
|
+
| `CEIL(x, scale?)` | Round up | `CEIL(3.1, 0)` | `Decimal(4)` |
|
|
633
|
+
| `TRUNCATE(x, scale?)` | Truncate | `TRUNCATE(3.999, 2)` | `Decimal(3.99)` |
|
|
634
|
+
| `SCALE(x)` | Get scale | `SCALE(123.45)` | `2` |
|
|
635
|
+
| `PRECISION(x)` | Get precision | `PRECISION(123.45)` | `5` |
|
|
636
|
+
| `SIGN(x)` | Get sign | `SIGN(-5)` | `-1` |
|
|
637
|
+
|
|
638
|
+
#### 4.6 Rounding Mode Examples
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// Value: 2.5, rounding to 0 decimal places
|
|
642
|
+
|
|
643
|
+
ROUND(2.5, 0, "CEIL") // 3 - Round towards +∞
|
|
644
|
+
ROUND(2.5, 0, "FLOOR") // 2 - Round towards -∞
|
|
645
|
+
ROUND(2.5, 0, "DOWN") // 2 - Round towards 0 (truncate)
|
|
646
|
+
ROUND(2.5, 0, "UP") // 3 - Round away from 0
|
|
647
|
+
ROUND(2.5, 0, "HALF_UP") // 3 - Ties go away from 0 (standard)
|
|
648
|
+
ROUND(2.5, 0, "HALF_DOWN") // 2 - Ties go towards 0
|
|
649
|
+
ROUND(2.5, 0, "HALF_EVEN") // 2 - Ties go to even (banker's)
|
|
650
|
+
ROUND(3.5, 0, "HALF_EVEN") // 4 - Ties go to even (banker's)
|
|
651
|
+
|
|
652
|
+
// Negative numbers
|
|
653
|
+
ROUND(-2.5, 0, "HALF_UP") // -3 - Away from zero
|
|
654
|
+
ROUND(-2.5, 0, "FLOOR") // -3 - Towards -∞
|
|
655
|
+
ROUND(-2.5, 0, "CEIL") // -2 - Towards +∞
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
#### 4.7 Division Behavior
|
|
659
|
+
|
|
660
|
+
Division requires special handling for scale (decimal places):
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
interface DivisionConfig {
|
|
664
|
+
/**
|
|
665
|
+
* Default scale for division results
|
|
666
|
+
* If the exact result has more decimals, it's rounded
|
|
667
|
+
*/
|
|
668
|
+
defaultScale: number; // Default: 10
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Rounding mode for division
|
|
672
|
+
*/
|
|
673
|
+
roundingMode: DecimalRoundingMode; // Default: HALF_UP
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Behavior when dividing by zero
|
|
677
|
+
*/
|
|
678
|
+
onDivideByZero: 'THROW' | 'NULL' | 'INFINITY';
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Examples:
|
|
682
|
+
"10 / 3" // 3.3333333333 (10 decimal places by default)
|
|
683
|
+
"10 / 3 | ROUND(2)" // 3.33 (pipe to round)
|
|
684
|
+
"DIVIDE(10, 3, 2)" // 3.33 (specify scale in function)
|
|
685
|
+
"DIVIDE(10, 3, 4, 'FLOOR')" // 3.3333 (specify scale and rounding)
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
#### 4.8 Automatic Type Coercion
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// Context values are automatically converted to Decimal when autoConvertFloats is true
|
|
692
|
+
const context = {
|
|
693
|
+
variables: {
|
|
694
|
+
price: 19.99, // number → Decimal(19.99)
|
|
695
|
+
quantity: 5, // number → Decimal(5)
|
|
696
|
+
rate: "0.19", // string → Decimal(0.19)
|
|
697
|
+
discount: Decimal("10.00"), // Already Decimal, kept as-is
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Mixed operations
|
|
702
|
+
"$price * $quantity" // Decimal * Decimal → Decimal
|
|
703
|
+
"$price + 100" // Decimal + Decimal(100) → Decimal
|
|
704
|
+
"$price * 1.19" // Decimal * Decimal(1.19) → Decimal
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
#### 4.9 Precision Preservation
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
// Precision is preserved through operations
|
|
711
|
+
const a = Decimal("1.10");
|
|
712
|
+
const b = Decimal("1.20");
|
|
713
|
+
const sum = a.add(b); // Decimal("2.30"), not "2.3"
|
|
714
|
+
|
|
715
|
+
// Configuration option for trailing zeros
|
|
716
|
+
const config = {
|
|
717
|
+
decimal: {
|
|
718
|
+
preserveTrailingZeros: true, // "2.30" instead of "2.3"
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// Scale is the maximum of operand scales for add/subtract
|
|
723
|
+
Decimal("1.1").add(Decimal("2.22")); // Scale 2: "3.32"
|
|
724
|
+
Decimal("1.100").add(Decimal("2.2")); // Scale 3: "3.300"
|
|
725
|
+
|
|
726
|
+
// Multiplication: sum of scales (then can be trimmed)
|
|
727
|
+
Decimal("1.5").multiply(Decimal("2.5")); // "3.75" (scale 1+1=2)
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
#### 4.10 Performance Considerations
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// Decimal operations are slower than native floats
|
|
734
|
+
// Use floats when:
|
|
735
|
+
// 1. Precision is not critical (physics, graphics, statistics)
|
|
736
|
+
// 2. Performance is paramount
|
|
737
|
+
// 3. Values will be rounded anyway
|
|
738
|
+
|
|
739
|
+
// Performance comparison (approximate):
|
|
740
|
+
// Float addition: ~1 nanosecond
|
|
741
|
+
// Decimal addition: ~100-500 nanoseconds
|
|
742
|
+
|
|
743
|
+
// Optimization strategies:
|
|
744
|
+
interface PerformanceConfig {
|
|
745
|
+
/**
|
|
746
|
+
* Use native floats for intermediate calculations,
|
|
747
|
+
* convert to Decimal only for final results
|
|
748
|
+
*/
|
|
749
|
+
useFloatIntermediates: boolean;
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Cache frequently used Decimal values
|
|
753
|
+
*/
|
|
754
|
+
cacheCommonValues: boolean;
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Common values to pre-cache
|
|
758
|
+
*/
|
|
759
|
+
cachedValues: string[]; // e.g., ["0", "1", "0.19", "100"]
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
#### 4.11 Serialization
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
// JSON serialization preserves precision
|
|
767
|
+
const result = {
|
|
768
|
+
total: Decimal("1234567890.123456789")
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
JSON.stringify(result);
|
|
772
|
+
// '{"total":"1234567890.123456789"}'
|
|
773
|
+
|
|
774
|
+
// Configuration for serialization format
|
|
775
|
+
interface SerializationConfig {
|
|
776
|
+
/**
|
|
777
|
+
* How to serialize Decimal values
|
|
778
|
+
*/
|
|
779
|
+
decimalFormat: 'STRING' | 'NUMBER' | 'OBJECT';
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* For OBJECT format, the property name
|
|
783
|
+
*/
|
|
784
|
+
decimalProperty?: string; // e.g., "$decimal"
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// STRING format (default, safest):
|
|
788
|
+
// { "total": "1234567890.123456789" }
|
|
789
|
+
|
|
790
|
+
// NUMBER format (may lose precision!):
|
|
791
|
+
// { "total": 1234567890.123456789 }
|
|
792
|
+
|
|
793
|
+
// OBJECT format (explicit typing):
|
|
794
|
+
// { "total": { "$decimal": "1234567890.123456789" } }
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
#### 4.12 Error Handling for Decimals
|
|
798
|
+
|
|
799
|
+
```typescript
|
|
800
|
+
class DecimalError extends FormulaEngineError {
|
|
801
|
+
code = 'DECIMAL_ERROR';
|
|
802
|
+
category = 'EVALUATION';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
class DecimalOverflowError extends DecimalError {
|
|
806
|
+
code = 'DECIMAL_OVERFLOW';
|
|
807
|
+
constructor(public value: string, public maxExponent: number) {
|
|
808
|
+
super(`Decimal overflow: exponent exceeds ${maxExponent}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
class DecimalUnderflowError extends DecimalError {
|
|
813
|
+
code = 'DECIMAL_UNDERFLOW';
|
|
814
|
+
constructor(public value: string, public minExponent: number) {
|
|
815
|
+
super(`Decimal underflow: exponent below ${minExponent}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
class DecimalDivisionByZeroError extends DecimalError {
|
|
820
|
+
code = 'DECIMAL_DIVISION_BY_ZERO';
|
|
821
|
+
constructor() {
|
|
822
|
+
super('Division by zero');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
class InvalidDecimalError extends DecimalError {
|
|
827
|
+
code = 'INVALID_DECIMAL';
|
|
828
|
+
constructor(public input: string) {
|
|
829
|
+
super(`Invalid decimal value: "${input}"`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
#### 4.13 Implementation Recommendation
|
|
835
|
+
|
|
836
|
+
The Formula Engine should use a well-tested arbitrary-precision decimal library. Recommended options:
|
|
837
|
+
|
|
838
|
+
| Library | Pros | Cons |
|
|
839
|
+
|---------|------|------|
|
|
840
|
+
| **decimal.js** | Full-featured, well-tested, good docs | Larger bundle size |
|
|
841
|
+
| **big.js** | Lightweight, simple API | Fewer features |
|
|
842
|
+
| **bignumber.js** | By same author as decimal.js | Similar to decimal.js |
|
|
843
|
+
| **Custom** | Full control, minimal deps | Development effort |
|
|
844
|
+
|
|
845
|
+
**Recommended: `decimal.js`** for its comprehensive feature set and battle-tested reliability.
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
// Example integration with decimal.js
|
|
849
|
+
import Decimal from 'decimal.js';
|
|
850
|
+
|
|
851
|
+
// Configure globally
|
|
852
|
+
Decimal.set({
|
|
853
|
+
precision: 20,
|
|
854
|
+
rounding: Decimal.ROUND_HALF_UP,
|
|
855
|
+
toExpNeg: -1000,
|
|
856
|
+
toExpPos: 1000,
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Wrap for Formula Engine
|
|
860
|
+
class FormulaDecimal {
|
|
861
|
+
private value: Decimal;
|
|
862
|
+
|
|
863
|
+
constructor(value: DecimalLike) {
|
|
864
|
+
this.value = new Decimal(value);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
add(other: DecimalLike): FormulaDecimal {
|
|
868
|
+
return new FormulaDecimal(this.value.plus(other));
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ... other methods
|
|
872
|
+
}
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
### 5. Dependency Management
|
|
878
|
+
|
|
879
|
+
#### 5.1 Dependency Extraction
|
|
880
|
+
|
|
881
|
+
The engine automatically extracts dependencies by parsing expressions:
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
interface DependencyExtractor {
|
|
885
|
+
/**
|
|
886
|
+
* Extract all variable references from an expression
|
|
887
|
+
* @param expression - The formula expression
|
|
888
|
+
* @returns Set of variable names (without prefix)
|
|
889
|
+
*/
|
|
890
|
+
extract(expression: string): Set<string>;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Example:
|
|
894
|
+
const expr = "$lineTotalHT + $productVAT - $discount";
|
|
895
|
+
const deps = extractor.extract(expr);
|
|
896
|
+
// Result: Set { "lineTotalHT", "productVAT", "discount" }
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**Extraction Rules:**
|
|
900
|
+
|
|
901
|
+
1. **Simple variables:** `$varName` → extracts `varName`
|
|
902
|
+
2. **Nested variables:** `$product.price` → extracts `product`
|
|
903
|
+
3. **Array access:** `$items[0].price` → extracts `items`
|
|
904
|
+
4. **Function arguments:** `SUM($items, $it.price)` → extracts `items`
|
|
905
|
+
5. **Conditional branches:** `$a > 0 ? $b : $c` → extracts `a`, `b`, `c`
|
|
906
|
+
|
|
907
|
+
#### 5.2 Dependency Graph
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
interface DependencyGraph {
|
|
911
|
+
/** All nodes in the graph */
|
|
912
|
+
nodes: Set<string>;
|
|
913
|
+
|
|
914
|
+
/** Adjacency list: node → dependencies */
|
|
915
|
+
edges: Map<string, Set<string>>;
|
|
916
|
+
|
|
917
|
+
/** Check if graph has cycles */
|
|
918
|
+
hasCycles(): boolean;
|
|
919
|
+
|
|
920
|
+
/** Get nodes with no dependencies */
|
|
921
|
+
getRoots(): Set<string>;
|
|
922
|
+
|
|
923
|
+
/** Get all dependents of a node */
|
|
924
|
+
getDependents(nodeId: string): Set<string>;
|
|
925
|
+
|
|
926
|
+
/** Get all dependencies of a node */
|
|
927
|
+
getDependencies(nodeId: string): Set<string>;
|
|
928
|
+
|
|
929
|
+
/** Get the full dependency chain (transitive) */
|
|
930
|
+
getTransitiveDependencies(nodeId: string): Set<string>;
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
**Graph Construction Example:**
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
const formulas: FormulaDefinition[] = [
|
|
938
|
+
{ id: 'basePrice', expression: '$unitPrice * $quantity' },
|
|
939
|
+
{ id: 'discount', expression: '$basePrice * $discountRate' },
|
|
940
|
+
{ id: 'subtotal', expression: '$basePrice - $discount' },
|
|
941
|
+
{ id: 'vat', expression: '$subtotal * $vatRate' },
|
|
942
|
+
{ id: 'total', expression: '$subtotal + $vat' },
|
|
943
|
+
];
|
|
944
|
+
|
|
945
|
+
// Resulting graph:
|
|
946
|
+
// unitPrice ──┐
|
|
947
|
+
// ├──▶ basePrice ──┬──▶ discount ──┐
|
|
948
|
+
// quantity ───┘ │ │
|
|
949
|
+
// └───────────────┴──▶ subtotal ──┬──▶ vat ──┐
|
|
950
|
+
// discountRate ──────────────────▶ discount │ │
|
|
951
|
+
// vatRate ────────────────────────────────────────────────────┼──▶ vat │
|
|
952
|
+
// └──────────┴──▶ total
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
#### 5.3 Topological Sort
|
|
956
|
+
|
|
957
|
+
```typescript
|
|
958
|
+
interface TopologicalSorter {
|
|
959
|
+
/**
|
|
960
|
+
* Sort formulas in evaluation order
|
|
961
|
+
* @param graph - The dependency graph
|
|
962
|
+
* @returns Array of formula IDs in correct evaluation order
|
|
963
|
+
* @throws CircularDependencyError if cycle detected
|
|
964
|
+
*/
|
|
965
|
+
sort(graph: DependencyGraph): string[];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Using Kahn's algorithm:
|
|
969
|
+
// 1. Find all nodes with no incoming edges (roots)
|
|
970
|
+
// 2. Remove roots from graph, add to result
|
|
971
|
+
// 3. Repeat until graph is empty
|
|
972
|
+
// 4. If nodes remain, there's a cycle
|
|
973
|
+
|
|
974
|
+
// Example output for above formulas:
|
|
975
|
+
// ['unitPrice', 'quantity', 'discountRate', 'vatRate', 'basePrice', 'discount', 'subtotal', 'vat', 'total']
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
#### 5.4 Circular Dependency Detection
|
|
979
|
+
|
|
980
|
+
```typescript
|
|
981
|
+
interface CircularDependencyError extends Error {
|
|
982
|
+
/** The cycle that was detected */
|
|
983
|
+
cycle: string[];
|
|
984
|
+
|
|
985
|
+
/** All formulas involved in cycles */
|
|
986
|
+
involvedFormulas: string[];
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Example:
|
|
990
|
+
// Formula A: $B + 1
|
|
991
|
+
// Formula B: $C + 1
|
|
992
|
+
// Formula C: $A + 1
|
|
993
|
+
//
|
|
994
|
+
// Throws: CircularDependencyError {
|
|
995
|
+
// message: "Circular dependency detected: A → B → C → A",
|
|
996
|
+
// cycle: ['A', 'B', 'C', 'A'],
|
|
997
|
+
// involvedFormulas: ['A', 'B', 'C']
|
|
998
|
+
// }
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
### 6. Evaluation Engine
|
|
1004
|
+
|
|
1005
|
+
#### 6.1 Evaluation Pipeline
|
|
1006
|
+
|
|
1007
|
+
```
|
|
1008
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
1009
|
+
│ Expression │────▶│ Parse │────▶│ AST │────▶│ Evaluate │
|
|
1010
|
+
│ String │ │ │ │ │ │ │
|
|
1011
|
+
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
|
1012
|
+
│ │
|
|
1013
|
+
▼ ▼
|
|
1014
|
+
┌─────────────┐ ┌─────────────┐
|
|
1015
|
+
│ Cache │ │ Result │
|
|
1016
|
+
│ │ │ │
|
|
1017
|
+
└─────────────┘ └─────────────┘
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
#### 6.2 AST Node Types
|
|
1021
|
+
|
|
1022
|
+
```typescript
|
|
1023
|
+
type ASTNode =
|
|
1024
|
+
| DecimalLiteral
|
|
1025
|
+
| NumberLiteral
|
|
1026
|
+
| StringLiteral
|
|
1027
|
+
| BooleanLiteral
|
|
1028
|
+
| NullLiteral
|
|
1029
|
+
| ArrayLiteral
|
|
1030
|
+
| VariableReference
|
|
1031
|
+
| BinaryOperation
|
|
1032
|
+
| UnaryOperation
|
|
1033
|
+
| ConditionalExpression
|
|
1034
|
+
| FunctionCall
|
|
1035
|
+
| MemberAccess
|
|
1036
|
+
| IndexAccess;
|
|
1037
|
+
|
|
1038
|
+
interface DecimalLiteral {
|
|
1039
|
+
type: 'DecimalLiteral';
|
|
1040
|
+
value: string; // String to preserve precision
|
|
1041
|
+
raw: string; // Original text from expression
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
interface NumberLiteral {
|
|
1045
|
+
type: 'NumberLiteral';
|
|
1046
|
+
value: number; // Native JS float (when explicitly requested)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
interface VariableReference {
|
|
1050
|
+
type: 'VariableReference';
|
|
1051
|
+
prefix: '$' | '@';
|
|
1052
|
+
name: string;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
interface BinaryOperation {
|
|
1056
|
+
type: 'BinaryOperation';
|
|
1057
|
+
operator: string;
|
|
1058
|
+
left: ASTNode;
|
|
1059
|
+
right: ASTNode;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
interface FunctionCall {
|
|
1063
|
+
type: 'FunctionCall';
|
|
1064
|
+
name: string;
|
|
1065
|
+
arguments: ASTNode[];
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ... etc
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
#### 6.3 Evaluator Interface
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
interface Evaluator {
|
|
1075
|
+
/**
|
|
1076
|
+
* Evaluate a single expression
|
|
1077
|
+
*/
|
|
1078
|
+
evaluate(
|
|
1079
|
+
expression: string,
|
|
1080
|
+
context: EvaluationContext
|
|
1081
|
+
): EvaluationResult;
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Evaluate multiple formulas in dependency order
|
|
1085
|
+
*/
|
|
1086
|
+
evaluateAll(
|
|
1087
|
+
formulas: FormulaDefinition[],
|
|
1088
|
+
context: EvaluationContext
|
|
1089
|
+
): EvaluationResultSet;
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Evaluate with incremental updates
|
|
1093
|
+
* Only re-evaluates affected formulas when context changes
|
|
1094
|
+
*/
|
|
1095
|
+
evaluateIncremental(
|
|
1096
|
+
formulas: FormulaDefinition[],
|
|
1097
|
+
context: EvaluationContext,
|
|
1098
|
+
changedVariables: Set<string>,
|
|
1099
|
+
previousResults: EvaluationResultSet
|
|
1100
|
+
): EvaluationResultSet;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
interface EvaluationResult {
|
|
1104
|
+
/** The computed value */
|
|
1105
|
+
value: unknown;
|
|
1106
|
+
|
|
1107
|
+
/** Whether evaluation succeeded */
|
|
1108
|
+
success: boolean;
|
|
1109
|
+
|
|
1110
|
+
/** Error if evaluation failed */
|
|
1111
|
+
error?: EvaluationError;
|
|
1112
|
+
|
|
1113
|
+
/** Execution time in milliseconds */
|
|
1114
|
+
executionTimeMs: number;
|
|
1115
|
+
|
|
1116
|
+
/** Variables that were accessed during evaluation */
|
|
1117
|
+
accessedVariables: Set<string>;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
interface EvaluationResultSet {
|
|
1121
|
+
/** Results keyed by formula ID */
|
|
1122
|
+
results: Map<string, EvaluationResult>;
|
|
1123
|
+
|
|
1124
|
+
/** Overall success */
|
|
1125
|
+
success: boolean;
|
|
1126
|
+
|
|
1127
|
+
/** All errors encountered */
|
|
1128
|
+
errors: EvaluationError[];
|
|
1129
|
+
|
|
1130
|
+
/** Total execution time */
|
|
1131
|
+
totalExecutionTimeMs: number;
|
|
1132
|
+
|
|
1133
|
+
/** Evaluation order that was used */
|
|
1134
|
+
evaluationOrder: string[];
|
|
1135
|
+
}
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
#### 6.4 Evaluation Context Merging
|
|
1139
|
+
|
|
1140
|
+
During batch evaluation, computed results are merged into the context:
|
|
1141
|
+
|
|
1142
|
+
```typescript
|
|
1143
|
+
// Initial context:
|
|
1144
|
+
{
|
|
1145
|
+
variables: {
|
|
1146
|
+
unitPrice: 100,
|
|
1147
|
+
quantity: 5,
|
|
1148
|
+
discountRate: 0.1,
|
|
1149
|
+
vatRate: 0.19,
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// After evaluating 'basePrice = $unitPrice * $quantity':
|
|
1154
|
+
{
|
|
1155
|
+
variables: {
|
|
1156
|
+
unitPrice: 100,
|
|
1157
|
+
quantity: 5,
|
|
1158
|
+
discountRate: 0.1,
|
|
1159
|
+
vatRate: 0.19,
|
|
1160
|
+
basePrice: 500, // <-- Added
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// After evaluating 'discount = $basePrice * $discountRate':
|
|
1165
|
+
{
|
|
1166
|
+
variables: {
|
|
1167
|
+
unitPrice: 100,
|
|
1168
|
+
quantity: 5,
|
|
1169
|
+
discountRate: 0.1,
|
|
1170
|
+
vatRate: 0.19,
|
|
1171
|
+
basePrice: 500,
|
|
1172
|
+
discount: 50, // <-- Added
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// ... and so on
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
---
|
|
1180
|
+
|
|
1181
|
+
### 7. Custom Function Registration
|
|
1182
|
+
|
|
1183
|
+
#### 7.1 Function Definition Interface
|
|
1184
|
+
|
|
1185
|
+
```typescript
|
|
1186
|
+
interface FunctionDefinition {
|
|
1187
|
+
/** Function name (case-insensitive) */
|
|
1188
|
+
name: string;
|
|
1189
|
+
|
|
1190
|
+
/** Minimum number of arguments */
|
|
1191
|
+
minArgs: number;
|
|
1192
|
+
|
|
1193
|
+
/** Maximum number of arguments (-1 for unlimited) */
|
|
1194
|
+
maxArgs: number;
|
|
1195
|
+
|
|
1196
|
+
/** Argument type definitions */
|
|
1197
|
+
argTypes?: ArgumentType[];
|
|
1198
|
+
|
|
1199
|
+
/** Return type */
|
|
1200
|
+
returnType: ValueType;
|
|
1201
|
+
|
|
1202
|
+
/** The implementation */
|
|
1203
|
+
implementation: FunctionImplementation;
|
|
1204
|
+
|
|
1205
|
+
/** Description for documentation */
|
|
1206
|
+
description?: string;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
type FunctionImplementation = (
|
|
1210
|
+
args: unknown[],
|
|
1211
|
+
context: EvaluationContext,
|
|
1212
|
+
engine: FormulaEngine
|
|
1213
|
+
) => unknown;
|
|
1214
|
+
|
|
1215
|
+
type ValueType = 'number' | 'string' | 'boolean' | 'array' | 'object' | 'any';
|
|
1216
|
+
|
|
1217
|
+
interface ArgumentType {
|
|
1218
|
+
name: string;
|
|
1219
|
+
type: ValueType;
|
|
1220
|
+
required: boolean;
|
|
1221
|
+
default?: unknown;
|
|
1222
|
+
}
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
#### 7.2 Registration Example
|
|
1226
|
+
|
|
1227
|
+
```typescript
|
|
1228
|
+
const engine = new FormulaEngine();
|
|
1229
|
+
|
|
1230
|
+
// Register a custom function
|
|
1231
|
+
engine.registerFunction({
|
|
1232
|
+
name: 'DISCOUNT_TIER',
|
|
1233
|
+
minArgs: 2,
|
|
1234
|
+
maxArgs: 2,
|
|
1235
|
+
argTypes: [
|
|
1236
|
+
{ name: 'amount', type: 'number', required: true },
|
|
1237
|
+
{ name: 'tiers', type: 'array', required: true },
|
|
1238
|
+
],
|
|
1239
|
+
returnType: 'number',
|
|
1240
|
+
description: 'Calculate discount based on amount and tier thresholds',
|
|
1241
|
+
implementation: (args, context) => {
|
|
1242
|
+
const [amount, tiers] = args as [number, Array<{ threshold: number; rate: number }>];
|
|
1243
|
+
|
|
1244
|
+
// Find applicable tier
|
|
1245
|
+
const tier = tiers
|
|
1246
|
+
.filter(t => amount >= t.threshold)
|
|
1247
|
+
.sort((a, b) => b.threshold - a.threshold)[0];
|
|
1248
|
+
|
|
1249
|
+
return tier ? amount * tier.rate : 0;
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// Usage in formula:
|
|
1254
|
+
// DISCOUNT_TIER($totalAmount, @discountTiers)
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
---
|
|
1258
|
+
|
|
1259
|
+
### 8. Error Handling
|
|
1260
|
+
|
|
1261
|
+
#### 8.1 Error Types
|
|
1262
|
+
|
|
1263
|
+
```typescript
|
|
1264
|
+
abstract class FormulaEngineError extends Error {
|
|
1265
|
+
abstract readonly code: string;
|
|
1266
|
+
abstract readonly category: ErrorCategory;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
type ErrorCategory = 'PARSE' | 'VALIDATION' | 'EVALUATION' | 'CONFIGURATION';
|
|
1270
|
+
|
|
1271
|
+
// Parse errors
|
|
1272
|
+
class SyntaxError extends FormulaEngineError {
|
|
1273
|
+
code = 'PARSE_SYNTAX_ERROR';
|
|
1274
|
+
category = 'PARSE';
|
|
1275
|
+
|
|
1276
|
+
constructor(
|
|
1277
|
+
message: string,
|
|
1278
|
+
public position: number,
|
|
1279
|
+
public line: number,
|
|
1280
|
+
public column: number,
|
|
1281
|
+
public expression: string
|
|
1282
|
+
) {
|
|
1283
|
+
super(message);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
class UnexpectedTokenError extends FormulaEngineError {
|
|
1288
|
+
code = 'PARSE_UNEXPECTED_TOKEN';
|
|
1289
|
+
category = 'PARSE';
|
|
1290
|
+
|
|
1291
|
+
constructor(
|
|
1292
|
+
public token: string,
|
|
1293
|
+
public expected: string[],
|
|
1294
|
+
public position: number
|
|
1295
|
+
) {
|
|
1296
|
+
super(`Unexpected token '${token}', expected one of: ${expected.join(', ')}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Validation errors
|
|
1301
|
+
class CircularDependencyError extends FormulaEngineError {
|
|
1302
|
+
code = 'VALIDATION_CIRCULAR_DEPENDENCY';
|
|
1303
|
+
category = 'VALIDATION';
|
|
1304
|
+
|
|
1305
|
+
constructor(
|
|
1306
|
+
public cycle: string[],
|
|
1307
|
+
public involvedFormulas: string[]
|
|
1308
|
+
) {
|
|
1309
|
+
super(`Circular dependency detected: ${cycle.join(' → ')}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
class UndefinedVariableError extends FormulaEngineError {
|
|
1314
|
+
code = 'VALIDATION_UNDEFINED_VARIABLE';
|
|
1315
|
+
category = 'VALIDATION';
|
|
1316
|
+
|
|
1317
|
+
constructor(
|
|
1318
|
+
public variableName: string,
|
|
1319
|
+
public expression: string
|
|
1320
|
+
) {
|
|
1321
|
+
super(`Undefined variable: ${variableName}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
class UndefinedFunctionError extends FormulaEngineError {
|
|
1326
|
+
code = 'VALIDATION_UNDEFINED_FUNCTION';
|
|
1327
|
+
category = 'VALIDATION';
|
|
1328
|
+
|
|
1329
|
+
constructor(
|
|
1330
|
+
public functionName: string
|
|
1331
|
+
) {
|
|
1332
|
+
super(`Undefined function: ${functionName}`);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Evaluation errors
|
|
1337
|
+
class DivisionByZeroError extends FormulaEngineError {
|
|
1338
|
+
code = 'EVAL_DIVISION_BY_ZERO';
|
|
1339
|
+
category = 'EVALUATION';
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
class TypeMismatchError extends FormulaEngineError {
|
|
1343
|
+
code = 'EVAL_TYPE_MISMATCH';
|
|
1344
|
+
category = 'EVALUATION';
|
|
1345
|
+
|
|
1346
|
+
constructor(
|
|
1347
|
+
public expected: ValueType,
|
|
1348
|
+
public actual: ValueType,
|
|
1349
|
+
public context: string
|
|
1350
|
+
) {
|
|
1351
|
+
super(`Type mismatch: expected ${expected}, got ${actual} in ${context}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
class ArgumentCountError extends FormulaEngineError {
|
|
1356
|
+
code = 'EVAL_ARGUMENT_COUNT';
|
|
1357
|
+
category = 'EVALUATION';
|
|
1358
|
+
|
|
1359
|
+
constructor(
|
|
1360
|
+
public functionName: string,
|
|
1361
|
+
public expected: { min: number; max: number },
|
|
1362
|
+
public actual: number
|
|
1363
|
+
) {
|
|
1364
|
+
super(
|
|
1365
|
+
`Function ${functionName} expects ${expected.min}-${expected.max} arguments, got ${actual}`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
#### 8.2 Error Recovery
|
|
1372
|
+
|
|
1373
|
+
```typescript
|
|
1374
|
+
interface ErrorRecoveryConfig {
|
|
1375
|
+
/** How to handle parse errors */
|
|
1376
|
+
onParseError: 'THROW' | 'SKIP' | 'DEFAULT';
|
|
1377
|
+
|
|
1378
|
+
/** How to handle undefined variables */
|
|
1379
|
+
onUndefinedVariable: 'THROW' | 'NULL' | 'ZERO' | 'DEFAULT';
|
|
1380
|
+
|
|
1381
|
+
/** How to handle division by zero */
|
|
1382
|
+
onDivisionByZero: 'THROW' | 'INFINITY' | 'NULL' | 'ZERO';
|
|
1383
|
+
|
|
1384
|
+
/** How to handle type mismatches */
|
|
1385
|
+
onTypeMismatch: 'THROW' | 'COERCE' | 'NULL';
|
|
1386
|
+
|
|
1387
|
+
/** Default values for recovery */
|
|
1388
|
+
defaults: {
|
|
1389
|
+
number: number;
|
|
1390
|
+
string: string;
|
|
1391
|
+
boolean: boolean;
|
|
1392
|
+
array: unknown[];
|
|
1393
|
+
object: Record<string, unknown>;
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
---
|
|
1399
|
+
|
|
1400
|
+
### 9. API Reference
|
|
1401
|
+
|
|
1402
|
+
#### 9.1 FormulaEngine Class
|
|
1403
|
+
|
|
1404
|
+
```typescript
|
|
1405
|
+
class FormulaEngine {
|
|
1406
|
+
/**
|
|
1407
|
+
* Create a new Formula Engine instance
|
|
1408
|
+
*/
|
|
1409
|
+
constructor(config?: FormulaEngineConfig);
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Parse an expression into an AST
|
|
1413
|
+
* @throws SyntaxError if expression is invalid
|
|
1414
|
+
*/
|
|
1415
|
+
parse(expression: string): ASTNode;
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Extract variable dependencies from an expression
|
|
1419
|
+
*/
|
|
1420
|
+
extractDependencies(expression: string): Set<string>;
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Build a dependency graph from formula definitions
|
|
1424
|
+
* @throws CircularDependencyError if cycles detected
|
|
1425
|
+
*/
|
|
1426
|
+
buildDependencyGraph(formulas: FormulaDefinition[]): DependencyGraph;
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Get the correct evaluation order for formulas
|
|
1430
|
+
* @throws CircularDependencyError if cycles detected
|
|
1431
|
+
*/
|
|
1432
|
+
getEvaluationOrder(formulas: FormulaDefinition[]): string[];
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* Validate formulas without evaluating
|
|
1436
|
+
*/
|
|
1437
|
+
validate(formulas: FormulaDefinition[]): ValidationResult;
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Evaluate a single expression
|
|
1441
|
+
*/
|
|
1442
|
+
evaluate(expression: string, context: EvaluationContext): EvaluationResult;
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Evaluate all formulas in correct order
|
|
1446
|
+
*/
|
|
1447
|
+
evaluateAll(
|
|
1448
|
+
formulas: FormulaDefinition[],
|
|
1449
|
+
context: EvaluationContext
|
|
1450
|
+
): EvaluationResultSet;
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Register a custom function
|
|
1454
|
+
*/
|
|
1455
|
+
registerFunction(definition: FunctionDefinition): void;
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Register multiple custom functions
|
|
1459
|
+
*/
|
|
1460
|
+
registerFunctions(definitions: FunctionDefinition[]): void;
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Get registered function names
|
|
1464
|
+
*/
|
|
1465
|
+
getRegisteredFunctions(): string[];
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Clear the AST cache
|
|
1469
|
+
*/
|
|
1470
|
+
clearCache(): void;
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Get cache statistics
|
|
1474
|
+
*/
|
|
1475
|
+
getCacheStats(): CacheStats;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
interface ValidationResult {
|
|
1479
|
+
valid: boolean;
|
|
1480
|
+
errors: FormulaEngineError[];
|
|
1481
|
+
warnings: string[];
|
|
1482
|
+
dependencyGraph: DependencyGraph;
|
|
1483
|
+
evaluationOrder: string[];
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
interface CacheStats {
|
|
1487
|
+
size: number;
|
|
1488
|
+
hits: number;
|
|
1489
|
+
misses: number;
|
|
1490
|
+
hitRate: number;
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
#### 9.2 Usage Examples
|
|
1495
|
+
|
|
1496
|
+
**Basic Evaluation:**
|
|
1497
|
+
|
|
1498
|
+
```typescript
|
|
1499
|
+
const engine = new FormulaEngine();
|
|
1500
|
+
|
|
1501
|
+
const result = engine.evaluate('$a + $b * 2', {
|
|
1502
|
+
variables: { a: 10, b: 5 }
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
console.log(result.value); // 20
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
**Batch Evaluation with Dependencies:**
|
|
1509
|
+
|
|
1510
|
+
```typescript
|
|
1511
|
+
const engine = new FormulaEngine();
|
|
1512
|
+
|
|
1513
|
+
const formulas: FormulaDefinition[] = [
|
|
1514
|
+
{ id: 'gross', expression: '$unitPrice * $quantity' },
|
|
1515
|
+
{ id: 'discount', expression: '$gross * $discountRate' },
|
|
1516
|
+
{ id: 'net', expression: '$gross - $discount' },
|
|
1517
|
+
{ id: 'tax', expression: '$net * $taxRate' },
|
|
1518
|
+
{ id: 'total', expression: '$net + $tax' },
|
|
1519
|
+
];
|
|
1520
|
+
|
|
1521
|
+
const context: EvaluationContext = {
|
|
1522
|
+
variables: {
|
|
1523
|
+
unitPrice: 100,
|
|
1524
|
+
quantity: 5,
|
|
1525
|
+
discountRate: 0.1,
|
|
1526
|
+
taxRate: 0.2,
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// Engine automatically:
|
|
1531
|
+
// 1. Extracts dependencies from each formula
|
|
1532
|
+
// 2. Builds dependency graph
|
|
1533
|
+
// 3. Detects any circular dependencies
|
|
1534
|
+
// 4. Determines correct evaluation order
|
|
1535
|
+
// 5. Evaluates in order, merging results into context
|
|
1536
|
+
|
|
1537
|
+
const results = engine.evaluateAll(formulas, context);
|
|
1538
|
+
|
|
1539
|
+
console.log(results.evaluationOrder);
|
|
1540
|
+
// ['gross', 'discount', 'net', 'tax', 'total']
|
|
1541
|
+
|
|
1542
|
+
console.log(results.results.get('total')?.value);
|
|
1543
|
+
// 540 (gross=500, discount=50, net=450, tax=90, total=540)
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
**With Custom Functions:**
|
|
1547
|
+
|
|
1548
|
+
```typescript
|
|
1549
|
+
const engine = new FormulaEngine();
|
|
1550
|
+
|
|
1551
|
+
// Register domain-specific function (but engine doesn't know it's domain-specific)
|
|
1552
|
+
engine.registerFunction({
|
|
1553
|
+
name: 'TIERED_RATE',
|
|
1554
|
+
minArgs: 2,
|
|
1555
|
+
maxArgs: 2,
|
|
1556
|
+
implementation: (args) => {
|
|
1557
|
+
const [amount, tiers] = args as [number, Array<{ min: number; rate: number }>];
|
|
1558
|
+
const tier = [...tiers].reverse().find(t => amount >= t.min);
|
|
1559
|
+
return tier?.rate ?? 0;
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
const result = engine.evaluate(
|
|
1564
|
+
'TIERED_RATE($amount, @tiers)',
|
|
1565
|
+
{
|
|
1566
|
+
variables: { amount: 15000 },
|
|
1567
|
+
extra: {
|
|
1568
|
+
tiers: [
|
|
1569
|
+
{ min: 0, rate: 0.05 },
|
|
1570
|
+
{ min: 10000, rate: 0.03 },
|
|
1571
|
+
{ min: 50000, rate: 0.02 },
|
|
1572
|
+
]
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
console.log(result.value); // 0.03
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
**Decimal Precision:**
|
|
1581
|
+
|
|
1582
|
+
```typescript
|
|
1583
|
+
const engine = new FormulaEngine({
|
|
1584
|
+
decimal: {
|
|
1585
|
+
precision: 20,
|
|
1586
|
+
roundingMode: 'HALF_UP',
|
|
1587
|
+
divisionScale: 10,
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// Floating-point would fail here
|
|
1592
|
+
const result = engine.evaluate('0.1 + 0.2', { variables: {} });
|
|
1593
|
+
console.log(result.value.toString()); // "0.3" (exact!)
|
|
1594
|
+
|
|
1595
|
+
// Financial calculation
|
|
1596
|
+
const invoice = engine.evaluateAll(
|
|
1597
|
+
[
|
|
1598
|
+
{ id: 'subtotal', expression: '$price * $quantity' },
|
|
1599
|
+
{ id: 'tax', expression: 'ROUND($subtotal * 0.19, 2)' },
|
|
1600
|
+
{ id: 'total', expression: '$subtotal + $tax' },
|
|
1601
|
+
],
|
|
1602
|
+
{
|
|
1603
|
+
variables: {
|
|
1604
|
+
price: "19.99", // String preserves precision
|
|
1605
|
+
quantity: 3,
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
console.log(invoice.results.get('subtotal')?.value.toString()); // "59.97"
|
|
1611
|
+
console.log(invoice.results.get('tax')?.value.toString()); // "11.39"
|
|
1612
|
+
console.log(invoice.results.get('total')?.value.toString()); // "71.36"
|
|
1613
|
+
|
|
1614
|
+
// Division with explicit scale
|
|
1615
|
+
const division = engine.evaluate('DIVIDE(10, 3, 4)', { variables: {} });
|
|
1616
|
+
console.log(division.value.toString()); // "3.3333"
|
|
1617
|
+
|
|
1618
|
+
// Banker's rounding
|
|
1619
|
+
const engineBanker = new FormulaEngine({
|
|
1620
|
+
decimal: { roundingMode: 'HALF_EVEN' }
|
|
1621
|
+
});
|
|
1622
|
+
console.log(engineBanker.evaluate('ROUND(2.5, 0)', { variables: {} }).value); // 2
|
|
1623
|
+
console.log(engineBanker.evaluate('ROUND(3.5, 0)', { variables: {} }).value); // 4
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
**Validation Before Evaluation:**
|
|
1627
|
+
|
|
1628
|
+
```typescript
|
|
1629
|
+
const engine = new FormulaEngine();
|
|
1630
|
+
|
|
1631
|
+
const formulas: FormulaDefinition[] = [
|
|
1632
|
+
{ id: 'a', expression: '$b + 1' },
|
|
1633
|
+
{ id: 'b', expression: '$c + 1' },
|
|
1634
|
+
{ id: 'c', expression: '$a + 1' }, // Creates cycle!
|
|
1635
|
+
];
|
|
1636
|
+
|
|
1637
|
+
const validation = engine.validate(formulas);
|
|
1638
|
+
|
|
1639
|
+
if (!validation.valid) {
|
|
1640
|
+
for (const error of validation.errors) {
|
|
1641
|
+
if (error instanceof CircularDependencyError) {
|
|
1642
|
+
console.error(`Circular dependency: ${error.cycle.join(' → ')}`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
---
|
|
1649
|
+
|
|
1650
|
+
### 10. Performance Considerations
|
|
1651
|
+
|
|
1652
|
+
#### 10.1 Caching Strategy
|
|
1653
|
+
|
|
1654
|
+
```typescript
|
|
1655
|
+
interface CacheConfig {
|
|
1656
|
+
/** Enable AST caching */
|
|
1657
|
+
enableASTCache: boolean;
|
|
1658
|
+
|
|
1659
|
+
/** Enable dependency cache */
|
|
1660
|
+
enableDependencyCache: boolean;
|
|
1661
|
+
|
|
1662
|
+
/** Maximum AST cache entries */
|
|
1663
|
+
maxASTCacheSize: number;
|
|
1664
|
+
|
|
1665
|
+
/** Cache eviction policy */
|
|
1666
|
+
evictionPolicy: 'LRU' | 'LFU' | 'FIFO';
|
|
1667
|
+
|
|
1668
|
+
/** Cache TTL in milliseconds (0 = no expiration) */
|
|
1669
|
+
cacheTTL: number;
|
|
1670
|
+
}
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
#### 10.2 Optimization Techniques
|
|
1674
|
+
|
|
1675
|
+
1. **Expression Normalization:** Normalize expressions before caching to improve hit rate
|
|
1676
|
+
2. **Constant Folding:** Pre-compute constant sub-expressions during parsing
|
|
1677
|
+
3. **Short-Circuit Evaluation:** For `&&` and `||` operators
|
|
1678
|
+
4. **Lazy Collection Evaluation:** Don't materialize arrays until needed
|
|
1679
|
+
5. **Result Memoization:** Cache intermediate results during batch evaluation
|
|
1680
|
+
|
|
1681
|
+
#### 10.3 Benchmarks (Target)
|
|
1682
|
+
|
|
1683
|
+
| Operation | Target | Notes |
|
|
1684
|
+
|-----------|--------|-------|
|
|
1685
|
+
| Parse simple expression | < 0.1ms | With cache hit |
|
|
1686
|
+
| Parse complex expression | < 1ms | First parse |
|
|
1687
|
+
| Evaluate simple expression | < 0.05ms | Cached AST |
|
|
1688
|
+
| Evaluate 100 formulas | < 5ms | With dependencies |
|
|
1689
|
+
| Build dependency graph (100 nodes) | < 1ms | |
|
|
1690
|
+
| Topological sort (100 nodes) | < 0.5ms | |
|
|
1691
|
+
|
|
1692
|
+
---
|
|
1693
|
+
|
|
1694
|
+
### 11. Security Considerations
|
|
1695
|
+
|
|
1696
|
+
#### 11.1 Safe Evaluation
|
|
1697
|
+
|
|
1698
|
+
The formula engine is designed to be safe by default:
|
|
1699
|
+
|
|
1700
|
+
1. **No Code Execution:** Expressions cannot execute arbitrary code
|
|
1701
|
+
2. **No File System Access:** No built-in functions access the filesystem
|
|
1702
|
+
3. **No Network Access:** No HTTP/network capabilities
|
|
1703
|
+
4. **No Global Access:** Cannot access global objects or prototypes
|
|
1704
|
+
5. **Sandboxed Context:** Only provided variables are accessible
|
|
1705
|
+
6. **Resource Limits:** Configurable limits on:
|
|
1706
|
+
- Maximum expression length
|
|
1707
|
+
- Maximum recursion depth
|
|
1708
|
+
- Maximum loop iterations (for array functions)
|
|
1709
|
+
- Maximum execution time
|
|
1710
|
+
|
|
1711
|
+
#### 11.2 Security Configuration
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
interface SecurityConfig {
|
|
1715
|
+
/** Maximum expression length in characters */
|
|
1716
|
+
maxExpressionLength: number; // Default: 10000
|
|
1717
|
+
|
|
1718
|
+
/** Maximum recursion depth */
|
|
1719
|
+
maxRecursionDepth: number; // Default: 100
|
|
1720
|
+
|
|
1721
|
+
/** Maximum iterations for array functions */
|
|
1722
|
+
maxIterations: number; // Default: 10000
|
|
1723
|
+
|
|
1724
|
+
/** Maximum execution time in milliseconds */
|
|
1725
|
+
maxExecutionTime: number; // Default: 5000
|
|
1726
|
+
|
|
1727
|
+
/** Allowed function names (whitelist) */
|
|
1728
|
+
allowedFunctions?: string[];
|
|
1729
|
+
|
|
1730
|
+
/** Blocked function names (blacklist) */
|
|
1731
|
+
blockedFunctions?: string[];
|
|
1732
|
+
}
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
---
|
|
1736
|
+
|
|
1737
|
+
### 12. Testing Strategy
|
|
1738
|
+
|
|
1739
|
+
#### 12.1 Test Categories
|
|
1740
|
+
|
|
1741
|
+
1. **Parser Tests:** Valid/invalid syntax, edge cases, Unicode support
|
|
1742
|
+
2. **Dependency Tests:** Extraction accuracy, graph construction, cycle detection
|
|
1743
|
+
3. **Evaluation Tests:** All operators, all functions, type coercion
|
|
1744
|
+
4. **Integration Tests:** Full pipeline with complex formula sets
|
|
1745
|
+
5. **Performance Tests:** Benchmarks, stress tests, memory usage
|
|
1746
|
+
6. **Security Tests:** Injection attempts, resource exhaustion
|
|
1747
|
+
|
|
1748
|
+
#### 12.2 Test Coverage Requirements
|
|
1749
|
+
|
|
1750
|
+
- Line coverage: > 95%
|
|
1751
|
+
- Branch coverage: > 90%
|
|
1752
|
+
- All built-in functions: 100%
|
|
1753
|
+
- All operators: 100%
|
|
1754
|
+
- All error paths: 100%
|
|
1755
|
+
|
|
1756
|
+
---
|
|
1757
|
+
|
|
1758
|
+
### 13. Implementation Phases
|
|
1759
|
+
|
|
1760
|
+
#### Phase 1: Core Engine (Week 1-2)
|
|
1761
|
+
|
|
1762
|
+
- [ ] Lexer and tokenizer
|
|
1763
|
+
- [ ] Parser and AST generation
|
|
1764
|
+
- [ ] Basic evaluator (arithmetic, comparisons, variables)
|
|
1765
|
+
- [ ] Core built-in functions (math, logic)
|
|
1766
|
+
- [ ] Basic error handling
|
|
1767
|
+
|
|
1768
|
+
#### Phase 2: Dependency Management (Week 3)
|
|
1769
|
+
|
|
1770
|
+
- [ ] Dependency extractor
|
|
1771
|
+
- [ ] Dependency graph builder
|
|
1772
|
+
- [ ] Topological sorter
|
|
1773
|
+
- [ ] Circular dependency detection
|
|
1774
|
+
- [ ] Batch evaluation with dependency resolution
|
|
1775
|
+
|
|
1776
|
+
#### Phase 3: Advanced Features (Week 4)
|
|
1777
|
+
|
|
1778
|
+
- [ ] Aggregation functions (SUM, AVG, etc.)
|
|
1779
|
+
- [ ] Array manipulation functions
|
|
1780
|
+
- [ ] String functions
|
|
1781
|
+
- [ ] Custom function registration
|
|
1782
|
+
- [ ] Expression caching
|
|
1783
|
+
|
|
1784
|
+
#### Phase 4: Optimization & Polish (Week 5)
|
|
1785
|
+
|
|
1786
|
+
- [ ] Performance optimization
|
|
1787
|
+
- [ ] Comprehensive error messages
|
|
1788
|
+
- [ ] Security hardening
|
|
1789
|
+
- [ ] Documentation
|
|
1790
|
+
- [ ] Comprehensive test suite
|
|
1791
|
+
|
|
1792
|
+
---
|
|
1793
|
+
|
|
1794
|
+
### 14. Appendix: Example Configurations
|
|
1795
|
+
|
|
1796
|
+
#### 14.1 Simple Calculator
|
|
1797
|
+
|
|
1798
|
+
```typescript
|
|
1799
|
+
const calculatorFormulas: FormulaDefinition[] = [
|
|
1800
|
+
{ id: 'sum', expression: '$a + $b' },
|
|
1801
|
+
{ id: 'difference', expression: '$a - $b' },
|
|
1802
|
+
{ id: 'product', expression: '$a * $b' },
|
|
1803
|
+
{ id: 'quotient', expression: '$b != 0 ? $a / $b : null' },
|
|
1804
|
+
];
|
|
1805
|
+
```
|
|
1806
|
+
|
|
1807
|
+
#### 14.2 Financial Calculations
|
|
1808
|
+
|
|
1809
|
+
```typescript
|
|
1810
|
+
const financialFormulas: FormulaDefinition[] = [
|
|
1811
|
+
{ id: 'principal', expression: '$loanAmount' },
|
|
1812
|
+
{ id: 'monthlyRate', expression: '$annualRate / 12' },
|
|
1813
|
+
{ id: 'numPayments', expression: '$years * 12' },
|
|
1814
|
+
{
|
|
1815
|
+
id: 'monthlyPayment',
|
|
1816
|
+
expression: '$principal * $monthlyRate * POW(1 + $monthlyRate, $numPayments) / (POW(1 + $monthlyRate, $numPayments) - 1)'
|
|
1817
|
+
},
|
|
1818
|
+
{ id: 'totalPayment', expression: '$monthlyPayment * $numPayments' },
|
|
1819
|
+
{ id: 'totalInterest', expression: '$totalPayment - $principal' },
|
|
1820
|
+
];
|
|
1821
|
+
```
|
|
1822
|
+
|
|
1823
|
+
#### 14.3 Scoring System
|
|
1824
|
+
|
|
1825
|
+
```typescript
|
|
1826
|
+
const scoringFormulas: FormulaDefinition[] = [
|
|
1827
|
+
{ id: 'rawScore', expression: 'SUM($answers, IF($it.correct, $it.points, 0))' },
|
|
1828
|
+
{ id: 'maxScore', expression: 'SUM($answers, $it.points)' },
|
|
1829
|
+
{ id: 'percentage', expression: '$maxScore > 0 ? ($rawScore / $maxScore) * 100 : 0' },
|
|
1830
|
+
{ id: 'grade', expression: 'IF($percentage >= 90, "A", IF($percentage >= 80, "B", IF($percentage >= 70, "C", IF($percentage >= 60, "D", "F"))))' },
|
|
1831
|
+
{ id: 'passed', expression: '$percentage >= 60' },
|
|
1832
|
+
];
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
---
|
|
1836
|
+
|
|
1837
|
+
## Glossary
|
|
1838
|
+
|
|
1839
|
+
| Term | Definition |
|
|
1840
|
+
|------|------------|
|
|
1841
|
+
| **AST** | Abstract Syntax Tree - tree representation of parsed expression |
|
|
1842
|
+
| **Decimal** | Arbitrary-precision decimal number that avoids floating-point errors |
|
|
1843
|
+
| **Dependency Graph** | Directed graph showing which formulas depend on others |
|
|
1844
|
+
| **Precision** | Total number of significant digits in a Decimal value |
|
|
1845
|
+
| **Scale** | Number of digits after the decimal point |
|
|
1846
|
+
| **Rounding Mode** | Algorithm for rounding numbers (HALF_UP, HALF_EVEN, FLOOR, CEIL, etc.) |
|
|
1847
|
+
| **Topological Sort** | Algorithm to order nodes so dependencies come before dependents |
|
|
1848
|
+
| **Context** | The data environment in which expressions are evaluated |
|
|
1849
|
+
| **Formula** | A named expression that can reference variables and other formulas |
|
|
1850
|
+
| **Variable** | A named value in the evaluation context |
|
|
1851
|
+
|
|
1852
|
+
---
|
|
1853
|
+
|
|
1854
|
+
## References
|
|
1855
|
+
|
|
1856
|
+
1. Expression parsing: Pratt Parser / Recursive Descent
|
|
1857
|
+
2. Topological sorting: Kahn's algorithm / DFS-based
|
|
1858
|
+
3. Similar systems: Excel formulas, JSON Logic, Mathjs
|
|
1859
|
+
4. Decimal arithmetic: decimal.js library, IEEE 754-2008 decimal floating-point
|
|
1860
|
+
|
|
1861
|
+
---
|
|
1862
|
+
|
|
1863
|
+
*End of PRD*
|