Skip to main content

fermat_core/
decimal.rs

1//! Core `Decimal` type definition and constants.
2
3use crate::error::ArithmeticError;
4
5/// Maximum allowed scale (decimal places). Fixed at 28 to stay within `i128` range.
6///
7/// At scale 28 the mantissa unit is `10^-28`, giving sub-yocto precision.
8pub const MAX_SCALE: u8 = 28;
9
10/// Conventional scale for USDC (6 decimal places).
11pub const USDC_SCALE: u8 = 6;
12
13/// Conventional scale for SOL lamports (9 decimal places).
14pub const SOL_SCALE: u8 = 9;
15
16/// Conventional scale for percentage values expressed as basis points (4 d.p.).
17pub const BPS_SCALE: u8 = 4;
18
19/// A 128-bit signed fixed-point decimal.
20///
21/// ```text
22/// value = mantissa × 10^(-scale)
23/// ```
24///
25/// - `mantissa`: signed coefficient stored as `i128`
26/// - `scale`: number of decimal places in `[0, MAX_SCALE]`
27///
28/// On-chain Borsh encoding: 17 bytes (16-byte LE mantissa + 1-byte scale).
29#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
30pub struct Decimal {
31    pub(crate) mantissa: i128,
32    pub(crate) scale: u8,
33}
34
35impl Decimal {
36    /// Additive identity: `0 × 10^0`.
37    pub const ZERO: Self = Self { mantissa: 0, scale: 0 };
38
39    /// Multiplicative identity: `1 × 10^0`.
40    pub const ONE: Self = Self { mantissa: 1, scale: 0 };
41
42    /// Maximum representable value: `i128::MAX × 10^0`.
43    pub const MAX: Self = Self { mantissa: i128::MAX, scale: 0 };
44
45    /// Minimum representable value: `i128::MIN × 10^0`.
46    pub const MIN: Self = Self { mantissa: i128::MIN, scale: 0 };
47
48    /// Negative one: `-1 × 10^0`.
49    pub const NEG_ONE: Self = Self { mantissa: -1, scale: 0 };
50
51    /// `100 × 10^0` — useful for percentage arithmetic.
52    pub const HUNDRED: Self = Self { mantissa: 100, scale: 0 };
53
54    /// `10_000 × 10^0` — basis-points denominator.
55    pub const TEN_THOUSAND: Self = Self { mantissa: 10_000, scale: 0 };
56
57    /// Construct a `Decimal` from a raw mantissa and scale.
58    ///
59    /// Returns `Err(ArithmeticError::ScaleExceeded)` if `scale > MAX_SCALE`.
60    #[inline]
61    pub fn new(mantissa: i128, scale: u8) -> Result<Self, ArithmeticError> {
62        if scale > MAX_SCALE {
63            return Err(ArithmeticError::ScaleExceeded);
64        }
65        Ok(Self { mantissa, scale })
66    }
67
68    /// Construct without validating scale. Caller must ensure `scale <= MAX_SCALE`.
69    ///
70    /// # Safety (logical)
71    /// This is not `unsafe` in the Rust sense, but violating the scale invariant
72    /// will cause incorrect arithmetic results. Prefer `Decimal::new`.
73    #[inline]
74    pub(crate) const fn new_unchecked(mantissa: i128, scale: u8) -> Self {
75        Self { mantissa, scale }
76    }
77
78    /// Returns the raw mantissa.
79    #[inline]
80    pub fn mantissa(self) -> i128 {
81        self.mantissa
82    }
83
84    /// Returns the scale (number of decimal places).
85    #[inline]
86    pub fn scale(self) -> u8 {
87        self.scale
88    }
89
90    /// Returns `true` if the value is exactly zero.
91    #[inline]
92    pub fn is_zero(self) -> bool {
93        self.mantissa == 0
94    }
95
96    /// Returns `true` if the value is strictly positive.
97    #[inline]
98    pub fn is_positive(self) -> bool {
99        self.mantissa > 0
100    }
101
102    /// Returns `true` if the value is strictly negative.
103    #[inline]
104    pub fn is_negative(self) -> bool {
105        self.mantissa < 0
106    }
107}