nanoexpr 0.2.1

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 = "fraction")]

use std::collections::HashSet;

use nanoexpr::{Error, ErrorKind, Fraction, Number, Span};

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

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

fn shown(input: &str) -> String {
    evaluate(input)
        .unwrap_or_else(|error| panic!("{input}: {error}"))
        .to_string()
}

#[test]
fn expressions_match_python_fractions() {
    let (mut checked, mut values) = (0, 0);
    for line in include_str!("data/fraction.txt").lines() {
        let (input, result) = line.split_once(" = ").unwrap();
        let expected = match result {
            "Overflow" => Err(ErrorKind::Overflow),
            "DivisionByZero" => Err(ErrorKind::DivisionByZero),
            _ => Ok(fraction(result)),
        };
        assert_eq!(evaluate(input), expected, "{line}");
        checked += 1;
        values += usize::from(expected.is_ok());
    }
    assert_eq!((checked, values), (4000, 2871));
}

#[test]
fn arithmetic_is_exact() {
    assert_eq!(shown("1/3 + 1/6"), "1/2");
    assert_eq!(shown("2/3 * 3"), "2");
    assert_eq!(shown("1/3 * 3"), "1");
    assert_eq!(shown("0.1 + 0.2"), "3/10");
    assert_eq!(shown("1/3 - 1/2"), "-1/6");
    assert_eq!(shown("(1/7) / (2/21)"), "3/2");
    assert_eq!(shown("7/2 % 1"), "1/2");
    assert_eq!(shown("-7/2 % 1"), "-1/2");
    assert_eq!(shown("(7/2) % (-2/3)"), "1/6");
    assert_eq!(shown("1/2 % 3"), "1/2");
    assert_eq!(shown("-(1/3)"), "-1/3");
    assert_eq!(shown("1/3 - 1/3"), "0");
    assert_eq!(shown("1/4 == 0.25"), "1");
    assert_eq!(shown("1/3 < 0.33333333333333"), "0");
    assert_eq!(shown("1/3 > 0.3 && 1/3 < 0.34"), "1");
}

#[test]
fn literals_are_exact() {
    assert_eq!(shown("0.75"), "3/4");
    assert_eq!(shown("1e-3"), "1/1000");
    assert_eq!(shown("1.5e-3"), "3/2000");
    assert_eq!(shown(".5"), "1/2");
    assert_eq!(shown("5."), "5");
    assert_eq!(shown("2.50000000000000000000000000000000000000000"), "5/2");
    assert_eq!(
        shown("0.00000000000000000000000000000000000000001e40"),
        "1/10"
    );
    assert_eq!(shown("1e19"), "10000000000000000000");
    assert_eq!(shown("18446744073709551615"), "18446744073709551615");
    assert_eq!(shown("0e-1000000"), "0");
    for invalid in ["1e20", "18446744073709551616", "1e-20", "1e-999999"] {
        assert_eq!(
            nanoexpr::evaluate::<Fraction>(invalid),
            Err(Error::new(
                ErrorKind::InvalidNumber,
                Span::new(0, invalid.len())
            )),
            "{invalid}"
        );
    }
}

#[test]
fn results_are_reduced() {
    let value = evaluate("6/8 + 2/8").unwrap();
    assert_eq!((value.numerator(), value.denominator()), (1, 1));
    let value = Fraction::new(true, 150, 1000).unwrap();
    assert_eq!(
        (value.is_negative(), value.numerator(), value.denominator()),
        (true, 3, 20)
    );
    assert_eq!(
        Fraction::new(true, 0, 5).unwrap(),
        Fraction::ZERO,
        "zero is never negative"
    );
    assert_eq!(Fraction::new(false, 1, 0), Err(ErrorKind::DivisionByZero));
    assert_eq!(Fraction::from(-4_i64), fraction("-8/2"));
    assert_eq!(Fraction::from(u64::MAX).to_string(), "18446744073709551615");
    assert_eq!(Fraction::from(i64::MIN).to_string(), "-9223372036854775808");
}

