Skip to main content

fermat_core/
convert.rs

1//! Conversions between `Decimal` and primitive types.
2//!
3//! ## Supported Conversions
4//!
5//! | From/To          | Method                       | Notes                               |
6//! |------------------|------------------------------|-------------------------------------|
7//! | `u64` → Decimal  | `Decimal::from_u64`          | scale = 0                           |
8//! | `i64` → Decimal  | `Decimal::from_i64`          | scale = 0                           |
9//! | `u128` → Decimal | `Decimal::from_u128`         | scale = 0, fails if > i128::MAX     |
10//! | `i128` → Decimal | `Decimal::from_i128`         | scale = 0                           |
11//! | Decimal → `i128` | `Decimal::to_i128_truncated` | truncates toward zero               |
12//! | Decimal → `u64`  | `Decimal::to_u64_truncated`  | fails if negative or overflows u64  |
13//! | `str` → Decimal  | `Decimal::from_str_exact`    | parses decimal notation             |
14
15use crate::arithmetic::{pow10, POW10};
16use crate::decimal::{Decimal, MAX_SCALE};
17use crate::error::ArithmeticError;
18
19// ─── From primitives ─────────────────────────────────────────────────────────
20
21impl Decimal {
22    /// Create a `Decimal` with scale 0 from a `u64`.
23    #[inline]
24    pub fn from_u64(v: u64) -> Self {
25        Decimal::new_unchecked(v as i128, 0)
26    }
27
28    /// Create a `Decimal` with scale 0 from an `i64`.
29    #[inline]
30    pub fn from_i64(v: i64) -> Self {
31        Decimal::new_unchecked(v as i128, 0)
32    }
33
34    /// Create a `Decimal` with scale 0 from a `u128`.
35    ///
36    /// Returns `Err(Overflow)` if `v > i128::MAX`.
37    pub fn from_u128(v: u128) -> Result<Self, ArithmeticError> {
38        let mantissa = i128::try_from(v).map_err(|_| ArithmeticError::Overflow)?;
39        Decimal::new(mantissa, 0)
40    }
41
42    /// Create a `Decimal` with scale 0 from an `i128`.
43    #[inline]
44    pub fn from_i128(v: i128) -> Self {
45        Decimal::new_unchecked(v, 0)
46    }
47
48    /// Create a `Decimal` from a raw SPL token `amount` and the mint's `decimals`.
49    ///
50    /// `from_token_amount(1_500_000, 6)` represents `1.500000 USDC`.
51    pub fn from_token_amount(amount: u64, decimals: u8) -> Result<Self, ArithmeticError> {
52        Decimal::new(amount as i128, decimals)
53    }
54}
55
56// ─── To primitives ────────────────────────────────────────────────────────────
57
58impl Decimal {
59    /// Truncate toward zero and return the integer part as `i128`.
60    ///
61    /// `Decimal { mantissa: 157, scale: 2 }` (= `1.57`) → `1`
62    pub fn to_i128_truncated(self) -> i128 {
63        if self.scale == 0 {
64            return self.mantissa;
65        }
66        let factor = POW10[self.scale as usize];
67        self.mantissa / factor
68    }
69
70    /// Truncate toward zero and return the integer part as `u64`.
71    ///
72    /// Returns `Err(Overflow)` if the value is negative or exceeds `u64::MAX`.
73    pub fn to_u64_truncated(self) -> Result<u64, ArithmeticError> {
74        let v = self.to_i128_truncated();
75        u64::try_from(v).map_err(|_| ArithmeticError::Overflow)
76    }
77
78    /// Convert to a raw SPL token `u64` amount with explicit rounding.
79    ///
80    /// Rounds to `decimals` decimal places first, then converts to an integer.
81    pub fn to_token_amount(
82        self,
83        decimals: u8,
84        mode: crate::rounding::RoundingMode,
85    ) -> Result<u64, ArithmeticError> {
86        let rounded = self.round(decimals, mode)?;
87        let diff = decimals.saturating_sub(rounded.scale());
88        let factor = pow10(diff)?;
89        let raw = rounded
90            .mantissa()
91            .checked_mul(factor)
92            .ok_or(ArithmeticError::Overflow)?;
93        u64::try_from(raw).map_err(|_| ArithmeticError::Overflow)
94    }
95}
96
97// ─── String parsing ───────────────────────────────────────────────────────────
98
99impl Decimal {
100    /// Parse a decimal string into a `Decimal`.
101    ///
102    /// Accepted formats:
103    /// - `"123"`    → `{ mantissa: 123, scale: 0 }`
104    /// - `"1.23"`   → `{ mantissa: 123, scale: 2 }`
105    /// - `"-1.23"`  → `{ mantissa: -123, scale: 2 }`
106    /// - `"+1.23"`  → `{ mantissa: 123, scale: 2 }`
107    ///
108    /// Returns `Err(ScaleExceeded)` if there are more than 28 fractional digits,
109    /// or `Err(InvalidInput)` for malformed strings.
110    pub fn from_str_exact(s: &str) -> Result<Self, ArithmeticError> {
111        let s = s.trim();
112        if s.is_empty() {
113            return Err(ArithmeticError::InvalidInput);
114        }
115
116        let (negative, rest) = if s.starts_with('-') {
117            (true, &s[1..])
118        } else if s.starts_with('+') {
119            (false, &s[1..])
120        } else {
121            (false, s)
122        };
123
124        if rest.is_empty() {
125            return Err(ArithmeticError::InvalidInput);
126        }
127
128        let (int_part, frac_part, scale) = if let Some(dot) = rest.find('.') {
129            let frac = &rest[dot + 1..];
130            if frac.len() > MAX_SCALE as usize {
131                return Err(ArithmeticError::ScaleExceeded);
132            }
133            (&rest[..dot], frac, frac.len() as u8)
134        } else {
135            (rest, "", 0u8)
136        };
137
138        if int_part.is_empty() && frac_part.is_empty() {
139            return Err(ArithmeticError::InvalidInput);
140        }
141
142        let mut mantissa: i128 = 0;
143
144        for ch in int_part.bytes() {
145            let digit = ch.wrapping_sub(b'0');
146            if digit > 9 {
147                return Err(ArithmeticError::InvalidInput);
148            }
149            mantissa = mantissa
150                .checked_mul(10)
151                .and_then(|m| m.checked_add(digit as i128))
152                .ok_or(ArithmeticError::Overflow)?;
153        }
154
155        for ch in frac_part.bytes() {
156            let digit = ch.wrapping_sub(b'0');
157            if digit > 9 {
158                return Err(ArithmeticError::InvalidInput);
159            }
160            mantissa = mantissa
161                .checked_mul(10)
162                .and_then(|m| m.checked_add(digit as i128))
163                .ok_or(ArithmeticError::Overflow)?;
164        }
165
166        if negative {
167            mantissa = mantissa.checked_neg().ok_or(ArithmeticError::Overflow)?;
168        }
169
170        Decimal::new(mantissa, scale)
171    }
172}
173
174// ─── Tests ───────────────────────────────────────────────────────────────────
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn d(m: i128, s: u8) -> Decimal {
181        Decimal::new(m, s).unwrap()
182    }
183
184    #[test]
185    fn from_u64_basic() {
186        let x = Decimal::from_u64(1_000_000);
187        assert_eq!(x.mantissa(), 1_000_000);
188        assert_eq!(x.scale(), 0);
189    }
190
191    #[test]
192    fn from_i64_negative() {
193        let x = Decimal::from_i64(-42);
194        assert_eq!(x.mantissa(), -42);
195        assert_eq!(x.scale(), 0);
196    }
197
198    #[test]
199    fn from_u128_fits() {
200        assert!(Decimal::from_u128(u128::from(u64::MAX)).is_ok());
201    }
202
203    #[test]
204    fn from_u128_overflow() {
205        assert!(Decimal::from_u128((i128::MAX as u128) + 1).is_err());
206    }
207
208    #[test]
209    fn from_token_amount() {
210        let x = Decimal::from_token_amount(1_500_000, 6).unwrap();
211        assert_eq!(x.mantissa(), 1_500_000);
212        assert_eq!(x.scale(), 6);
213    }
214
215    #[test]
216    fn to_i128_truncated_no_scale() {
217        assert_eq!(d(42, 0).to_i128_truncated(), 42);
218    }
219
220    #[test]
221    fn to_i128_truncated_rounds_toward_zero() {
222        assert_eq!(d(157, 2).to_i128_truncated(), 1);
223        assert_eq!(d(-157, 2).to_i128_truncated(), -1);
224    }
225
226    #[test]
227    fn to_u64_truncated_positive() {
228        assert_eq!(d(157, 2).to_u64_truncated().unwrap(), 1u64);
229    }
230
231    #[test]
232    fn to_u64_truncated_negative_fails() {
233        assert!(d(-1, 0).to_u64_truncated().is_err());
234    }
235
236    #[test]
237    fn parse_integer() {
238        assert_eq!(Decimal::from_str_exact("42").unwrap(), d(42, 0));
239    }
240
241    #[test]
242    fn parse_decimal_two_places() {
243        assert_eq!(Decimal::from_str_exact("1.23").unwrap(), d(123, 2));
244    }
245
246    #[test]
247    fn parse_negative_decimal() {
248        assert_eq!(Decimal::from_str_exact("-1.23").unwrap(), d(-123, 2));
249    }
250
251    #[test]
252    fn parse_positive_sign() {
253        assert_eq!(Decimal::from_str_exact("+1.23").unwrap(), d(123, 2));
254    }
255
256    #[test]
257    fn parse_zero_int_part() {
258        assert_eq!(Decimal::from_str_exact("0.001").unwrap(), d(1, 3));
259    }
260
261    #[test]
262    fn parse_trailing_zeros_set_scale() {
263        assert_eq!(Decimal::from_str_exact("1.00").unwrap().scale(), 2);
264    }
265
266    #[test]
267    fn parse_empty_fails() {
268        assert!(Decimal::from_str_exact("").is_err());
269    }
270
271    #[test]
272    fn parse_alpha_fails() {
273        assert!(Decimal::from_str_exact("abc").is_err());
274    }
275
276    #[test]
277    fn parse_too_many_decimals_fails() {
278        let s = "0.00000000000000000000000000001"; // 29 dp
279        assert!(matches!(
280            Decimal::from_str_exact(s),
281            Err(ArithmeticError::ScaleExceeded)
282        ));
283    }
284
285    #[test]
286    fn roundtrip_token_amount() {
287        use crate::rounding::RoundingMode;
288        let x = Decimal::from_token_amount(1_234_567, 6).unwrap();
289        let back = x.to_token_amount(6, RoundingMode::HalfEven).unwrap();
290        assert_eq!(back, 1_234_567u64);
291    }
292}