@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
@@ -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*