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);