lib

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

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 }