Skip to main content

nexus_decimal/
convert.rs

1//! String parsing and numeric conversions for `Decimal`.
2//!
3//! Parsing accumulates into i128 for uniform overflow handling across
4//! all backing types, then narrows to the target type.
5//!
6//! Uses SWAR (SIMD Within A Register) to parse 8 ASCII digits in ~6
7//! operations on any 64-bit platform (~1.3ns vs ~2.2ns scalar).
8
9use crate::Decimal;
10#[cfg(feature = "std")]
11use crate::error::ConvertError;
12use crate::error::ParseError;
13use crate::pow10::pow10_i128;
14
15/// Parse exactly 8 ASCII digit bytes into a u64 using SWAR.
16///
17/// Returns `None` if any byte is not an ASCII digit (`0x30..=0x39`).
18/// Uses shift-and-mask with explicit pair/quad/final combination.
19/// Portable — no SIMD intrinsics, works on all 64-bit platforms.
20#[inline(always)]
21fn parse_8_digits(bytes: &[u8; 8]) -> Option<u64> {
22    let chunk = u64::from_le_bytes(*bytes);
23
24    // Validate: all bytes must be ASCII digits (0x30..=0x39)
25    let lower = chunk.wrapping_sub(0x3030_3030_3030_3030);
26    let upper = chunk.wrapping_add(0x4646_4646_4646_4646);
27    if (lower | upper) & 0x8080_8080_8080_8080 != 0 {
28        return None;
29    }
30
31    // Mask to digit values (0-9 per byte). LE layout:
32    // byte0=d1 (first char), byte1=d2, ..., byte7=d8 (last char)
33    let d = chunk & 0x0f0f_0f0f_0f0f_0f0f;
34
35    // Step 1: combine adjacent pairs → 4 × u16
36    // d1*10+d2, d3*10+d4, d5*10+d6, d7*10+d8
37    let lo = d & 0x00ff_00ff_00ff_00ff;
38    let hi = (d >> 8) & 0x00ff_00ff_00ff_00ff;
39    let pairs = lo * 10 + hi;
40
41    // Step 2: combine pairs → 2 × u32
42    let lo2 = pairs & 0x0000_ffff_0000_ffff;
43    let hi2 = (pairs >> 16) & 0x0000_ffff_0000_ffff;
44    let quads = lo2 * 100 + hi2;
45
46    // Step 3: combine to single u64
47    let lo3 = quads & 0x0000_0000_ffff_ffff;
48    let hi3 = quads >> 32;
49    Some(lo3 * 10000 + hi3)
50}
51
52/// Parse a byte slice of ASCII digits into u64, using SWAR for ≥8 digits.
53///
54/// Returns `Err(Overflow)` if the value exceeds u64. Callers that need
55/// wider results should fall back to `parse_digits_wide`.
56#[inline]
57fn parse_digits_u64(bytes: &[u8]) -> Result<u64, ParseError> {
58    let mut result: u64 = 0;
59    let mut pos = 0;
60
61    // SWAR fast path: 8 digits at a time
62    while pos + 8 <= bytes.len() {
63        let chunk: &[u8; 8] = bytes[pos..pos + 8]
64            .try_into()
65            .expect("loop invariant: pos + 8 <= bytes.len()");
66        let val = parse_8_digits(chunk).ok_or(ParseError::InvalidFormat)?;
67        result = result
68            .checked_mul(100_000_000)
69            .and_then(|v| v.checked_add(val))
70            .ok_or(ParseError::Overflow)?;
71        pos += 8;
72    }
73
74    // Scalar tail
75    for &b in &bytes[pos..] {
76        let digit = b.wrapping_sub(b'0');
77        if digit > 9 {
78            return Err(ParseError::InvalidFormat);
79        }
80        result = result
81            .checked_mul(10)
82            .and_then(|v| v.checked_add(digit as u64))
83            .ok_or(ParseError::Overflow)?;
84    }
85
86    Ok(result)
87}
88
89/// Wide fallback: parse into i128 for strings that overflow u64 (>18 digits).
90#[inline]
91fn parse_digits_wide(bytes: &[u8]) -> Result<i128, ParseError> {
92    let mut result: i128 = 0;
93    let mut pos = 0;
94
95    while pos + 8 <= bytes.len() {
96        let chunk: &[u8; 8] = bytes[pos..pos + 8]
97            .try_into()
98            .expect("loop invariant: pos + 8 <= bytes.len()");
99        let val = parse_8_digits(chunk).ok_or(ParseError::InvalidFormat)?;
100        result = result
101            .checked_mul(100_000_000)
102            .and_then(|v| v.checked_add(val as i128))
103            .ok_or(ParseError::Overflow)?;
104        pos += 8;
105    }
106
107    for &b in &bytes[pos..] {
108        let digit = b.wrapping_sub(b'0');
109        if digit > 9 {
110            return Err(ParseError::InvalidFormat);
111        }
112        result = result
113            .checked_mul(10)
114            .and_then(|v| v.checked_add(digit as i128))
115            .ok_or(ParseError::Overflow)?;
116    }
117
118    Ok(result)
119}
120
121macro_rules! impl_decimal_convert {
122    ($backing:ty, $unsigned:ty) => {
123        impl<const D: u8> Decimal<$backing, D> {
124            // ========================================================
125            // String parsing
126            // ========================================================
127
128            /// Parses a decimal string exactly. Rejects inputs with more
129            /// fractional digits than `DECIMALS`.
130            ///
131            /// # Examples
132            ///
133            /// ```
134            /// use nexus_decimal::Decimal;
135            /// type D64 = Decimal<i64, 8>;
136            ///
137            /// let price = D64::from_str_exact("123.45").unwrap();
138            /// assert_eq!(price, D64::new(123, 45_000_000));
139            /// ```
140            pub fn from_str_exact(s: &str) -> Result<Self, ParseError> {
141                Self::parse_str(s.as_bytes(), false)
142            }
143
144            /// Parses a decimal string, rounding excess precision using
145            /// banker's rounding (round half to even).
146            ///
147            /// # Examples
148            ///
149            /// ```
150            /// use nexus_decimal::Decimal;
151            /// type D64 = Decimal<i64, 8>;
152            ///
153            /// // Input has 10 decimal places, D64 has 8 — rounds
154            /// let price = D64::from_str_lossy("1.2345678951").unwrap();
155            /// assert_eq!(price, D64::new(1, 23_456_790)); // rounded up
156            /// ```
157            pub fn from_str_lossy(s: &str) -> Result<Self, ParseError> {
158                Self::parse_str(s.as_bytes(), true)
159            }
160
161            /// Parses from a UTF-8 byte slice.
162            pub fn from_utf8_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
163                Self::parse_str(bytes, false)
164            }
165
166            fn parse_str(bytes: &[u8], lossy: bool) -> Result<Self, ParseError> {
167                if bytes.is_empty() {
168                    return Err(ParseError::InvalidFormat);
169                }
170
171                // Sign
172                let (negative, start) = match bytes[0] {
173                    b'-' => (true, 1),
174                    b'+' => (false, 1),
175                    _ => (false, 0),
176                };
177
178                if start >= bytes.len() {
179                    return Err(ParseError::InvalidFormat);
180                }
181
182                // Find decimal point
183                let dot_pos = bytes[start..].iter().position(|&b| b == b'.');
184
185                let (int_bytes, frac_bytes) = match dot_pos {
186                    Some(pos) => (&bytes[start..start + pos], &bytes[start + pos + 1..]),
187                    None => (&bytes[start..], &b""[..]),
188                };
189
190                // Must have at least one digit somewhere
191                if int_bytes.is_empty() && frac_bytes.is_empty() {
192                    return Err(ParseError::InvalidFormat);
193                }
194
195                // Parse integer part — u64 fast path, i128 fallback
196                let integer_u64 = parse_digits_u64(int_bytes);
197                let scaled_integer: i128 = match integer_u64 {
198                    Ok(v) => {
199                        // Fast path: u64 → i128 widen, then scale
200                        (v as i128)
201                            .checked_mul(Self::SCALE as i128)
202                            .ok_or(ParseError::Overflow)?
203                    }
204                    Err(ParseError::Overflow) => {
205                        // Integer part > u64 — fall back to i128
206                        let wide = parse_digits_wide(int_bytes)?;
207                        wide.checked_mul(Self::SCALE as i128)
208                            .ok_or(ParseError::Overflow)?
209                    }
210                    Err(e) => return Err(e),
211                };
212
213                // Parse fractional part
214                let frac_len = frac_bytes.len();
215                let d = D as usize;
216
217                if !lossy && frac_len > d {
218                    return Err(ParseError::PrecisionLoss);
219                }
220
221                // Parse up to D digits — u64 is always sufficient
222                // (max D=38 digits, but parsed digits ≤ D which for
223                // i64 backing is ≤18, fitting u64 easily)
224                let parse_len = frac_len.min(d);
225                let mut frac_value: i128 = match parse_digits_u64(&frac_bytes[..parse_len]) {
226                    Ok(v) => v as i128,
227                    Err(ParseError::Overflow) => parse_digits_wide(&frac_bytes[..parse_len])?,
228                    Err(e) => return Err(e),
229                };
230
231                // Validate remaining digits are actual digits (even if not used)
232                for &b in &frac_bytes[parse_len..] {
233                    let digit = b.wrapping_sub(b'0');
234                    if digit > 9 {
235                        return Err(ParseError::InvalidFormat);
236                    }
237                }
238
239                // Scale fractional value to fill remaining decimal places
240                if parse_len < d {
241                    let fill_scale = pow10_i128((d - parse_len) as u8);
242                    frac_value *= fill_scale;
243                }
244
245                // Banker's rounding for lossy mode
246                if lossy && frac_len > d {
247                    let rounding_digit = frac_bytes[d].wrapping_sub(b'0');
248
249                    let round_up = if rounding_digit > 5 {
250                        true
251                    } else if rounding_digit < 5 {
252                        false
253                    } else {
254                        // Exactly 5 — check subsequent digits
255                        let has_trailing = frac_bytes[d + 1..].iter().any(|&b| b != b'0');
256                        if has_trailing {
257                            true // > 0.5, round up
258                        } else {
259                            // Exactly 0.5 — banker's: round to even
260                            frac_value % 2 != 0
261                        }
262                    };
263
264                    if round_up {
265                        frac_value += 1;
266                        // Handle carry: if frac_value == SCALE, roll into integer
267                        let scale_i128 = Self::SCALE as i128;
268                        if frac_value >= scale_i128 {
269                            frac_value -= scale_i128;
270                            let carry = scale_i128;
271                            let new_scaled = scaled_integer
272                                .checked_add(carry)
273                                .ok_or(ParseError::Overflow)?;
274                            let abs_value = new_scaled
275                                .checked_add(frac_value)
276                                .ok_or(ParseError::Overflow)?;
277                            let value = if negative {
278                                abs_value.checked_neg().ok_or(ParseError::Overflow)?
279                            } else {
280                                abs_value
281                            };
282                            return Self::narrow(value);
283                        }
284                    }
285                }
286
287                // Combine
288                let abs_value = scaled_integer
289                    .checked_add(frac_value)
290                    .ok_or(ParseError::Overflow)?;
291
292                let value = if negative {
293                    abs_value.checked_neg().ok_or(ParseError::Overflow)?
294                } else {
295                    abs_value
296                };
297
298                Self::narrow(value)
299            }
300
301            /// Narrow an i128 value to the backing type, returning ParseError::Overflow
302            /// if it doesn't fit.
303            #[inline]
304            fn narrow(value: i128) -> Result<Self, ParseError> {
305                if value > <$backing>::MAX as i128 || value < <$backing>::MIN as i128 {
306                    Err(ParseError::Overflow)
307                } else {
308                    Ok(Self {
309                        value: value as $backing,
310                    })
311                }
312            }
313
314            // ========================================================
315            // Integer conversions
316            // ========================================================
317
318            /// Creates a `Decimal` from an `i32`. Returns `None` on overflow.
319            #[inline]
320            pub const fn from_i32(value: i32) -> Option<Self> {
321                let scaled = (value as i128).checked_mul(Self::SCALE as i128);
322                match scaled {
323                    Some(v) if v >= <$backing>::MIN as i128 && v <= <$backing>::MAX as i128 => {
324                        Some(Self {
325                            value: v as $backing,
326                        })
327                    }
328                    _ => None,
329                }
330            }
331
332            /// Creates a `Decimal` from an `i64`. Returns `None` on overflow.
333            #[inline]
334            pub const fn from_i64(value: i64) -> Option<Self> {
335                let scaled = (value as i128).checked_mul(Self::SCALE as i128);
336                match scaled {
337                    Some(v) if v >= <$backing>::MIN as i128 && v <= <$backing>::MAX as i128 => {
338                        Some(Self {
339                            value: v as $backing,
340                        })
341                    }
342                    _ => None,
343                }
344            }
345
346            /// Creates a `Decimal` from a `u32`. Returns `None` on overflow.
347            #[inline]
348            pub const fn from_u32(value: u32) -> Option<Self> {
349                Self::from_i64(value as i64)
350            }
351
352            /// Creates a `Decimal` from a `u64`. Returns `None` on overflow.
353            #[inline]
354            pub const fn from_u64(value: u64) -> Option<Self> {
355                if value > i64::MAX as u64 {
356                    // Could overflow i128 multiplication for large SCALE
357                    let scaled = (value as i128).checked_mul(Self::SCALE as i128);
358                    match scaled {
359                        Some(v) if v <= <$backing>::MAX as i128 => Some(Self {
360                            value: v as $backing,
361                        }),
362                        _ => None,
363                    }
364                } else {
365                    Self::from_i64(value as i64)
366                }
367            }
368
369            /// Constructs a `Decimal` representing `value * 10^-scale`.
370            ///
371            /// Useful for tick sizes and precision-boundary construction.
372            /// For example, `from_scaled(1, 5)` returns a value equal to
373            /// `0.00001`.
374            ///
375            /// Returns `None` if:
376            /// - `scale > D` (would require rounding; use
377            ///   [`from_str_lossy`](Self::from_str_lossy) for a rounding policy)
378            /// - The scaled value overflows the backing type
379            ///
380            /// # Examples
381            ///
382            /// ```
383            /// use nexus_decimal::Decimal;
384            /// type D64 = Decimal<i64, 8>;
385            ///
386            /// let tick = D64::from_scaled(1, 5).unwrap();
387            /// assert_eq!(tick, D64::from_str_exact("0.00001").unwrap());
388            ///
389            /// // scale > D
390            /// assert!(D64::from_scaled(1, 9).is_none());
391            /// ```
392            #[inline]
393            pub const fn from_scaled(value: $backing, scale: u8) -> Option<Self> {
394                if scale > D {
395                    return None;
396                }
397                // 10^scale ≤ 10^D = SCALE, so the divisor fits the backing.
398                // Self::SCALE forces compile-time validation that D fits the backing.
399                let divisor = (10 as $backing).pow(scale as u32);
400                let multiplier = Self::SCALE / divisor;
401                match value.checked_mul(multiplier) {
402                    Some(v) => Some(Self { value: v }),
403                    None => None,
404                }
405            }
406
407            // ========================================================
408            // Float conversions
409            // ========================================================
410
411            /// Converts to `f64`. Exact for values with ≤15 significant digits.
412            #[inline]
413            pub fn to_f64(self) -> f64 {
414                let scale = Self::SCALE as f64;
415                let integer = (self.value / Self::SCALE) as f64;
416                let frac = (self.value % Self::SCALE) as f64 / scale;
417                integer + frac
418            }
419
420            /// Converts to `f32`.
421            #[inline]
422            pub fn to_f32(self) -> f32 {
423                self.to_f64() as f32
424            }
425
426            /// Creates a `Decimal` from an `f64`. Returns error on NaN, Inf, or overflow.
427            ///
428            /// Requires the `std` feature (uses `f64::round()`).
429            #[cfg(feature = "std")]
430            #[inline]
431            pub fn from_f64(value: f64) -> Result<Self, ConvertError> {
432                if !value.is_finite() {
433                    return Err(ConvertError::Overflow);
434                }
435
436                let scaled = value * (Self::SCALE as f64);
437
438                // Bounds check (f64 comparison is safe for this range)
439                if scaled > <$backing>::MAX as f64 || scaled < <$backing>::MIN as f64 {
440                    return Err(ConvertError::Overflow);
441                }
442
443                Ok(Self {
444                    value: scaled.round() as $backing,
445                })
446            }
447
448            /// Creates a `Decimal` from an `f32`.
449            ///
450            /// Requires the `std` feature (uses `f64::round()`).
451            #[cfg(feature = "std")]
452            #[inline]
453            pub fn from_f32(value: f32) -> Result<Self, ConvertError> {
454                Self::from_f64(value as f64)
455            }
456        }
457    };
458}
459
460impl_decimal_convert!(i32, u32);
461impl_decimal_convert!(i64, u64);
462impl_decimal_convert!(i128, u128);