math-expr-eval
v1.0.1
Published
A lightweight mathematical expression evaluator that works seamlessly with any math library (such as bignumber.js, decimal.js)
Maintainers
Readme
math-expr-eval
A lightweight mathematical expression evaluator designed to work with arbitrary computation libraries like bignumber.js, decimal.js, etc.
Design Goals
Background
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.
Core Design Objectives
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.
Supported Expressions
| 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 |
Installation
pnpm add math-expr-eval
# or
npm install math-expr-evalUsage Guide
Using createEvaluator with bignumber.js
import { 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.4Using createCalculator with Template Literals
import { 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: 51API Reference
Primary APIs
createEvaluator<Computable>(options: EvaluatorOptions<Computable>)
Creates 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
createCalculator<Computable>(options: EvaluatorOptions<Computable>)
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
Parser.parse(expression: string)
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
Core Type Definitions
EvaluatorOptions<Computable>
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;
};
}Performance & Optimization
Benchmark Results
1. Computation Engine Performance
- 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
2. Caching Impact
- Standard evaluator: 153.741ms (100k iterations)
- Cached evaluator: 37.271ms (100k iterations)
- Improvement: Caching provides 4.1x speedup
3. Parsing Overhead
- Simple expressions: 93.1% of total time
- Medium complexity: 74.5%-80.0% of total time
- Complex expressions: 64.1% of total time
Optimization Strategies
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
Critical Notes & Limitations
⚠️ Security Warning
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 Operation Precision
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.
⚠️ Disabling Unsupported 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") },
}License
MIT License © @AEPKILL
