xex.js
Lightweight and extensible math-like expression parser and compiler.
Introduction
xex.js is a lightweight library that provides API to parse mathematic-like expressions and to compile them into javascript functions of user-provided signatures. The library was designed to be small, hackable, and embeddable in other projects - it has no dependencies and all functionality fits into a single file. It was primarily designed for xschema library to provide support for expression constraints and access control rules, but since the library has much more use-cases it was separated from the original project.
The expression is internally represented as AST, unlike many other expression evaluators that use postfix notation. The reason to use AST was to make the library friendly for implementing new features. The library currently provides nearly full access to JS Math
functionality with some additional features. It treats all variables as numbers and is currently designed to work only with numbers - that's it, no user-defined types, objects, etc...
The library can be probably extended in the future to provide more built-in functions and features - pull requests that add useful functionality to xex
are more than welcome!
Basic Usage
Simple example:
const xex = require("xex");
const exp = xex.exp("sin(x) * cos(y)");
console.log(exp instanceof xex.Expression);
console.log(exp.root);
console.log(exp.vars);
const args = { x: 0.5, y: 0.25 };
console.log(exp.eval(args));
const f = exp.eval;
console.log(f({ x: 0.5, y: 0.25 }));
By default the compiled expression expects a single argument, which is an object where each property describes a variable inside the expression. Expressions are not static in most cases and there are currently two ways of checking whether the expression contains invalid variables:
- Validating
exp.vars
after the expression was parsed (ideal for inspecting) - Using a variable whitelist (ideal for compiling user-defined expressions)
For example if the expression can use only variables x
and y
it's better to use the whitelise:
const whitelist = { x: true, y: 0 };
const exp0 = xex.exp("sin(x) * cos(y)", { varsWhitelist: whitelist });
const exp1 = xex.exp("sin(z) * cos(w)", { varsWhitelist: whitelist });
If you need a different signature, for example a function where the first parameter is x
and the second parameter y
, you can compile it as well:
const exp = xex.exp("sin(x) * cos(y)");
const fn = exp.compile(["x", "y"]);
console.log(fn(0.5, 0.25));
When the expression is compiled this way it checks all variables and will throw if it uses a variable that is not provided. The xex.exp()
may throw as well, so it's always a good practice to enclose it in try-catch
block:
var fn;
try {
fn = xex.exp("sin(x) * cos(y) + z").compile(["x", "y"]);
}
catch (ex) {
console.log(ex instanceof xex.ExpressionError);
console.log(`ERROR: ${ex.message}`);
}
The ExpressionError
instance also contains a position
, which describes where the error happened if it was a parser error:
try {
xex.exp("a : b");
}
catch (ex) {
console.log(`ERROR: ${ex.message} at ${ex.position}`);
}
The position
describes an index of the first token character from the beginning of the input. If the expression has multiple lines then you have to count lines and columns manually.
By default the expression is simplified (constant folding). If you intend to process the expression tree and would like to see all nodes you can disable it, or trigger it manually:
const exp = xex.exp("sin(0.6) + cos(0.5) + x", { noFolding: true });
console.log(JSON.stringify(exp.root, null, 2));
exp.fold({ x: 1 });
console.log(JSON.stringify(exp.root, null, 2));
Extending Guide
The library can be extended by user-defined constants, operators, and functions. The base environment xex
is frozen and cannot be extended, however, it can be cloned and the clone can be then used the same way as xex
:
const env = xex.clone()
.addConstant({ name: "ONE", value: 0 })
.addConstant("ANSWER_TO_LIFE", 42)
env.addFunction({
name: "sum"
args: 1,
amax: Infinity
safe: true,
eval: function() {
var x = 0;
for (var i = 0; i < arguments.length; i++)
x += arguments[i];
return x;
}
});
const exp = env.exp("sum(ANSWER_TO_LIFE, ONE)")
console.log(exp.eval());
The safe
option is pessimistic by default (false), so it's a good practice to always provide it depending on the function you are adding. If your custom function has side effects or returns a different answer every time it's called (like Math.random()
) then it must not be considered safe. Safe functions can be evaluated by constant folding of all their arguments are known (constants or already folded expressions).
Extending operators is similar to extending functions with some minor differences: operators associativity and precedence:
const env = xex.clone();
env.addBinary({
name: "**",
prec: 2,
rtl : true,
safe: true,
eval: Math.pow
});
console.log(env.exp("2 ** 3" ).eval());
console.log(env.exp("2 ** 3 ** 2" ).eval());
console.log(env.exp("(2 ** 3) ** 2").eval());
Built-In Features
- Unary operators:
- Arithmetic operators:
- Assignment
x = y
- Addition
x + y
- Subtraction
x - y
- Multiplication
x * y
- Division
x / y
- Modulo
x % y
- Comparison operators:
- Equal
x == y
- Not equal
x != y
- Greater
x > y
- Greater or equal
x >= y
- Lesser
x < y
- Lesser or equal
x <= y
- Language constructs:
- Ternary if-else
condition ? taken : not-taken
- Functions:
- Check for NaN
isnan(x)
- Check for infinity
isinf(x)
- Check for finite number
isfinite(x)
- Check for integer
isint(x)
- Check for safe integer
issafeint(x)
- Check for binary equality
isequal(x, y)
isequal(0, -0)
-> 0
isequal(42, 42)
-> 1
isequal(NaN, NaN)
-> 1
- Check between
isbetween(x, min, max)
- Clamp
clamp(x, min, max)
- Sign
sign(x)
sign(0)
-> 0
sign(-0)
-> -0
sign(NaN)
-> NaN
- Round to nearest
round(x)
- Truncate
trunc(x)
- Floor
floor(x)
- Ceil
ceil(x)
- Absolute value
abs(x)
- Exponential
exp(x)
- Exponential minus one
expm1(x)
- the same as
exp(x) - 1
, but more precise.
- Logarithm
log(x)
- Logarithm plus one
logp1(x)
- the same as
log(x + 1)
, but more precise.
- Logarithm of base 2
log2(x)
- Logarithm of base 10
log10(x)
- Square root
sqrt(x)
- Cube root
cbrt(x)
- Fraction
frac(x)
- Sine
sin(x)
- Cosine
cos(x)
- Tangent
tan(x)
- Hyperbolic sine
sinh(x)
- Hyperbolic cosine
cosh(x)
- Hyperbolic tangent
tanh(x)
- Arcsine
asin(x)
- Arccosine
acos(x)
- Arctangent
atan(x)
- Arctangent
atan2(x, y)
- Hyperbolic arcsine
asinh(x)
- Hyperbolic arccosine
acosh(x)
- Hyperbolic arctangent
atanh(x)
- Power
pow(x, y)
- Square root of the sum of squares
hypot(x, y)
- Min/max
min(x, y...)
and max(x, y...)
- returns
NaN
if one or more argument is NaN
- Min/max value
minval(x, y...)
and maxval(x, y...)
- skips
NaN
values, only returns NaN
if all values are NaN
- Constants:
- Infinity
Infinity
- Not a Number
NaN
- PI
PI = 3.14159265358979323846