A lightweight mathematical expression evaluator designed to work with arbitrary computation libraries like bignumber.js, decimal.js, etc.
In financial and scientific computing, JavaScript's native Number type suffers from precision limitations. Libraries like bignumber.js and decimal.js solve this but introduce verbose API patterns that obscure mathematical expressions.
Consider the compound interest formula P * (1 + r/n)^(n*t). With bignumber.js, it becomes:
P.times(new BigNumber(1).plus(r.dividedBy(n)).pow(n.times(t)));This approach harms readability and increases error-proneness. While math.js provides an elegant eval solution, its large size (100KB+) is prohibitive for many projects.
math-expr-eval addresses this gap—a lightweight evaluator focused specifically on mathematical expressions, enabling intuitive high-precision computations.
This library specializes in mathematical expression evaluation. It deliberately supports only numeric computation and data access syntax, excluding control flow expressions (e.g., a ? b : c, a && b, a || b) and side-effecting statements (e.g., ++a, a++). This ensures computational purity and predictability.
| Type | Syntax | Example |
|---|---|---|
| Literals | Numbers, ...n |
123, 1.23, 0xFF, 0b101, 0o777, 123n |
| Identifiers | myVar |
PI, a, myVar |
| Unary Expr | +, -, ~ |
-a, +a, ~a |
| Binary Expr | +, -, *, /, %, **, &, |, ^, <<, >>, >>> |
a + b, a * b, a & b |
| Member Expr | a.b, a[b] |
console.log, data['key'] |
| Call Expr | fn(...) |
myFunc(a, b) |
| Optional Chaining | ?., ?.(), ?.[] |
a?.b, myFunc?.(), data?.[key] |
| Grouping | (...) |
(a + b) * c |
pnpm add math-expr-eval
# or
npm install math-expr-evalimport { createEvaluator } from "math-expr-eval";
import { BigNumber } from "bignumber.js";
// 1. Configure evaluator with BigNumber logic
const evaluatorOptions = {
// Validate computable type
isComputable: (value: unknown): value is BigNumber =>
BigNumber.isBigNumber(value),
// Convert values to BigNumber (handle null/undefined)
toComputable: (value: string | number | undefined | null) => {
if (value == null) return new BigNumber(NaN); // Optional chaining → NaN
return new BigNumber(value);
},
// Define BigNumber operators
binaryOperators: {
"+": (a: BigNumber, b: BigNumber) => a.plus(b),
"-": (a, b) => a.minus(b),
"*": (a, b) => a.multipliedBy(b),
"/": (a, b) => a.dividedBy(b),
"%": (a, b) => a.modulo(b),
"**": (a, b) => a.exponentiatedBy(b),
"<<": (a, b) => new BigNumber(a.toNumber() << b.toNumber()),
">>": (a, b) => new BigNumber(a.toNumber() >> b.toNumber()),
">>>": (a, b) => new BigNumber(a.toNumber() >>> b.toNumber()),
"|": (a, b) => new BigNumber(a.toNumber() | b.toNumber()),
"&": (a, b) => new BigNumber(a.toNumber() & b.toNumber()),
"^": (a, b) => new BigNumber(a.toNumber() ^ b.toNumber()),
},
unaryOperators: {
"+": (a: BigNumber) => a.abs(), // Note: Absolute value
"-": (a) => a.negated(),
"~": (a) => new BigNumber(~a.toNumber()),
},
};
// 2. Create evaluator
const evaluate = createEvaluator(evaluatorOptions);
// 3. Define computation context
const context = {
rocket: {
thrust: (p: BigNumber) => p.multipliedBy(100),
fuel: new BigNumber(99),
},
GRAVITY: new BigNumber("9.8"),
};
// 4. Evaluate expression
const result = evaluate("(rocket.thrust(rocket.fuel) - GRAVITY) * 2", context);
console.log(result.toString()); // Output: 19780.4import { createCalculator } from "math-expr-eval";
// 1. Create calculator using native numbers
const calc = createCalculator<number>({
isComputable: (v): v is number => typeof v === "number",
toComputable: (v) => {
if (typeof v === "string") return Number.parseFloat(v);
if (typeof v === "number") return v;
return 0; // null/undefined → 0
},
binaryOperators: {
"+": (a, b) => a + b,
"-": (a, b) => a - b,
"*": (a, b) => a * b,
"/": (a, b) => a / b,
"%": (a, b) => a % b,
"**": (a, b) => a ** b,
"<<": (a, b) => a << b,
">>": (a, b) => a >> b,
">>>": (a, b) => a >>> b,
"|": (a, b) => a | b,
"&": (a, b) => a & b,
"^": (a, b) => a ^ b,
},
unaryOperators: {
"+": (a) => a, // Identity function
"-": (a) => -a,
"~": (a) => ~a,
},
});
// 2. Evaluate via template literal
const a = 10,
b = 20.5;
const result = calc`${a} + ${b} * 2`; // = 10 + 20.5 * 2
console.log(result); // Output: 51Creates an expression evaluator function.
const evaluate = createEvaluator(options);
const result = evaluate("a + b * 2", { a: 10, b: 20 });Parameters:
options:EvaluatorOptions<Computable>- Configuration object
Returns:
(expression: string, context?: Record<string, any>) => Computable- Evaluation function
Creates a template literal calculator.
const calc = createCalculator(options);
const result = calc`${a} + ${b} * 2`;Parameters:
options:EvaluatorOptions<Computable>- Configuration object
Returns:
- Template literal function:
(strings: TemplateStringsArray, ...values: Array<Computable | Primitive>) => Computable
Parses an expression string into an Abstract Syntax Tree (AST).
const ast = Parser.parse("a + b * 2");Parameters:
expression:string- Expression to parse
Returns:
Expression- Root AST node
interface EvaluatorOptions<Computable> {
// Type guard for computable values
isComputable: (value: unknown) => value is Computable;
// Enable expression AST caching (optional)
cacheExpression?: boolean;
// Value conversion handler
toComputable: (value: string | number | undefined | null) => Computable;
// Binary operator implementations
binaryOperators: {
[op in BinaryOperator]: (a: Computable, b: Computable) => Computable;
};
// Unary operator implementations
unaryOperators: {
[op in UnaryOperator]: (a: Computable) => Computable;
};
}- Number evaluator: 777,727 ops/sec
bignumber.jsevaluator: 439,247 ops/sec- Performance difference: Number evaluator is 1.77x faster
Memory Usage:
- Number evaluator: 507.9 B/op
- BigNumber evaluator: 1.1 KB/op
- Standard evaluator: 153.741ms (100k iterations)
- Cached evaluator: 37.271ms (100k iterations)
- Improvement: Caching provides 4.1x speedup
- Simple expressions: 93.1% of total time
- Medium complexity: 74.5%-80.0% of total time
- Complex expressions: 64.1% of total time
-
Select Appropriate Engine
- Standard precision: Use native
Number - High precision: Use
BigNumber/decimal.js(accept performance cost)
- Standard precision: Use native
-
Enable Expression Caching
- Always enable
cacheExpressionfor repeated evaluations
- Always enable
-
Simplify Expressions
- Avoid deep nesting
- Break complex expressions into smaller parts
- Minimize function calls and member access
-
Run Performance Tests
pnpm run benchmark:demo # View detailed reports
This library allows function execution and property access from the evaluation context. Evaluating expressions from untrusted users is inherently unsafe and may lead to arbitrary code execution.
Security Best Practices:
- Use Pure Contexts: Create contexts with
Object.create(null)to prevent prototype pollution - Apply Least Privilege: Expose only essential variables/functions in contexts
- Never Evaluate Untrusted Input: Restrict usage to controlled environments
Bitwise operators (&, |, ~, etc.) are defined for fixed-width integers. When applied to arbitrary-precision numbers (especially non-integers), behavior is implementation-defined.
Implementation Responsibility:
- You must provide all bitwise operator implementations
- The BigNumber example demonstrates a pragmatic approach: downgrading to JavaScript
Numberfor bitwise ops - Precision is limited to JavaScript integers in this approach
Carefully consider your precision requirements when implementing these operators.
Explicitly block unsupported operators by throwing errors in their implementations:
binaryOperators: {
// ...supported operators
"&": () => { throw new Error("Bitwise AND not supported") },
"|": () => { throw new Error("Bitwise OR not supported") },
},
unaryOperators: {
"~": () => { throw new Error("Bitwise NOT not supported") },
}MIT License © @AEPKILL