|
| 1 | +# Configuration |
| 2 | + |
| 3 | +Exprify follows a minimal-configuration philosophy: rather than exposing a large global settings object, almost all behavior is controlled per-instance through the API methods documented below. This keeps configuration explicit, predictable, and easy to reason. |
| 4 | + |
| 5 | +## BigNumber Precision |
| 6 | + |
| 7 | +The `ExprDecimal` class wraps an arbitrary-precision decimal library and is used internally whenever `bignumber()`-mode arithmetic is requested. By default, Exprify uses 20 decimal places of precision, which is sufficient for most financial and scientific calculations. You can raise this (up to a hard cap of 100) when you need finer-grained results, such as for cryptographic computations, high-precision scientific simulations, or chained division operations where rounding error could otherwise accumulate. |
| 8 | + |
| 9 | +```js |
| 10 | +const { Exprify } = require('exprify'); |
| 11 | +const { ExprDecimal } = require('exprify/src/utils/decimal'); |
| 12 | + |
| 13 | +// Default precision is 20 decimal places |
| 14 | +console.log(ExprDecimal.DP); // 20 |
| 15 | + |
| 16 | +// Increase precision to 50 decimal places |
| 17 | +ExprDecimal.DP = 50; |
| 18 | + |
| 19 | +const expr = new Exprify(); |
| 20 | + |
| 21 | +// Division now returns up to 50 significant decimal digits |
| 22 | +const result = expr.evaluate('bignumber(1) / bignumber(3)'); |
| 23 | +console.log(result.toString()); |
| 24 | +// "0.33333333333333333333333333333333333333333333333333" |
| 25 | + |
| 26 | +// Reset back to a lower precision if needed |
| 27 | +ExprDecimal.DP = 20; |
| 28 | +``` |
| 29 | + |
| 30 | +**Important considerations:** |
| 31 | + |
| 32 | +- `ExprDecimal.DP` is a **static, global** setting - changing it affects *all* `ExprDecimal` instances and all Exprify instances in the running process, not just the one you're configuring it from. If your application uses Exprify in multiple places with different precision needs, set `DP` immediately before the relevant evaluation and consider resetting it afterward. |
| 33 | +- Values above 100 are rejected; attempting to set `ExprDecimal.DP = 150` will either clamp to 100 or throw, depending on your version - always validate against the documented maximum. |
| 34 | +- Higher precision comes with a performance cost. Arbitrary-precision arithmetic at DP=100 is meaningfully slower than DP=20, so only raise it when correctness genuinely requires it. |
| 35 | +- This setting only affects results produced via `bignumber()` - ordinary numeric expressions (e.g. `1 / 3`) continue to use standard double-precision floating point and are unaffected by `ExprDecimal.DP`. |
| 36 | + |
| 37 | +## Evaluation Scope |
| 38 | + |
| 39 | +A *scope* is a plain JavaScript object mapping variable names to values. It lets you supply or override variables for a single call to `evaluate()` or `compile()` without permanently mutating the engine's internal variable table. This is the recommended way to run the same expression repeatedly with different inputs - for example, evaluating a formula once per row of a dataset. |
| 40 | + |
| 41 | +```js |
| 42 | +const expr = new Exprify(); |
| 43 | + |
| 44 | +// One-off evaluation with scope |
| 45 | +console.log(expr.evaluate('x^2 + y', { x: 3, y: 4 })); // 13 |
| 46 | + |
| 47 | +// Scope variables do not persist after evaluation |
| 48 | +console.log(expr.evaluate('x')); // throws: x is undefined (unless set globally) |
| 49 | + |
| 50 | +// Compiling once, evaluating many times with different scopes |
| 51 | +const compiled = expr.compile('a * b + c'); |
| 52 | + |
| 53 | +const rows = [ |
| 54 | + { a: 2, b: 3, c: 1 }, |
| 55 | + { a: 5, b: 5, c: 0 }, |
| 56 | + { a: 10, b: 0.5, c: 2 }, |
| 57 | +]; |
| 58 | + |
| 59 | +const results = rows.map(row => compiled.evaluate(row)); |
| 60 | +console.log(results); // [7, 25, 7] |
| 61 | +``` |
| 62 | + |
| 63 | +**Scope precedence:** if a variable exists both in the scope object *and* as a globally-set variable on the engine instance (via `expr.setVariable()` or similar), the scope value takes precedence for that evaluation only. This makes scope ideal for "default value with override" patterns: |
| 64 | + |
| 65 | +```js |
| 66 | +expr.setVariable('taxRate', 0.07); // global default |
| 67 | + |
| 68 | +expr.evaluate('price * (1 + taxRate)', { price: 100 }); // 107 (uses global taxRate) |
| 69 | +expr.evaluate('price * (1 + taxRate)', { price: 100, taxRate: 0 }); // 100 (override) |
| 70 | +``` |
| 71 | + |
| 72 | +## Custom Functions |
| 73 | + |
| 74 | +`addFunction()` registers a JavaScript function under a given name so it can be called from within expressions, just like a built-in function such as `sin()` or `sqrt()`. This is the primary extension point for domain-specific logic - unit conversions, lookups, string formatting, business rules, and so on. |
| 75 | + |
| 76 | +```js |
| 77 | +const expr = new Exprify(); |
| 78 | + |
| 79 | +// Simple single-argument function |
| 80 | +expr.addFunction('double', (x) => x * 2); |
| 81 | +console.log(expr.evaluate('double(21)')); // 42 |
| 82 | + |
| 83 | +// Multi-argument function |
| 84 | +expr.addFunction('clamp', (value, min, max) => Math.max(min, Math.min(max, value))); |
| 85 | +console.log(expr.evaluate('clamp(150, 0, 100)')); // 100 |
| 86 | + |
| 87 | +// Functions can call other built-ins or do arbitrary computation |
| 88 | +expr.addFunction('hypotenuse', (a, b) => Math.sqrt(a * a + b * b)); |
| 89 | +console.log(expr.evaluate('hypotenuse(3, 4)')); // 5 |
| 90 | + |
| 91 | +// Functions can be used inside larger expressions, including nested calls |
| 92 | +console.log(expr.evaluate('double(clamp(75, 0, 50))')); // 100 |
| 93 | + |
| 94 | +// Variadic-style functions using rest parameters |
| 95 | +expr.addFunction('sumAll', (...args) => args.reduce((acc, v) => acc + v, 0)); |
| 96 | +console.log(expr.evaluate('sumAll(1, 2, 3, 4, 5)')); // 15 |
| 97 | +``` |
| 98 | + |
| 99 | +**Notes on custom functions:** |
| 100 | + |
| 101 | +- Function names follow the same identifier rules as variables: they should start with a letter or underscore and contain only letters, digits, and underscores. |
| 102 | +- Registering a function with a name that already exists (including built-in function names) overwrites the existing definition for that engine instance - use this carefully, as it can change the meaning of expressions that worked before. |
| 103 | +- Custom functions are included when you call `exportState()`, provided they're serializable (see below) - functions defined as closures over external state will not survive serialization and must be re-registered manually after `importState()`. |
| 104 | + |
| 105 | +## State Serialization |
| 106 | + |
| 107 | +`exportState()` and `importState()` allow you to capture the full configuration of an engine instance - its variables, registered functions, and any custom units - and restore it later, either in the same process or after persisting it to disk, a database, or a network transfer. |
| 108 | + |
| 109 | +```js |
| 110 | +const expr = new Exprify(); |
| 111 | + |
| 112 | +expr.setVariable('x', 10); |
| 113 | +expr.setVariable('y', 20); |
| 114 | +expr.addFunction('double', (x) => x * 2); |
| 115 | +expr.addUnit('smoot', { definition: '1.7018 m' }); |
| 116 | + |
| 117 | +// Export the full state |
| 118 | +const state = expr.exportState(); |
| 119 | +console.log(state); |
| 120 | +/* |
| 121 | +{ |
| 122 | + variables: { x: 10, y: 20 }, |
| 123 | + functions: { double: '(x) => x * 2' }, // serialized as source where possible |
| 124 | + units: { smoot: { definition: '1.7018 m' } } |
| 125 | +} |
| 126 | +*/ |
| 127 | + |
| 128 | +// Persist to disk (example) |
| 129 | +const fs = require('fs'); |
| 130 | +fs.writeFileSync('exprify-state.json', JSON.stringify(state)); |
| 131 | + |
| 132 | +// ... later, in a new process ... |
| 133 | +const restoredState = JSON.parse(fs.readFileSync('exprify-state.json', 'utf8')); |
| 134 | + |
| 135 | +const expr2 = new Exprify(); |
| 136 | +expr2.importState(restoredState); |
| 137 | + |
| 138 | +console.log(expr2.evaluate('double(x) + y')); // 40 |
| 139 | +console.log(expr2.evaluate('5 smoot to m')); // ~8.509 m |
| 140 | +``` |
| 141 | + |
| 142 | +**Caveats:** |
| 143 | + |
| 144 | +- Functions registered via closures over non-serializable values (e.g. database connections, file handles) will not round-trip correctly through `exportState()`/`importState()`. For these, export only the simple state and re-register such functions manually after import. |
| 145 | +- `importState()` merges into the target instance by default in most implementations - if you need a completely clean slate, create a fresh `Exprify` instance before importing. |
| 146 | +- State serialization does not include `ExprDecimal.DP`, since that setting is global rather than per-instance; you must set it separately on the receiving side. |
| 147 | + |
| 148 | +## Constants |
| 149 | + |
| 150 | +Exprify ships with a set of built-in mathematical constants, available by name in any expression without prior declaration: |
| 151 | + |
| 152 | +| Name | Value | Description | |
| 153 | +|---|---|---| |
| 154 | +| `pi` | 3.141592653589793 | Ratio of a circle's circumference to its diameter | |
| 155 | +| `e` | 2.718281828459045 | Euler's number, base of the natural logarithm | |
| 156 | +| `PHI` | 1.618033988749895 | The golden ratio, (1 + √5) / 2 | |
| 157 | +| `TAU` | 6.283185307179586 | The full circle constant, 2π | |
| 158 | +| `INFINITY` | Infinity | Positive infinity | |
| 159 | +| `NaN` | NaN | "Not a Number" - result of undefined numeric operations | |
| 160 | + |
| 161 | +```js |
| 162 | +const expr = new Exprify(); |
| 163 | + |
| 164 | +console.log(expr.evaluate('2 * pi * 5')); // ~31.4159265358979 (circumference, r=5) |
| 165 | +console.log(expr.evaluate('e^1')); // 2.718281828459045 |
| 166 | +console.log(expr.evaluate('PHI - 1')); // 0.618033988749895 (== 1/PHI) |
| 167 | +console.log(expr.evaluate('TAU / 2 == pi')); // true |
| 168 | +console.log(expr.evaluate('1 / 0 == INFINITY')); // true |
| 169 | +console.log(expr.evaluate('isNaN(0 / 0)')); // true |
| 170 | +``` |
| 171 | + |
| 172 | +**Overriding constants:** because constants are just pre-populated entries in the variable table, they can be shadowed within a given scope or instance if your application has a specific need (for example, using a higher-precision value of `pi`). However, this is discouraged for general use, since it can make expressions confusing to read and debug - prefer introducing a new, clearly-named variable instead (e.g. `piHighPrecision`) rather than redefining a well-known constant. |
0 commit comments