#expression-parser #library #math #parser #higher-order

bin+lib xprs

Xprs is a flexible and extensible mathematical expression parser and evaluator for Rust, designed for simplicity and ease of use

6 releases

0.1.0 Mar 9, 2024
0.0.2 Feb 14, 2024
0.0.1 Jan 7, 2024
0.0.1-beta2 Dec 8, 2023

#370 in Parser implementations

28 downloads per month

WTFPL license

155KB
3K SLoC

Xprs

github crates.io build status docs.rs downloads

Xprs is a flexible and extensible mathematical expression parser and evaluator for Rust, designed for simplicity and ease of use (and ideally, speed).

Installation

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.

MSRV (Minimum Supported Rust Version)

Currently, the minimum supported Rust version is 1.70.0.

Crate Features

  • compile-time-optimizations (enabled by default) :

    Enable optimization and evaluation during parsing. This feature will automagically transform expressions like 1 + 2 * 3 into 7 during parsing allowing for faster evaluation. It also works on functions (e.g. sin(0) will be transformed into 0) and "logical" result like (x - x) * (....) will be transformed into 0 since x - x is 0 no matter what x 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 as 6/2*(2+1) which gives 9 as a result.
    • 1/2x gets interpreted as (1/2)*x which, with x being 2, gives 1 as a result.

    Note: Display and Debug 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 as 6/(2*(2+1)) which gives 1 as a result.
    • 1/2x gets interpreted as 1/(2*x) which, with x being 2, gives 0.25 as a result.

    Note: Display and Debug shows additional parenthesis to make the order of operations more obvious.

Usage

Simple examples

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.

Context and Parser

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:#?}");
}

Error handling

All errors are implemented using the thiserror. And parsing errors are implemented using the miette crate. Error message

Supported operations, built-in constants & functions

Operations

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)!.

Built-in constants

Constant Value Approximation
PI π 3.141592653589793
E e 2.718281828459045

Built-in functions

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 as logn(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 and gamma.

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).

Advanced examples

Xprs simplification

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)
}

Higher order functions

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.

Documentation

Complete documentation can be found on docs.rs.

License

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.

TODOs

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 like byteyarn.
  • Complex numbers support.
  • Macro for defining the Context like the one in evalexpr.
  • 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!

Dependencies

~2–14MB
~143K SLoC