percent.rs (2652B)
1 //! Signed percentage values and deterministic percentage calculations. 2 3 use core::fmt; 4 use core::str::FromStr; 5 6 use crate::Decimal; 7 use crate::money::Money; 8 9 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 10 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 11 #[cfg_attr(all(test, feature = "std"), dto(export))] 12 #[derive(Clone, Debug, PartialEq, Eq)] 13 pub struct Percent { 14 #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))] 15 #[cfg_attr(all(test, feature = "std"), dto(as = "string"))] 16 value: Decimal, 17 } 18 19 impl Percent { 20 /// Creates a percentage value. 21 /// 22 /// `Percent` is intentionally signed and unbounded because it is also 23 /// used for deltas. Domain-specific non-negative rules belong to values 24 /// such as [`crate::pricing::Discount`]. 25 #[inline] 26 pub fn new(value: Decimal) -> Self { 27 Self { value } 28 } 29 30 #[inline] 31 pub const fn value(&self) -> Decimal { 32 self.value 33 } 34 35 #[inline] 36 pub fn try_from_ratio(ratio_0_to_1: Decimal) -> Result<Self, crate::decimal::Error> { 37 Ok(Self { 38 value: ratio_0_to_1.checked_mul(Decimal::from(100u32))?, 39 }) 40 } 41 42 #[inline] 43 pub fn try_to_ratio(&self) -> Result<Decimal, crate::decimal::Error> { 44 self.value.checked_div(Decimal::from(100u32)) 45 } 46 47 #[inline] 48 pub fn try_of_money(&self, base: &Money) -> Result<Money, crate::money::Error> { 49 base.checked_mul_decimal(self.try_to_ratio().map_err(crate::money::Error::from)?) 50 } 51 52 #[inline] 53 pub fn try_of_money_quantized(&self, base: &Money) -> Result<Money, crate::money::Error> { 54 Ok(self.try_of_money(base)?.quantize_to_currency()) 55 } 56 } 57 58 impl fmt::Display for Percent { 59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 60 write!(f, "{}%", self.value.normalize()) 61 } 62 } 63 64 impl FromStr for Percent { 65 type Err = ParseError; 66 67 fn from_str(s: &str) -> Result<Self, Self::Err> { 68 let trimmed = s.trim_end(); 69 let no_pct = trimmed.strip_suffix('%').unwrap_or(trimmed).trim(); 70 let dec = no_pct 71 .parse::<Decimal>() 72 .map_err(|_| ParseError::InvalidNumber)?; 73 Ok(Percent::new(dec)) 74 } 75 } 76 77 #[non_exhaustive] 78 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 79 pub enum ParseError { 80 InvalidNumber, 81 } 82 83 impl fmt::Display for ParseError { 84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 85 match self { 86 ParseError::InvalidNumber => write!(f, "invalid percent string"), 87 } 88 } 89 } 90 91 #[cfg(feature = "std")] 92 impl std::error::Error for ParseError {}