formula-evaluator 1.0.0 → 1.2.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/README.md +79 -6
- package/package.json +3 -2
- package/src/functions.js +256 -0
- package/src/index.js +122 -19
package/README.md
CHANGED
|
@@ -86,18 +86,91 @@ const ast = evaluator.parse(tokens);
|
|
|
86
86
|
- **Strings**: `"hello world"`
|
|
87
87
|
- **Booleans**: `true`, `false`
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
### `registerFunction(name, fn)`
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Register a custom function that can be called in formulas. Returns the evaluator instance so calls can be chained.
|
|
92
92
|
|
|
93
93
|
```js
|
|
94
94
|
const evaluator = new FormulaEvaluator();
|
|
95
95
|
|
|
96
|
-
evaluator
|
|
97
|
-
|
|
96
|
+
evaluator
|
|
97
|
+
.registerFunction('double', (x) => x * 2)
|
|
98
|
+
.registerFunction('clamp', (val, min, max) => Math.min(Math.max(val, min), max));
|
|
98
99
|
|
|
99
|
-
evaluator.evaluate('double(5)');
|
|
100
|
-
evaluator.evaluate('
|
|
100
|
+
evaluator.evaluate('double(5)'); // 10
|
|
101
|
+
evaluator.evaluate('clamp(15, 0, 10)'); // 10
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Custom functions receive their arguments already evaluated, so they work naturally with variables, operators, and nested calls:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
evaluator.registerFunction('double', (x) => x * 2);
|
|
108
|
+
|
|
109
|
+
evaluator.evaluate('double(n)', { n: 4 }); // 8
|
|
110
|
+
evaluator.evaluate('double(3) + 1'); // 7
|
|
111
|
+
evaluator.evaluate('double(sum(1, 2))'); // 6
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You can also override built-in functions:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
evaluator.registerFunction('sum', (...args) => args.reduce((a, b) => a + b, 0));
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `listFunctions()`
|
|
121
|
+
|
|
122
|
+
Returns the names of all registered public functions (excludes internal operator mappings):
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
evaluator.listFunctions(); // ['upper', 'join', 'sum', 'avg', 'if']
|
|
126
|
+
|
|
127
|
+
evaluator.registerFunction('double', (x) => x * 2);
|
|
128
|
+
evaluator.listFunctions(); // ['upper', 'join', 'sum', 'avg', 'if', 'double']
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Function Registry
|
|
132
|
+
|
|
133
|
+
For advanced use cases you can work with the function registry directly. The `createFunctionRegistry` factory and the `builtinFunctions` map are available as named exports:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { createFunctionRegistry, builtinFunctions } from 'formula-evaluator';
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `createFunctionRegistry(initialFunctions?)`
|
|
140
|
+
|
|
141
|
+
Creates a standalone registry pre-loaded with the built-in functions. Useful when you want to prepare a set of functions before constructing an evaluator, or share a registry definition across modules.
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { createFunctionRegistry } from 'formula-evaluator';
|
|
145
|
+
|
|
146
|
+
const registry = createFunctionRegistry({
|
|
147
|
+
double: (x) => x * 2,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
registry.register('triple', (x) => x * 3);
|
|
151
|
+
registry.has('sum'); // true (built-in)
|
|
152
|
+
registry.has('double'); // true (initial)
|
|
153
|
+
registry.has('triple'); // true (registered)
|
|
154
|
+
registry.get('double')(5); // 10
|
|
155
|
+
registry.list(); // ['upper', 'join', 'sum', 'avg', 'if', 'double', 'triple']
|
|
156
|
+
registry.unregister('triple');
|
|
157
|
+
registry.has('triple'); // false
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Built-in functions are protected and cannot be unregistered:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
registry.unregister('sum'); // throws Error: Cannot unregister built-in function "sum"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `builtinFunctions`
|
|
167
|
+
|
|
168
|
+
A frozen object containing the default function implementations. Useful for inspection or when building a custom registry from scratch:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { builtinFunctions } from 'formula-evaluator';
|
|
172
|
+
|
|
173
|
+
Object.keys(builtinFunctions); // ['upper', 'join', 'sum', 'avg', 'if', '__add', '__sub', '__eq']
|
|
101
174
|
```
|
|
102
175
|
|
|
103
176
|
## Development
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "formula-evaluator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A lightweight formula evaluator with dependency tracking and static analysis.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.js"
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./functions": "./src/functions.js"
|
|
8
9
|
},
|
|
9
10
|
"type": "module",
|
|
10
11
|
"files": [
|
package/src/functions.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module functions
|
|
3
|
+
* Built-in function library and registry for the formula evaluator.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A function that can be called during formula evaluation.
|
|
8
|
+
* @callback FormulaFunction
|
|
9
|
+
* @param {...*} args - Evaluated arguments from the formula
|
|
10
|
+
* @returns {*} The computed result
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A function definition containing the implementation and a human-readable description.
|
|
15
|
+
* @typedef {Object} FunctionDef
|
|
16
|
+
* @property {FormulaFunction} fn - The function implementation
|
|
17
|
+
* @property {string} description - A human-readable description of what the function does
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Built-in functions included in every new evaluator instance.
|
|
22
|
+
* Each entry contains a `fn` implementation and a human-readable `description`.
|
|
23
|
+
* @type {Readonly<Record<string, FunctionDef>>}
|
|
24
|
+
*/
|
|
25
|
+
export const builtinFunctions = Object.freeze({
|
|
26
|
+
upper: {
|
|
27
|
+
fn: (str) => String(str).toUpperCase(),
|
|
28
|
+
description: 'Converts a value to an uppercase string',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
join: {
|
|
32
|
+
fn: (sep, ...args) => args.join(sep),
|
|
33
|
+
description: 'Joins arguments with a separator',
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
sum: {
|
|
37
|
+
fn: (...args) => args.reduce((a, b) => Number(a) + Number(b), 0),
|
|
38
|
+
description: 'Sums all arguments numerically',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
avg: {
|
|
42
|
+
fn: (...args) => args.reduce((a, b) => a + b, 0) / args.length,
|
|
43
|
+
description: 'Returns the arithmetic mean of all arguments',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
if: {
|
|
47
|
+
fn: (cond, a, b) => (cond ? a : b),
|
|
48
|
+
description: 'Returns the second argument if the condition is truthy, otherwise the third',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
coalesce: {
|
|
52
|
+
fn: (...args) => args.find(a => a != null),
|
|
53
|
+
description: 'Returns the first non-null/non-undefined value',
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
isblank: {
|
|
57
|
+
fn: (val) => val === '' || val == null,
|
|
58
|
+
description: 'Returns true if a value is an empty string or null/undefined',
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
and: {
|
|
62
|
+
fn: (...args) => args.every(Boolean),
|
|
63
|
+
description: 'Returns true if all arguments are truthy',
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
or: {
|
|
67
|
+
fn: (...args) => args.some(Boolean),
|
|
68
|
+
description: 'Returns true if any argument is truthy',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
iferr: {
|
|
72
|
+
fn: (val) => val,
|
|
73
|
+
description: 'Returns the first argument, or the second if the first throws an error',
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
round: {
|
|
77
|
+
fn: (n, d) => Number(Math.round(n + 'e' + d) + 'e-' + d),
|
|
78
|
+
description: 'Rounds a number to a specific decimal precision',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
clamp: {
|
|
82
|
+
fn: (val, min, max) => Math.min(Math.max(val, min), max),
|
|
83
|
+
description: 'Restricts a number to a given range',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
abs: {
|
|
87
|
+
fn: (n) => Math.abs(n),
|
|
88
|
+
description: 'Returns the absolute value of a number',
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
concat: {
|
|
92
|
+
fn: (...args) => args.join(''),
|
|
93
|
+
description: 'Concatenates all arguments without a separator',
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** @private */
|
|
97
|
+
__add: {
|
|
98
|
+
fn: (a, b) => a + b,
|
|
99
|
+
description: 'Addition operator',
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** @private */
|
|
103
|
+
__sub: {
|
|
104
|
+
fn: (a, b) => a - b,
|
|
105
|
+
description: 'Subtraction operator',
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** @private */
|
|
109
|
+
__eq: {
|
|
110
|
+
fn: (a, b) => a === b,
|
|
111
|
+
description: 'Equality operator',
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/** @private */
|
|
115
|
+
__gt: {
|
|
116
|
+
fn: (a, b) => a > b,
|
|
117
|
+
description: 'Greater-than operator',
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/** @private */
|
|
121
|
+
__gte: {
|
|
122
|
+
fn: (a, b) => a >= b,
|
|
123
|
+
description: 'Greater-than-or-equal operator',
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/** @private */
|
|
127
|
+
__lt: {
|
|
128
|
+
fn: (a, b) => a < b,
|
|
129
|
+
description: 'Less-than operator',
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** @private */
|
|
133
|
+
__lte: {
|
|
134
|
+
fn: (a, b) => a <= b,
|
|
135
|
+
description: 'Less-than-or-equal operator',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @typedef {Object} FunctionRegistry
|
|
141
|
+
* @property {(name: string, fn: FormulaFunction, description?: string) => void} register - Register a new function
|
|
142
|
+
* @property {(name: string) => FormulaFunction|undefined} get - Retrieve a function by name
|
|
143
|
+
* @property {(name: string) => boolean} has - Check whether a function is registered
|
|
144
|
+
* @property {(name: string) => boolean} unregister - Remove a custom (non-built-in) function
|
|
145
|
+
* @property {() => string[]} list - List public function names (excludes internal __ operators)
|
|
146
|
+
* @property {() => Array<{name: string, description: string}>} describe - List public function names and descriptions
|
|
147
|
+
* @property {() => Record<string, FunctionDef>} getAll - Get a snapshot of all registered function definitions
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Creates a function registry pre-loaded with the built-in functions.
|
|
152
|
+
*
|
|
153
|
+
* Use this to build a standalone registry, or let {@link FormulaEvaluator}
|
|
154
|
+
* create one automatically via its constructor.
|
|
155
|
+
*
|
|
156
|
+
* @param {Record<string, FormulaFunction>} [initialFunctions={}] - Extra functions to register on creation
|
|
157
|
+
* @returns {FunctionRegistry} A new registry instance
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* import { createFunctionRegistry } from 'formula-evaluator/functions';
|
|
161
|
+
*
|
|
162
|
+
* const registry = createFunctionRegistry({
|
|
163
|
+
* double: (x) => x * 2,
|
|
164
|
+
* });
|
|
165
|
+
* registry.register('triple', (x) => x * 3, 'Triples a number');
|
|
166
|
+
* registry.get('double')(5); // 10
|
|
167
|
+
* registry.list(); // ['upper', 'join', 'sum', 'avg', 'if', 'double', 'triple']
|
|
168
|
+
* registry.describe(); // [{ name: 'upper', description: 'Converts a value to an uppercase string' }, ...]
|
|
169
|
+
*/
|
|
170
|
+
export function createFunctionRegistry(initialFunctions = {}) {
|
|
171
|
+
/** @type {Record<string, FunctionDef>} */
|
|
172
|
+
const functions = { ...builtinFunctions };
|
|
173
|
+
|
|
174
|
+
for (const [name, val] of Object.entries(initialFunctions)) {
|
|
175
|
+
functions[name] = typeof val === 'function'
|
|
176
|
+
? { fn: val, description: '' }
|
|
177
|
+
: { ...val };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
/**
|
|
182
|
+
* Registers a function under the given name.
|
|
183
|
+
* @param {string} name - Function name (used in formula strings)
|
|
184
|
+
* @param {FormulaFunction} fn - The implementation
|
|
185
|
+
* @param {string} [description=''] - A human-readable description of the function
|
|
186
|
+
* @throws {Error} If name is not a non-empty string or fn is not a function
|
|
187
|
+
*/
|
|
188
|
+
register(name, fn, description = '') {
|
|
189
|
+
if (typeof name !== 'string' || !name) {
|
|
190
|
+
throw new Error('Function name must be a non-empty string');
|
|
191
|
+
}
|
|
192
|
+
if (typeof fn !== 'function') {
|
|
193
|
+
throw new Error('Function implementation must be a function');
|
|
194
|
+
}
|
|
195
|
+
functions[name] = { fn, description };
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Retrieves a function implementation by name.
|
|
200
|
+
* @param {string} name
|
|
201
|
+
* @returns {FormulaFunction|undefined}
|
|
202
|
+
*/
|
|
203
|
+
get(name) {
|
|
204
|
+
return functions[name]?.fn;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Checks whether a function is registered.
|
|
209
|
+
* @param {string} name
|
|
210
|
+
* @returns {boolean}
|
|
211
|
+
*/
|
|
212
|
+
has(name) {
|
|
213
|
+
return name in functions;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Removes a custom function. Built-in functions cannot be unregistered.
|
|
218
|
+
* @param {string} name
|
|
219
|
+
* @returns {boolean} `true` if the function was removed
|
|
220
|
+
* @throws {Error} If attempting to unregister a built-in function
|
|
221
|
+
*/
|
|
222
|
+
unregister(name) {
|
|
223
|
+
if (name in builtinFunctions) {
|
|
224
|
+
throw new Error(`Cannot unregister built-in function "${name}"`);
|
|
225
|
+
}
|
|
226
|
+
return delete functions[name];
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Lists all public function names (excludes internal `__` operators).
|
|
231
|
+
* @returns {string[]}
|
|
232
|
+
*/
|
|
233
|
+
list() {
|
|
234
|
+
return Object.keys(functions).filter(name => !name.startsWith('__'));
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Returns names and descriptions of all public functions
|
|
239
|
+
* (excludes internal `__` operators).
|
|
240
|
+
* @returns {Array<{name: string, description: string}>}
|
|
241
|
+
*/
|
|
242
|
+
describe() {
|
|
243
|
+
return Object.entries(functions)
|
|
244
|
+
.filter(([name]) => !name.startsWith('__'))
|
|
245
|
+
.map(([name, { description }]) => ({ name, description }));
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns a shallow copy of all registered function definitions.
|
|
250
|
+
* @returns {Record<string, FunctionDef>}
|
|
251
|
+
*/
|
|
252
|
+
getAll() {
|
|
253
|
+
return { ...functions };
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @module formula-evaluator
|
|
3
|
+
* A lightweight, extensible formula engine with variable tracking,
|
|
4
|
+
* nested functions, and operator precedence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createFunctionRegistry } from './functions.js';
|
|
8
|
+
|
|
9
|
+
export { builtinFunctions, createFunctionRegistry } from './functions.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Evaluates string-based formulas with support for variables, functions,
|
|
13
|
+
* and arithmetic operators.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const evaluator = new FormulaEvaluator({ tax: 0.2 });
|
|
17
|
+
* evaluator.registerFunction('discount', (price, pct) => price * (1 - pct));
|
|
18
|
+
* evaluator.evaluate('discount(100, tax)'); // 80
|
|
4
19
|
*/
|
|
5
20
|
class FormulaEvaluator {
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new FormulaEvaluator instance.
|
|
23
|
+
*
|
|
24
|
+
* @param {Record<string, *>} [globalContext={}] - Variables available to every evaluation
|
|
25
|
+
*/
|
|
6
26
|
constructor(globalContext = {}) {
|
|
27
|
+
/** @type {Record<string, *>} */
|
|
7
28
|
this.context = globalContext;
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if: (cond, a, b) => (cond ? a : b),
|
|
16
|
-
// Internal Operator Mappings
|
|
17
|
-
__add: (a, b) => a + b,
|
|
18
|
-
__sub: (a, b) => a - b,
|
|
19
|
-
__eq: (a, b) => a === b,
|
|
20
|
-
};
|
|
30
|
+
/**
|
|
31
|
+
* Internal function registry.
|
|
32
|
+
* @private
|
|
33
|
+
* @type {import('./functions.js').FunctionRegistry}
|
|
34
|
+
*/
|
|
35
|
+
this._functions = createFunctionRegistry();
|
|
21
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Token type constants used by the tokenizer.
|
|
39
|
+
* @type {Readonly<Record<string, string>>}
|
|
40
|
+
*/
|
|
22
41
|
this.TOKEN_TYPES = {
|
|
23
42
|
NUMBER: 'number',
|
|
24
43
|
STRING: 'string',
|
|
@@ -29,13 +48,62 @@ class FormulaEvaluator {
|
|
|
29
48
|
};
|
|
30
49
|
}
|
|
31
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Registers a custom function that can be called in formulas.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} name - The function name (used in formula strings)
|
|
55
|
+
* @param {import('./functions.js').FormulaFunction} fn - The function implementation
|
|
56
|
+
* @param {string} [description=''] - A human-readable description of the function
|
|
57
|
+
* @returns {this} The evaluator instance, for chaining
|
|
58
|
+
* @throws {Error} If name is not a non-empty string or fn is not a function
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* evaluator
|
|
62
|
+
* .registerFunction('double', (x) => x * 2, 'Doubles a number')
|
|
63
|
+
* .registerFunction('clamp', (val, min, max) => Math.min(Math.max(val, min), max), 'Restricts a number to a range');
|
|
64
|
+
*
|
|
65
|
+
* evaluator.evaluate('double(5)'); // 10
|
|
66
|
+
* evaluator.evaluate('clamp(15, 0, 10)'); // 10
|
|
67
|
+
*/
|
|
68
|
+
registerFunction(name, fn, description = '') {
|
|
69
|
+
this._functions.register(name, fn, description);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lists the names of all registered public functions
|
|
75
|
+
* (excludes internal operator mappings).
|
|
76
|
+
*
|
|
77
|
+
* @returns {string[]} Array of function names
|
|
78
|
+
*/
|
|
79
|
+
listFunctions() {
|
|
80
|
+
return this._functions.list();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the names and descriptions of all registered public functions
|
|
85
|
+
* (excludes internal operator mappings).
|
|
86
|
+
*
|
|
87
|
+
* @returns {Array<{name: string, description: string}>}
|
|
88
|
+
*/
|
|
89
|
+
describeFunctions() {
|
|
90
|
+
return this._functions.describe();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Tokenizes a formula string into an array of tokens.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} str - The formula string to tokenize
|
|
97
|
+
* @returns {Array<{type: string, value: string, start: number, end: number}>} The token array
|
|
98
|
+
* @throws {Error} If an unexpected character is encountered
|
|
99
|
+
*/
|
|
32
100
|
tokenize(str) {
|
|
33
101
|
const tokens = [];
|
|
34
102
|
const rules = [
|
|
35
103
|
{ type: this.TOKEN_TYPES.STRING, regex: /"([^"]*)"/g },
|
|
36
104
|
{ type: this.TOKEN_TYPES.NUMBER, regex: /\d*\.?\d+/g },
|
|
37
105
|
{ type: this.TOKEN_TYPES.IDENTIFIER, regex: /[a-zA-Z][\w\d]*/g },
|
|
38
|
-
{ type: this.TOKEN_TYPES.OPERATOR, regex:
|
|
106
|
+
{ type: this.TOKEN_TYPES.OPERATOR, regex: />=|<=|[+=\-><]/g },
|
|
39
107
|
{ type: this.TOKEN_TYPES.DELIMITER, regex: /[(),]/g },
|
|
40
108
|
{ type: this.TOKEN_TYPES.WHITESPACE, regex: /\s+/g }
|
|
41
109
|
];
|
|
@@ -65,7 +133,12 @@ class FormulaEvaluator {
|
|
|
65
133
|
return tokens;
|
|
66
134
|
}
|
|
67
135
|
|
|
68
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Parses an array of tokens into an abstract syntax tree (AST).
|
|
138
|
+
*
|
|
139
|
+
* @param {Array<{type: string, value: string}>} tokens - Tokens produced by {@link tokenize}
|
|
140
|
+
* @returns {*} The root AST node
|
|
141
|
+
*/
|
|
69
142
|
parse(tokens) {
|
|
70
143
|
let pos = 0;
|
|
71
144
|
|
|
@@ -76,7 +149,7 @@ class FormulaEvaluator {
|
|
|
76
149
|
if (currentToken && currentToken.type === this.TOKEN_TYPES.OPERATOR) {
|
|
77
150
|
const opToken = tokens[pos++];
|
|
78
151
|
const right = parseExpression();
|
|
79
|
-
const opMap = { '+': '__add', '-': '__sub', '=': '__eq' };
|
|
152
|
+
const opMap = { '+': '__add', '-': '__sub', '=': '__eq', '>': '__gt', '>=': '__gte', '<': '__lt', '<=': '__lte' };
|
|
80
153
|
return { type: 'function', name: opMap[opToken.value], args: [node, right] };
|
|
81
154
|
}
|
|
82
155
|
return node;
|
|
@@ -116,7 +189,19 @@ class FormulaEvaluator {
|
|
|
116
189
|
return parseExpression();
|
|
117
190
|
}
|
|
118
191
|
|
|
119
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Evaluates a formula string and returns the result.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} formula - The formula to evaluate
|
|
196
|
+
* @param {Record<string, *>} [localContext={}] - Variables scoped to this evaluation
|
|
197
|
+
* (overrides global context for matching keys)
|
|
198
|
+
* @returns {*} The evaluation result
|
|
199
|
+
* @throws {Error} If a referenced variable or function is not found
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* evaluator.evaluate('sum(1, 2, 3)'); // 6
|
|
203
|
+
* evaluator.evaluate('x + 1', { x: 10 }); // 11
|
|
204
|
+
*/
|
|
120
205
|
evaluate(formula, localContext = {}) {
|
|
121
206
|
const ast = this.parse(this.tokenize(formula));
|
|
122
207
|
const ctx = { ...this.context, ...localContext };
|
|
@@ -130,7 +215,16 @@ class FormulaEvaluator {
|
|
|
130
215
|
}
|
|
131
216
|
|
|
132
217
|
if (node.type === 'function') {
|
|
133
|
-
|
|
218
|
+
// iferr requires lazy evaluation: catch errors from the first arg
|
|
219
|
+
if (node.name === 'iferr') {
|
|
220
|
+
try {
|
|
221
|
+
return run(node.args[0]);
|
|
222
|
+
} catch {
|
|
223
|
+
return run(node.args[1]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fn = this._functions.get(node.name);
|
|
134
228
|
if (!fn) throw new Error(`Function "${node.name}" not found`);
|
|
135
229
|
return fn(...node.args.map(run));
|
|
136
230
|
}
|
|
@@ -139,6 +233,15 @@ class FormulaEvaluator {
|
|
|
139
233
|
return run(ast);
|
|
140
234
|
}
|
|
141
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Returns the variable names referenced in a formula (excludes function names).
|
|
238
|
+
*
|
|
239
|
+
* @param {string} formula - The formula to analyze
|
|
240
|
+
* @returns {string[]} Deduplicated array of variable names
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* evaluator.getDependencies('sum(x, y) + z'); // ['x', 'y', 'z']
|
|
244
|
+
*/
|
|
142
245
|
getDependencies(formula) {
|
|
143
246
|
const ast = this.parse(this.tokenize(formula));
|
|
144
247
|
const deps = new Set();
|