use std::cmp::Ordering;
use bigdecimal::{FromPrimitive, RoundingMode, Signed, ToPrimitive};
use num_bigint::BigInt;
use crate::error::{DogeError, DogeResult};
use crate::value::Value;
fn numeric(fname: &str, v: &Value) -> DogeResult<f64> {
let f = match v {
Value::Int(n) => n.to_f64(),
Value::Float(f) => Some(*f),
Value::Decimal(d) => d.to_f64(),
_ => {
return Err(DogeError::type_error(format!(
"nerd.{fname} needs a number, got {}",
v.describe()
)))
}
};
f.ok_or_else(|| DogeError::overflow(format!("{v} is too large for nerd.{fname}")))
}
fn ensure_number(fname: &str, v: &Value) -> DogeResult<()> {
if matches!(v, Value::Int(_) | Value::Float(_) | Value::Decimal(_)) {
Ok(())
} else {
Err(DogeError::type_error(format!(
"nerd.{fname} needs a number, got {}",
v.describe()
)))
}
}
fn float_to_int(f: f64) -> DogeResult {
if f.is_finite() {
BigInt::from_f64(f)
.map(Value::Int)
.ok_or_else(|| DogeError::overflow("the result is outside the Int range"))
} else {
Err(DogeError::overflow("the result is outside the Int range"))
}
}
fn round_like(
fname: &str,
x: &Value,
float_op: impl Fn(f64) -> f64,
mode: RoundingMode,
) -> DogeResult {
match x {
Value::Int(n) => Ok(Value::Int(n.clone())),
Value::Float(f) => float_to_int(float_op(*f)),
Value::Decimal(d) => {
let (digits, _) = d.with_scale_round(0, mode).into_bigint_and_exponent();
Ok(Value::Int(digits))
}
_ => Err(DogeError::type_error(format!(
"nerd.{fname} needs a number, got {}",
x.describe()
))),
}
}
pub fn nerd_abs(x: &Value) -> DogeResult {
match x {
Value::Int(n) => Ok(Value::Int(n.abs())),
Value::Float(f) => Ok(Value::Float(f.abs())),
Value::Decimal(d) => Ok(Value::decimal(d.abs())),
_ => Err(DogeError::type_error(format!(
"nerd.abs needs a number, got {}",
x.describe()
))),
}
}
pub fn nerd_sqrt(x: &Value) -> DogeResult {
let f = numeric("sqrt", x)?;
if f < 0.0 {
return Err(DogeError::value_error("cannot sqrt a negative number"));
}
Ok(Value::Float(f.sqrt()))
}
pub fn nerd_floor(x: &Value) -> DogeResult {
round_like("floor", x, f64::floor, RoundingMode::Floor)
}
pub fn nerd_ceil(x: &Value) -> DogeResult {
round_like("ceil", x, f64::ceil, RoundingMode::Ceiling)
}
pub fn nerd_round(x: &Value) -> DogeResult {
round_like("round", x, f64::round, RoundingMode::HalfUp)
}
pub fn nerd_min(a: &Value, b: &Value) -> DogeResult {
ensure_number("min", a)?;
ensure_number("min", b)?;
Ok(if crate::ops::order(a, b)? == Ordering::Greater {
b.clone()
} else {
a.clone()
})
}
pub fn nerd_max(a: &Value, b: &Value) -> DogeResult {
ensure_number("max", a)?;
ensure_number("max", b)?;
Ok(if crate::ops::order(a, b)? == Ordering::Less {
b.clone()
} else {
a.clone()
})
}
pub fn nerd_pow(a: &Value, b: &Value) -> DogeResult {
crate::ops::pow(a.clone(), b.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
use crate::ops::values_equal;
fn dec(s: &str) -> Value {
Value::decimal(s.parse().unwrap())
}
#[test]
fn abs_keeps_type_and_never_overflows() {
assert!(values_equal(
&nerd_abs(&Value::int(-5)).unwrap(),
&Value::int(5)
));
assert!(matches!(nerd_abs(&Value::Float(-2.5)).unwrap(), Value::Float(f) if f == 2.5));
assert!(values_equal(&nerd_abs(&dec("-2.5")).unwrap(), &dec("2.5")));
let expected = Value::Int(-BigInt::from(i64::MIN));
assert!(values_equal(
&nerd_abs(&Value::int(i64::MIN)).unwrap(),
&expected
));
assert_eq!(
nerd_abs(&Value::str("x")).unwrap_err().kind,
ErrorKind::TypeError
);
}
#[test]
fn sqrt_is_float_and_rejects_negatives() {
assert!(matches!(nerd_sqrt(&Value::int(16)).unwrap(), Value::Float(f) if f == 4.0));
assert_eq!(
nerd_sqrt(&Value::int(-1)).unwrap_err().kind,
ErrorKind::ValueError
);
}
#[test]
fn floor_ceil_round_yield_ints() {
assert!(values_equal(
&nerd_floor(&Value::Float(2.9)).unwrap(),
&Value::int(2)
));
assert!(values_equal(
&nerd_ceil(&Value::Float(2.1)).unwrap(),
&Value::int(3)
));
assert!(values_equal(
&nerd_round(&Value::Float(2.5)).unwrap(),
&Value::int(3)
));
assert!(values_equal(
&nerd_floor(&Value::int(7)).unwrap(),
&Value::int(7)
));
assert!(values_equal(
&nerd_floor(&dec("2.9")).unwrap(),
&Value::int(2)
));
assert!(values_equal(
&nerd_ceil(&dec("-2.9")).unwrap(),
&Value::int(-2)
));
assert!(values_equal(
&nerd_round(&dec("2.5")).unwrap(),
&Value::int(3)
));
}
#[test]
fn round_of_a_non_finite_float_is_overflow() {
assert_eq!(
nerd_floor(&Value::Float(f64::INFINITY)).unwrap_err().kind,
ErrorKind::Overflow
);
}
#[test]
fn min_max_keep_the_winners_type() {
assert!(
matches!(nerd_min(&Value::int(3), &Value::Float(2.5)).unwrap(), Value::Float(f) if f == 2.5)
);
assert!(values_equal(
&nerd_max(&Value::int(3), &Value::Float(2.5)).unwrap(),
&Value::int(3)
));
assert!(values_equal(
&nerd_min(&Value::int(3), &dec("1.5")).unwrap(),
&dec("1.5")
));
assert!(values_equal(
&nerd_min(&Value::int(4), &Value::Float(4.0)).unwrap(),
&Value::int(4)
));
assert_eq!(
nerd_min(&Value::str("a"), &Value::str("b"))
.unwrap_err()
.kind,
ErrorKind::TypeError
);
}
#[test]
fn pow_is_int_for_int_base_and_grows_past_i64() {
assert!(values_equal(
&nerd_pow(&Value::int(2), &Value::int(10)).unwrap(),
&Value::int(1024)
));
assert!(matches!(
nerd_pow(&Value::int(2), &Value::Float(0.5)).unwrap(),
Value::Float(_)
));
let expected = Value::Int(BigInt::from(10).pow(100u32));
assert!(values_equal(
&nerd_pow(&Value::int(10), &Value::int(100)).unwrap(),
&expected
));
}
}