use compact_str::format_compact;
use crate::num::constants::{
NAN,INFINITY,
};
use crate::str::Str;
use crate::macros::{
return_bad_float,str_u64,str_i64,
impl_common,impl_const,
impl_usize,impl_isize,
impl_math,impl_traits,
impl_impl_math,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Percent(f64, Str<{ Percent::MAX_LEN }>);
const LEN: usize = 22;
impl_math!(Percent, f64);
impl_traits!(Percent, f64);
impl Percent {
pub const ZERO: Self = Self(0.0, Str::from_static_str("0.00%"));
pub const NAN: Self = Self(f64::NAN, Str::from_static_str(NAN));
pub const INFINITY: Self = Self(f64::INFINITY, Str::from_static_str(INFINITY));
pub const UNKNOWN: Self = Self(0.0, Str::from_static_str("?.??%"));
pub const MAX_LEN: usize = LEN;
}
macro_rules! impl_new {
( $num:tt ) => {
paste::item! {
#[doc = "Same as [`Percent::from`] but with `" $num "` floating point."]
#[must_use]
pub fn [<new_ $num>](f: f64) -> Self {
return_bad_float!(f, Self::NAN, Self::INFINITY);
let fract = &format_compact!(concat!("{:.", $num, "}"), f.fract())[2..];
let string = format_compact!("{}.{}%", str_u64!(f as u64), fract);
if string.len() > Self::MAX_LEN {
Self::UNKNOWN
} else {
let mut s = Str::new();
s.push_str_panic(string);
Self(f, s)
}
}
}
}
}
impl Percent {
impl_common!(f64);
impl_const!();
impl_usize!();
impl_isize!();
#[inline]
#[must_use]
pub fn is_nan(&self) -> bool {
self.0.is_nan()
}
#[inline]
#[must_use]
pub fn is_infinite(&self) -> bool {
self.0.is_infinite()
}
#[inline]
#[must_use]
pub const fn is_unknown(&self) -> bool {
matches!(self.as_str().as_bytes(), b"?.??%")
}
#[inline]
#[must_use]
pub fn new_0(f: f64) -> Self {
return_bad_float!(f, Self::NAN, Self::INFINITY);
let string = format_compact!("{}%", str_u64!(f as u64));
if string.len() > Self::MAX_LEN {
Self::UNKNOWN
} else {
let mut s = Str::new();
s.push_str_panic(string);
Self(f, s)
}
}
impl_new!(1);
seq_macro::seq!(N in 3..=14 {
impl_new!(N);
});
}
macro_rules! impl_u {
($( $number:ty ),*) => {
$(
impl From<$number> for Percent {
#[inline]
fn from(number: $number) -> Self {
let string = format_compact!("{}.00%", str_u64!(number as u64));
if string.len() > Self::MAX_LEN {
Self::UNKNOWN
} else {
let mut s = Str::new();
s.push_str_panic(string);
Self(number as f64, s)
}
}
}
)*
}
}
impl_u!(u8,u16,u32,u64,usize);
macro_rules! impl_i {
($( $number:ty ),*) => {
$(
impl From<$number> for Percent {
#[inline]
fn from(number: $number) -> Self {
let string = format_compact!("{}.00%", str_i64!(number as i64));
if string.len() > Self::MAX_LEN {
Self::UNKNOWN
} else {
let mut s = Str::new();
s.push_str_panic(string);
Self(number as f64, s)
}
}
}
)*
}
}
impl_i!(i8,i16,i32,i64,isize);
impl From<f32> for Percent {
#[inline]
fn from(f: f32) -> Self {
return_bad_float!(f, Self::NAN, Self::INFINITY);
#[allow(clippy::cast_lossless)]
Self::from(f as f64)
}
}
impl From<f64> for Percent {
#[inline]
fn from(f: f64) -> Self {
return_bad_float!(f, Self::NAN, Self::INFINITY);
let fract = &format_compact!("{:.2}", f.fract())[2..];
let string = format_compact!("{}.{}%", str_u64!(f as u64), fract);
if string.len() > Self::MAX_LEN {
Self::UNKNOWN
} else {
let mut s = Str::new();
s.push_str_panic(string);
Self(f, s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn special() {
assert_eq!(Percent::ZERO, "0.00%");
assert_eq!(Percent::UNKNOWN, "?.??%");
assert_eq!(Percent::NAN, NAN);
assert_eq!(Percent::INFINITY, INFINITY);
assert_eq!(Percent::from(0.0), "0.00%");
assert_eq!(Percent::from(f64::NAN), NAN);
assert_eq!(Percent::from(f64::INFINITY), INFINITY);
assert_eq!(Percent::from(f64::NEG_INFINITY), INFINITY);
}
#[test]
fn percent() {
assert_eq!(Percent::from(0.0), "0.00%");
assert_eq!(Percent::from(0.001), "0.00%");
assert_eq!(Percent::from(0.1), "0.10%");
assert_eq!(Percent::from(1.0), "1.00%");
assert_eq!(Percent::from(50.0), "50.00%");
assert_eq!(Percent::from(100.0), "100.00%");
assert_eq!(Percent::from(150.0), "150.00%");
assert_eq!(Percent::from(1_000.0), "1,000.00%");
assert_eq!(Percent::from(250_000.0), "250,000.00%");
}
#[test]
fn percent_dot() {
assert_eq!(Percent::new_1(0.0), "0.0%");
assert_eq!(Percent::new_1(1_000.123_4), "1,000.1%");
assert_eq!(Percent::new_3(1_000.123_4), "1,000.123%");
assert_eq!(Percent::new_4(1_000.123_4), "1,000.1234%");
assert_eq!(Percent::new_1(0.1), "0.1%");
assert_eq!(Percent::new_1(10_000.123_4), "10,000.1%");
assert_eq!(Percent::new_3(100_000.123_4), "100,000.123%");
assert_eq!(Percent::new_4(1_000_000.123_4), "1,000,000.1234%");
}
#[test]
fn from_unsigned() {
assert_eq!(Percent::from(1_u32), "1.00%");
assert_eq!(Percent::from(1_000_u32), "1,000.00%");
assert_eq!(Percent::from(10_000_u32), "10,000.00%");
assert_eq!(Percent::from(100_000_u32), "100,000.00%");
assert_eq!(Percent::from(1_000_000_u32), "1,000,000.00%");
}
#[test]
fn from_int() {
assert_eq!(Percent::from(-1_i32), "-1.00%");
assert_eq!(Percent::from(-1_000_i32), "-1,000.00%");
assert_eq!(Percent::from(-10_000_i32), "-10,000.00%");
assert_eq!(Percent::from(-100_000_i32), "-100,000.00%");
assert_eq!(Percent::from(-1_000_000_i32), "-1,000,000.00%");
}
#[test]
#[cfg(feature = "serde")]
fn serde() {
let this: Percent = Percent::from(1.0);
let json = serde_json::to_string(&this).unwrap();
assert_eq!(json, r#"[1.0,"1.00%"]"#);
let this: Percent = serde_json::from_str(&json).unwrap();
assert_eq!(this, 1.0);
assert_eq!(this, "1.00%");
assert!(serde_json::from_str::<Percent>(&"---").is_err());
let json = serde_json::to_string(&Percent::UNKNOWN).unwrap();
assert_eq!(json, r#"[0.0,"?.??%"]"#);
assert!(serde_json::from_str::<Percent>(&json).unwrap().is_unknown());
}
#[test]
#[cfg(feature = "bincode")]
fn bincode() {
let this: Percent = Percent::from(1.0);
let config = bincode::config::standard();
let bytes = bincode::encode_to_vec(&this, config).unwrap();
let this: Percent = bincode::decode_from_slice(&bytes, config).unwrap().0;
assert_eq!(this, 1.0);
assert_eq!(this, "1.00%");
let bytes = bincode::encode_to_vec(&Percent::UNKNOWN, config).unwrap();
let this: Percent = bincode::decode_from_slice(&bytes, config).unwrap().0;
assert!(this.is_unknown());
}
#[test]
#[cfg(feature = "borsh")]
fn borsh() {
let this: Percent = Percent::from(1.0);
let bytes = borsh::to_vec(&this).unwrap();
let this: Percent = borsh::from_slice(&bytes).unwrap();
assert_eq!(this, 1.0);
assert_eq!(this, "1.00%");
assert!(borsh::from_slice::<Percent>(b"bad .-;[]124/ bytes").is_err());
let bytes = borsh::to_vec(&Percent::UNKNOWN).unwrap();
let this: Percent = borsh::from_slice(&bytes).unwrap();
assert!(this.is_unknown());
}
}