lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 {}