nanoexpr 0.1.0

Fast no_std math expression parser and evaluator with zero heap allocation: formulas with variables and functions for microcontrollers, WebAssembly and C, in f32, f64, integers or calculator-exact decimals
Documentation
#![cfg(feature = "decimal")]

use nanoexpr::{Decimal, ErrorKind, Number};

fn decimal(text: &str) -> Decimal {
    text.parse()
        .unwrap_or_else(|error| panic!("{text}: {error}"))
}

fn expected(text: &str) -> Result<Decimal, ErrorKind> {
    match text {
        "Overflow" => Err(ErrorKind::Overflow),
        "DivisionByZero" => Err(ErrorKind::DivisionByZero),
        _ => Ok(decimal(text)),
    }
}

#[test]
fn arithmetic_matches_python_decimal() {
    let mut checked = 0;
    for line in include_str!("data/decimal.txt").lines() {
        let (operation, result) = line.split_once(" = ").unwrap();
        let actual = match operation.split(' ').collect::<Vec<_>>()[..] {
            [left, operator, right] => {
                let (left, right) = (decimal(left), decimal(right));
                match operator {
                    "+" => left.add(right),
                    "-" => left.subtract(right),
                    "*" => left.multiply(right),
                    "/" => left.divide(right),
                    "%" => left.remainder(right),
                    _ => unreachable!(),
                }
            }
            [literal] => literal.parse::<Decimal>().map_err(|error| error.kind),
            _ => unreachable!(),
        };
        assert_eq!(actual, expected(result), "{line}");
        checked += 1;
    }
    assert_eq!(checked, 4500);
}

fn evaluate(input: &str) -> Result<Decimal, ErrorKind> {
    nanoexpr::evaluate(input).map_err(|error| error.kind)
}

fn shown(input: &str) -> String {
    evaluate(input).unwrap().to_string()
}

fn calculator(input: &str) -> String {
    format!("{:.10}", evaluate(input).unwrap())
}

#[test]
fn decimal_fractions_are_exact() {
    assert_eq!(shown("0.1 + 0.2"), "0.3");
    assert_eq!(shown("1.1 * 1.1"), "1.21");
    assert_eq!(shown("0.3 - 0.1 - 0.2"), "0");
    assert_eq!(shown("19.99 * 3"), "59.97");
}

#[test]
fn display_hides_rounding_with_a_precision() {
    let one = evaluate("1 / 3 * 3").unwrap();
    assert_eq!(one.to_string(), "0.99999999999999");
    assert_eq!(format!("{one:.10}"), "1");
    assert_eq!(
        format!("{:.10}", evaluate("2 / 3").unwrap()),
        "0.6666666667"
    );
    assert_eq!(format!("{:.3}", evaluate("-9.9996").unwrap()), "-10");
    assert_eq!(format!("{:>8}", decimal("1.5")), "     1.5");
}

#[test]
fn display_switches_to_scientific_notation() {
    assert_eq!(decimal("123456789").to_string(), "123456789");
    assert_eq!(decimal("1e13").to_string(), "10000000000000");
    assert_eq!(decimal("1e14").to_string(), "1e14");
    assert_eq!(decimal("6.02e23").to_string(), "6.02e23");
    assert_eq!(decimal("0.00001").to_string(), "0.00001");
    assert_eq!(decimal("0.000001").to_string(), "1e-6");
    assert_eq!(decimal("-1.5e-99").to_string(), "-1.5e-99");
    assert_eq!(format!("{:.10}", decimal("12345678901")), "1.23456789e10");
    assert_eq!(<Decimal>::ZERO.to_string(), "0");
}

