exmex 0.9.3

fast, simple, and extendable mathematical expression evaluator able to compute partial derivatives
Documentation

Exmex is a fast, simple, and extendable mathematical expression evaluator with the ability to compute partial derivatives of expressions.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
assert!((exmex::eval_str("1.5 * ((cos(0) + 23.0) / 2.0)")? - 18.0).abs() < 1e-12);
#
#     Ok(())
# }

For floats, we have a list of predifined operators containing ^, *, /, +, -, sin, cos, tan, exp, log, and log2. The full list is defined in make_default_operators.

Variables

For variables we can use strings that are not in the list of operators as shown in the following expression. Additionally, variables should consist only of letters, numbers, and underscores. More precisely, they need to fit the regular expression r"^[a-zA-Z_]+[a-zA-Z_0-9]*". Variables' values are passed as slices to eval.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
let to_be_parsed = "log(z) + 2* (-z^2 + sin(4*y))";
let expr = exmex::parse_with_default_ops::<f64>(to_be_parsed)?;
assert!((expr.eval(&[3.7, 2.5])? - 14.992794866624788 as f64).abs() < 1e-12);
#
#     Ok(())
# }

The n-th number in the slice corresponds to the n-th variable. Thereby, the alphatical order of the variables is relevant. In this example, we have y=3.7 and z=2.5. If variables are between curly brackets, they can have arbitrary names, e.g., {456/549*(}, {x}, and confusingly even {x+y} are valid variable names as shown in the following.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
let x = 2.1f64;
let y = 0.1f64;
let to_be_parsed = "log({x+y})";  // {x+y} is the name of one(!) variable 😕.
let expr = exmex::parse::<f64>(to_be_parsed, &exmex::make_default_operators::<f64>())?;
assert!((expr.eval(&[x+y])? - 2.2f64.ln()).abs() < 1e-12);
#
#     Ok(())
# }

Extendability

Library users can define their own set of operators as shown in the following.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex::{BinOp, Operator};
let ops = [
Operator {
repr: "%",
bin_op: Some(BinOp{ apply: |a: i32, b: i32| a % b, prio: 1 }),
unary_op: None,
},
Operator {
repr: "/",
bin_op: Some(BinOp{ apply: |a: i32, b: i32| a / b, prio: 1 }),
unary_op: None,
},
];
let to_be_parsed = "19 % 5 / 2 / a";
let expr = exmex::parse::<i32>(to_be_parsed, &ops)?;
assert_eq!(expr.eval(&[1])?, 2);
#
#     Ok(())
# }

Operators

Operators are instances of the struct Operator that has its representation in the field repr, a binary and a unary operator of type Option<BinOp<T>> and Option<fn(T) -> T>, respectively, as members. BinOp contains in addition to the function pointer apply of type fn(T, T) -> T an integer prio. Operators can be both, binary and unary. See, e.g., - defined in the list of default operators. Note that we expect a unary operator to be always on the left of a number.

Data Types of Numbers

You can use any type that implements Copy and FromStr. In case the representation of your data type in the string does not match the number regex r"\.?[0-9]+(\.[0-9]+)?", you have to pass a suitable regex and use the function parse_with_number_pattern instead of parse. Here is an example for bool.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex::{self, BinOp, Operator};
let ops = [
Operator {
repr: "&&",
bin_op: Some(BinOp{ apply: |a: bool, b: bool| a && b, prio: 1 }),
unary_op: None,
},
Operator {
repr: "||",
bin_op: Some(BinOp{ apply: |a: bool, b: bool| a || b, prio: 1 }),
unary_op: None,
},
Operator {
repr: "!",
bin_op: None,
unary_op: Some(|a: bool| !a),
},
];
let to_be_parsed = "!(true && false) || (!false || (true && false))";
let expr = exmex::parse_with_number_pattern::<bool>(to_be_parsed, &ops, "true|false")?;
assert_eq!(expr.eval(&[])?, true);
#
#     Ok(())
# }

Partial Derivatives

For default operators, expressions can be transformed into their partial derivatives again represented by expressions.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
let expr = exmex::parse_with_default_ops::<f64>("x^2 + y^2")?;
let dexpr_dx = expr.clone().partial(0)?;
let dexpr_dy = expr.partial(1)?;
assert!((dexpr_dx.eval(&[3.0, 2.0])? - 6.0).abs() < 1e-12);
assert!((dexpr_dy.eval(&[3.0, 2.0])? - 4.0).abs() < 1e-12);
#
#     Ok(())
# }

Owned Expression

You cannot return a usual expression from a function without a lifetime parameter, since expressions that are instances of FlatEx keep &strs instead of Strings of variable or operator names to make faster parsing possible.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex::{self, ExParseError, FlatEx};
fn make<'a>() -> Result<FlatEx::<'a, f64>, ExParseError> {
//            |                        |
//           lifetime parameter necessary

let to_be_parsed = "log(z) + 2* (-z^2 + sin(4*y))";
exmex::parse_with_default_ops::<f64>(to_be_parsed)
}
let expr_owned = make()?;
assert!((expr_owned.eval(&[3.7, 2.5])? - 14.992794866624788 as f64).abs() < 1e-12);
#
#     Ok(())
# }

If you are willing to pay the price of roughly doubled parsing times, you can obtain an expression that is an instance of OwnedFlatEx and owns its strings. Evaluation times should be comparable. However, a lifetime parameter is not needed anymore as shown in the following.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex::{self, ExParseError, OwnedFlatEx};
fn make() -> Result<OwnedFlatEx::<f64>, ExParseError> {
let to_be_parsed = "log(z) + 2* (-z^2 + sin(4*y))";
let expr = exmex::parse_with_default_ops::<f64>(to_be_parsed)?;
Ok(OwnedFlatEx::from_flatex(expr))
}
let expr_owned = make()?;
assert!((expr_owned.eval(&[3.7, 2.5])? - 14.992794866624788 as f64).abs() < 1e-12);
#
#     Ok(())
# }

Priorities and Parentheses

In Exmex-land, unary operators always have higher priority than binary operators, e.g., -2^2=4 instead of -2^2=-4. Moreover, we are not too strict regarding parentheses. For instance

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
assert_eq!(exmex::eval_str("---1")?, -1.0);
#
#     Ok(())
# }

If you want to be on the safe side, we suggest using parentheses.

Display

An instance of FlatEx can be displayed as string. Note that this unparsed string does not necessarily coincide with the original string, since, e.g., curly brackets are added and expressions are compiled.

# use std::error::Error;
# fn main() -> Result<(), Box<dyn Error>> {
#
use exmex;
let flatex = exmex::parse_with_default_ops::<f64>("-sin(z)/cos(mother_of_names) + 2^7")?;
assert_eq!(format!("{}", flatex), "-(sin({z}))/cos({mother_of_names})+128.0");
#
#     Ok(())
# }

Serialization and Deserialization

To use serde you can activate the feature serde_support. Currently, this only works for default operators. The implementation un-parses and re-parses the whole expression. Deserialize and Serialize are implemented for for both, FlatEx and OwnedFlatEx.

Unicode

Unicode input strings are currently not supported 😕 but might be added in the future 😀.