Skip to main content

nanoexpr/
math.rs

1//! The [`Math`] context of constants and scientific functions, and [`evaluate`] that uses it.
2
3#[cfg(feature = "exact")]
4use core::cmp::Ordering;
5
6#[cfg(feature = "decimal")]
7use crate::Decimal;
8#[cfg(feature = "exact")]
9use crate::Exact;
10#[cfg(feature = "fraction")]
11use crate::Fraction;
12use crate::{Context, Error, ErrorKind, Evaluator, Number};
13
14/// The usual constants and functions of a scientific calculator, for `f32` and `f64` with the `libm` feature, for
15/// `Decimal` with the `decimal` feature, the exact ones for `Fraction` with the `fraction` feature, and both for
16/// `Exact` with the `exact` feature.
17///
18/// | Name | Meaning |
19/// |---|---|
20/// | `pi`, `PI`, `tau`, `e`, `E` | Constants |
21/// | `sqrt`, `cbrt`, `abs`, `sign` | Roots, absolute value, sign (`-1`, `0` or `1`) |
22/// | `exp`, `ln`, `log2`, `log10` | Exponential and logarithms |
23/// | `log(x)`, `log(x, base)` | Logarithm in base 10, or in `base` |
24/// | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2(y, x)` | Trigonometry, in radians |
25/// | `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh` | Hyperbolic functions |
26/// | `floor`, `ceil`, `round`, `trunc` | Rounding |
27/// | `pow(x, y)`, `hypot(x, y)` | Power, length of the hypotenuse |
28/// | `min(...)`, `max(...)`, `clamp(x, low, high)` | Comparisons, `min` and `max` take one argument or more |
29/// | `fact(n)` | Factorial of a non-negative integer, the same as `n!` |
30///
31/// Operands outside a function's domain, such as `sqrt(-1)` or `ln(0)`, give [`ErrorKind::Domain`], and finite
32/// operands whose result is infinite give [`ErrorKind::Overflow`].
33///
34/// `f32` and `f64` call the functions of [`libm`](https://crates.io/crates/libm), except `fact`, which multiplies
35/// the integers up to its argument, rounding each product, and has no gamma function for other arguments: they are
36/// [`ErrorKind::Domain`] errors, and factorials above `34!` for `f32` and `170!` for `f64` [`ErrorKind::Overflow`]
37/// errors. `Decimal` computes every function in decimal with 36 significant digits, then rounds once to its own
38/// digits: results are correctly rounded except in rare cases, which are one unit in the last place away. Results
39/// beyond its exponent range, such as factorials above `69!` by default, are [`ErrorKind::Overflow`] errors and
40/// results too small for it become zero. Trigonometric functions of `Decimal` arguments of `10^343` and beyond are
41/// [`ErrorKind::Domain`] errors.
42///
43/// `Fraction` never approximates: it has no constants, and only the functions whose results are exact fractions.
44/// `abs`, `sign`, rounding, `min`, `max`, `clamp` and `fact` always are, and `sqrt`, `cbrt`, `hypot` and `pow` are
45/// when their result is rational, such as `sqrt(4/9) = 2/3`. Other results, and every other function, are
46/// [`ErrorKind::Inexact`] errors. Factorials above `20!` do not fit and are [`ErrorKind::Overflow`] errors.
47///
48/// `Exact` computes a function exactly when its arguments are fractions and its result is exact, and with the
49/// `Decimal` function otherwise. Its constants are decimals, and `min`, `max` and `clamp` return one of their
50/// arguments as it is, comparing values exactly.
51///
52/// Combine it with your own context to add variables or functions, yours taking precedence:
53///
54/// ```
55/// # #[cfg(feature = "libm")] {
56/// use nanoexpr::Math;
57///
58/// let context = ([("r", 2.0)], Math);
59/// assert_eq!(nanoexpr::evaluate_with("pi * r ^ 2", &context), Ok(core::f64::consts::PI * 4.0));
60/// # }
61/// ```
62///
63/// ```
64/// # #[cfg(feature = "decimal")] {
65/// use nanoexpr::Decimal;
66///
67/// let sine: Decimal = nanoexpr::evaluate("sin(pi)").unwrap();
68/// assert_eq!(sine.to_string(), "-6.7615373566167e-15");
69/// # }
70/// ```
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
72pub struct Math;
73
74/// The names of the functions of [`Math`], which take a wrong number of arguments when no other arm matches.
75const FUNCTIONS: [&str; 32] = [
76    "sqrt", "cbrt", "abs", "sign", "exp", "ln", "log2", "log10", "log", "sin", "cos", "tan",
77    "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", "floor",
78    "ceil", "round", "trunc", "pow", "hypot", "min", "max", "clamp", "fact",
79];
80
81/// Returns whether `value` is neither `NaN` nor infinite.
82#[cfg(feature = "libm")]
83fn finite<T: Into<f64>>(value: T) -> bool {
84    value.into().is_finite()
85}
86
87/// Implements [`Context`] for a float type on [`Math`].
88///
89/// `$module` gathers wrappers named after the functions they implement, which call the `libm` function of the
90/// right precision: `unary` ones take one argument and `binary` ones two.
91#[cfg(feature = "libm")]
92macro_rules! math {
93    (
94        $type:ident in $module:ident,
95        unary: [$($unary:ident = $unary_libm:ident),* $(,)?],
96        binary: [$($binary:ident = $binary_libm:ident),* $(,)?] $(,)?
97    ) => {
98        #[doc = concat!("`libm` functions for `", stringify!($type), "`, under the names expressions use.")]
99        mod $module {
100            $(
101            #[doc = concat!("Calls `libm::", stringify!($unary_libm), "`.")]
102            pub(super) fn $unary(x: $type) -> $type {
103                libm::$unary_libm(x)
104            })*
105            $(
106            #[doc = concat!("Calls `libm::", stringify!($binary_libm), "`.")]
107            pub(super) fn $binary(x: $type, y: $type) -> $type {
108                libm::$binary_libm(x, y)
109            })*
110        }
111
112        impl Context<$type> for Math {
113            fn variable(&self, name: &str) -> Option<$type> {
114                use core::$type::consts;
115                match name {
116                    "pi" | "PI" => Some(consts::PI),
117                    "tau" => Some(consts::TAU),
118                    "e" | "E" => Some(consts::E),
119                    _ => None,
120                }
121            }
122
123            fn call(&self, name: &str, arguments: &[$type]) -> Option<Result<$type, ErrorKind>> {
124                use $module::*;
125                let domain = |valid: bool, value: $type| if valid { Ok(value) } else { Err(ErrorKind::Domain) };
126                let result = match (name, arguments) {
127                    ("sqrt", &[x]) => domain(x >= 0.0, sqrt(x)),
128                    ("cbrt", &[x]) => Ok(cbrt(x)),
129                    ("abs", &[x]) => Ok(x.abs()),
130                    ("sign", &[x]) => domain(!x.is_nan(), if x == 0.0 { 0.0 } else { x.signum() }),
131                    ("exp", &[x]) => Ok(exp(x)),
132                    ("ln", &[x]) => domain(x > 0.0, ln(x)),
133                    ("log2", &[x]) => domain(x > 0.0, log2(x)),
134                    ("log10" | "log", &[x]) => domain(x > 0.0, log10(x)),
135                    ("log", &[x, base]) => domain(x > 0.0 && base > 0.0 && base != 1.0, ln(x) / ln(base)),
136                    ("sin", &[x]) => Ok(sin(x)),
137                    ("cos", &[x]) => Ok(cos(x)),
138                    ("tan", &[x]) => Ok(tan(x)),
139                    ("asin", &[x]) => domain((-1.0..=1.0).contains(&x), asin(x)),
140                    ("acos", &[x]) => domain((-1.0..=1.0).contains(&x), acos(x)),
141                    ("atan", &[x]) => Ok(atan(x)),
142                    ("atan2", &[y, x]) => Ok(atan2(y, x)),
143                    ("sinh", &[x]) => Ok(sinh(x)),
144                    ("cosh", &[x]) => Ok(cosh(x)),
145                    ("tanh", &[x]) => Ok(tanh(x)),
146                    ("asinh", &[x]) => Ok(asinh(x)),
147                    ("acosh", &[x]) => domain(x >= 1.0, acosh(x)),
148                    ("atanh", &[x]) => domain(x > -1.0 && x < 1.0, atanh(x)),
149                    ("floor", &[x]) => Ok(floor(x)),
150                    ("ceil", &[x]) => Ok(ceil(x)),
151                    ("round", &[x]) => Ok(round(x)),
152                    ("trunc", &[x]) => Ok(trunc(x)),
153                    ("pow", &[x, y]) => return Some(x.power(y)),
154                    ("fact", &[x]) => return Some(x.factorial()),
155                    ("hypot", &[x, y]) => Ok(hypot(x, y)),
156                    ("min", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.min(*b))),
157                    ("max", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.max(*b))),
158                    ("clamp", &[x, low, high]) => domain(low <= high, x.max(low).min(high)),
159                    _ if FUNCTIONS.contains(&name) => Err(ErrorKind::ArgumentCount),
160                    _ => return None,
161                };
162                Some(result.and_then(|value| {
163                    if !finite(value) && arguments.iter().all(|argument| finite(*argument)) {
164                        Err(if value.is_nan() { ErrorKind::Domain } else { ErrorKind::Overflow })
165                    } else {
166                        Ok(value)
167                    }
168                }))
169            }
170        }
171    };
172}
173
174#[cfg(feature = "libm")]
175math!(
176    f32 in single,
177    unary: [
178        sqrt = sqrtf, cbrt = cbrtf, exp = expf, ln = logf, log2 = log2f, log10 = log10f,
179        sin = sinf, cos = cosf, tan = tanf, asin = asinf, acos = acosf, atan = atanf,
180        sinh = sinhf, cosh = coshf, tanh = tanhf, asinh = asinhf, acosh = acoshf, atanh = atanhf,
181        floor = floorf, ceil = ceilf, round = roundf, trunc = truncf,
182    ],
183    binary: [atan2 = atan2f, hypot = hypotf],
184);
185
186#[cfg(feature = "libm")]
187math!(
188    f64 in double,
189    unary: [
190        sqrt = sqrt, cbrt = cbrt, exp = exp, ln = log, log2 = log2, log10 = log10,
191        sin = sin, cos = cos, tan = tan, asin = asin, acos = acos, atan = atan,
192        sinh = sinh, cosh = cosh, tanh = tanh, asinh = asinh, acosh = acosh, atanh = atanh,
193        floor = floor, ceil = ceil, round = round, trunc = trunc,
194    ],
195    binary: [atan2 = atan2, hypot = hypot],
196);
197
198/// Evaluates `input` with the constants and functions of [`Math`].
199///
200/// ```
201/// # #[cfg(feature = "libm")] {
202/// assert_eq!(nanoexpr::evaluate::<f64>("sqrt(16) + 2 ^ 3"), Ok(12.0));
203/// assert_eq!(nanoexpr::evaluate::<f32>("max(1, cos(0), 0.5)"), Ok(1.0));
204/// # }
205/// ```
206pub fn evaluate<N: Number>(input: &str) -> Result<N, Error>
207where
208    Math: Context<N>,
209{
210    Evaluator::<Math>::new(&Math).evaluate(input)
211}
212
213#[cfg(feature = "decimal")]
214impl<const DIGITS: u32, const MAX_EXPONENT: i32> Context<Decimal<DIGITS, MAX_EXPONENT>> for Math {
215    fn variable(&self, name: &str) -> Option<Decimal<DIGITS, MAX_EXPONENT>> {
216        let literal = match name {
217            "pi" | "PI" => "3.14159265358979323846264338327950288",
218            "tau" => "6.28318530717958647692528676655900577",
219            "e" | "E" => "2.71828182845904523536028747135266250",
220            _ => return None,
221        };
222        Number::parse(literal)
223    }
224
225    fn call(
226        &self,
227        name: &str,
228        arguments: &[Decimal<DIGITS, MAX_EXPONENT>],
229    ) -> Option<Result<Decimal<DIGITS, MAX_EXPONENT>, ErrorKind>> {
230        let result = match (name, arguments) {
231            ("abs", &[x]) => Ok(x.abs()),
232            ("sign", &[x]) if x.is_zero() => Ok(x),
233            ("sign", &[x]) => Decimal::new(if x.is_negative() { -1 } else { 1 }, 0),
234            ("floor", &[x]) => x.floor(),
235            ("ceil", &[x]) => x.ceil(),
236            ("round", &[x]) => x.round(),
237            ("trunc", &[x]) => x.trunc(),
238            ("pow", &[x, y]) => x.power(y),
239            ("fact", &[x]) => x.factorial(),
240            ("sqrt", &[x]) => x.sqrt(),
241            ("cbrt", &[x]) => x.cbrt(),
242            ("exp", &[x]) => x.exp(),
243            ("ln", &[x]) => x.ln(),
244            ("log2", &[x]) => x.log2(),
245            ("log10" | "log", &[x]) => x.log10(),
246            ("log", &[x, base]) => x.log(base),
247            ("sin", &[x]) => x.sin(),
248            ("cos", &[x]) => x.cos(),
249            ("tan", &[x]) => x.tan(),
250            ("asin", &[x]) => x.asin(),
251            ("acos", &[x]) => x.acos(),
252            ("atan", &[x]) => x.atan(),
253            ("atan2", &[y, x]) => y.atan2(x),
254            ("sinh", &[x]) => x.sinh(),
255            ("cosh", &[x]) => x.cosh(),
256            ("tanh", &[x]) => x.tanh(),
257            ("asinh", &[x]) => x.asinh(),
258            ("acosh", &[x]) => x.acosh(),
259            ("atanh", &[x]) => x.atanh(),
260            ("hypot", &[x, y]) => x.hypot(y),
261            ("min", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.min(*b))),
262            ("max", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.max(*b))),
263            ("clamp", &[x, low, high]) if low <= high => Ok(x.clamp(low, high)),
264            ("clamp", &[_, _, _]) => Err(ErrorKind::Domain),
265            _ if FUNCTIONS.contains(&name) => Err(ErrorKind::ArgumentCount),
266            _ => return None,
267        };
268        Some(result)
269    }
270}
271
272/// The functions of [`Math`] whose results are irrational for most arguments, which a [`Fraction`] does not compute,
273/// with the number of arguments they take.
274#[cfg(feature = "fraction")]
275const INEXACT: [(&str, usize); 21] = [
276    ("exp", 1),
277    ("ln", 1),
278    ("log2", 1),
279    ("log10", 1),
280    ("log", 1),
281    ("log", 2),
282    ("sin", 1),
283    ("cos", 1),
284    ("tan", 1),
285    ("asin", 1),
286    ("acos", 1),
287    ("atan", 1),
288    ("atan2", 2),
289    ("sinh", 1),
290    ("cosh", 1),
291    ("tanh", 1),
292    ("asinh", 1),
293    ("acosh", 1),
294    ("atanh", 1),
295    ("sqrt", 1),
296    ("cbrt", 1),
297];
298
299#[cfg(feature = "fraction")]
300impl Context<Fraction> for Math {
301    fn call(&self, name: &str, arguments: &[Fraction]) -> Option<Result<Fraction, ErrorKind>> {
302        let result = match (name, arguments) {
303            ("abs", &[x]) => Ok(x.abs()),
304            ("sign", &[x]) => Ok(x.sign()),
305            ("floor", &[x]) => Ok(x.floor()),
306            ("ceil", &[x]) => Ok(x.ceil()),
307            ("round", &[x]) => Ok(x.round()),
308            ("trunc", &[x]) => Ok(x.trunc()),
309            ("pow", &[x, y]) => x.power(y),
310            ("fact", &[x]) => x.factorial(),
311            ("sqrt", &[x]) => x.root(2),
312            ("cbrt", &[x]) => x.root(3),
313            ("hypot", &[x, y]) => x
314                .multiply(x)
315                .and_then(|square| square.add(y.multiply(y)?))
316                .and_then(|sum| sum.root(2)),
317            ("min", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.min(*b))),
318            ("max", &[first, ref rest @ ..]) => Ok(rest.iter().fold(first, |a, b| a.max(*b))),
319            ("clamp", &[x, low, high]) if low <= high => Ok(x.clamp(low, high)),
320            ("clamp", &[_, _, _]) => Err(ErrorKind::Domain),
321            _ if INEXACT.contains(&(name, arguments.len())) => Err(ErrorKind::Inexact),
322            _ if FUNCTIONS.contains(&name) => Err(ErrorKind::ArgumentCount),
323            _ => return None,
324        };
325        Some(result)
326    }
327}
328
329#[cfg(feature = "exact")]
330impl<const DIGITS: u32, const MAX_EXPONENT: i32> Context<Exact<DIGITS, MAX_EXPONENT>> for Math {
331    fn variable(&self, name: &str) -> Option<Exact<DIGITS, MAX_EXPONENT>> {
332        Context::<Decimal<DIGITS, MAX_EXPONENT>>::variable(self, name).map(Exact::from)
333    }
334
335    fn call(
336        &self,
337        name: &str,
338        arguments: &[Exact<DIGITS, MAX_EXPONENT>],
339    ) -> Option<Result<Exact<DIGITS, MAX_EXPONENT>, ErrorKind>> {
340        let pick = |first, rest: &[Exact<DIGITS, MAX_EXPONENT>], keep: Ordering| {
341            rest.iter().fold(first, |chosen, candidate| {
342                if candidate.cmp_value(chosen) == keep {
343                    *candidate
344                } else {
345                    chosen
346                }
347            })
348        };
349        match (name, arguments) {
350            ("min", &[first, ref rest @ ..]) => return Some(Ok(pick(first, rest, Ordering::Less))),
351            ("max", &[first, ref rest @ ..]) => {
352                return Some(Ok(pick(first, rest, Ordering::Greater)));
353            }
354            ("clamp", &[x, low, high]) => {
355                return Some(
356                    match (low.cmp_value(high), x.cmp_value(low), x.cmp_value(high)) {
357                        (Ordering::Greater, _, _) => Err(ErrorKind::Domain),
358                        (_, Ordering::Less, _) => Ok(low),
359                        (_, _, Ordering::Greater) => Ok(high),
360                        _ => Ok(x),
361                    },
362                );
363            }
364            _ => {}
365        }
366        if arguments.len() > 3 {
367            return FUNCTIONS
368                .contains(&name)
369                .then_some(Err(ErrorKind::ArgumentCount));
370        }
371        let mut fractions = [Fraction::ZERO; 3];
372        let mut exact = true;
373        for (slot, argument) in fractions.iter_mut().zip(arguments) {
374            match argument.as_fraction() {
375                Some(fraction) => *slot = fraction,
376                None => exact = false,
377            }
378        }
379        if exact {
380            match Context::<Fraction>::call(self, name, &fractions[..arguments.len()]) {
381                Some(Err(ErrorKind::Overflow | ErrorKind::Inexact)) | None => {}
382                result => return result.map(|result| result.map(Exact::from)),
383            }
384        }
385        let mut decimals = [Decimal::ZERO; 3];
386        for (slot, argument) in decimals.iter_mut().zip(arguments) {
387            *slot = argument.to_decimal();
388        }
389        Context::<Decimal<DIGITS, MAX_EXPONENT>>::call(self, name, &decimals[..arguments.len()])
390            .map(|result| result.map(Exact::from))
391    }
392}