expy 0.0.2

Embeddable & extensible expression evaluator
Documentation
//! Built-in functions.

use std::iter;

use strum::{EnumIter, EnumProperty, IntoEnumIterator, IntoStaticStr};


#[derive(Clone, Copy, Debug, EnumIter, EnumProperty, Eq, Hash, IntoStaticStr, PartialEq)]
#[cfg_attr(serde, derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(serde, serde(rename_all = "snake_case"))]
#[strum(serialize_all = "lowercase")]
pub enum Function {
    /// Identity function.
    #[cfg_attr(serde, serde(rename = "id"))]
    #[strum(serialize = "id")]
    Identity,

    /// Cast a value to boolean.
    #[cfg_attr(serde, serde(rename = "bool"))]
    #[strum(serialize = "bool")]
    ToBool,

    /// Cast a value to integer.
    #[cfg_attr(serde, serde(rename = "int"))]
    #[strum(serialize = "int")]
    #[strum(props(alias = "integer"))]
    ToInteger,

    /// Cast a value to float.
    #[cfg_attr(serde, serde(rename = "float"))]
    #[strum(serialize = "float")]
    ToFloat,

    /// Absolute value.
    Abs,

    /// Fractional part of a number.
    #[strum(props(alias = "fract"))]
    Frac,

    /// Truncate fractional part.
    Trunc,

    /// Round down.
    Floor,

    /// Round up.
    Ceil,

    /// Round to nearest.
    Round,

    /// Square root.
    #[cfg_attr(serde, serde(rename = "sqrt"))]
    #[strum(serialize = "sqrt")]
    SquareRoot,

    /// Cube root.
    #[cfg_attr(serde, serde(rename = "cbrt"))]
    #[strum(serialize = "cbrt")]
    CubeRoot,

    /// Exponential function.
    Exp,

    /// Natural (base `e`) logarithm.
    Ln,

    /// Base 2 logarithm.
    #[cfg_attr(serde, serde(rename = "log2"))]
    #[strum(serialize = "log2")]
    Log2,

    /// Base 10 logarithm.
    #[cfg_attr(serde, serde(rename = "log10"))]
    #[strum(serialize = "log10")]
    Log10,

    /// Sine function.
    #[cfg_attr(serde, serde(rename = "sin"))]
    #[strum(serialize = "sin")]
    Sine,

    /// Cosine function.
    #[cfg_attr(serde, serde(rename = "cos"))]
    #[strum(serialize = "cos")]
    Cosine,

    /// Tangent function.
    #[cfg_attr(serde, serde(rename = "tan"))]
    #[strum(serialize = "tan")]
    Tangent,

    /// Random float from the 0..1 range.
    #[cfg(rng)]
    Rand,

    /// Vector length.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "len"))]
    #[strum(serialize = "len")]
    #[strum(props(aliases = "length, mag, magnitude"))]
    Length,

    /// Normalize the vector if it's nonzero, or return a zero vector.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "normalize"))]
    #[strum(serialize = "normalize")]
    #[strum(props(aliases = "normalize_or_zero"))]
    NormalizeOrZero,

    /// Vector length squared.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "len_sq"))]
    #[strum(serialize = "len_sq")]
    #[strum(props(aliases = "len2, length_sq, length2, mag_sq, mag2, magnitude_sq, magnitude2"))]
    LengthSquared,

    /// Distance between two vectors.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "dist"))]
    #[strum(serialize = "dist")]
    #[strum(props(alias = "distance"))]
    Distance,

    /// Distance between two vectors squared.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "dist_sq"))]
    #[strum(serialize = "dist_sq")]
    #[strum(props(alias = "dist2, distance2, distance_sq"))]
    DistanceSquared,

    /// Dot product of two vectors.
    #[cfg(glam)]
    #[cfg_attr(serde, serde(rename = "dot"))]
    #[strum(serialize = "dot")]
    DotProduct,
}

impl TryFrom<&str> for Function {
    type Error = strum::ParseError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        Self::iter()
            .find(|f| {
                // Using .any() w/ comparison instead of Itertools::contains() because the latter
                // does some weird Borrow-related tricks that only work if aliases() returns
                // non-'static &str items.
                Into::<&str>::into(f) == s || f.aliases().any(|a| a == s)
            })
            .ok_or(strum::ParseError::VariantNotFound)
    }
}

impl TryFrom<String> for Function {
    type Error = strum::ParseError;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        Self::try_from(s.as_str())
    }
}

impl Function {
    /// Canonical name of the function.
    pub fn name(&self) -> &'static str {
        self.into()
    }

    /// Aliases with which the function can also be invoked.
    pub fn aliases(&self) -> impl Iterator<Item=&'static str> + '_ {
        // TODO: cache this as HashMap<String, Vec<&str>> perhaps?
        ["alias", "aliases"].iter()
            .filter_map(|prop| self.get_str(prop))
            .flat_map(|aliases| aliases.split(','))
            .map(str::trim)
    }

    /// All the names that the function is known under.
    pub fn names(&self) -> impl Iterator<Item=&'static str> + '_ {
        iter::once(self.name()).chain(self.aliases())
    }
}


#[cfg(test)]
mod checks {
    use itertools::{Itertools, Position};
    use super::*;

    fn is_valid_identifier(s: &str) -> bool {
        !s.is_empty() && s.chars().with_position().all(|(pos, c)| {
            match pos {
                Position::First | Position::Only => c.is_ascii_alphabetic() || c == '_',
                _ => c.is_ascii_alphanumeric() || c == '_',
            }
        })
    }

    #[test]
    fn names_are_valid() {
        for func in Function::iter() {
            assert!(
                is_valid_identifier(func.name()),
                "{:?} has an invalid name: `{}`", func, func.name());
        }
    }

    #[test]
    fn aliases_are_valid() {
        for func in Function::iter() {
            for alias in func.aliases() {
                assert!(
                    is_valid_identifier(alias), "{:?} has an invalid alias: `{}`", func, alias);
            }
        }
    }

    #[test]
    fn aliases_are_unique() {
        for func in Function::iter() {
            assert!(
                func.aliases().all_unique(),
                "{:?} has non-unique aliases: {}", func, func.aliases().duplicates().format(", "));
        }
    }

    #[test]
    fn aliases_are_distinct_from_name() {
        for func in Function::iter() {
            for alias in func.aliases() {
                assert_ne!(
                    alias, func.name(), "`{}` is already the name of function {:?}", alias, func);
            }
        }
    }
}