formulakit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trananhtung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,216 @@
1
+ # formulakit
2
+
3
+ [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
4
+
5
+ > Zero-dependency safe arithmetic expression evaluator for TypeScript. Parse and evaluate math formulas with variables, custom functions, and ternary logic — no `eval()`. Port of Python `simpleeval` / Go `expr` / C# `DynamicExpresso`.
6
+
7
+ [![npm](https://img.shields.io/npm/v/formulakit)](https://www.npmjs.com/package/formulakit)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install formulakit
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```typescript
19
+ import { evaluate, compile } from "formulakit";
20
+
21
+ // one-shot evaluation
22
+ evaluate("2 + 3 * 4"); // 14
23
+ evaluate("sqrt(a^2 + b^2)", { a: 3, b: 4 }); // 5
24
+
25
+ // compile once, evaluate many times (faster)
26
+ const bmi = compile("weight / (height * height)");
27
+ bmi.evaluate({ weight: 70, height: 1.75 }); // 22.86
28
+ bmi.evaluate({ weight: 90, height: 1.80 }); // 27.78
29
+
30
+ // ternary / conditional
31
+ evaluate("qty >= 100 ? price * 0.8 : price", { qty: 150, price: 10 }); // 8
32
+ ```
33
+
34
+ ## Why formulakit?
35
+
36
+ | Package | Downloads/week | Status | TypeScript |
37
+ |---|---|---|---|
38
+ | `expr-eval` | ~200k | **Abandoned March 2021** | ❌ |
39
+ | `mathjs` | ~1.2M | Active | ✅ | Has 3 deps, 180KB |
40
+ | **`formulakit`** | — | **Active** | ✅ native |
41
+
42
+ `expr-eval` is the de-facto choice for safe expression evaluation but has been abandoned for 3+ years and has no TypeScript types. `mathjs` is excellent but ships 180KB of linear algebra, units, and symbolic math — overkill if you just need `"2 * x + 1"`. `formulakit` fills the gap: 0 dependencies, native TypeScript, ESM+CJS.
43
+
44
+ ## Features
45
+
46
+ - **Safe** — pure AST evaluator, never calls `eval()` or `Function()`
47
+ - **Variables** — pass any `Record<string, number>` as context
48
+ - **Custom functions** — extend with your own named functions
49
+ - **Ternary operator** — `condition ? a : b`
50
+ - **Comparison & logical operators** — `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`, `!`
51
+ - **Right-associative `^`** — `2^3^2` = `2^9` = 512 (standard math)
52
+ - **Short-circuit** — `&&` and `||` skip the right side when not needed
53
+ - **compile()** — parse once, evaluate many times
54
+ - **variables()** — introspect which variables a formula uses
55
+
56
+ ## API
57
+
58
+ ### `evaluate(expr, vars?, opts?)`
59
+
60
+ ```typescript
61
+ evaluate("x^2 + y^2", { x: 3, y: 4 }) // 25
62
+ evaluate("sin(PI/6)") // 0.5
63
+ ```
64
+
65
+ ### `tryEvaluate(expr, vars?, opts?)`
66
+
67
+ Returns `null` instead of throwing on parse/evaluation errors.
68
+
69
+ ```typescript
70
+ tryEvaluate("1 + 2") // 3
71
+ tryEvaluate("unknown_var") // null
72
+ tryEvaluate("2 @ 3") // null (bad syntax)
73
+ ```
74
+
75
+ ### `compile(expr): CompiledFormula`
76
+
77
+ Pre-parse an expression for repeated use:
78
+
79
+ ```typescript
80
+ const f = compile("a * b + c");
81
+ f.evaluate({ a: 1, b: 2, c: 3 }) // 5
82
+ f.evaluate({ a: 10, b: 20, c: 30 }) // 230
83
+
84
+ f.variables() // ["a", "b", "c"] — sorted list of identifiers
85
+ f.source // "a * b + c"
86
+ ```
87
+
88
+ ### `parse(expr): ASTNode` / `evalAST(ast, opts?)`
89
+
90
+ Low-level access to the parse tree:
91
+
92
+ ```typescript
93
+ import { parse, evalAST } from "formulakit";
94
+ const ast = parse("x + y");
95
+ evalAST(ast, { vars: { x: 10, y: 20 } }); // 30
96
+ ```
97
+
98
+ ## Operators
99
+
100
+ | Operator | Description | Example |
101
+ |---|---|---|
102
+ | `+` `-` | Addition / subtraction | `2 + 3`, `5 - 1` |
103
+ | `*` `/` | Multiplication / division | `3 * 4`, `10 / 2` |
104
+ | `%` | Modulo | `7 % 3` → 1 |
105
+ | `^` | Power (right-associative) | `2^3^2` = `2^9` = 512 |
106
+ | `==` `!=` | Equality (returns 0 or 1) | `x == y` |
107
+ | `<` `<=` `>` `>=` | Comparison | `a < b` |
108
+ | `&&` | Logical AND (short-circuit) | `x > 0 && x < 10` |
109
+ | `\|\|` | Logical OR (short-circuit) | `x < 0 \|\| x > 100` |
110
+ | `!` | Logical NOT | `!0` → 1 |
111
+ | `? :` | Ternary | `x > 0 ? x : -x` |
112
+ | `(` `)` | Grouping | `(a + b) * c` |
113
+
114
+ ## Built-in constants
115
+
116
+ `PI`, `E`, `LN2`, `LN10`, `LOG2E`, `LOG10E`, `SQRT2`, `Infinity`, `NaN`
117
+
118
+ ## Built-in functions
119
+
120
+ | Function | Description |
121
+ |---|---|
122
+ | `abs(x)` | Absolute value |
123
+ | `ceil(x)` / `floor(x)` / `round(x)` / `trunc(x)` | Rounding |
124
+ | `sign(x)` | Sign (-1 / 0 / 1) |
125
+ | `sqrt(x)` / `cbrt(x)` | Square/cube root |
126
+ | `pow(x, y)` | Power (same as `x^y`) |
127
+ | `exp(x)` | e^x |
128
+ | `log(x)` / `log2(x)` / `log10(x)` | Logarithms |
129
+ | `sin(x)` / `cos(x)` / `tan(x)` | Trig (radians) |
130
+ | `asin(x)` / `acos(x)` / `atan(x)` / `atan2(y,x)` | Inverse trig |
131
+ | `sinh(x)` / `cosh(x)` / `tanh(x)` | Hyperbolic |
132
+ | `min(...)` / `max(...)` | Min/max of N args |
133
+ | `clamp(x, lo, hi)` | Clamp to [lo, hi] |
134
+ | `hypot(x, y, ...)` | Hypotenuse |
135
+ | `isNaN(x)` / `isFinite(x)` | Tests (returns 0 or 1) |
136
+
137
+ ## Options
138
+
139
+ ```typescript
140
+ evaluate(expr, vars, {
141
+ functions: { double: (x) => x * 2 }, // custom functions
142
+ maxDepth: 200, // max AST recursion (default 200)
143
+ });
144
+ ```
145
+
146
+ ## Examples
147
+
148
+ ### BMI calculator
149
+
150
+ ```typescript
151
+ const bmi = compile("weight / height^2");
152
+ bmi.evaluate({ weight: 70, height: 1.75 }); // 22.86
153
+ ```
154
+
155
+ ### Compound interest
156
+
157
+ ```typescript
158
+ evaluate("P * (1 + r/n) ^ (n * t)", { P: 1000, r: 0.05, n: 12, t: 10 });
159
+ // 1647.01
160
+ ```
161
+
162
+ ### Spreadsheet-style formula
163
+
164
+ ```typescript
165
+ const formula = compile("IF > threshold ? value * multiplier : value");
166
+ // ... or with ternary:
167
+ compile("value > threshold ? value * multiplier : value")
168
+ .evaluate({ value: 200, threshold: 100, multiplier: 1.5 }); // 300
169
+ ```
170
+
171
+ ### Dynamic pricing
172
+
173
+ ```typescript
174
+ const price = compile("qty >= 100 ? base * 0.8 : qty >= 50 ? base * 0.9 : base");
175
+ price.evaluate({ qty: 150, base: 10 }); // 8
176
+ price.evaluate({ qty: 75, base: 10 }); // 9
177
+ price.evaluate({ qty: 10, base: 10 }); // 10
178
+ ```
179
+
180
+ ### Custom functions
181
+
182
+ ```typescript
183
+ import { evaluate } from "formulakit";
184
+
185
+ evaluate("toCelsius(98.6)", {}, {
186
+ functions: {
187
+ toCelsius: (f) => (f - 32) * 5/9,
188
+ },
189
+ }); // 37
190
+ ```
191
+
192
+ ## Contributors ✨
193
+
194
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the [emoji key](https://allcontributors.org/docs/en/emoji-key) for how each contribution is recognized, and open a PR or issue to get involved.
195
+
196
+ Thanks goes to these wonderful people:
197
+
198
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
199
+ <!-- prettier-ignore-start -->
200
+ <!-- markdownlint-disable -->
201
+ <table>
202
+ <tbody>
203
+ <tr>
204
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/trananhtung"><img src="https://avatars.githubusercontent.com/u/30992229?v=4?s=100" width="100px;" alt="Tung Tran"/><br /><sub><b>Tung Tran</b></sub></a><br /><a href="https://github.com/trananhtung/formulakit/commits?author=trananhtung" title="Code">💻</a> <a href="#maintenance-trananhtung" title="Maintenance">🚧</a></td>
205
+ </tr>
206
+ </tbody>
207
+ </table>
208
+
209
+ <!-- markdownlint-restore -->
210
+ <!-- prettier-ignore-end -->
211
+
212
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
213
+
214
+ ## License
215
+
216
+ MIT © [trananhtung](https://github.com/trananhtung)
package/dist/index.cjs ADDED
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BUILTIN_FUNCTIONS: () => BUILTIN_FUNCTIONS,
24
+ BUILTIN_VARS: () => BUILTIN_VARS,
25
+ CompiledFormula: () => CompiledFormula,
26
+ FormulaError: () => FormulaError,
27
+ compile: () => compile,
28
+ evalAST: () => evalAST,
29
+ evaluate: () => evaluate,
30
+ parse: () => parse,
31
+ tryEvaluate: () => tryEvaluate
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/types.ts
36
+ var FormulaError = class extends Error {
37
+ constructor(message, position) {
38
+ super(message);
39
+ this.position = position;
40
+ this.name = "FormulaError";
41
+ }
42
+ };
43
+
44
+ // src/lexer.ts
45
+ function tokenize(src) {
46
+ const tokens = [];
47
+ let i = 0;
48
+ while (i < src.length) {
49
+ if (/\s/.test(src[i])) {
50
+ i++;
51
+ continue;
52
+ }
53
+ const pos = i;
54
+ const ch = src[i];
55
+ if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(src[i + 1] ?? "")) {
56
+ let raw = "";
57
+ while (i < src.length && /[0-9.]/.test(src[i])) raw += src[i++];
58
+ if (src[i] === "e" || src[i] === "E") {
59
+ raw += src[i++];
60
+ if (src[i] === "+" || src[i] === "-") raw += src[i++];
61
+ while (i < src.length && /[0-9]/.test(src[i])) raw += src[i++];
62
+ }
63
+ tokens.push({ type: "Number" /* Number */, raw, pos });
64
+ continue;
65
+ }
66
+ if (/[a-zA-Z_]/.test(ch)) {
67
+ let raw = "";
68
+ while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) raw += src[i++];
69
+ tokens.push({ type: "Ident" /* Ident */, raw, pos });
70
+ continue;
71
+ }
72
+ const two = src.slice(i, i + 2);
73
+ if (two === "==" || two === "!=" || two === "<=" || two === ">=" || two === "&&" || two === "||") {
74
+ tokens.push({ type: two, raw: two, pos });
75
+ i += 2;
76
+ continue;
77
+ }
78
+ const singles = {
79
+ "+": "+" /* Plus */,
80
+ "-": "-" /* Minus */,
81
+ "*": "*" /* Star */,
82
+ "/": "/" /* Slash */,
83
+ "%": "%" /* Percent */,
84
+ "^": "^" /* Caret */,
85
+ "(": "(" /* LParen */,
86
+ ")": ")" /* RParen */,
87
+ ",": "," /* Comma */,
88
+ "?": "?" /* Question */,
89
+ ":": ":" /* Colon */,
90
+ "<": "<" /* Lt */,
91
+ ">": ">" /* Gt */,
92
+ "!": "!" /* Not */
93
+ };
94
+ if (ch in singles) {
95
+ tokens.push({ type: singles[ch], raw: ch, pos });
96
+ i++;
97
+ continue;
98
+ }
99
+ throw new FormulaError(`Unexpected character '${ch}'`, pos);
100
+ }
101
+ tokens.push({ type: "EOF" /* EOF */, raw: "", pos: i });
102
+ return tokens;
103
+ }
104
+
105
+ // src/parser.ts
106
+ var BP = {
107
+ ["||" /* Or */]: 10,
108
+ ["&&" /* And */]: 20,
109
+ ["==" /* Eq */]: 30,
110
+ ["!=" /* NEq */]: 30,
111
+ ["<" /* Lt */]: 40,
112
+ ["<=" /* Lte */]: 40,
113
+ [">" /* Gt */]: 40,
114
+ [">=" /* Gte */]: 40,
115
+ ["+" /* Plus */]: 50,
116
+ ["-" /* Minus */]: 50,
117
+ ["*" /* Star */]: 60,
118
+ ["/" /* Slash */]: 60,
119
+ ["%" /* Percent */]: 60,
120
+ ["^" /* Caret */]: 70
121
+ // right-assoc → parse at BP-1
122
+ };
123
+ var Parser = class {
124
+ constructor(src) {
125
+ this.pos = 0;
126
+ this.tokens = tokenize(src);
127
+ }
128
+ peek() {
129
+ return this.tokens[this.pos];
130
+ }
131
+ consume() {
132
+ return this.tokens[this.pos++];
133
+ }
134
+ expect(type) {
135
+ const t = this.consume();
136
+ if (t.type !== type) throw new FormulaError(`Expected '${type}' but got '${t.raw}'`, t.pos);
137
+ return t;
138
+ }
139
+ parse() {
140
+ const node = this.expr(0);
141
+ if (this.peek().type !== "EOF" /* EOF */) {
142
+ const t = this.peek();
143
+ throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);
144
+ }
145
+ return node;
146
+ }
147
+ expr(minBP) {
148
+ let left = this.prefix();
149
+ while (true) {
150
+ const t = this.peek();
151
+ if (t.type === "?" /* Question */ && minBP < 5) {
152
+ this.consume();
153
+ const consequent = this.expr(0);
154
+ this.expect(":" /* Colon */);
155
+ const alternate = this.expr(0);
156
+ left = { kind: "conditional", condition: left, consequent, alternate };
157
+ continue;
158
+ }
159
+ const bp = BP[t.type];
160
+ if (bp === void 0 || bp <= minBP) break;
161
+ this.consume();
162
+ const rightBP = t.type === "^" /* Caret */ ? bp - 1 : bp;
163
+ const right = this.expr(rightBP);
164
+ left = { kind: "binary", op: t.type, left, right };
165
+ }
166
+ return left;
167
+ }
168
+ prefix() {
169
+ const t = this.peek();
170
+ if (t.type === "Number" /* Number */) {
171
+ this.consume();
172
+ return { kind: "number", value: parseFloat(t.raw) };
173
+ }
174
+ if (t.type === "Ident" /* Ident */) {
175
+ this.consume();
176
+ if (this.peek().type === "(" /* LParen */) {
177
+ this.consume();
178
+ const args = [];
179
+ if (this.peek().type !== ")" /* RParen */) {
180
+ args.push(this.expr(0));
181
+ while (this.peek().type === "," /* Comma */) {
182
+ this.consume();
183
+ args.push(this.expr(0));
184
+ }
185
+ }
186
+ this.expect(")" /* RParen */);
187
+ return { kind: "call", name: t.raw, args };
188
+ }
189
+ return { kind: "identifier", name: t.raw };
190
+ }
191
+ if (t.type === "-" /* Minus */) {
192
+ this.consume();
193
+ return { kind: "unary", op: "-", operand: this.expr(65) };
194
+ }
195
+ if (t.type === "!" /* Not */) {
196
+ this.consume();
197
+ return { kind: "unary", op: "!", operand: this.expr(65) };
198
+ }
199
+ if (t.type === "+" /* Plus */) {
200
+ this.consume();
201
+ return this.expr(65);
202
+ }
203
+ if (t.type === "(" /* LParen */) {
204
+ this.consume();
205
+ const inner = this.expr(0);
206
+ this.expect(")" /* RParen */);
207
+ return inner;
208
+ }
209
+ throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);
210
+ }
211
+ };
212
+ function parse(src) {
213
+ return new Parser(src).parse();
214
+ }
215
+
216
+ // src/builtins.ts
217
+ var BUILTIN_VARS = {
218
+ PI: Math.PI,
219
+ E: Math.E,
220
+ LN2: Math.LN2,
221
+ LN10: Math.LN10,
222
+ LOG2E: Math.LOG2E,
223
+ LOG10E: Math.LOG10E,
224
+ SQRT2: Math.SQRT2,
225
+ Infinity: Infinity,
226
+ NaN: NaN
227
+ };
228
+ var BUILTIN_FUNCTIONS = {
229
+ abs: (x) => Math.abs(x),
230
+ ceil: (x) => Math.ceil(x),
231
+ floor: (x) => Math.floor(x),
232
+ round: (x) => Math.round(x),
233
+ trunc: (x) => Math.trunc(x),
234
+ sign: (x) => Math.sign(x),
235
+ sqrt: (x) => Math.sqrt(x),
236
+ cbrt: (x) => Math.cbrt(x),
237
+ pow: (x, y) => Math.pow(x, y),
238
+ exp: (x) => Math.exp(x),
239
+ log: (x) => Math.log(x),
240
+ log2: (x) => Math.log2(x),
241
+ log10: (x) => Math.log10(x),
242
+ sin: (x) => Math.sin(x),
243
+ cos: (x) => Math.cos(x),
244
+ tan: (x) => Math.tan(x),
245
+ asin: (x) => Math.asin(x),
246
+ acos: (x) => Math.acos(x),
247
+ atan: (x) => Math.atan(x),
248
+ atan2: (y, x) => Math.atan2(y, x),
249
+ sinh: (x) => Math.sinh(x),
250
+ cosh: (x) => Math.cosh(x),
251
+ tanh: (x) => Math.tanh(x),
252
+ min: (...args) => Math.min(...args),
253
+ max: (...args) => Math.max(...args),
254
+ clamp: (x, lo, hi) => Math.min(Math.max(x, lo), hi),
255
+ hypot: (...args) => Math.hypot(...args),
256
+ random: () => Math.random(),
257
+ isNaN: (x) => isNaN(x) ? 1 : 0,
258
+ isFinite: (x) => isFinite(x) ? 1 : 0
259
+ };
260
+
261
+ // src/evaluator.ts
262
+ function evalNode(node, opts, depth) {
263
+ if (depth > opts.maxDepth) throw new FormulaError("Maximum recursion depth exceeded");
264
+ const d = depth + 1;
265
+ switch (node.kind) {
266
+ case "number":
267
+ return node.value;
268
+ case "identifier": {
269
+ const name = node.name;
270
+ if (name in opts.vars) return opts.vars[name];
271
+ if (name in BUILTIN_VARS) return BUILTIN_VARS[name];
272
+ throw new FormulaError(`Unknown variable '${name}'`);
273
+ }
274
+ case "unary": {
275
+ const v = evalNode(node.operand, opts, d);
276
+ if (node.op === "-") return -v;
277
+ if (node.op === "!") return v === 0 ? 1 : 0;
278
+ throw new FormulaError(`Unknown unary operator '${node.op}'`);
279
+ }
280
+ case "binary": {
281
+ const l = evalNode(node.left, opts, d);
282
+ if (node.op === "&&") return l !== 0 ? evalNode(node.right, opts, d) : 0;
283
+ if (node.op === "||") return l !== 0 ? l : evalNode(node.right, opts, d);
284
+ const r = evalNode(node.right, opts, d);
285
+ switch (node.op) {
286
+ case "+":
287
+ return l + r;
288
+ case "-":
289
+ return l - r;
290
+ case "*":
291
+ return l * r;
292
+ case "/":
293
+ if (r === 0) throw new FormulaError("Division by zero");
294
+ return l / r;
295
+ case "%":
296
+ if (r === 0) throw new FormulaError("Modulo by zero");
297
+ return l % r;
298
+ case "^":
299
+ return Math.pow(l, r);
300
+ case "==":
301
+ return l === r ? 1 : 0;
302
+ case "!=":
303
+ return l !== r ? 1 : 0;
304
+ case "<":
305
+ return l < r ? 1 : 0;
306
+ case "<=":
307
+ return l <= r ? 1 : 0;
308
+ case ">":
309
+ return l > r ? 1 : 0;
310
+ case ">=":
311
+ return l >= r ? 1 : 0;
312
+ default:
313
+ throw new FormulaError(`Unknown operator '${node.op}'`);
314
+ }
315
+ }
316
+ case "call": {
317
+ const fn = opts.functions[node.name] ?? BUILTIN_FUNCTIONS[node.name];
318
+ if (!fn) throw new FormulaError(`Unknown function '${node.name}'`);
319
+ const args = node.args.map((a) => evalNode(a, opts, d));
320
+ return fn(...args);
321
+ }
322
+ case "conditional": {
323
+ const cond = evalNode(node.condition, opts, d);
324
+ return cond !== 0 ? evalNode(node.consequent, opts, d) : evalNode(node.alternate, opts, d);
325
+ }
326
+ }
327
+ }
328
+ function evalAST(node, opts = {}) {
329
+ const resolved = {
330
+ vars: opts.vars ?? {},
331
+ functions: opts.functions ?? {},
332
+ maxDepth: opts.maxDepth ?? 200
333
+ };
334
+ return evalNode(node, resolved, 0);
335
+ }
336
+
337
+ // src/formula.ts
338
+ function compile(expr) {
339
+ const ast = parse(expr);
340
+ return new CompiledFormula(ast, expr);
341
+ }
342
+ function evaluate(expr, vars, opts) {
343
+ const ast = parse(expr);
344
+ return evalAST(ast, { ...opts, vars });
345
+ }
346
+ function tryEvaluate(expr, vars, opts) {
347
+ try {
348
+ return evaluate(expr, vars, opts);
349
+ } catch {
350
+ return null;
351
+ }
352
+ }
353
+ var CompiledFormula = class {
354
+ constructor(ast, source) {
355
+ this._ast = ast;
356
+ this.source = source;
357
+ }
358
+ evaluate(vars, opts) {
359
+ return evalAST(this._ast, { ...opts, vars });
360
+ }
361
+ /** Return all variable names referenced in this formula. */
362
+ variables() {
363
+ const names = /* @__PURE__ */ new Set();
364
+ collectVars(this._ast, names);
365
+ return [...names].sort();
366
+ }
367
+ };
368
+ function collectVars(node, out) {
369
+ switch (node.kind) {
370
+ case "number":
371
+ break;
372
+ case "identifier":
373
+ out.add(node.name);
374
+ break;
375
+ case "unary":
376
+ collectVars(node.operand, out);
377
+ break;
378
+ case "binary":
379
+ collectVars(node.left, out);
380
+ collectVars(node.right, out);
381
+ break;
382
+ case "call":
383
+ node.args.forEach((a) => collectVars(a, out));
384
+ break;
385
+ case "conditional":
386
+ collectVars(node.condition, out);
387
+ collectVars(node.consequent, out);
388
+ collectVars(node.alternate, out);
389
+ break;
390
+ }
391
+ }
392
+ // Annotate the CommonJS export names for ESM import in node:
393
+ 0 && (module.exports = {
394
+ BUILTIN_FUNCTIONS,
395
+ BUILTIN_VARS,
396
+ CompiledFormula,
397
+ FormulaError,
398
+ compile,
399
+ evalAST,
400
+ evaluate,
401
+ parse,
402
+ tryEvaluate
403
+ });
404
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/lexer.ts","../src/parser.ts","../src/builtins.ts","../src/evaluator.ts","../src/formula.ts"],"sourcesContent":["export type { Value, Variables, FunctionMap, EvalOptions, ASTNode } from \"./types.js\";\nexport { FormulaError } from \"./types.js\";\nexport { parse } from \"./parser.js\";\nexport { evalAST } from \"./evaluator.js\";\nexport { evaluate, tryEvaluate, compile, CompiledFormula } from \"./formula.js\";\nexport { BUILTIN_VARS, BUILTIN_FUNCTIONS } from \"./builtins.js\";\n","export type Value = number;\n\nexport type Variables = Record<string, Value>;\n\nexport type FunctionMap = Record<string, (...args: Value[]) => Value>;\n\nexport interface EvalOptions {\n vars?: Variables;\n functions?: FunctionMap;\n maxDepth?: number;\n}\n\nexport class FormulaError extends Error {\n constructor(\n message: string,\n public readonly position?: number\n ) {\n super(message);\n this.name = \"FormulaError\";\n }\n}\n\n// ── AST node types ────────────────────────────────────────────────────────────\n\nexport type NodeKind =\n | \"number\"\n | \"identifier\"\n | \"binary\"\n | \"unary\"\n | \"call\"\n | \"conditional\";\n\nexport interface NumberNode {\n kind: \"number\";\n value: number;\n}\n\nexport interface IdentifierNode {\n kind: \"identifier\";\n name: string;\n}\n\nexport interface BinaryNode {\n kind: \"binary\";\n op: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryNode {\n kind: \"unary\";\n op: string;\n operand: ASTNode;\n}\n\nexport interface CallNode {\n kind: \"call\";\n name: string;\n args: ASTNode[];\n}\n\nexport interface ConditionalNode {\n kind: \"conditional\";\n condition: ASTNode;\n consequent: ASTNode;\n alternate: ASTNode;\n}\n\nexport type ASTNode =\n | NumberNode\n | IdentifierNode\n | BinaryNode\n | UnaryNode\n | CallNode\n | ConditionalNode;\n","import { FormulaError } from \"./types.js\";\n\nexport const enum TT {\n Number = \"Number\",\n Ident = \"Ident\",\n Plus = \"+\",\n Minus = \"-\",\n Star = \"*\",\n Slash = \"/\",\n Percent = \"%\",\n Caret = \"^\",\n LParen = \"(\",\n RParen = \")\",\n Comma = \",\",\n Question = \"?\",\n Colon = \":\",\n Eq = \"==\",\n NEq = \"!=\",\n Lt = \"<\",\n Lte = \"<=\",\n Gt = \">\",\n Gte = \">=\",\n And = \"&&\",\n Or = \"||\",\n Not = \"!\",\n EOF = \"EOF\",\n}\n\nexport interface Token {\n type: TT;\n raw: string;\n pos: number;\n}\n\nexport function tokenize(src: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n while (i < src.length) {\n // skip whitespace\n if (/\\s/.test(src[i])) { i++; continue; }\n\n const pos = i;\n const ch = src[i];\n\n // numbers (int or float, optional exponent)\n if (/[0-9]/.test(ch) || (ch === \".\" && /[0-9]/.test(src[i + 1] ?? \"\"))) {\n let raw = \"\";\n while (i < src.length && /[0-9.]/.test(src[i])) raw += src[i++];\n if (src[i] === \"e\" || src[i] === \"E\") {\n raw += src[i++];\n if (src[i] === \"+\" || src[i] === \"-\") raw += src[i++];\n while (i < src.length && /[0-9]/.test(src[i])) raw += src[i++];\n }\n tokens.push({ type: TT.Number, raw, pos });\n continue;\n }\n\n // identifiers and keywords (only PI/E)\n if (/[a-zA-Z_]/.test(ch)) {\n let raw = \"\";\n while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) raw += src[i++];\n tokens.push({ type: TT.Ident, raw, pos });\n continue;\n }\n\n // two-char operators\n const two = src.slice(i, i + 2);\n if (two === \"==\" || two === \"!=\" || two === \"<=\" || two === \">=\" || two === \"&&\" || two === \"||\") {\n tokens.push({ type: two as TT, raw: two, pos });\n i += 2;\n continue;\n }\n\n // one-char operators\n const singles: Record<string, TT> = {\n \"+\": TT.Plus, \"-\": TT.Minus, \"*\": TT.Star, \"/\": TT.Slash,\n \"%\": TT.Percent, \"^\": TT.Caret,\n \"(\": TT.LParen, \")\": TT.RParen, \",\": TT.Comma,\n \"?\": TT.Question, \":\": TT.Colon,\n \"<\": TT.Lt, \">\": TT.Gt, \"!\": TT.Not,\n };\n if (ch in singles) {\n tokens.push({ type: singles[ch], raw: ch, pos });\n i++;\n continue;\n }\n\n throw new FormulaError(`Unexpected character '${ch}'`, pos);\n }\n\n tokens.push({ type: TT.EOF, raw: \"\", pos: i });\n return tokens;\n}\n","import { tokenize, Token, TT } from \"./lexer.js\";\nimport {\n ASTNode, NumberNode, IdentifierNode, BinaryNode,\n UnaryNode, CallNode, ConditionalNode, FormulaError,\n} from \"./types.js\";\n\n// Pratt parser (top-down operator precedence)\n\nconst BP: Partial<Record<TT, number>> = {\n [TT.Or]: 10,\n [TT.And]: 20,\n [TT.Eq]: 30, [TT.NEq]: 30,\n [TT.Lt]: 40, [TT.Lte]: 40, [TT.Gt]: 40, [TT.Gte]: 40,\n [TT.Plus]: 50, [TT.Minus]: 50,\n [TT.Star]: 60, [TT.Slash]: 60, [TT.Percent]: 60,\n [TT.Caret]: 70, // right-assoc → parse at BP-1\n};\n\nclass Parser {\n private tokens: Token[];\n private pos: number = 0;\n\n constructor(src: string) {\n this.tokens = tokenize(src);\n }\n\n private peek(): Token { return this.tokens[this.pos]; }\n private consume(): Token { return this.tokens[this.pos++]; }\n private expect(type: TT): Token {\n const t = this.consume();\n if (t.type !== type) throw new FormulaError(`Expected '${type}' but got '${t.raw}'`, t.pos);\n return t;\n }\n\n parse(): ASTNode {\n const node = this.expr(0);\n if (this.peek().type !== TT.EOF) {\n const t = this.peek();\n throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);\n }\n return node;\n }\n\n private expr(minBP: number): ASTNode {\n let left = this.prefix();\n\n while (true) {\n const t = this.peek();\n\n // ternary ? :\n if (t.type === TT.Question && minBP < 5) {\n this.consume();\n const consequent = this.expr(0);\n this.expect(TT.Colon);\n const alternate = this.expr(0);\n left = { kind: \"conditional\", condition: left, consequent, alternate } as ConditionalNode;\n continue;\n }\n\n const bp = BP[t.type as TT];\n if (bp === undefined || bp <= minBP) break;\n\n this.consume();\n const rightBP = t.type === TT.Caret ? bp - 1 : bp; // ^ is right-assoc\n const right = this.expr(rightBP);\n left = { kind: \"binary\", op: t.type, left, right } as BinaryNode;\n }\n\n return left;\n }\n\n private prefix(): ASTNode {\n const t = this.peek();\n\n if (t.type === TT.Number) {\n this.consume();\n return { kind: \"number\", value: parseFloat(t.raw) } as NumberNode;\n }\n\n if (t.type === TT.Ident) {\n this.consume();\n // function call?\n if (this.peek().type === TT.LParen) {\n this.consume(); // (\n const args: ASTNode[] = [];\n if (this.peek().type !== TT.RParen) {\n args.push(this.expr(0));\n while (this.peek().type === TT.Comma) {\n this.consume();\n args.push(this.expr(0));\n }\n }\n this.expect(TT.RParen);\n return { kind: \"call\", name: t.raw, args } as CallNode;\n }\n return { kind: \"identifier\", name: t.raw } as IdentifierNode;\n }\n\n if (t.type === TT.Minus) {\n this.consume();\n return { kind: \"unary\", op: \"-\", operand: this.expr(65) } as UnaryNode;\n }\n\n if (t.type === TT.Not) {\n this.consume();\n return { kind: \"unary\", op: \"!\", operand: this.expr(65) } as UnaryNode;\n }\n\n if (t.type === TT.Plus) {\n this.consume();\n return this.expr(65); // unary +\n }\n\n if (t.type === TT.LParen) {\n this.consume();\n const inner = this.expr(0);\n this.expect(TT.RParen);\n return inner;\n }\n\n throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);\n }\n}\n\nexport function parse(src: string): ASTNode {\n return new Parser(src).parse();\n}\n","import { FunctionMap, Variables } from \"./types.js\";\n\nexport const BUILTIN_VARS: Variables = {\n PI: Math.PI,\n E: Math.E,\n LN2: Math.LN2,\n LN10: Math.LN10,\n LOG2E: Math.LOG2E,\n LOG10E: Math.LOG10E,\n SQRT2: Math.SQRT2,\n Infinity: Infinity,\n NaN: NaN,\n};\n\nexport const BUILTIN_FUNCTIONS: FunctionMap = {\n abs: (x) => Math.abs(x),\n ceil: (x) => Math.ceil(x),\n floor: (x) => Math.floor(x),\n round: (x) => Math.round(x),\n trunc: (x) => Math.trunc(x),\n sign: (x) => Math.sign(x),\n\n sqrt: (x) => Math.sqrt(x),\n cbrt: (x) => Math.cbrt(x),\n pow: (x, y) => Math.pow(x, y),\n exp: (x) => Math.exp(x),\n log: (x) => Math.log(x),\n log2: (x) => Math.log2(x),\n log10: (x) => Math.log10(x),\n\n sin: (x) => Math.sin(x),\n cos: (x) => Math.cos(x),\n tan: (x) => Math.tan(x),\n asin: (x) => Math.asin(x),\n acos: (x) => Math.acos(x),\n atan: (x) => Math.atan(x),\n atan2: (y, x) => Math.atan2(y, x),\n sinh: (x) => Math.sinh(x),\n cosh: (x) => Math.cosh(x),\n tanh: (x) => Math.tanh(x),\n\n min: (...args) => Math.min(...args),\n max: (...args) => Math.max(...args),\n clamp: (x, lo, hi) => Math.min(Math.max(x, lo), hi),\n\n hypot: (...args) => Math.hypot(...args),\n\n random: () => Math.random(),\n\n isNaN: (x) => (isNaN(x) ? 1 : 0),\n isFinite: (x) => (isFinite(x) ? 1 : 0),\n};\n","import { ASTNode, EvalOptions, FormulaError, Value } from \"./types.js\";\nimport { BUILTIN_VARS, BUILTIN_FUNCTIONS } from \"./builtins.js\";\n\nfunction evalNode(node: ASTNode, opts: Required<EvalOptions>, depth: number): Value {\n if (depth > opts.maxDepth) throw new FormulaError(\"Maximum recursion depth exceeded\");\n\n const d = depth + 1;\n\n switch (node.kind) {\n case \"number\":\n return node.value;\n\n case \"identifier\": {\n const name = node.name;\n if (name in opts.vars) return opts.vars[name];\n if (name in BUILTIN_VARS) return BUILTIN_VARS[name];\n throw new FormulaError(`Unknown variable '${name}'`);\n }\n\n case \"unary\": {\n const v = evalNode(node.operand, opts, d);\n if (node.op === \"-\") return -v;\n if (node.op === \"!\") return v === 0 ? 1 : 0;\n throw new FormulaError(`Unknown unary operator '${node.op}'`);\n }\n\n case \"binary\": {\n const l = evalNode(node.left, opts, d);\n // short-circuit for && / ||\n if (node.op === \"&&\") return (l !== 0 ? evalNode(node.right, opts, d) : 0);\n if (node.op === \"||\") return (l !== 0 ? l : evalNode(node.right, opts, d));\n\n const r = evalNode(node.right, opts, d);\n switch (node.op) {\n case \"+\": return l + r;\n case \"-\": return l - r;\n case \"*\": return l * r;\n case \"/\":\n if (r === 0) throw new FormulaError(\"Division by zero\");\n return l / r;\n case \"%\":\n if (r === 0) throw new FormulaError(\"Modulo by zero\");\n return l % r;\n case \"^\": return Math.pow(l, r);\n case \"==\": return l === r ? 1 : 0;\n case \"!=\": return l !== r ? 1 : 0;\n case \"<\": return l < r ? 1 : 0;\n case \"<=\": return l <= r ? 1 : 0;\n case \">\": return l > r ? 1 : 0;\n case \">=\": return l >= r ? 1 : 0;\n default:\n throw new FormulaError(`Unknown operator '${node.op}'`);\n }\n }\n\n case \"call\": {\n const fn = opts.functions[node.name] ?? BUILTIN_FUNCTIONS[node.name];\n if (!fn) throw new FormulaError(`Unknown function '${node.name}'`);\n const args = node.args.map((a) => evalNode(a, opts, d));\n return fn(...args);\n }\n\n case \"conditional\": {\n const cond = evalNode(node.condition, opts, d);\n return cond !== 0\n ? evalNode(node.consequent, opts, d)\n : evalNode(node.alternate, opts, d);\n }\n }\n}\n\nexport function evalAST(node: ASTNode, opts: EvalOptions = {}): Value {\n const resolved: Required<EvalOptions> = {\n vars: opts.vars ?? {},\n functions: opts.functions ?? {},\n maxDepth: opts.maxDepth ?? 200,\n };\n return evalNode(node, resolved, 0);\n}\n","import { parse } from \"./parser.js\";\nimport { evalAST } from \"./evaluator.js\";\nimport { ASTNode, EvalOptions, Value } from \"./types.js\";\n\n/**\n * Parse an expression string into an AST (reuse for repeated evaluations).\n */\nexport function compile(expr: string): CompiledFormula {\n const ast = parse(expr);\n return new CompiledFormula(ast, expr);\n}\n\n/**\n * Parse and evaluate an expression in one step.\n */\nexport function evaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value {\n const ast = parse(expr);\n return evalAST(ast, { ...opts, vars });\n}\n\n/**\n * Parse and evaluate, returning null instead of throwing on parse/eval errors.\n */\nexport function tryEvaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value | null {\n try { return evaluate(expr, vars, opts); }\n catch { return null; }\n}\n\nexport class CompiledFormula {\n readonly source: string;\n private readonly _ast: ASTNode;\n\n constructor(ast: ASTNode, source: string) {\n this._ast = ast;\n this.source = source;\n }\n\n evaluate(vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value {\n return evalAST(this._ast, { ...opts, vars });\n }\n\n /** Return all variable names referenced in this formula. */\n variables(): string[] {\n const names = new Set<string>();\n collectVars(this._ast, names);\n return [...names].sort();\n }\n}\n\nfunction collectVars(node: ASTNode, out: Set<string>): void {\n switch (node.kind) {\n case \"number\": break;\n case \"identifier\": out.add(node.name); break;\n case \"unary\": collectVars(node.operand, out); break;\n case \"binary\": collectVars(node.left, out); collectVars(node.right, out); break;\n case \"call\": node.args.forEach((a) => collectVars(a, out)); break;\n case \"conditional\":\n collectVars(node.condition, out);\n collectVars(node.consequent, out);\n collectVars(node.alternate, out);\n break;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,73 @@
1
+ type Value = number;
2
+ type Variables = Record<string, Value>;
3
+ type FunctionMap = Record<string, (...args: Value[]) => Value>;
4
+ interface EvalOptions {
5
+ vars?: Variables;
6
+ functions?: FunctionMap;
7
+ maxDepth?: number;
8
+ }
9
+ declare class FormulaError extends Error {
10
+ readonly position?: number | undefined;
11
+ constructor(message: string, position?: number | undefined);
12
+ }
13
+ interface NumberNode {
14
+ kind: "number";
15
+ value: number;
16
+ }
17
+ interface IdentifierNode {
18
+ kind: "identifier";
19
+ name: string;
20
+ }
21
+ interface BinaryNode {
22
+ kind: "binary";
23
+ op: string;
24
+ left: ASTNode;
25
+ right: ASTNode;
26
+ }
27
+ interface UnaryNode {
28
+ kind: "unary";
29
+ op: string;
30
+ operand: ASTNode;
31
+ }
32
+ interface CallNode {
33
+ kind: "call";
34
+ name: string;
35
+ args: ASTNode[];
36
+ }
37
+ interface ConditionalNode {
38
+ kind: "conditional";
39
+ condition: ASTNode;
40
+ consequent: ASTNode;
41
+ alternate: ASTNode;
42
+ }
43
+ type ASTNode = NumberNode | IdentifierNode | BinaryNode | UnaryNode | CallNode | ConditionalNode;
44
+
45
+ declare function parse(src: string): ASTNode;
46
+
47
+ declare function evalAST(node: ASTNode, opts?: EvalOptions): Value;
48
+
49
+ /**
50
+ * Parse an expression string into an AST (reuse for repeated evaluations).
51
+ */
52
+ declare function compile(expr: string): CompiledFormula;
53
+ /**
54
+ * Parse and evaluate an expression in one step.
55
+ */
56
+ declare function evaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value;
57
+ /**
58
+ * Parse and evaluate, returning null instead of throwing on parse/eval errors.
59
+ */
60
+ declare function tryEvaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value | null;
61
+ declare class CompiledFormula {
62
+ readonly source: string;
63
+ private readonly _ast;
64
+ constructor(ast: ASTNode, source: string);
65
+ evaluate(vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value;
66
+ /** Return all variable names referenced in this formula. */
67
+ variables(): string[];
68
+ }
69
+
70
+ declare const BUILTIN_VARS: Variables;
71
+ declare const BUILTIN_FUNCTIONS: FunctionMap;
72
+
73
+ export { type ASTNode, BUILTIN_FUNCTIONS, BUILTIN_VARS, CompiledFormula, type EvalOptions, FormulaError, type FunctionMap, type Value, type Variables, compile, evalAST, evaluate, parse, tryEvaluate };
@@ -0,0 +1,73 @@
1
+ type Value = number;
2
+ type Variables = Record<string, Value>;
3
+ type FunctionMap = Record<string, (...args: Value[]) => Value>;
4
+ interface EvalOptions {
5
+ vars?: Variables;
6
+ functions?: FunctionMap;
7
+ maxDepth?: number;
8
+ }
9
+ declare class FormulaError extends Error {
10
+ readonly position?: number | undefined;
11
+ constructor(message: string, position?: number | undefined);
12
+ }
13
+ interface NumberNode {
14
+ kind: "number";
15
+ value: number;
16
+ }
17
+ interface IdentifierNode {
18
+ kind: "identifier";
19
+ name: string;
20
+ }
21
+ interface BinaryNode {
22
+ kind: "binary";
23
+ op: string;
24
+ left: ASTNode;
25
+ right: ASTNode;
26
+ }
27
+ interface UnaryNode {
28
+ kind: "unary";
29
+ op: string;
30
+ operand: ASTNode;
31
+ }
32
+ interface CallNode {
33
+ kind: "call";
34
+ name: string;
35
+ args: ASTNode[];
36
+ }
37
+ interface ConditionalNode {
38
+ kind: "conditional";
39
+ condition: ASTNode;
40
+ consequent: ASTNode;
41
+ alternate: ASTNode;
42
+ }
43
+ type ASTNode = NumberNode | IdentifierNode | BinaryNode | UnaryNode | CallNode | ConditionalNode;
44
+
45
+ declare function parse(src: string): ASTNode;
46
+
47
+ declare function evalAST(node: ASTNode, opts?: EvalOptions): Value;
48
+
49
+ /**
50
+ * Parse an expression string into an AST (reuse for repeated evaluations).
51
+ */
52
+ declare function compile(expr: string): CompiledFormula;
53
+ /**
54
+ * Parse and evaluate an expression in one step.
55
+ */
56
+ declare function evaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value;
57
+ /**
58
+ * Parse and evaluate, returning null instead of throwing on parse/eval errors.
59
+ */
60
+ declare function tryEvaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value | null;
61
+ declare class CompiledFormula {
62
+ readonly source: string;
63
+ private readonly _ast;
64
+ constructor(ast: ASTNode, source: string);
65
+ evaluate(vars?: Record<string, Value>, opts?: Omit<EvalOptions, "vars">): Value;
66
+ /** Return all variable names referenced in this formula. */
67
+ variables(): string[];
68
+ }
69
+
70
+ declare const BUILTIN_VARS: Variables;
71
+ declare const BUILTIN_FUNCTIONS: FunctionMap;
72
+
73
+ export { type ASTNode, BUILTIN_FUNCTIONS, BUILTIN_VARS, CompiledFormula, type EvalOptions, FormulaError, type FunctionMap, type Value, type Variables, compile, evalAST, evaluate, parse, tryEvaluate };
package/dist/index.js ADDED
@@ -0,0 +1,369 @@
1
+ // src/types.ts
2
+ var FormulaError = class extends Error {
3
+ constructor(message, position) {
4
+ super(message);
5
+ this.position = position;
6
+ this.name = "FormulaError";
7
+ }
8
+ };
9
+
10
+ // src/lexer.ts
11
+ function tokenize(src) {
12
+ const tokens = [];
13
+ let i = 0;
14
+ while (i < src.length) {
15
+ if (/\s/.test(src[i])) {
16
+ i++;
17
+ continue;
18
+ }
19
+ const pos = i;
20
+ const ch = src[i];
21
+ if (/[0-9]/.test(ch) || ch === "." && /[0-9]/.test(src[i + 1] ?? "")) {
22
+ let raw = "";
23
+ while (i < src.length && /[0-9.]/.test(src[i])) raw += src[i++];
24
+ if (src[i] === "e" || src[i] === "E") {
25
+ raw += src[i++];
26
+ if (src[i] === "+" || src[i] === "-") raw += src[i++];
27
+ while (i < src.length && /[0-9]/.test(src[i])) raw += src[i++];
28
+ }
29
+ tokens.push({ type: "Number" /* Number */, raw, pos });
30
+ continue;
31
+ }
32
+ if (/[a-zA-Z_]/.test(ch)) {
33
+ let raw = "";
34
+ while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) raw += src[i++];
35
+ tokens.push({ type: "Ident" /* Ident */, raw, pos });
36
+ continue;
37
+ }
38
+ const two = src.slice(i, i + 2);
39
+ if (two === "==" || two === "!=" || two === "<=" || two === ">=" || two === "&&" || two === "||") {
40
+ tokens.push({ type: two, raw: two, pos });
41
+ i += 2;
42
+ continue;
43
+ }
44
+ const singles = {
45
+ "+": "+" /* Plus */,
46
+ "-": "-" /* Minus */,
47
+ "*": "*" /* Star */,
48
+ "/": "/" /* Slash */,
49
+ "%": "%" /* Percent */,
50
+ "^": "^" /* Caret */,
51
+ "(": "(" /* LParen */,
52
+ ")": ")" /* RParen */,
53
+ ",": "," /* Comma */,
54
+ "?": "?" /* Question */,
55
+ ":": ":" /* Colon */,
56
+ "<": "<" /* Lt */,
57
+ ">": ">" /* Gt */,
58
+ "!": "!" /* Not */
59
+ };
60
+ if (ch in singles) {
61
+ tokens.push({ type: singles[ch], raw: ch, pos });
62
+ i++;
63
+ continue;
64
+ }
65
+ throw new FormulaError(`Unexpected character '${ch}'`, pos);
66
+ }
67
+ tokens.push({ type: "EOF" /* EOF */, raw: "", pos: i });
68
+ return tokens;
69
+ }
70
+
71
+ // src/parser.ts
72
+ var BP = {
73
+ ["||" /* Or */]: 10,
74
+ ["&&" /* And */]: 20,
75
+ ["==" /* Eq */]: 30,
76
+ ["!=" /* NEq */]: 30,
77
+ ["<" /* Lt */]: 40,
78
+ ["<=" /* Lte */]: 40,
79
+ [">" /* Gt */]: 40,
80
+ [">=" /* Gte */]: 40,
81
+ ["+" /* Plus */]: 50,
82
+ ["-" /* Minus */]: 50,
83
+ ["*" /* Star */]: 60,
84
+ ["/" /* Slash */]: 60,
85
+ ["%" /* Percent */]: 60,
86
+ ["^" /* Caret */]: 70
87
+ // right-assoc → parse at BP-1
88
+ };
89
+ var Parser = class {
90
+ constructor(src) {
91
+ this.pos = 0;
92
+ this.tokens = tokenize(src);
93
+ }
94
+ peek() {
95
+ return this.tokens[this.pos];
96
+ }
97
+ consume() {
98
+ return this.tokens[this.pos++];
99
+ }
100
+ expect(type) {
101
+ const t = this.consume();
102
+ if (t.type !== type) throw new FormulaError(`Expected '${type}' but got '${t.raw}'`, t.pos);
103
+ return t;
104
+ }
105
+ parse() {
106
+ const node = this.expr(0);
107
+ if (this.peek().type !== "EOF" /* EOF */) {
108
+ const t = this.peek();
109
+ throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);
110
+ }
111
+ return node;
112
+ }
113
+ expr(minBP) {
114
+ let left = this.prefix();
115
+ while (true) {
116
+ const t = this.peek();
117
+ if (t.type === "?" /* Question */ && minBP < 5) {
118
+ this.consume();
119
+ const consequent = this.expr(0);
120
+ this.expect(":" /* Colon */);
121
+ const alternate = this.expr(0);
122
+ left = { kind: "conditional", condition: left, consequent, alternate };
123
+ continue;
124
+ }
125
+ const bp = BP[t.type];
126
+ if (bp === void 0 || bp <= minBP) break;
127
+ this.consume();
128
+ const rightBP = t.type === "^" /* Caret */ ? bp - 1 : bp;
129
+ const right = this.expr(rightBP);
130
+ left = { kind: "binary", op: t.type, left, right };
131
+ }
132
+ return left;
133
+ }
134
+ prefix() {
135
+ const t = this.peek();
136
+ if (t.type === "Number" /* Number */) {
137
+ this.consume();
138
+ return { kind: "number", value: parseFloat(t.raw) };
139
+ }
140
+ if (t.type === "Ident" /* Ident */) {
141
+ this.consume();
142
+ if (this.peek().type === "(" /* LParen */) {
143
+ this.consume();
144
+ const args = [];
145
+ if (this.peek().type !== ")" /* RParen */) {
146
+ args.push(this.expr(0));
147
+ while (this.peek().type === "," /* Comma */) {
148
+ this.consume();
149
+ args.push(this.expr(0));
150
+ }
151
+ }
152
+ this.expect(")" /* RParen */);
153
+ return { kind: "call", name: t.raw, args };
154
+ }
155
+ return { kind: "identifier", name: t.raw };
156
+ }
157
+ if (t.type === "-" /* Minus */) {
158
+ this.consume();
159
+ return { kind: "unary", op: "-", operand: this.expr(65) };
160
+ }
161
+ if (t.type === "!" /* Not */) {
162
+ this.consume();
163
+ return { kind: "unary", op: "!", operand: this.expr(65) };
164
+ }
165
+ if (t.type === "+" /* Plus */) {
166
+ this.consume();
167
+ return this.expr(65);
168
+ }
169
+ if (t.type === "(" /* LParen */) {
170
+ this.consume();
171
+ const inner = this.expr(0);
172
+ this.expect(")" /* RParen */);
173
+ return inner;
174
+ }
175
+ throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);
176
+ }
177
+ };
178
+ function parse(src) {
179
+ return new Parser(src).parse();
180
+ }
181
+
182
+ // src/builtins.ts
183
+ var BUILTIN_VARS = {
184
+ PI: Math.PI,
185
+ E: Math.E,
186
+ LN2: Math.LN2,
187
+ LN10: Math.LN10,
188
+ LOG2E: Math.LOG2E,
189
+ LOG10E: Math.LOG10E,
190
+ SQRT2: Math.SQRT2,
191
+ Infinity: Infinity,
192
+ NaN: NaN
193
+ };
194
+ var BUILTIN_FUNCTIONS = {
195
+ abs: (x) => Math.abs(x),
196
+ ceil: (x) => Math.ceil(x),
197
+ floor: (x) => Math.floor(x),
198
+ round: (x) => Math.round(x),
199
+ trunc: (x) => Math.trunc(x),
200
+ sign: (x) => Math.sign(x),
201
+ sqrt: (x) => Math.sqrt(x),
202
+ cbrt: (x) => Math.cbrt(x),
203
+ pow: (x, y) => Math.pow(x, y),
204
+ exp: (x) => Math.exp(x),
205
+ log: (x) => Math.log(x),
206
+ log2: (x) => Math.log2(x),
207
+ log10: (x) => Math.log10(x),
208
+ sin: (x) => Math.sin(x),
209
+ cos: (x) => Math.cos(x),
210
+ tan: (x) => Math.tan(x),
211
+ asin: (x) => Math.asin(x),
212
+ acos: (x) => Math.acos(x),
213
+ atan: (x) => Math.atan(x),
214
+ atan2: (y, x) => Math.atan2(y, x),
215
+ sinh: (x) => Math.sinh(x),
216
+ cosh: (x) => Math.cosh(x),
217
+ tanh: (x) => Math.tanh(x),
218
+ min: (...args) => Math.min(...args),
219
+ max: (...args) => Math.max(...args),
220
+ clamp: (x, lo, hi) => Math.min(Math.max(x, lo), hi),
221
+ hypot: (...args) => Math.hypot(...args),
222
+ random: () => Math.random(),
223
+ isNaN: (x) => isNaN(x) ? 1 : 0,
224
+ isFinite: (x) => isFinite(x) ? 1 : 0
225
+ };
226
+
227
+ // src/evaluator.ts
228
+ function evalNode(node, opts, depth) {
229
+ if (depth > opts.maxDepth) throw new FormulaError("Maximum recursion depth exceeded");
230
+ const d = depth + 1;
231
+ switch (node.kind) {
232
+ case "number":
233
+ return node.value;
234
+ case "identifier": {
235
+ const name = node.name;
236
+ if (name in opts.vars) return opts.vars[name];
237
+ if (name in BUILTIN_VARS) return BUILTIN_VARS[name];
238
+ throw new FormulaError(`Unknown variable '${name}'`);
239
+ }
240
+ case "unary": {
241
+ const v = evalNode(node.operand, opts, d);
242
+ if (node.op === "-") return -v;
243
+ if (node.op === "!") return v === 0 ? 1 : 0;
244
+ throw new FormulaError(`Unknown unary operator '${node.op}'`);
245
+ }
246
+ case "binary": {
247
+ const l = evalNode(node.left, opts, d);
248
+ if (node.op === "&&") return l !== 0 ? evalNode(node.right, opts, d) : 0;
249
+ if (node.op === "||") return l !== 0 ? l : evalNode(node.right, opts, d);
250
+ const r = evalNode(node.right, opts, d);
251
+ switch (node.op) {
252
+ case "+":
253
+ return l + r;
254
+ case "-":
255
+ return l - r;
256
+ case "*":
257
+ return l * r;
258
+ case "/":
259
+ if (r === 0) throw new FormulaError("Division by zero");
260
+ return l / r;
261
+ case "%":
262
+ if (r === 0) throw new FormulaError("Modulo by zero");
263
+ return l % r;
264
+ case "^":
265
+ return Math.pow(l, r);
266
+ case "==":
267
+ return l === r ? 1 : 0;
268
+ case "!=":
269
+ return l !== r ? 1 : 0;
270
+ case "<":
271
+ return l < r ? 1 : 0;
272
+ case "<=":
273
+ return l <= r ? 1 : 0;
274
+ case ">":
275
+ return l > r ? 1 : 0;
276
+ case ">=":
277
+ return l >= r ? 1 : 0;
278
+ default:
279
+ throw new FormulaError(`Unknown operator '${node.op}'`);
280
+ }
281
+ }
282
+ case "call": {
283
+ const fn = opts.functions[node.name] ?? BUILTIN_FUNCTIONS[node.name];
284
+ if (!fn) throw new FormulaError(`Unknown function '${node.name}'`);
285
+ const args = node.args.map((a) => evalNode(a, opts, d));
286
+ return fn(...args);
287
+ }
288
+ case "conditional": {
289
+ const cond = evalNode(node.condition, opts, d);
290
+ return cond !== 0 ? evalNode(node.consequent, opts, d) : evalNode(node.alternate, opts, d);
291
+ }
292
+ }
293
+ }
294
+ function evalAST(node, opts = {}) {
295
+ const resolved = {
296
+ vars: opts.vars ?? {},
297
+ functions: opts.functions ?? {},
298
+ maxDepth: opts.maxDepth ?? 200
299
+ };
300
+ return evalNode(node, resolved, 0);
301
+ }
302
+
303
+ // src/formula.ts
304
+ function compile(expr) {
305
+ const ast = parse(expr);
306
+ return new CompiledFormula(ast, expr);
307
+ }
308
+ function evaluate(expr, vars, opts) {
309
+ const ast = parse(expr);
310
+ return evalAST(ast, { ...opts, vars });
311
+ }
312
+ function tryEvaluate(expr, vars, opts) {
313
+ try {
314
+ return evaluate(expr, vars, opts);
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ var CompiledFormula = class {
320
+ constructor(ast, source) {
321
+ this._ast = ast;
322
+ this.source = source;
323
+ }
324
+ evaluate(vars, opts) {
325
+ return evalAST(this._ast, { ...opts, vars });
326
+ }
327
+ /** Return all variable names referenced in this formula. */
328
+ variables() {
329
+ const names = /* @__PURE__ */ new Set();
330
+ collectVars(this._ast, names);
331
+ return [...names].sort();
332
+ }
333
+ };
334
+ function collectVars(node, out) {
335
+ switch (node.kind) {
336
+ case "number":
337
+ break;
338
+ case "identifier":
339
+ out.add(node.name);
340
+ break;
341
+ case "unary":
342
+ collectVars(node.operand, out);
343
+ break;
344
+ case "binary":
345
+ collectVars(node.left, out);
346
+ collectVars(node.right, out);
347
+ break;
348
+ case "call":
349
+ node.args.forEach((a) => collectVars(a, out));
350
+ break;
351
+ case "conditional":
352
+ collectVars(node.condition, out);
353
+ collectVars(node.consequent, out);
354
+ collectVars(node.alternate, out);
355
+ break;
356
+ }
357
+ }
358
+ export {
359
+ BUILTIN_FUNCTIONS,
360
+ BUILTIN_VARS,
361
+ CompiledFormula,
362
+ FormulaError,
363
+ compile,
364
+ evalAST,
365
+ evaluate,
366
+ parse,
367
+ tryEvaluate
368
+ };
369
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/lexer.ts","../src/parser.ts","../src/builtins.ts","../src/evaluator.ts","../src/formula.ts"],"sourcesContent":["export type Value = number;\n\nexport type Variables = Record<string, Value>;\n\nexport type FunctionMap = Record<string, (...args: Value[]) => Value>;\n\nexport interface EvalOptions {\n vars?: Variables;\n functions?: FunctionMap;\n maxDepth?: number;\n}\n\nexport class FormulaError extends Error {\n constructor(\n message: string,\n public readonly position?: number\n ) {\n super(message);\n this.name = \"FormulaError\";\n }\n}\n\n// ── AST node types ────────────────────────────────────────────────────────────\n\nexport type NodeKind =\n | \"number\"\n | \"identifier\"\n | \"binary\"\n | \"unary\"\n | \"call\"\n | \"conditional\";\n\nexport interface NumberNode {\n kind: \"number\";\n value: number;\n}\n\nexport interface IdentifierNode {\n kind: \"identifier\";\n name: string;\n}\n\nexport interface BinaryNode {\n kind: \"binary\";\n op: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryNode {\n kind: \"unary\";\n op: string;\n operand: ASTNode;\n}\n\nexport interface CallNode {\n kind: \"call\";\n name: string;\n args: ASTNode[];\n}\n\nexport interface ConditionalNode {\n kind: \"conditional\";\n condition: ASTNode;\n consequent: ASTNode;\n alternate: ASTNode;\n}\n\nexport type ASTNode =\n | NumberNode\n | IdentifierNode\n | BinaryNode\n | UnaryNode\n | CallNode\n | ConditionalNode;\n","import { FormulaError } from \"./types.js\";\n\nexport const enum TT {\n Number = \"Number\",\n Ident = \"Ident\",\n Plus = \"+\",\n Minus = \"-\",\n Star = \"*\",\n Slash = \"/\",\n Percent = \"%\",\n Caret = \"^\",\n LParen = \"(\",\n RParen = \")\",\n Comma = \",\",\n Question = \"?\",\n Colon = \":\",\n Eq = \"==\",\n NEq = \"!=\",\n Lt = \"<\",\n Lte = \"<=\",\n Gt = \">\",\n Gte = \">=\",\n And = \"&&\",\n Or = \"||\",\n Not = \"!\",\n EOF = \"EOF\",\n}\n\nexport interface Token {\n type: TT;\n raw: string;\n pos: number;\n}\n\nexport function tokenize(src: string): Token[] {\n const tokens: Token[] = [];\n let i = 0;\n\n while (i < src.length) {\n // skip whitespace\n if (/\\s/.test(src[i])) { i++; continue; }\n\n const pos = i;\n const ch = src[i];\n\n // numbers (int or float, optional exponent)\n if (/[0-9]/.test(ch) || (ch === \".\" && /[0-9]/.test(src[i + 1] ?? \"\"))) {\n let raw = \"\";\n while (i < src.length && /[0-9.]/.test(src[i])) raw += src[i++];\n if (src[i] === \"e\" || src[i] === \"E\") {\n raw += src[i++];\n if (src[i] === \"+\" || src[i] === \"-\") raw += src[i++];\n while (i < src.length && /[0-9]/.test(src[i])) raw += src[i++];\n }\n tokens.push({ type: TT.Number, raw, pos });\n continue;\n }\n\n // identifiers and keywords (only PI/E)\n if (/[a-zA-Z_]/.test(ch)) {\n let raw = \"\";\n while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) raw += src[i++];\n tokens.push({ type: TT.Ident, raw, pos });\n continue;\n }\n\n // two-char operators\n const two = src.slice(i, i + 2);\n if (two === \"==\" || two === \"!=\" || two === \"<=\" || two === \">=\" || two === \"&&\" || two === \"||\") {\n tokens.push({ type: two as TT, raw: two, pos });\n i += 2;\n continue;\n }\n\n // one-char operators\n const singles: Record<string, TT> = {\n \"+\": TT.Plus, \"-\": TT.Minus, \"*\": TT.Star, \"/\": TT.Slash,\n \"%\": TT.Percent, \"^\": TT.Caret,\n \"(\": TT.LParen, \")\": TT.RParen, \",\": TT.Comma,\n \"?\": TT.Question, \":\": TT.Colon,\n \"<\": TT.Lt, \">\": TT.Gt, \"!\": TT.Not,\n };\n if (ch in singles) {\n tokens.push({ type: singles[ch], raw: ch, pos });\n i++;\n continue;\n }\n\n throw new FormulaError(`Unexpected character '${ch}'`, pos);\n }\n\n tokens.push({ type: TT.EOF, raw: \"\", pos: i });\n return tokens;\n}\n","import { tokenize, Token, TT } from \"./lexer.js\";\nimport {\n ASTNode, NumberNode, IdentifierNode, BinaryNode,\n UnaryNode, CallNode, ConditionalNode, FormulaError,\n} from \"./types.js\";\n\n// Pratt parser (top-down operator precedence)\n\nconst BP: Partial<Record<TT, number>> = {\n [TT.Or]: 10,\n [TT.And]: 20,\n [TT.Eq]: 30, [TT.NEq]: 30,\n [TT.Lt]: 40, [TT.Lte]: 40, [TT.Gt]: 40, [TT.Gte]: 40,\n [TT.Plus]: 50, [TT.Minus]: 50,\n [TT.Star]: 60, [TT.Slash]: 60, [TT.Percent]: 60,\n [TT.Caret]: 70, // right-assoc → parse at BP-1\n};\n\nclass Parser {\n private tokens: Token[];\n private pos: number = 0;\n\n constructor(src: string) {\n this.tokens = tokenize(src);\n }\n\n private peek(): Token { return this.tokens[this.pos]; }\n private consume(): Token { return this.tokens[this.pos++]; }\n private expect(type: TT): Token {\n const t = this.consume();\n if (t.type !== type) throw new FormulaError(`Expected '${type}' but got '${t.raw}'`, t.pos);\n return t;\n }\n\n parse(): ASTNode {\n const node = this.expr(0);\n if (this.peek().type !== TT.EOF) {\n const t = this.peek();\n throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);\n }\n return node;\n }\n\n private expr(minBP: number): ASTNode {\n let left = this.prefix();\n\n while (true) {\n const t = this.peek();\n\n // ternary ? :\n if (t.type === TT.Question && minBP < 5) {\n this.consume();\n const consequent = this.expr(0);\n this.expect(TT.Colon);\n const alternate = this.expr(0);\n left = { kind: \"conditional\", condition: left, consequent, alternate } as ConditionalNode;\n continue;\n }\n\n const bp = BP[t.type as TT];\n if (bp === undefined || bp <= minBP) break;\n\n this.consume();\n const rightBP = t.type === TT.Caret ? bp - 1 : bp; // ^ is right-assoc\n const right = this.expr(rightBP);\n left = { kind: \"binary\", op: t.type, left, right } as BinaryNode;\n }\n\n return left;\n }\n\n private prefix(): ASTNode {\n const t = this.peek();\n\n if (t.type === TT.Number) {\n this.consume();\n return { kind: \"number\", value: parseFloat(t.raw) } as NumberNode;\n }\n\n if (t.type === TT.Ident) {\n this.consume();\n // function call?\n if (this.peek().type === TT.LParen) {\n this.consume(); // (\n const args: ASTNode[] = [];\n if (this.peek().type !== TT.RParen) {\n args.push(this.expr(0));\n while (this.peek().type === TT.Comma) {\n this.consume();\n args.push(this.expr(0));\n }\n }\n this.expect(TT.RParen);\n return { kind: \"call\", name: t.raw, args } as CallNode;\n }\n return { kind: \"identifier\", name: t.raw } as IdentifierNode;\n }\n\n if (t.type === TT.Minus) {\n this.consume();\n return { kind: \"unary\", op: \"-\", operand: this.expr(65) } as UnaryNode;\n }\n\n if (t.type === TT.Not) {\n this.consume();\n return { kind: \"unary\", op: \"!\", operand: this.expr(65) } as UnaryNode;\n }\n\n if (t.type === TT.Plus) {\n this.consume();\n return this.expr(65); // unary +\n }\n\n if (t.type === TT.LParen) {\n this.consume();\n const inner = this.expr(0);\n this.expect(TT.RParen);\n return inner;\n }\n\n throw new FormulaError(`Unexpected token '${t.raw}'`, t.pos);\n }\n}\n\nexport function parse(src: string): ASTNode {\n return new Parser(src).parse();\n}\n","import { FunctionMap, Variables } from \"./types.js\";\n\nexport const BUILTIN_VARS: Variables = {\n PI: Math.PI,\n E: Math.E,\n LN2: Math.LN2,\n LN10: Math.LN10,\n LOG2E: Math.LOG2E,\n LOG10E: Math.LOG10E,\n SQRT2: Math.SQRT2,\n Infinity: Infinity,\n NaN: NaN,\n};\n\nexport const BUILTIN_FUNCTIONS: FunctionMap = {\n abs: (x) => Math.abs(x),\n ceil: (x) => Math.ceil(x),\n floor: (x) => Math.floor(x),\n round: (x) => Math.round(x),\n trunc: (x) => Math.trunc(x),\n sign: (x) => Math.sign(x),\n\n sqrt: (x) => Math.sqrt(x),\n cbrt: (x) => Math.cbrt(x),\n pow: (x, y) => Math.pow(x, y),\n exp: (x) => Math.exp(x),\n log: (x) => Math.log(x),\n log2: (x) => Math.log2(x),\n log10: (x) => Math.log10(x),\n\n sin: (x) => Math.sin(x),\n cos: (x) => Math.cos(x),\n tan: (x) => Math.tan(x),\n asin: (x) => Math.asin(x),\n acos: (x) => Math.acos(x),\n atan: (x) => Math.atan(x),\n atan2: (y, x) => Math.atan2(y, x),\n sinh: (x) => Math.sinh(x),\n cosh: (x) => Math.cosh(x),\n tanh: (x) => Math.tanh(x),\n\n min: (...args) => Math.min(...args),\n max: (...args) => Math.max(...args),\n clamp: (x, lo, hi) => Math.min(Math.max(x, lo), hi),\n\n hypot: (...args) => Math.hypot(...args),\n\n random: () => Math.random(),\n\n isNaN: (x) => (isNaN(x) ? 1 : 0),\n isFinite: (x) => (isFinite(x) ? 1 : 0),\n};\n","import { ASTNode, EvalOptions, FormulaError, Value } from \"./types.js\";\nimport { BUILTIN_VARS, BUILTIN_FUNCTIONS } from \"./builtins.js\";\n\nfunction evalNode(node: ASTNode, opts: Required<EvalOptions>, depth: number): Value {\n if (depth > opts.maxDepth) throw new FormulaError(\"Maximum recursion depth exceeded\");\n\n const d = depth + 1;\n\n switch (node.kind) {\n case \"number\":\n return node.value;\n\n case \"identifier\": {\n const name = node.name;\n if (name in opts.vars) return opts.vars[name];\n if (name in BUILTIN_VARS) return BUILTIN_VARS[name];\n throw new FormulaError(`Unknown variable '${name}'`);\n }\n\n case \"unary\": {\n const v = evalNode(node.operand, opts, d);\n if (node.op === \"-\") return -v;\n if (node.op === \"!\") return v === 0 ? 1 : 0;\n throw new FormulaError(`Unknown unary operator '${node.op}'`);\n }\n\n case \"binary\": {\n const l = evalNode(node.left, opts, d);\n // short-circuit for && / ||\n if (node.op === \"&&\") return (l !== 0 ? evalNode(node.right, opts, d) : 0);\n if (node.op === \"||\") return (l !== 0 ? l : evalNode(node.right, opts, d));\n\n const r = evalNode(node.right, opts, d);\n switch (node.op) {\n case \"+\": return l + r;\n case \"-\": return l - r;\n case \"*\": return l * r;\n case \"/\":\n if (r === 0) throw new FormulaError(\"Division by zero\");\n return l / r;\n case \"%\":\n if (r === 0) throw new FormulaError(\"Modulo by zero\");\n return l % r;\n case \"^\": return Math.pow(l, r);\n case \"==\": return l === r ? 1 : 0;\n case \"!=\": return l !== r ? 1 : 0;\n case \"<\": return l < r ? 1 : 0;\n case \"<=\": return l <= r ? 1 : 0;\n case \">\": return l > r ? 1 : 0;\n case \">=\": return l >= r ? 1 : 0;\n default:\n throw new FormulaError(`Unknown operator '${node.op}'`);\n }\n }\n\n case \"call\": {\n const fn = opts.functions[node.name] ?? BUILTIN_FUNCTIONS[node.name];\n if (!fn) throw new FormulaError(`Unknown function '${node.name}'`);\n const args = node.args.map((a) => evalNode(a, opts, d));\n return fn(...args);\n }\n\n case \"conditional\": {\n const cond = evalNode(node.condition, opts, d);\n return cond !== 0\n ? evalNode(node.consequent, opts, d)\n : evalNode(node.alternate, opts, d);\n }\n }\n}\n\nexport function evalAST(node: ASTNode, opts: EvalOptions = {}): Value {\n const resolved: Required<EvalOptions> = {\n vars: opts.vars ?? {},\n functions: opts.functions ?? {},\n maxDepth: opts.maxDepth ?? 200,\n };\n return evalNode(node, resolved, 0);\n}\n","import { parse } from \"./parser.js\";\nimport { evalAST } from \"./evaluator.js\";\nimport { ASTNode, EvalOptions, Value } from \"./types.js\";\n\n/**\n * Parse an expression string into an AST (reuse for repeated evaluations).\n */\nexport function compile(expr: string): CompiledFormula {\n const ast = parse(expr);\n return new CompiledFormula(ast, expr);\n}\n\n/**\n * Parse and evaluate an expression in one step.\n */\nexport function evaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value {\n const ast = parse(expr);\n return evalAST(ast, { ...opts, vars });\n}\n\n/**\n * Parse and evaluate, returning null instead of throwing on parse/eval errors.\n */\nexport function tryEvaluate(expr: string, vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value | null {\n try { return evaluate(expr, vars, opts); }\n catch { return null; }\n}\n\nexport class CompiledFormula {\n readonly source: string;\n private readonly _ast: ASTNode;\n\n constructor(ast: ASTNode, source: string) {\n this._ast = ast;\n this.source = source;\n }\n\n evaluate(vars?: Record<string, Value>, opts?: Omit<EvalOptions, \"vars\">): Value {\n return evalAST(this._ast, { ...opts, vars });\n }\n\n /** Return all variable names referenced in this formula. */\n variables(): string[] {\n const names = new Set<string>();\n collectVars(this._ast, names);\n return [...names].sort();\n }\n}\n\nfunction collectVars(node: ASTNode, out: Set<string>): void {\n switch (node.kind) {\n case \"number\": break;\n case \"identifier\": out.add(node.name); break;\n case \"unary\": collectVars(node.operand, out); break;\n case \"binary\": collectVars(node.left, out); collectVars(node.right, out); break;\n case \"call\": node.args.forEach((a) => collectVars(a, out)); break;\n case \"conditional\":\n collectVars(node.condition, out);\n collectVars(node.consequent, out);\n collectVars(node.alternate, out);\n break;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "formulakit",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency safe arithmetic expression evaluator: parse/evaluate math formulas with variables and functions, no eval(). TypeScript-first, ESM+CJS. Port of Python simpleeval / Go expr / C# DynamicExpresso.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "node --experimental-vm-modules node_modules/.bin/jest --forceExit",
25
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
26
+ },
27
+ "keywords": [
28
+ "expression",
29
+ "evaluator",
30
+ "formula",
31
+ "math",
32
+ "parser",
33
+ "arithmetic",
34
+ "safe-eval",
35
+ "calculator",
36
+ "variables",
37
+ "typescript",
38
+ "zero-dependencies"
39
+ ],
40
+ "author": "trananhtung",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/trananhtung/formulakit.git"
45
+ },
46
+ "devDependencies": {
47
+ "@types/jest": "^30.0.0",
48
+ "jest": "^30.4.2",
49
+ "ts-jest": "^29.4.11",
50
+ "tsup": "^8.5.1",
51
+ "typescript": "^6.0.3"
52
+ }
53
+ }