deep_space/
decimal.rs

1//! Decimal type with equivalent semantics to the [Cosmos `sdk.Dec`][1] type.
2//! Imported from github.com/cosmos/cosmos-rust
3//!
4//! [1]: https://pkg.go.dev/github.com/cosmos/cosmos-sdk/types#Dec
5
6use rust_decimal::Error as DecimalLibraryError;
7use std::{
8    convert::{TryFrom, TryInto},
9    fmt::{self, Debug, Display},
10    str::FromStr,
11};
12
13#[derive(Debug)]
14pub enum DecimalError {
15    ExcessivePrecision,
16    InvalidPrecision,
17    DecimalError(DecimalLibraryError),
18}
19
20impl fmt::Display for DecimalError {
21    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
22        match self {
23            DecimalError::ExcessivePrecision => {
24                write!(f, "Decimal exceeds maximum fractional digits")
25            }
26            DecimalError::InvalidPrecision => {
27                write!(f, "Decimal is using an invalid precision must be 0 or 18")
28            }
29            DecimalError::DecimalError(v) => {
30                write!(f, "{v:?}")
31            }
32        }
33    }
34}
35
36impl std::error::Error for DecimalError {}
37
38impl From<DecimalLibraryError> for DecimalError {
39    fn from(error: DecimalLibraryError) -> Self {
40        DecimalError::DecimalError(error)
41    }
42}
43
44/// Number of decimal places required by an `sdk.Dec`
45/// See: <https://github.com/cosmos/cosmos-sdk/blob/018915b/types/decimal.go#L23>
46pub const PRECISION: u32 = 18;
47
48/// Maximum value of the decimal part of an `sdk.Dec`
49pub const FRACTIONAL_DIGITS_MAX: u64 = 9_999_999_999_999_999_999;
50
51/// Decimal type which follows Cosmos [Cosmos `sdk.Dec`][1] conventions.
52///
53/// [1]: https://pkg.go.dev/github.com/cosmos/cosmos-sdk/types#Dec
54#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
55pub struct Decimal(rust_decimal::Decimal);
56
57impl Decimal {
58    /// Create a new [`Decimal`] with the given whole number and decimal
59    /// parts. The decimal part assumes 18 digits of precision e.g. a
60    /// decimal with `(1, 1)` is `1.000000000000000001`.
61    ///
62    /// 18 digits required by the Cosmos SDK. See:
63    /// See: <https://github.com/cosmos/cosmos-sdk/blob/26d6e49/types/decimal.go#L23>
64    pub fn new(integral_digits: i64, fractional_digits: u64) -> Result<Self, DecimalError> {
65        if fractional_digits > FRACTIONAL_DIGITS_MAX {
66            return Err(DecimalError::ExcessivePrecision);
67        }
68
69        let integral_digits: rust_decimal::Decimal = integral_digits.into();
70        let fractional_digits: rust_decimal::Decimal = fractional_digits.into();
71        let precision_exp: rust_decimal::Decimal = 10u64.pow(PRECISION).into();
72
73        let mut combined_decimal = (integral_digits * precision_exp) + fractional_digits;
74        combined_decimal.set_scale(PRECISION)?;
75        Ok(Decimal(combined_decimal))
76    }
77}
78
79impl Debug for Decimal {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(f, "{:?}", self.0)
82    }
83}
84
85impl Display for Decimal {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.0)
88    }
89}
90
91impl FromStr for Decimal {
92    type Err = DecimalError;
93    fn from_str(s: &str) -> Result<Self, DecimalError> {
94        s.parse::<rust_decimal::Decimal>()?.try_into()
95    }
96}
97
98impl TryFrom<rust_decimal::Decimal> for Decimal {
99    type Error = DecimalError;
100    fn try_from(mut decimal_value: rust_decimal::Decimal) -> Result<Self, DecimalError> {
101        match decimal_value.scale() {
102            0 => {
103                let exp: rust_decimal::Decimal = 10u64.pow(PRECISION).into();
104                decimal_value *= exp;
105                decimal_value.set_scale(PRECISION)?;
106            }
107            PRECISION => (),
108            _other => return Err(DecimalError::InvalidPrecision),
109        }
110
111        Ok(Decimal(decimal_value))
112    }
113}
114
115macro_rules! impl_from_primitive_int_for_decimal {
116    ($($int:ty),+) => {
117        $(impl From<$int> for Decimal {
118            fn from(num: $int) -> Decimal {
119                #[allow(trivial_numeric_casts)]
120                Decimal::new(num as i64, 0).unwrap()
121            }
122        })+
123    };
124}
125
126impl_from_primitive_int_for_decimal!(i8, i16, i32, i64, isize);
127impl_from_primitive_int_for_decimal!(u8, u16, u32, u64, usize);
128
129#[cfg(test)]
130mod tests {
131    use super::Decimal;
132
133    #[test]
134    fn string_serialization_test() {
135        let num = Decimal::from(-1i8);
136        assert_eq!(num.to_string(), "-1.000000000000000000")
137    }
138}