lib

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

money.rs (6699B)


      1 use core::fmt;
      2 use rust_decimal::Decimal;
      3 use rust_decimal::RoundingStrategy;
      4 use rust_decimal::prelude::ToPrimitive;
      5 
      6 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
      7 #[derive(Clone, Debug, PartialEq, Eq)]
      8 pub struct RadrootsCoreMoney {
      9     pub amount: crate::RadrootsCoreDecimal,
     10     pub currency: crate::RadrootsCoreCurrency,
     11 }
     12 
     13 #[non_exhaustive]
     14 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     15 pub enum RadrootsCoreMoneyInvariantError {
     16     NegativeAmount,
     17     NotWholeMinorUnits,
     18     AmountOverflow,
     19     CurrencyMismatch,
     20 }
     21 
     22 impl fmt::Display for RadrootsCoreMoneyInvariantError {
     23     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     24         match self {
     25             Self::NegativeAmount => write!(f, "money amount must be ≥ 0"),
     26             Self::NotWholeMinorUnits => write!(f, "money not a whole number of minor units"),
     27             Self::AmountOverflow => write!(f, "money minor-unit conversion overflow"),
     28             Self::CurrencyMismatch => write!(f, "money currency mismatch"),
     29         }
     30     }
     31 }
     32 
     33 #[cfg(feature = "std")]
     34 impl std::error::Error for RadrootsCoreMoneyInvariantError {}
     35 
     36 impl RadrootsCoreMoney {
     37     #[inline]
     38     pub fn new(amount: crate::RadrootsCoreDecimal, currency: crate::RadrootsCoreCurrency) -> Self {
     39         Self { amount, currency }
     40     }
     41 
     42     #[inline]
     43     pub fn zero(currency: crate::RadrootsCoreCurrency) -> Self {
     44         Self {
     45             amount: crate::RadrootsCoreDecimal::ZERO,
     46             currency,
     47         }
     48     }
     49 
     50     #[inline]
     51     pub fn is_zero(&self) -> bool {
     52         self.amount.is_zero()
     53     }
     54 
     55     #[inline]
     56     pub fn ensure_non_negative(&self) -> Result<(), RadrootsCoreMoneyInvariantError> {
     57         if self.amount.is_sign_negative() {
     58             return Err(RadrootsCoreMoneyInvariantError::NegativeAmount);
     59         }
     60         Ok(())
     61     }
     62 
     63     #[inline]
     64     pub fn quantize_to_currency(self) -> Self {
     65         self.quantize_to_currency_with_strategy(RoundingStrategy::MidpointAwayFromZero)
     66     }
     67 
     68     #[inline]
     69     pub fn quantize_to_currency_with_strategy(mut self, strategy: RoundingStrategy) -> Self {
     70         let e = self.currency.minor_unit_exponent();
     71         self.amount.0 = self.amount.0.round_dp_with_strategy(e, strategy);
     72         self
     73     }
     74 
     75     #[inline]
     76     pub fn with_scale(mut self, scale: u32) -> Self {
     77         self.amount.rescale(scale);
     78         self
     79     }
     80 
     81     #[inline]
     82     pub fn checked_add(&self, rhs: &Self) -> Result<Self, RadrootsCoreMoneyInvariantError> {
     83         if self.currency != rhs.currency {
     84             return Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch);
     85         }
     86         Ok(Self::new(self.amount + rhs.amount, self.currency))
     87     }
     88 
     89     #[inline]
     90     pub fn checked_sub(&self, rhs: &Self) -> Result<Self, RadrootsCoreMoneyInvariantError> {
     91         if self.currency != rhs.currency {
     92             return Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch);
     93         }
     94         Ok(Self::new(self.amount - rhs.amount, self.currency))
     95     }
     96 
     97     #[inline]
     98     pub fn mul_decimal(&self, factor: crate::RadrootsCoreDecimal) -> Self {
     99         Self::new(self.amount * factor, self.currency)
    100     }
    101 
    102     #[inline]
    103     pub fn div_decimal(&self, divisor: crate::RadrootsCoreDecimal) -> Self {
    104         Self::new(self.amount / divisor, self.currency)
    105     }
    106 
    107     #[inline]
    108     pub fn from_minor_units_u64(amount_minor: u64, currency: crate::RadrootsCoreCurrency) -> Self {
    109         let e = currency.minor_unit_exponent();
    110         let major = Decimal::from_i128_with_scale(amount_minor as i128, e);
    111         Self::new(crate::RadrootsCoreDecimal(major), currency)
    112     }
    113 
    114     #[inline]
    115     pub fn from_minor_units_u32(amount_minor: u32, currency: crate::RadrootsCoreCurrency) -> Self {
    116         Self::from_minor_units_u64(amount_minor as u64, currency)
    117     }
    118 
    119     #[inline]
    120     fn pow10(e: u32) -> Decimal {
    121         match e {
    122             0 => Decimal::ONE,
    123             1 => Decimal::from(10u32),
    124             2 => Decimal::from(100u32),
    125             3 => Decimal::from(1_000u32),
    126             _ => {
    127                 let p = 10u128.pow(e.min(38));
    128                 Decimal::from(p)
    129             }
    130         }
    131     }
    132 
    133     #[inline]
    134     pub fn to_minor_units_u64_exact(&self) -> Result<u64, RadrootsCoreMoneyInvariantError> {
    135         let e = self.currency.minor_unit_exponent();
    136         let as_minor = self.amount.0 * Self::pow10(e);
    137 
    138         if !as_minor.fract().is_zero() {
    139             return Err(RadrootsCoreMoneyInvariantError::NotWholeMinorUnits);
    140         }
    141         as_minor
    142             .to_u64()
    143             .ok_or(RadrootsCoreMoneyInvariantError::AmountOverflow)
    144     }
    145 
    146     #[inline]
    147     pub fn to_minor_units_u64_rounded(
    148         &self,
    149         strategy: RoundingStrategy,
    150     ) -> Result<u64, RadrootsCoreMoneyInvariantError> {
    151         let e = self.currency.minor_unit_exponent();
    152         let scaled = self.amount.0.round_dp_with_strategy(e, strategy);
    153         let as_minor = scaled * Self::pow10(e);
    154         debug_assert!(as_minor.fract().is_zero());
    155         as_minor
    156             .to_u64()
    157             .ok_or(RadrootsCoreMoneyInvariantError::AmountOverflow)
    158     }
    159 
    160     #[inline]
    161     pub fn to_minor_units_u32_exact(&self) -> Result<u32, RadrootsCoreMoneyInvariantError> {
    162         let v = self.to_minor_units_u64_exact()?;
    163         u32::try_from(v).map_err(|_| RadrootsCoreMoneyInvariantError::AmountOverflow)
    164     }
    165 
    166     #[inline]
    167     pub fn to_minor_units_u32_rounded(
    168         &self,
    169         strategy: RoundingStrategy,
    170     ) -> Result<u32, RadrootsCoreMoneyInvariantError> {
    171         let v = self.to_minor_units_u64_rounded(strategy)?;
    172         u32::try_from(v).map_err(|_| RadrootsCoreMoneyInvariantError::AmountOverflow)
    173     }
    174 }
    175 
    176 impl fmt::Display for RadrootsCoreMoney {
    177     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    178         write!(f, "{} {}", self.amount, self.currency)
    179     }
    180 }
    181 
    182 use core::ops::{Div, Mul};
    183 
    184 impl Mul<crate::RadrootsCoreDecimal> for RadrootsCoreMoney {
    185     type Output = Self;
    186     fn mul(self, rhs: crate::RadrootsCoreDecimal) -> Self {
    187         self.mul_decimal(rhs)
    188     }
    189 }
    190 
    191 impl Div<crate::RadrootsCoreDecimal> for RadrootsCoreMoney {
    192     type Output = Self;
    193     fn div(self, rhs: crate::RadrootsCoreDecimal) -> Self {
    194         self.div_decimal(rhs)
    195     }
    196 }
    197 
    198 #[cfg(test)]
    199 mod tests {
    200     use super::*;
    201 
    202     #[test]
    203     fn pow10_internal_paths_cover_fallback_branches() {
    204         assert_eq!(RadrootsCoreMoney::pow10(0), Decimal::ONE);
    205         assert_eq!(RadrootsCoreMoney::pow10(1), Decimal::from(10u32));
    206         assert_eq!(RadrootsCoreMoney::pow10(2), Decimal::from(100u32));
    207         assert_eq!(RadrootsCoreMoney::pow10(3), Decimal::from(1_000u32));
    208         assert_eq!(RadrootsCoreMoney::pow10(6), Decimal::from(1_000_000u32));
    209     }
    210 }