use derive_more::{From, TryInto};
use doc_comment::doc_comment;
use paste::paste;
use std::{
cmp::Ordering,
hash::{Hash, Hasher},
};
use strum::{Display, EnumDiscriminants, EnumString};
#[derive(Copy, Clone, Debug, From, EnumDiscriminants, TryInto)]
#[cfg_attr(feature = "serde-1", derive(serde::Serialize, serde::Deserialize))]
#[try_into(owned, ref, ref_mut)]
#[strum_discriminants(derive(Display, EnumString))]
#[strum_discriminants(name(NumberType), strum(serialize_all = "snake_case"))]
#[cfg_attr(
feature = "serde-1",
strum_discriminants(derive(serde::Serialize, serde::Deserialize))
)]
pub enum Number {
F32(f32),
F64(f64),
I128(i128),
I16(i16),
I32(i32),
I64(i64),
I8(i8),
Isize(isize),
U128(u128),
U16(u16),
U32(u32),
U64(u64),
U8(u8),
Usize(usize),
}
macro_rules! impl_cast {
($type:ty) => {
paste! {
doc_comment! {
concat!(
"Naive casting of number's inner representation to ",
stringify!($type),
"by performing `x as ", stringify!($type), "`",
),
#[inline]
pub fn [<to_ $type>](&self) -> $type {
match self {
Self::F32(x) => *x as $type,
Self::F64(x) => *x as $type,
Self::I128(x) => *x as $type,
Self::I16(x) => *x as $type,
Self::I32(x) => *x as $type,
Self::I64(x) => *x as $type,
Self::I8(x) => *x as $type,
Self::Isize(x) => *x as $type,
Self::U128(x) => *x as $type,
Self::U16(x) => *x as $type,
Self::U32(x) => *x as $type,
Self::U64(x) => *x as $type,
Self::U8(x) => *x as $type,
Self::Usize(x) => *x as $type,
}
}
}
}
};
}
impl Number {
pub fn sign(&self) -> NumberSign {
if self.is_negative() {
NumberSign::Negative
} else if self.is_positive() {
NumberSign::Positive
} else {
NumberSign::Zero
}
}
#[inline]
pub fn is_zero(&self) -> bool {
match self {
Self::F32(x) => *x == 0.0,
Self::F64(x) => *x == 0.0,
Self::I128(x) => *x == 0,
Self::I16(x) => *x == 0,
Self::I32(x) => *x == 0,
Self::I64(x) => *x == 0,
Self::I8(x) => *x == 0,
Self::Isize(x) => *x == 0,
Self::U128(x) => *x == 0,
Self::U16(x) => *x == 0,
Self::U32(x) => *x == 0,
Self::U64(x) => *x == 0,
Self::U8(x) => *x == 0,
Self::Usize(x) => *x == 0,
}
}
#[inline]
pub fn is_positive(&self) -> bool {
!self.is_zero() && !self.is_negative()
}
#[inline]
pub fn is_negative(&self) -> bool {
match self {
Self::F32(x) => x.is_normal() && x.is_sign_negative(),
Self::F64(x) => x.is_normal() && x.is_sign_negative(),
Self::I128(x) => x.is_negative(),
Self::I16(x) => x.is_negative(),
Self::I32(x) => x.is_negative(),
Self::I64(x) => x.is_negative(),
Self::I8(x) => x.is_negative(),
Self::Isize(x) => x.is_negative(),
_ => false,
}
}
#[inline]
pub fn is_signed(&self) -> bool {
matches!(
self,
Self::I128(_)
| Self::I16(_)
| Self::I32(_)
| Self::I64(_)
| Self::I8(_)
| Self::Isize(_)
)
}
#[inline]
pub fn is_unsigned(&self) -> bool {
matches!(
self,
Self::U128(_)
| Self::U16(_)
| Self::U32(_)
| Self::U64(_)
| Self::U8(_)
| Self::Usize(_)
)
}
#[inline]
pub fn is_float(&self) -> bool {
matches!(self, Self::F32(_) | Self::F64(_))
}
pub fn has_nonzero_fraction(&self) -> bool {
match self {
Self::F32(x) => x.fract().is_normal(),
Self::F64(x) => x.fract().is_normal(),
_ => false,
}
}
#[inline]
pub fn is_normal(&self) -> bool {
match self {
Self::F32(x) => x.is_normal(),
Self::F64(x) => x.is_normal(),
_ => !self.is_zero(),
}
}
#[inline]
pub fn to_absolute(&self) -> Self {
match self {
Self::F32(x) => Self::from(x.abs()),
Self::F64(x) => Self::from(x.abs()),
Self::I128(x) => Self::from(x.abs()),
Self::I16(x) => Self::from(x.abs()),
Self::I32(x) => Self::from(x.abs()),
Self::I64(x) => Self::from(x.abs()),
Self::I8(x) => Self::from(x.abs()),
Self::Isize(x) => Self::from(x.abs()),
Self::U128(x) => Self::from(*x),
Self::U16(x) => Self::from(*x),
Self::U32(x) => Self::from(*x),
Self::U64(x) => Self::from(*x),
Self::U8(x) => Self::from(*x),
Self::Usize(x) => Self::from(*x),
}
}
impl_cast!(f64);
impl_cast!(f32);
impl_cast!(isize);
impl_cast!(i128);
impl_cast!(i64);
impl_cast!(i32);
impl_cast!(i16);
impl_cast!(i8);
impl_cast!(usize);
impl_cast!(u128);
impl_cast!(u64);
impl_cast!(u32);
impl_cast!(u16);
impl_cast!(u8);
#[inline]
pub fn to_type(&self) -> NumberType {
self.into()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde-1", derive(serde::Serialize, serde::Deserialize))]
pub enum NumberSign {
Positive,
Negative,
Zero,
}
impl NumberSign {
pub fn to_i8(&self) -> i8 {
match self {
Self::Positive => 1,
Self::Negative => -1,
Self::Zero => 0,
}
}
}
impl Hash for Number {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::F32(x) => x.to_string().hash(state),
Self::F64(x) => x.to_string().hash(state),
Self::I128(x) => x.hash(state),
Self::I16(x) => x.hash(state),
Self::I32(x) => x.hash(state),
Self::I64(x) => x.hash(state),
Self::I8(x) => x.hash(state),
Self::Isize(x) => x.hash(state),
Self::U128(x) => x.hash(state),
Self::U16(x) => x.hash(state),
Self::U32(x) => x.hash(state),
Self::U64(x) => x.hash(state),
Self::U8(x) => x.hash(state),
Self::Usize(x) => x.hash(state),
}
}
}
impl Eq for Number {}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
match self.partial_cmp(other) {
Some(o) => match o {
Ordering::Equal => true,
Ordering::Greater | Ordering::Less => false,
},
None => false,
}
}
}
impl PartialOrd for Number {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if !self.is_normal() && !self.is_zero() || !other.is_normal() && !other.is_zero() {
return None;
}
match (self.sign(), other.sign()) {
(NumberSign::Negative, NumberSign::Negative) => Some(
other
.to_absolute()
.to_u128()
.cmp(&self.to_absolute().to_u128()),
),
(NumberSign::Negative, NumberSign::Positive) => Some(Ordering::Less),
(NumberSign::Negative, NumberSign::Zero) => Some(Ordering::Less),
(NumberSign::Positive, NumberSign::Negative) => Some(Ordering::Greater),
(NumberSign::Positive, NumberSign::Positive) => {
Some(self.to_u128().cmp(&other.to_u128()))
}
(NumberSign::Positive, NumberSign::Zero) => Some(Ordering::Greater),
(NumberSign::Zero, NumberSign::Negative) => Some(Ordering::Greater),
(NumberSign::Zero, NumberSign::Positive) => Some(Ordering::Less),
(NumberSign::Zero, NumberSign::Zero) => Some(Ordering::Equal),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
macro_rules! check {
(eq $value:expr, $type:ty; $fname:ident, $x:literal) => {{
let number = Number::from(($x) as $type);
let result = number.clone().$fname();
assert_eq!(
result, $value,
"Calling {} on {:?}, expected {:?}, but got {:?}",
stringify!($fname), number, $value, result,
);
}};
(not eq $value:expr, $type:ty; $fname:ident, $x:literal) => {
let number = Number::from(($x) as $type);
assert_ne!(
number.clone().$fname(), $value,
"Calling {} on {:?}, unexpectedly got {:?}",
stringify!($fname), number, $value,
);
};
($type:ty; $fname:ident, $x:literal) => {
check!(eq true, $type; $fname, $x);
};
(not $type:ty; $fname:ident, $x:literal) => {
check!(eq false, $type; $fname, $x);
};
($type:ty; $fname:ident, $x:literal, $($y:literal),+) => {{
check!($type; $fname, $x);
check!($type; $fname, $($y),+);
}};
(not $type:ty; $fname:ident, $x:literal, $($y:literal),+) => {{
check!(not $type; $fname, $x);
check!(not $type; $fname, $($y),+);
}};
(eq $value:expr, $type:ty; $fname:ident, $x:literal, $($y:literal),+) => {{
check!(eq $value, $type; $fname, $x);
check!(eq $value, $type; $fname, $($y),+);
}};
(not eq $value:expr, $type:ty; $fname:ident, $x:literal, $($y:literal),+) => {{
check!(not eq $value, $type; $fname, $x);
check!(not eq $value, $type; $fname, $($y),+);
}};
($type:ty; $fname:ident, $(eq $value:expr, )?$($x:literal),+) => {{
check!($(eq $value, )?$type; $fname, $($x),+);
}};
($a:ty, $($b:ty),+; $fname:ident, $(eq $value:expr, )?$($x:literal),+) => {{
check!($a; $fname, $(eq $value, )?$($x),+);
check!($($b),+; $fname, $(eq $value, )?$($x),+);
}};
($type:ty; $fname:ident, $(eq $value:expr, )?not $($x:literal),+) => {{
check!(not $(eq $value, )?$type; $fname, $($x),+);
}};
($a:ty, $($b:ty),+; $fname:ident, $(eq $value:expr, )?not $($x:literal),+) => {{
check!($a; $fname, $(eq $value, )?not $($x),+);
check!($($b),+; $fname, $(eq $value, )?not $($x),+);
}};
($type:ty; $fname:ident, $(eq $value:expr, )?$($x:literal),+, not $($y:literal),+) => {{
check!($type; $fname, $(eq $value, )?$($x),+);
check!($type; $fname, $(eq $value, )?not $($y),+);
}};
($a:ty, $($b:ty),+; $fname:ident, $(eq $value:expr, )?$($x:literal),+, not $($y:literal),+) => {{
check!($a; $fname, $(eq $value, )?$($x),+, not $($y),+);
check!($($b),+; $fname, $(eq $value, )?$($x),+, not $($y),+);
}};
}
#[test]
fn partial_cmp_should_return_none_if_either_number_is_nan() {
let a = Number::from(1);
let b = Number::from(f32::NAN);
assert_eq!(a.partial_cmp(&b), None);
let a = Number::from(f32::NAN);
let b = Number::from(1);
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
fn partial_cmp_should_return_none_if_either_number_is_infinite() {
let a = Number::from(1);
let b = Number::from(f32::INFINITY);
assert_eq!(a.partial_cmp(&b), None);
let a = Number::from(f32::INFINITY);
let b = Number::from(1);
assert_eq!(a.partial_cmp(&b), None);
let a = Number::from(1);
let b = Number::from(f32::NEG_INFINITY);
assert_eq!(a.partial_cmp(&b), None);
let a = Number::from(f32::NEG_INFINITY);
let b = Number::from(1);
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
fn partial_cmp_should_return_none_if_either_number_is_subnormal() {
let a = Number::from(1);
let b = Number::from(1.0e-40_f32);
assert_eq!(a.partial_cmp(&b), None);
let a = Number::from(1.0e-40_f32);
let b = Number::from(1);
assert_eq!(a.partial_cmp(&b), None);
}
#[test]
fn partial_cmp_should_perform_cmp_if_both_numbers_negative() {
let a = Number::from(-2isize);
let b = Number::from(-1i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
let a = Number::from(-1isize);
let b = Number::from(-1i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
let a = Number::from(-1isize);
let b = Number::from(-2i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
}
#[test]
fn partial_cmp_should_return_less_than_if_negative_and_other_positive() {
let a = Number::from(-1isize);
let b = Number::from(1usize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
}
#[test]
fn partial_cmp_should_return_less_than_if_negative_and_other_zero() {
let a = Number::from(-1isize);
let b = Number::from(0usize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
}
#[test]
fn partial_cmp_should_return_greater_than_if_positive_and_other_negative() {
let a = Number::from(1usize);
let b = Number::from(-1isize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
}
#[test]
fn partial_cmp_should_perform_cmp_if_both_numbers_positive() {
let a = Number::from(2isize);
let b = Number::from(1i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
let a = Number::from(1isize);
let b = Number::from(1i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
let a = Number::from(1isize);
let b = Number::from(2i8);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
}
#[test]
fn partial_cmp_should_return_greater_than_if_positive_and_other_zero() {
let a = Number::from(1isize);
let b = Number::from(0usize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
}
#[test]
fn partial_cmp_should_return_greater_than_if_zero_and_other_negative() {
let a = Number::from(0usize);
let b = Number::from(-1isize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Greater));
}
#[test]
fn partial_cmp_should_return_less_than_if_zero_and_other_positive() {
let a = Number::from(0usize);
let b = Number::from(1isize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
}
#[test]
fn partial_cmp_should_return_equal_if_both_zero() {
let a = Number::from(0usize);
let b = Number::from(0isize);
assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
}
#[test]
fn sign_should_return_positive_if_inner_value_is_positive() {
check!(f32, f64; sign, eq NumberSign::Positive, 0.1, not 0.0, -0.1);
check!(i128, i64, i32, i16, i8, isize; sign, eq NumberSign::Positive, 1, not 0, -1);
check!(u128, u64, u32, u16, u8, usize; sign, eq NumberSign::Positive, 1, not 0);
}
#[test]
fn sign_should_return_negative_if_inner_value_is_negative() {
check!(f32, f64; sign, eq NumberSign::Negative, -0.1, not 0.0, 0.1);
check!(i128, i64, i32, i16, i8, isize; sign, eq NumberSign::Negative, -1, not 0, 1);
check!(u128, u64, u32, u16, u8, usize; sign, eq NumberSign::Negative, not 0, 1);
}
#[test]
fn sign_should_return_zero_if_inner_value_is_zero() {
check!(f32, f64; sign, eq NumberSign::Zero, 0.0, not -0.1, 0.1);
check!(i128, i64, i32, i16, i8, isize; sign, eq NumberSign::Zero, 0, not -1, 1);
check!(u128, u64, u32, u16, u8, usize; sign, eq NumberSign::Zero, 0, not 1);
}
#[test]
fn is_zero_should_return_true_if_inner_value_is_zero() {
check!(f32, f64; is_zero, 0.0, not -0.1, 0.1);
check!(i128, i64, i32, i16, i8, isize; is_zero, 0, not -1, 1);
check!(u128, u64, u32, u16, u8, usize; is_zero, 0, not 1);
}
#[test]
fn is_positive_should_return_true_if_inner_value_is_positive() {
check!(f32, f64; is_positive, 0.1, not 0.0, -0.1);
check!(i128, i64, i32, i16, i8, isize; is_positive, 1, not -1, 0);
check!(u128, u64, u32, u16, u8, usize; is_positive, 1, not 0);
}
#[test]
fn is_negative_should_return_true_if_inner_value_is_negative() {
check!(f32, f64; is_negative, -0.1, not 0.0, 0.1);
check!(i128, i64, i32, i16, i8, isize; is_negative, -1, not 0, 1);
check!(u128, u64, u32, u16, u8, usize; is_negative, not 0, 1);
}
#[test]
fn is_signed_should_return_true_if_inner_value_is_a_signed_integer() {
check!(f32, f64; is_signed, not -0.1, 0.0, 0.1);
check!(i128, i64, i32, i16, i8, isize; is_signed, -1, 0, 1);
check!(u128, u64, u32, u16, u8, usize; is_signed, not 0, 1);
}
#[test]
fn is_unsigned_should_return_true_if_inner_value_is_an_unsigned_integer() {
check!(f32, f64; is_unsigned, not -0.1, 0.0, 0.1);
check!(i128, i64, i32, i16, i8, isize; is_unsigned, not -1, 0, 1);
check!(u128, u64, u32, u16, u8, usize; is_unsigned, 0, 1);
}
#[test]
fn is_float_should_return_true_if_inner_value_is_a_float() {
check!(f32, f64; is_float, -0.1, 0.0, 0.1);
check!(i128, i64, i32, i16, i8, isize; is_float, not -1, 0, 1);
check!(u128, u64, u32, u16, u8, usize; is_float, not 0, 1);
}
#[test]
fn has_nonzero_fraction_should_return_true_if_float_with_fractional_part() {
check!(f32, f64; has_nonzero_fraction, -0.1, -1.1, 1.1, 0.1, not 0.0, 1.0, -1.0);
check!(i128, i64, i32, i16, i8, isize; has_nonzero_fraction, not -1, 0, 1);
check!(u128, u64, u32, u16, u8, usize; has_nonzero_fraction, not 0, 1);
}
#[test]
fn is_normal_should_return_true_if_inner_value_is_not_zero_infinite_nan_or_subnormal() {
check!(f32; is_normal, -0.1, -1.1, 1.1, 0.1, not 0.0, 1.0e-40_f32);
assert!(!Number::from(f32::NAN).is_normal());
assert!(!Number::from(f32::INFINITY).is_normal());
assert!(!Number::from(f32::NEG_INFINITY).is_normal());
check!(f64; is_normal, -0.1, -1.1, 1.1, 0.1, not 0.0, 1.0e-308_f64);
assert!(!Number::from(f64::NAN).is_normal());
assert!(!Number::from(f64::INFINITY).is_normal());
assert!(!Number::from(f64::NEG_INFINITY).is_normal());
check!(i128, i64, i32, i16, i8, isize; is_normal, -1, 1, not 0);
check!(u128, u64, u32, u16, u8, usize; is_normal, 1, not 0);
}
#[test]
fn to_absolute_should_return_new_number_converted_to_absolute_value() {
macro_rules! check_abs_match {
($variant:ident; $x:literal, $y:literal) => {
match (Number::$variant($x).to_absolute(), Number::$variant($y)) {
(Number::$variant(x), Number::$variant(y)) => {
assert_eq!(x, y, "Expected abs({}) -> {}, but was {}", $x, $y, x);
}
x => panic!("Unexpected comparison: {:?}", x),
}
};
($a:ident, $($b:ident),+; $x:literal, $y:literal) => {
check_abs_match!($a; $x, $y);
check_abs_match!($($b),+; $x, $y);
}
}
check_abs_match!(F32, F64; -0.1, 0.1);
check_abs_match!(F32, F64; 0.0, 0.0);
check_abs_match!(F32, F64; 0.1, 0.1);
check_abs_match!(I128, I64, I32, I16, I8, Isize; -1, 1);
check_abs_match!(I128, I64, I32, I16, I8, Isize; 0, 0);
check_abs_match!(I128, I64, I32, I16, I8, Isize; 1, 1);
check_abs_match!(U128, U64, U32, U16, U8, Usize; 0, 0);
check_abs_match!(U128, U64, U32, U16, U8, Usize; 1, 1);
}
}