decimal.rs (7839B)
1 //! Fixed-precision decimal values, checked arithmetic, and exact conversions. 2 3 use core::fmt; 4 use core::str::FromStr; 5 use rust_decimal::Decimal as RustDecimal; 6 use rust_decimal::prelude::ToPrimitive; 7 8 #[cfg(all(feature = "serde", not(feature = "std")))] 9 use alloc::string::String; 10 #[cfg(not(feature = "std"))] 11 use alloc::{format, string::ToString}; 12 13 #[cfg(feature = "serde")] 14 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; 15 16 #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] 17 #[cfg_attr(all(test, feature = "std"), dto(as = "string"))] 18 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 19 pub struct Decimal(RustDecimal); 20 21 /// Errors produced while constructing or calculating with a [`Decimal`]. 22 /// 23 /// This type intentionally normalizes `rust_decimal` failures so dependency 24 /// implementation details do not become part of the Radroots public contract. 25 #[non_exhaustive] 26 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 27 pub enum Error { 28 InvalidFormat, 29 OutOfRange, 30 ScaleOutOfRange, 31 PrecisionLoss, 32 ArithmeticOverflow, 33 DivisionByZero, 34 } 35 36 impl fmt::Display for Error { 37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 match self { 39 Self::InvalidFormat => f.write_str("invalid decimal string"), 40 Self::OutOfRange => f.write_str("decimal is outside the supported range"), 41 Self::ScaleOutOfRange => f.write_str("decimal scale is outside the supported range"), 42 Self::PrecisionLoss => f.write_str("decimal operation would lose precision"), 43 Self::ArithmeticOverflow => f.write_str("decimal arithmetic overflow"), 44 Self::DivisionByZero => f.write_str("decimal division by zero"), 45 } 46 } 47 } 48 49 #[cfg(feature = "std")] 50 impl std::error::Error for Error {} 51 52 fn normalize_parse_error(error: rust_decimal::Error) -> Error { 53 match error { 54 rust_decimal::Error::ExceedsMaximumPossibleValue 55 | rust_decimal::Error::LessThanMinimumPossibleValue 56 | rust_decimal::Error::Underflow 57 | rust_decimal::Error::ScaleExceedsMaximumPrecision(_) 58 | rust_decimal::Error::ConversionTo(_) => Error::OutOfRange, 59 rust_decimal::Error::ErrorString(_) => Error::InvalidFormat, 60 } 61 } 62 63 #[inline] 64 fn canonicalize_zero(value: RustDecimal) -> RustDecimal { 65 if value.is_zero() { 66 RustDecimal::ZERO 67 } else { 68 value 69 } 70 } 71 72 impl Decimal { 73 pub const ZERO: Self = Self(RustDecimal::ZERO); 74 pub const ONE: Self = Self(RustDecimal::ONE); 75 pub const MAX: Self = Self(RustDecimal::MAX); 76 pub const MIN: Self = Self(RustDecimal::MIN); 77 pub const MAX_SCALE: u32 = RustDecimal::MAX_SCALE; 78 79 #[inline] 80 pub(crate) const fn from_parts(lo: u32, mid: u32, hi: u32, scale: u32) -> Self { 81 Self(RustDecimal::from_parts(lo, mid, hi, false, scale)) 82 } 83 84 #[inline] 85 pub(crate) const fn from_backend(value: RustDecimal) -> Self { 86 Self(value) 87 } 88 89 #[inline] 90 pub(crate) const fn into_backend(self) -> RustDecimal { 91 self.0 92 } 93 94 #[inline] 95 pub fn is_zero(&self) -> bool { 96 self.0.is_zero() 97 } 98 #[inline] 99 pub fn is_sign_negative(&self) -> bool { 100 self.0.is_sign_negative() 101 } 102 /// Rescales with deterministic midpoint-away-from-zero rounding when 103 /// reducing precision. When increasing precision, the closest 104 /// representable scale is used. Use [`Self::try_rescale_exact`] when 105 /// either behavior would be ambiguous at a boundary. 106 #[inline] 107 pub fn rescale(&mut self, scale: u32) { 108 self.0.rescale(scale); 109 } 110 111 /// Changes the scale only when the requested representation is exact. 112 /// 113 /// Unlike [`Self::rescale`], this method never rounds and never silently 114 /// substitutes a smaller scale. The value is left unchanged on error. 115 #[inline] 116 pub fn try_rescale_exact(&mut self, scale: u32) -> Result<(), Error> { 117 if scale > Self::MAX_SCALE { 118 return Err(Error::ScaleOutOfRange); 119 } 120 121 let original = self.0; 122 let mut candidate = original; 123 candidate.rescale(scale); 124 if candidate.scale() != scale { 125 return Err(Error::ScaleOutOfRange); 126 } 127 if candidate != original { 128 return Err(Error::PrecisionLoss); 129 } 130 131 self.0 = candidate; 132 Ok(()) 133 } 134 #[inline] 135 pub fn normalize(&self) -> Self { 136 Self(self.0.normalize()) 137 } 138 139 #[inline] 140 pub fn scale(&self) -> u32 { 141 self.0.scale() 142 } 143 144 #[inline] 145 pub fn from_str_exact(s: &str) -> Result<Self, Error> { 146 RustDecimal::from_str_exact(s) 147 .map(canonicalize_zero) 148 .map(Self) 149 .map_err(normalize_parse_error) 150 } 151 152 /// Converts the shortest deterministic 17-digit display of a finite 153 /// `f64`. This is a decimal representation of the displayed float, not a 154 /// promise to preserve the float's binary representation exactly. 155 #[inline] 156 pub fn from_f64_display(n: f64) -> Result<Self, Error> { 157 if !n.is_finite() { 158 return Err(Error::InvalidFormat); 159 } 160 let s = format!("{:.17}", n); 161 s.parse() 162 } 163 #[inline] 164 pub fn to_f64_lossy(&self) -> Option<f64> { 165 self.0.to_f64() 166 } 167 168 #[inline] 169 pub fn to_u64_exact(&self) -> Option<u64> { 170 if self.0.fract().is_zero() { 171 self.0.to_u64() 172 } else { 173 None 174 } 175 } 176 177 #[inline] 178 pub fn checked_add(self, rhs: Self) -> Result<Self, Error> { 179 self.0 180 .checked_add(rhs.0) 181 .map(canonicalize_zero) 182 .map(Self) 183 .ok_or(Error::ArithmeticOverflow) 184 } 185 186 #[inline] 187 pub fn checked_sub(self, rhs: Self) -> Result<Self, Error> { 188 self.0 189 .checked_sub(rhs.0) 190 .map(canonicalize_zero) 191 .map(Self) 192 .ok_or(Error::ArithmeticOverflow) 193 } 194 195 #[inline] 196 pub fn checked_mul(self, rhs: Self) -> Result<Self, Error> { 197 self.0 198 .checked_mul(rhs.0) 199 .map(canonicalize_zero) 200 .map(Self) 201 .ok_or(Error::ArithmeticOverflow) 202 } 203 204 #[inline] 205 pub fn checked_div(self, rhs: Self) -> Result<Self, Error> { 206 if rhs.is_zero() { 207 return Err(Error::DivisionByZero); 208 } 209 self.0 210 .checked_div(rhs.0) 211 .map(canonicalize_zero) 212 .map(Self) 213 .ok_or(Error::ArithmeticOverflow) 214 } 215 } 216 217 #[cfg(feature = "serde")] 218 impl Serialize for Decimal { 219 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { 220 serializer.serialize_str(&self.0.normalize().to_string()) 221 } 222 } 223 224 #[cfg(feature = "serde")] 225 impl<'de> Deserialize<'de> for Decimal { 226 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { 227 let s = String::deserialize(deserializer)?; 228 s.parse::<Decimal>().map_err(D::Error::custom) 229 } 230 } 231 232 impl fmt::Display for Decimal { 233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 234 f.write_str(&self.0.normalize().to_string()) 235 } 236 } 237 238 impl From<u32> for Decimal { 239 fn from(v: u32) -> Self { 240 Self(RustDecimal::from(v)) 241 } 242 } 243 impl From<i32> for Decimal { 244 fn from(v: i32) -> Self { 245 Self(RustDecimal::from(v)) 246 } 247 } 248 impl From<u64> for Decimal { 249 fn from(v: u64) -> Self { 250 Self(RustDecimal::from(v)) 251 } 252 } 253 impl From<i64> for Decimal { 254 fn from(v: i64) -> Self { 255 Self(RustDecimal::from(v)) 256 } 257 } 258 259 impl FromStr for Decimal { 260 type Err = Error; 261 fn from_str(s: &str) -> Result<Self, Self::Err> { 262 RustDecimal::from_str(s) 263 .map(canonicalize_zero) 264 .map(Decimal) 265 .map_err(normalize_parse_error) 266 } 267 }