#[test]
fn range_is_bounded() {
    assert_eq!(
        evaluate("9.9999999999999e99 * 10"),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(evaluate("1e-99 / 10"), Ok(<Decimal>::ZERO));
    assert_eq!(evaluate("1e100"), Err(ErrorKind::InvalidNumber));
    assert_eq!(
        "1e100".parse::<Decimal>().unwrap_err().kind,
        ErrorKind::Overflow
    );
    assert_eq!(
        "1.2.3".parse::<Decimal>().unwrap_err().kind,
        ErrorKind::InvalidNumber
    );
    assert_eq!(
        "".parse::<Decimal>().unwrap_err().kind,
        ErrorKind::InvalidNumber
    );
    assert_eq!(
        "e5".parse::<Decimal>().unwrap_err().kind,
        ErrorKind::InvalidNumber
    );
    assert_eq!(
        "1e".parse::<Decimal>().unwrap_err().kind,
        ErrorKind::InvalidNumber
    );
}

#[test]
fn tiny_differences_round_correctly() {
    assert_eq!(shown("1 - 1e-30"), "1");
    assert_eq!(shown("1 + 1e-30"), "1");
    assert_eq!(shown("1 - 1e-14"), "0.99999999999999");
    assert_eq!(shown("1e20 + 1 - 1e20"), "0");
}

#[test]
fn powers() {
    assert_eq!(shown("2 ^ 10"), "1024");
    assert_eq!(shown("2 ^ -2"), "0.25");
    assert_eq!(shown("(-2) ^ 3"), "-8");
    assert_eq!(shown("0 ^ 0"), "1");
    assert_eq!(shown("10 ^ -200"), "0");
    assert_eq!(shown("4 ^ 0.5"), "2");
    assert_eq!(evaluate("1.5 ^ 1e30"), Err(ErrorKind::Overflow));
    assert_eq!(shown("(-1) ^ 1e30 + 0.5 ^ 1e30"), "1");
    assert_eq!(evaluate("2 ^ 1000"), Err(ErrorKind::Overflow));
    assert_eq!(evaluate("0 ^ -1"), Err(ErrorKind::DivisionByZero));
    assert_eq!(evaluate("(-8) ^ 0.5"), Err(ErrorKind::Domain));
}

#[test]
fn remainders() {
    assert_eq!(shown("7 mod 3"), "1");
    assert_eq!(shown("-7 mod 3"), "-1");
    assert_eq!(shown("7.5 mod 2"), "1.5");
    assert_eq!(shown("1 mod 3"), "1");
    assert_eq!(shown("1e50 mod 7"), "2");
    assert_eq!(evaluate("1 mod 0"), Err(ErrorKind::DivisionByZero));
}

#[test]
fn math_functions() {
    assert_eq!(shown("pi"), "3.1415926535898");
    assert_eq!(shown("e"), "2.718281828459");
    assert_eq!(shown("tau"), "6.2831853071796");
    assert_eq!(calculator("sqrt(2)"), "1.414213562");
    assert_eq!(shown("sqrt(2)"), "1.4142135623731");
    assert_eq!(shown("sqrt(16)"), "4");
    assert_eq!(calculator("sin(pi / 6)"), "0.5");
    assert_eq!(calculator("ln(e)"), "1");
    assert_eq!(calculator("log(1000)"), "3");
    assert_eq!(shown("abs(-2.5) + sign(-3) + sign(0)"), "1.5");
    assert_eq!(
        shown("floor(-2.5) + ceil(-2.5) + round(-2.5) + trunc(-2.5)"),
        "-10"
    );
    assert_eq!(
        shown("round(0.5) + round(0.49999999999999) + floor(0.001)"),
        "1"
    );
    assert_eq!(shown("floor(123456.7e-20)"), "0");
    assert_eq!(
        shown("min(3, 1.5, 2) + max(0.1, 0.2) + clamp(5, 0, 1)"),
        "2.7"
    );
    assert_eq!(calculator("pow(2, 0.5) ^ 2"), "2");
    assert_eq!(evaluate("sqrt(-1)"), Err(ErrorKind::Domain));
    assert_eq!(evaluate("clamp(1, 2, 0)"), Err(ErrorKind::Domain));
    assert_eq!(evaluate("exp(1000)"), Err(ErrorKind::Overflow));
    assert_eq!(evaluate("sqrt(1, 2)"), Err(ErrorKind::ArgumentCount));
    assert_eq!(evaluate("min()"), Err(ErrorKind::ArgumentCount));
    assert_eq!(
        evaluate("hypot(1, 2, 3, 4, 5)"),
        Err(ErrorKind::ArgumentCount)
    );
    assert_eq!(evaluate("gamma(1)"), Err(ErrorKind::UnknownFunction));
}

#[test]
fn conversions() {
    assert_eq!(<Decimal>::try_from(-42_i64).unwrap().to_string(), "-42");
    assert_eq!(<Decimal>::try_from(0.1_f64).unwrap().to_string(), "0.1");
    assert_eq!(<Decimal>::try_from(f64::NAN), Err(ErrorKind::Domain));
    assert_eq!(<Decimal>::try_from(f64::INFINITY), Err(ErrorKind::Overflow));
    assert_eq!(<Decimal>::try_from(1e300), Err(ErrorKind::Overflow));
    assert_eq!(f64::from(decimal("-2.5")), -2.5);
    assert_eq!(
        Decimal::<14, 99>::new(1999, -2).unwrap().to_string(),
        "19.99"
    );
    assert!(decimal("-0").is_zero() && !decimal("-0").is_negative());
    assert_eq!(decimal("-3").abs(), decimal("3"));
    assert_eq!(decimal("123").scientific_exponent(), 2);
    assert_eq!(<Decimal>::default(), <Decimal>::ZERO);
}

#[test]
fn ordering() {
    let mut values = ["2", "-1e5", "0", "-3", "1e-5", "1e5", "-1e-5"].map(decimal);
    values.sort();
    assert_eq!(
        values.map(|value| value.to_string()),
        ["-100000", "-3", "-0.00001", "0", "0.00001", "2", "100000"]
    );
}

#[test]
fn other_precisions() {
    type Short = Decimal<6, 9>;
    let third: Short = nanoexpr::evaluate("1 / 3").unwrap();
    assert_eq!(third.to_string(), "0.333333");
    assert_eq!(
        nanoexpr::evaluate::<Short>("1e9 * 10").unwrap_err().kind,
        ErrorKind::Overflow
    );
    type Wide = Decimal<18, 9999>;
    let large: Wide = nanoexpr::evaluate("10 ^ 5000 / 3").unwrap();
    assert_eq!(large.to_string(), "3.33333333333333333e4999");
}