#[test]
fn overflow_is_detected_at_the_limits() {
    let max = "18446744073709551615";
    assert_eq!(evaluate(&format!("{max} + 1")), Err(ErrorKind::Overflow));
    assert_eq!(evaluate(&format!("-{max} - 1")), Err(ErrorKind::Overflow));
    assert_eq!(shown(&format!("{max} - 1 + 1")), max);
    assert_eq!(shown(&format!("-{max} + {max}")), "0");
    assert_eq!(evaluate(&format!("{max} * 2")), Err(ErrorKind::Overflow));
    assert_eq!(
        evaluate(&format!("1 / {max} / 2")),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(shown(&format!("1 / {max} * 2")), format!("2/{max}"));
    assert_eq!(
        evaluate(&format!("1 / {max} + 1 / ({max} - 1)")),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(
        shown(&format!("{max} / ({max} - 1) - 1")),
        "1/18446744073709551614"
    );
    assert_eq!(shown(&format!("({max} / 3) * (3 / {max})")), "1");
    assert_eq!(evaluate("2 ^ 64"), Err(ErrorKind::Overflow));
    assert_eq!(shown("2 ^ 63 - 1 + 2 ^ 63"), max);
    assert_eq!(evaluate("2 ^ (-64)"), Err(ErrorKind::Overflow));
    assert_eq!(shown("1 ^ 1e19 + (-1) ^ 1e19 + 0 ^ 1e19"), "2");
    assert_eq!(shown("(-1) ^ 18446744073709551615"), "-1");
    assert_eq!(evaluate("2 ^ 1e19"), Err(ErrorKind::Overflow));
}

#[test]
fn powers_are_exact_when_rational() {
    assert_eq!(shown("(2/3) ^ 3"), "8/27");
    assert_eq!(shown("(2/3) ^ (-2)"), "9/4");
    assert_eq!(shown("(-2/3) ^ 3"), "-8/27");
    assert_eq!(shown("(-2) ^ (-1)"), "-1/2");
    assert_eq!(shown("0 ^ 0"), "1");
    assert_eq!(shown("(4/9) ^ (1/2)"), "2/3");
    assert_eq!(shown("8 ^ (1/3)"), "2");
    assert_eq!(shown("8 ^ (2/3)"), "4");
    assert_eq!(shown("8 ^ (-2/3)"), "1/4");
    assert_eq!(shown("(-8) ^ (1/3)"), "-2");
    assert_eq!(shown("(-8) ^ (2/3)"), "4");
    assert_eq!(shown("0 ^ (1/2)"), "0");
    assert_eq!(shown("1 ^ (1/1000)"), "1");
    assert_eq!(shown("(2 ^ 63) ^ (1/63)"), "2");
    assert_eq!(
        shown("18446744073709551615 ^ (1/1)"),
        "18446744073709551615"
    );
    assert_eq!(shown("(4294967295 ^ 2) ^ 0.5"), "4294967295");
    assert_eq!(evaluate("2 ^ (1/2)"), Err(ErrorKind::Inexact));
    assert_eq!(evaluate("(2 ^ 63) ^ (1/64)"), Err(ErrorKind::Inexact));
    assert_eq!(evaluate("(4/5) ^ (1/2)"), Err(ErrorKind::Inexact));
    assert_eq!(evaluate("(-4) ^ (1/2)"), Err(ErrorKind::Domain));
    assert_eq!(evaluate("0 ^ (-1)"), Err(ErrorKind::DivisionByZero));
    assert_eq!(evaluate("0 ^ (-1/2)"), Err(ErrorKind::DivisionByZero));
}

#[test]
fn division_by_zero() {
    assert_eq!(evaluate("1 / 0"), Err(ErrorKind::DivisionByZero));
    assert_eq!(evaluate("1 / (1/2 - 0.5)"), Err(ErrorKind::DivisionByZero));
    assert_eq!(evaluate("1 % 0"), Err(ErrorKind::DivisionByZero));
    assert_eq!(
        nanoexpr::evaluate::<Fraction>("1 + 2 / (1/3 - 1/3)"),
        Err(Error::new(ErrorKind::DivisionByZero, Span::new(4, 19)))
    );
}

#[test]
fn math_functions_are_exact() {
    assert_eq!(shown("abs(-3/4) + sign(-2/3) * 10"), "-37/4");
    assert_eq!(shown("sign(0)"), "0");
    assert_eq!(shown("floor(7/2)"), "3");
    assert_eq!(shown("floor(-7/2)"), "-4");
    assert_eq!(shown("ceil(7/2)"), "4");
    assert_eq!(shown("ceil(-7/2)"), "-3");
    assert_eq!(shown("trunc(-7/2)"), "-3");
    assert_eq!(shown("round(5/2)"), "3");
    assert_eq!(shown("round(-5/2)"), "-3");
    assert_eq!(shown("round(7/3)"), "2");
    assert_eq!(shown("round(-8/3)"), "-3");
    assert_eq!(
        shown("floor(18446744073709551615 / 2)"),
        "9223372036854775807"
    );
    assert_eq!(
        shown("ceil(18446744073709551615 / 2)"),
        "9223372036854775808"
    );
    assert_eq!(shown("sqrt(4/9)"), "2/3");
    assert_eq!(shown("cbrt(-27/8)"), "-3/2");
    assert_eq!(shown("hypot(3/5, 4/5)"), "1");
    assert_eq!(shown("pow(4/9, -1/2)"), "3/2");
    assert_eq!(shown("min(1/2, 1/3, 2/3) + max(1/2, 1/3)"), "5/6");
    assert_eq!(shown("clamp(5/2, 0, 1) + clamp(1/3, 0, 1)"), "4/3");
    assert_eq!(evaluate("clamp(1, 1, 0)"), Err(ErrorKind::Domain));
    assert_eq!(evaluate("sqrt(-4)"), Err(ErrorKind::Domain));
    for inexact in [
        "sqrt(2)",
        "cbrt(2)",
        "hypot(1, 1)",
        "exp(0)",
        "ln(1)",
        "log(100)",
        "log(8, 2)",
        "sin(0)",
        "atan2(1, 1)",
        "atanh(0)",
    ] {
        assert_eq!(evaluate(inexact), Err(ErrorKind::Inexact), "{inexact}");
    }
    assert_eq!(evaluate("sin(1, 2)"), Err(ErrorKind::ArgumentCount));
    assert_eq!(evaluate("sqrt()"), Err(ErrorKind::ArgumentCount));
    assert_eq!(evaluate("min()"), Err(ErrorKind::ArgumentCount));
    assert_eq!(evaluate("pi"), Err(ErrorKind::UnknownVariable));
    assert_eq!(evaluate("gamma(1)"), Err(ErrorKind::UnknownFunction));
}

#[test]
fn display_and_parsing() {
    assert_eq!(fraction("3/4").to_string(), "3/4");
    assert_eq!(fraction("-5/4").to_string(), "-5/4");
    assert_eq!(format!("{:#}", fraction("-5/4")), "-1 1/4");
    assert_eq!(format!("{:#}", fraction("3/4")), "3/4");
    assert_eq!(format!("{:#}", fraction("8/4")), "2");
    assert_eq!(fraction("7").to_string(), "7");
    assert_eq!(fraction("+7").to_string(), "7");
    assert_eq!(fraction("1.5/2").to_string(), "3/4");
    assert_eq!(fraction("0.125").to_string(), "1/8");
    assert_eq!(fraction("-0").to_string(), "0");
    for (text, kind) in [
        ("", ErrorKind::InvalidNumber),
        ("-", ErrorKind::InvalidNumber),
        ("1/", ErrorKind::InvalidNumber),
        ("1/-2", ErrorKind::InvalidNumber),
        ("1 / 2", ErrorKind::InvalidNumber),
        ("1/2/3", ErrorKind::InvalidNumber),
        ("x", ErrorKind::InvalidNumber),
        ("1/0", ErrorKind::DivisionByZero),
        ("1e30", ErrorKind::Overflow),
    ] {
        assert_eq!(
            text.parse::<Fraction>().map_err(|error| error.kind),
            Err(kind),
            "{text}"
        );
    }
    assert_eq!(
        "1/0".parse::<Fraction>().unwrap_err().to_string(),
        "division by zero"
    );
}

#[test]
fn ordering_and_hashing() {
    let mut values = ["1/2", "-1/3", "0", "1/3", "-2", "5/2", "-1/2"].map(fraction);
    values.sort();
    assert_eq!(
        values.map(|value| value.to_string()),
        ["-2", "-1/2", "-1/3", "0", "1/3", "1/2", "5/2"]
    );
    let set: HashSet<Fraction> = ["1/2", "2/4", "0.5", "-0"].map(fraction).into();
    assert_eq!(set.len(), 2);
    assert_eq!(Fraction::default(), Fraction::ZERO);
    assert!(fraction("18446744073709551615/2") > fraction("18446744073709551614/2"));
    assert!(fraction("1/18446744073709551615") < fraction("1/18446744073709551614"));
}

#[test]
fn conversions() {
    assert_eq!(f64::from(fraction("-3/4")), -0.75);
    assert_eq!(fraction("1/3").to_f64(), 1.0 / 3.0);
    assert_eq!(Fraction::ONE.add(Fraction::ONE), Ok(fraction("2")));
    assert_eq!(Fraction::from_bool(true), Ok(Fraction::ONE));
    assert_eq!(Fraction::parse("1/2"), None);
}

#[cfg(feature = "decimal")]
#[test]
fn decimal_conversions() {
    use nanoexpr::Decimal;

    let decimal = |text: &str| text.parse::<Decimal>().unwrap();
    assert_eq!(
        <Decimal>::try_from(fraction("1/3")).unwrap().to_string(),
        "0.33333333333333"
    );
    assert_eq!(
        <Decimal>::try_from(fraction("2/3")).unwrap().to_string(),
        "0.66666666666667"
    );
    assert_eq!(
        <Decimal>::try_from(fraction("-18446744073709551615"))
            .unwrap()
            .to_string(),
        "-1.844674407371e19"
    );
    assert_eq!(
        <Decimal>::try_from(fraction("1/18446744073709551615"))
            .unwrap()
            .to_string(),
        "5.4210108624275e-20"
    );
    assert_eq!(
        Decimal::<6, 9>::try_from(fraction("10000000000")),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(
        Decimal::<6, 9>::try_from(fraction("1/10000000000")),
        Ok(Decimal::ZERO)
    );
    let rounded = |text: &str| {
        Decimal::<6, 99>::try_from(fraction(text))
            .unwrap()
            .to_string()
    };
    assert_eq!(rounded("1234565/100"), "12345.6");
    assert_eq!(rounded("1234575/100"), "12345.8");
    assert_eq!(rounded("1234565001/100000"), "12345.7");
    assert_eq!(Fraction::try_from(decimal("0.125")), Ok(fraction("1/8")));
    assert_eq!(
        Fraction::try_from(decimal("-0.33333333333333")),
        Ok(fraction("-33333333333333/100000000000000"))
    );
    assert_eq!(
        Fraction::try_from(decimal("1.5e18")),
        Ok(fraction("1500000000000000000"))
    );
    assert_eq!(
        Fraction::try_from(decimal("1e20")),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(
        Fraction::try_from(decimal("1.2345678901234e-10")),
        Err(ErrorKind::Overflow)
    );
    assert_eq!(Fraction::try_from(decimal("0")), Ok(Fraction::ZERO));
    let third: Decimal = nanoexpr::evaluate("1/3").unwrap();
    assert_eq!(third.to_fraction(9999), Some(fraction("1/3")));
}