proof_of_sql/base/math/
big_decimal_ext.rs

1use super::decimal::{IntermediateDecimalError, IntermediateDecimalError::LossyCast};
2use bigdecimal::BigDecimal;
3use num_bigint::BigInt;
4
5/// Extension trait for `BigDecimal` to provide additional functionality specific to Proof of SQL.
6pub trait BigDecimalExt {
7    /// Get the precision of the fixed-point representation of this decimal.
8    fn precision(&self) -> u64;
9    /// Get the scale of the fixed-point representation of this decimal.
10    fn scale(&self) -> i64;
11    /// Attempts to convert the decimal to `BigInt` while adjusting it to the specified precision and scale.
12    fn try_into_bigint_with_precision_and_scale(
13        &self,
14        precision: u8,
15        scale: i8,
16    ) -> Result<BigInt, IntermediateDecimalError>;
17}
18impl BigDecimalExt for BigDecimal {
19    /// Get the precision of the fixed-point representation of this intermediate decimal.
20    fn precision(&self) -> u64 {
21        self.normalized().digits()
22    }
23
24    /// Get the scale of the fixed-point representation of this intermediate decimal.
25    fn scale(&self) -> i64 {
26        self.normalized().fractional_digit_count()
27    }
28
29    /// Attempts to convert the decimal to `BigInt` while adjusting it to the specified precision and scale.
30    /// Returns an error if the conversion cannot be performed due to precision or scale constraints.
31    ///
32    /// # Errors
33    /// Returns an `IntermediateDecimalError::LossyCast` error if the number of digits in the scaled decimal exceeds the specified precision.
34    fn try_into_bigint_with_precision_and_scale(
35        &self,
36        precision: u8,
37        scale: i8,
38    ) -> Result<BigInt, IntermediateDecimalError> {
39        if self.scale() > scale.into() {
40            Err(IntermediateDecimalError::ConversionFailure)?;
41        }
42        let scaled_decimal = self.normalized().with_scale(scale.into());
43        if scaled_decimal.digits() > precision.into() {
44            return Err(LossyCast);
45        }
46        let (d, _) = scaled_decimal.into_bigint_and_exponent();
47        Ok(d)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use alloc::string::ToString;
55
56    #[test]
57    fn test_valid_decimal_simple() {
58        let decimal = "123.45".parse::<BigDecimal>();
59        assert!(decimal.is_ok());
60        let unwrapped_decimal: BigDecimal = decimal.unwrap().normalized();
61        assert_eq!(unwrapped_decimal.to_string(), "123.45");
62        assert_eq!(unwrapped_decimal.precision(), 5);
63        assert_eq!(unwrapped_decimal.scale(), 2);
64    }
65
66    #[test]
67    fn test_valid_decimal_with_leading_and_trailing_zeros() {
68        let decimal = "000123.45000".parse::<BigDecimal>();
69        assert!(decimal.is_ok());
70        let unwrapped_decimal: BigDecimal = decimal.unwrap().normalized();
71        assert_eq!(unwrapped_decimal.to_string(), "123.45");
72        assert_eq!(unwrapped_decimal.precision(), 5);
73        assert_eq!(unwrapped_decimal.scale(), 2);
74    }
75
76    #[test]
77    fn test_accessors() {
78        let decimal: BigDecimal = "123.456".parse::<BigDecimal>().unwrap().normalized();
79        assert_eq!(decimal.to_string(), "123.456");
80        assert_eq!(decimal.precision(), 6);
81        assert_eq!(decimal.scale(), 3);
82    }
83
84    #[test]
85    fn we_can_catch_conversion_error() {
86        let big_decimal: BigDecimal = "123.456".parse::<BigDecimal>().unwrap().normalized();
87        let err = big_decimal
88            .try_into_bigint_with_precision_and_scale(2, 1)
89            .unwrap_err();
90        assert!(matches!(err, IntermediateDecimalError::ConversionFailure));
91    }
92}