Skip to content

A lightweight mathematical expression evaluator designed to work with arbitrary computation libraries like bignumber.js, decimal.js, etc.

License

Notifications You must be signed in to change notification settings

AEPKILL/math-expr-eval

Repository files navigation

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-eval

Usage 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.4

Using 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: 51

API 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.js evaluator: 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

  1. Select Appropriate Engine

    • Standard precision: Use native Number
    • High precision: Use BigNumber/decimal.js (accept performance cost)
  2. Enable Expression Caching

    • Always enable cacheExpression for repeated evaluations
  3. Simplify Expressions

    • Avoid deep nesting
    • Break complex expressions into smaller parts
    • Minimize function calls and member access
  4. 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:

  1. Use Pure Contexts: Create contexts with Object.create(null) to prevent prototype pollution
  2. Apply Least Privilege: Expose only essential variables/functions in contexts
  3. 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 Number for 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

About

A lightweight mathematical expression evaluator designed to work with arbitrary computation libraries like bignumber.js, decimal.js, etc.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published