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 }