Xprs is a flexible and extensible mathematical expression parser and evaluator for Rust, designed for simplicity and ease of use (and ideally, speed).
Add this to your Cargo.toml
:
[dependencies]
xprs = "0.1.0"
or run this command in your terminal:
cargo add xprs
Make sure to check the Crates.io page for the latest version.
Currently, the minimum supported Rust version is 1.70.0
.
-
compile-time-optimizations
(enabled by default) :Enable optimization and evaluation during parsing. This feature will automagically transform expressions like
1 + 2 * 3
into7
during parsing allowing for faster evaluation. It also works on functions (e.g.sin(0)
will be transformed into0
) and "logical" result like(x - x) * (....)
will be transformed into0
sincex - x
is0
no matter whatx
is.Note: nightly channel enables even more optimizations thanks to
box_patterns
feature gate.
-
pemdas
(enabled by default):Conflicts with the
pejmdas
feature. Uses the PEMDAS order of operations. This implies that implicit multiplication has the same precedence as explicit multiplication. For example:6/2(2+1)
gets interpreted as6/2*(2+1)
which gives9
as a result.1/2x
gets interpreted as(1/2)*x
which, withx
being2
, gives1
as a result.
Note:
Display
andDebug
shows additional parenthesis to make the order of operations more obvious.
-
pejmdas
:Conflicts with the
pemdas
feature. Uses the PEJMDAS order of operations. This implies that implicit multiplication has a higher precedence than explicit multiplication. For example:6/2(2+1)
gets interpreted as6/(2*(2+1))
which gives1
as a result.1/2x
gets interpreted as1/(2*x)
which, withx
being2
, gives0.25
as a result.
Note:
Display
andDebug
shows additional parenthesis to make the order of operations more obvious.
If you want to evaluate a simple calculus that doesn't contains any variables, you can use the eval_no_vars
method (or eval_no_vars_unchecked
if you know for sure that no variables are present):
use xprs::Xprs;
fn main() {
let xprs = Xprs::try_from("1 + sin(2) * 3").unwrap();
println!("1 + sin(2) * 3 = {}", xprs.eval_no_vars().unwrap());
}
Note: Numbers are parsed as [f64
] so you can use scientific notation (e.g. 1e-3
) with underscores (e.g. 1_000_000e2
).
If you want to evaluate a calculus that contains variables, you can use the eval
method (or eval_unchecked
if you know for sure you're not missing any variables):
use xprs::Xprs;
fn main() {
let xprs = Xprs::try_from("1 + sin(2) * x").unwrap();
println!(
"1 + sin(2) * x = {}",
xprs.eval(&[("x", 3.0)].into()).unwrap()
);
}
You can also turn the calculus into a function and use it later:
use xprs::Xprs;
fn main() {
let xprs = Xprs::try_from("1 + sin(2) * x").unwrap();
let fn_xprs = xprs.bind("x").unwrap();
println!("1 + sin(2) * 3 = {}", fn_xprs(3.0));
}
You can use functions bind
, bind2
etc up to bind9
to bind variables to the calculus.
If you ever need more, you can use the bind_n
and bind_n_runtime
methods which takes an array of size N or a slice respectively.
Notes:
All bind
function (except bind_n_runtime
) returns a [Result
] of a function which is guaranteed to return a [f64
].
bind_n_runtime
returns a [Result
] of a function which also returns a [Result
] of a [f64
] since there are no guarantees that the array/slice will be of the correct size.
You can also create a [Context
] and a [Parser
] instance if you want to define your own functions and/or constants and use them repeatedly.
Constants and Functions can have any name that starts with a letter (uppercase of not) and contains only [A-Za-z0-9_']
.
Functions need to have a signature of fn(&[f64]) -> f64
so they all have the same signature and can be called the same way.
We also need a name and the number of arguments the function takes, which is an [Option<usize>
], if [None
] then the function can take any number of arguments.
You can define functions like so:
use xprs::{Function, xprs_fn};
fn double(x: f64) -> f64 {
x * 2.0
}
const DOUBLE: Function = Function::new_static("double", move |args| double(args[0]), Some(1));
// or with the macro (will do an automatic wrapping)
const DOUBLE_MACRO: Function = xprs_fn!("double", double, 1);
fn variadic_sum(args: &[f64]) -> f64 {
args.iter().sum()
}
const SUM: Function = Function::new_static("sum", variadic_sum, None);
// or with the macro (no wrapping is done for variadic functions)
const SUM_MACRO: Function = xprs_fn!("sum", variadic_sum);
// if a functions captures a variable (cannot be coerced to a static function)
const X: f64 = 42.0;
fn show_capture() {
let captures = |arg: f64| { X + arg };
let CAPTURES: Function = Function::new_dyn("captures", move |args| captures(args[0]), Some(1));
// or with the macro (will do an automatic wrapping)
let CAPTURES_MACRO: Function = xprs_fn!("captures", dyn captures, 1);
}
To use a [Context
] and a [Parser
] you can do the following:
use xprs::{xprs_fn, Context, Parser};
fn main() {
let mut context = Context::default()
.with_fn(xprs_fn!("double", |x| 2. * x, 1))
.with_var("foo", 1.0);
context.set_var("bar", 2.0);
let xprs = Parser::new_with_ctx(context)
.parse("double(foo) + bar")
.unwrap();
println!("double(foo) + bar = {}", xprs.eval_no_vars().unwrap());
}
Note: [Context
] is just a wrapper around a HashMap
so you cannot have a function and a constant with the same name (the last one will override the first one).
You can also use the [Context
] to restrict the allowed variables in the calculus:
use xprs::{Context, Parser};
fn main() {
let context = Context::default()
.with_expected_vars(["x", "y"].into());
let parser = Parser::new_with_ctx(context);
let result = parser.parse("x + y"); // OK
let fail = parser.parse("x + z"); // Error
println!("{result:#?} {fail:#?}");
}
All errors are implemented using the thiserror
.
And parsing errors are implemented using the miette
crate.
Xprs supports the following operations:
- Binary operations:
+
,-
,*
,/
,^
,%
. - Unary operations:
+
,-
,!
.
Note: !
(factorial) is only supported on positive integers. Calling it on a negative integer or a float will result in f64::NAN
. Also -4!
is interpreted as -(4!)
and not (-4)!
.
Constant | Value | Approximation |
---|---|---|
PI |
π |
3.141592653589793 |
E |
e |
2.718281828459045 |
Xprs supports a variety of functions:
- trigonometric functions:
sin
,cos
,tan
,asin
,acos
,atan
,atan2
,sinh
,cosh
,tanh
,asinh
,acosh
,atanh
. - logarithmic functions:
ln
(base 2),log
(base 10),logn
(base n, used aslogn(num, base)
). - power functions:
sqrt
,cbrt
,exp
. - rounding functions:
floor
,ceil
,round
,trunc
. - other functions:
abs
,min
,max
,hypot
,fract
,recip
(invert
alias),sum
,mean
,factorial
andgamma
.
Note: min
and max
can take any number of arguments (if none, returns f64::INFINITY
and -f64::INFINITY
respectively).
Note2: sum
and mean
can take any number of arguments (if none, returns 0
and f64::NAN
respectively).
You can simplify an [Xprs
], in-place or not, for a given variable (or set of variables) using the simplify_for
or simplify_for_multiple
methods.
use xprs::Xprs;
fn main() {
let mut xprs = Xprs::try_from("w + sin(x + 2y) * (3 * z)").unwrap();
println!("{xprs}"); // (w + (sin((x + (2 * y))) * (3 * z)))
xprs.simplify_for_in_place(("z", 4.0));
println!("{xprs}"); // (w + (sin((x + (2 * y))) * 12))
let xprs = xprs.simplify_for_multiple(&[("x", 1.0), ("y", 2.0)]);
println!("{xprs}"); // (w + -11.507091295957661)
}
You can define functions in a context based on a previously parsed expression.
use xprs::{xprs_fn, Context, Parser, Xprs};
fn main() {
let xprs_hof = Xprs::try_from("2x + y").unwrap();
let fn_hof = xprs_hof.bind2("x", "y").unwrap();
let hof = xprs_fn!("hof", dyn fn_hof, 2);
let ctx = Context::default().with_fn(hof);
let parser = Parser::new_with_ctx(ctx);
let xprs = parser.parse("hof(2, 3)").unwrap();
println!("hof(2, 3) = {}", xprs.eval_no_vars().unwrap());
}
These examples and others can be found in the examples directory.
Complete documentation can be found on docs.rs.
Copyright © 2023 Victor LEFEBVRE This work is free. You can redistribute it and/or modify it under the terms of the Do What The Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See the LICENSE. file for more details.
Here is a non-exhaustive list of the things I want to do/add in the future:
- Better CI/CD.
- Remove lifetimes by replacing
&str
with something likebyteyarn
. - Complex numbers support.
- Macro for defining the [
Context
] like the one inevalexpr
. - Support for dynamic [
Function
] name. - Native variadics (when rust supports them in stable).
- Have [
Xprs
] be generic, taking float for its return type if that's even possible (regarding the dependency on [Context
]).
If one of them picks your interest feel free to open a PR!