use super::MathEvalErrorKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Arity {
Exact(usize),
#[allow(dead_code)] AtLeast(usize),
}
impl Arity {
pub(crate) fn accepts(self, n: usize) -> bool {
match self {
Arity::Exact(k) => n == k,
Arity::AtLeast(k) => n >= k,
}
}
}
impl std::fmt::Display for Arity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Arity::Exact(1) => f.write_str("1 argument"),
Arity::Exact(n) => write!(f, "{n} arguments"),
Arity::AtLeast(n) => write!(f, "at least {n} arguments"),
}
}
}
pub(crate) struct FunctionDef {
pub(crate) name: &'static str,
pub(crate) arity: Arity,
pub(crate) eval: fn(&[f64]) -> Result<f64, MathEvalErrorKind>,
pub(crate) doc: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Assoc {
Left,
Right,
}
pub(crate) struct BinaryOpDef {
pub(crate) symbol: char,
pub(crate) precedence: u16,
pub(crate) assoc: Assoc,
pub(crate) eval: fn(f64, f64) -> Result<f64, MathEvalErrorKind>,
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) doc: &'static str,
}
pub(crate) struct UnaryOpDef {
pub(crate) symbol: char,
pub(crate) precedence: u16,
pub(crate) eval: fn(f64) -> f64,
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) doc: &'static str,
}
pub(crate) struct ConstantDef {
pub(crate) name: &'static str,
pub(crate) value: f64,
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) doc: &'static str,
}
const PREC_ADD: u16 = 500;
const PREC_MUL: u16 = 1000;
const PREC_UNARY: u16 = 5000;
const PREC_POW: u16 = 10000;
pub(crate) fn java_pow(x: f64, y: f64) -> f64 {
if y == 0.0 {
1.0
} else if y.is_nan() || (x.abs() == 1.0 && y.is_infinite()) {
f64::NAN
} else {
x.powf(y)
}
}
fn checked_divisor(b: f64) -> Result<(), MathEvalErrorKind> {
if b == 0.0 {
Err(MathEvalErrorKind::DivisionByZero)
} else {
Ok(())
}
}
pub(crate) static BINARY_OPS: &[BinaryOpDef] = &[
BinaryOpDef {
symbol: '+',
precedence: PREC_ADD,
assoc: Assoc::Left,
eval: |a, b| Ok(a + b),
doc: "addition",
},
BinaryOpDef {
symbol: '-',
precedence: PREC_ADD,
assoc: Assoc::Left,
eval: |a, b| Ok(a - b),
doc: "subtraction",
},
BinaryOpDef {
symbol: '*',
precedence: PREC_MUL,
assoc: Assoc::Left,
eval: |a, b| Ok(a * b),
doc: "multiplication",
},
BinaryOpDef {
symbol: '/',
precedence: PREC_MUL,
assoc: Assoc::Left,
eval: |a, b| checked_divisor(b).map(|()| a / b),
doc: "division; a zero divisor is an error",
},
BinaryOpDef {
symbol: '%',
precedence: PREC_MUL,
assoc: Assoc::Left,
eval: |a, b| checked_divisor(b).map(|()| a % b),
doc: "remainder with the sign of the dividend (Java `%`); a zero divisor is an error",
},
BinaryOpDef {
symbol: '^',
precedence: PREC_POW,
assoc: Assoc::Right,
eval: |a, b| Ok(java_pow(a, b)),
doc: "power (right-associative)",
},
];
pub(crate) static UNARY_OPS: &[UnaryOpDef] = &[
UnaryOpDef {
symbol: '-',
precedence: PREC_UNARY,
eval: |a| -a,
doc: "negation",
},
UnaryOpDef {
symbol: '+',
precedence: PREC_UNARY,
eval: |a| a,
doc: "identity",
},
];
#[allow(clippy::excessive_precision)]
pub(crate) static CONSTANTS: &[ConstantDef] = &[
ConstantDef {
name: "pi",
value: std::f64::consts::PI,
doc: "Ï€",
},
ConstantDef {
name: "Ï€",
value: std::f64::consts::PI,
doc: "Ï€",
},
ConstantDef {
name: "e",
value: std::f64::consts::E,
doc: "Euler's number",
},
ConstantDef {
name: "φ",
value: 1.61803398874,
doc: "golden ratio (exp4j's 12-digit value)",
},
];
fn signum(x: f64) -> f64 {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
fn cot(x: f64) -> Result<f64, MathEvalErrorKind> {
let t = x.tan();
if t == 0.0 {
Err(MathEvalErrorKind::CotangentDivisionByZero)
} else {
Ok(1.0 / t)
}
}
macro_rules! unary_fn {
($name:literal, $f:expr_2021, $doc:literal) => {
FunctionDef {
name: $name,
arity: Arity::Exact(1),
eval: |a| Ok(($f)(arg(a, 0))),
doc: $doc,
}
};
}
fn arg(args: &[f64], i: usize) -> f64 {
args.get(i).copied().unwrap_or(f64::NAN)
}
pub(crate) static FUNCTIONS: &[FunctionDef] = &[
unary_fn!("abs", f64::abs, "absolute value"),
unary_fn!("acos", f64::acos, "arc cosine (radians)"),
unary_fn!("asin", f64::asin, "arc sine (radians)"),
unary_fn!("atan", f64::atan, "arc tangent (radians)"),
unary_fn!("cbrt", f64::cbrt, "cube root"),
unary_fn!("ceil", f64::ceil, "round up to an integer"),
unary_fn!("cos", f64::cos, "cosine (radians)"),
unary_fn!("cosh", f64::cosh, "hyperbolic cosine"),
FunctionDef {
name: "cot",
arity: Arity::Exact(1),
eval: |a| cot(arg(a, 0)),
doc: "cotangent; an error where tan(x) = 0",
},
unary_fn!("exp", f64::exp, "e raised to x"),
unary_fn!("expm1", f64::exp_m1, "exp(x) - 1"),
unary_fn!("floor", f64::floor, "round down to an integer"),
unary_fn!("log", f64::ln, "natural logarithm"),
unary_fn!("log10", f64::log10, "base-10 logarithm"),
unary_fn!("log1p", f64::ln_1p, "log(1 + x)"),
unary_fn!("log2", |x: f64| x.ln() / 2f64.ln(), "base-2 logarithm"),
FunctionDef {
name: "pow",
arity: Arity::Exact(2),
eval: |a| Ok(java_pow(arg(a, 0), arg(a, 1))),
doc: "x raised to y",
},
unary_fn!("signum", signum, "sign: -1, 0 or 1"),
unary_fn!("sin", f64::sin, "sine (radians)"),
unary_fn!("sinh", f64::sinh, "hyperbolic sine"),
unary_fn!("sqrt", f64::sqrt, "square root"),
unary_fn!("tan", f64::tan, "tangent (radians)"),
unary_fn!("tanh", f64::tanh, "hyperbolic tangent"),
];
pub(crate) fn function(name: &str) -> Option<&'static FunctionDef> {
FUNCTIONS.iter().find(|f| f.name == name)
}
pub(crate) fn binary_op(symbol: char) -> Option<&'static BinaryOpDef> {
BINARY_OPS.iter().find(|o| o.symbol == symbol)
}
pub(crate) fn unary_op(symbol: char) -> Option<&'static UnaryOpDef> {
UNARY_OPS.iter().find(|o| o.symbol == symbol)
}
pub(crate) fn constant(name: &str) -> Option<&'static ConstantDef> {
CONSTANTS.iter().find(|c| c.name == name)
}
#[cfg(test)]
pub(crate) fn reference_rows() -> Vec<String> {
let mut rows = Vec::new();
for op in BINARY_OPS {
let assoc = match op.assoc {
Assoc::Left => "left",
Assoc::Right => "right",
};
rows.push(format!(
"| `a {} b` | {} | {assoc} | {} |",
op.symbol, op.precedence, op.doc
));
}
for op in UNARY_OPS {
rows.push(format!(
"| `{}a` | {} | right | {} |",
op.symbol, op.precedence, op.doc
));
}
for f in FUNCTIONS {
let args = match f.arity {
Arity::Exact(1) => "x".to_string(),
Arity::Exact(2) => "x, y".to_string(),
Arity::Exact(n) => format!("{n} arguments"),
Arity::AtLeast(n) => format!("x1, ..., x{n}, ..."),
};
rows.push(format!("| `{}({args})` | {} |", f.name, f.doc));
}
for c in CONSTANTS {
rows.push(format!("| `{}` | {} | {} |", c.name, c.value, c.doc));
}
rows
}