Skip to main content

decimal_scaled/
display.rs

1//! [`core::fmt`] formatters and [`core::str::FromStr`] for [`I128`].
2//!
3//! # Display format
4//!
5//! [`fmt::Display`] formats as a base-10 decimal literal: integer digits,
6//! a `.`, then exactly `SCALE` fractional digits (trailing zeros are always
7//! emitted). At `SCALE = 12`, `1.5` displays as `1.500000000000`. The output
8//! is bit-faithful: parsing it back through [`FromStr`] returns the identical
9//! storage value.
10//!
11//! # Debug format
12//!
13//! [`fmt::Debug`] wraps the [`fmt::Display`] output with a scale annotation:
14//! `I128<SCALE>(...)`. This replaces the default derived format, which would
15//! show only the raw `i128` storage.
16//!
17//! # Scientific notation
18//!
19//! [`fmt::LowerExp`] and [`fmt::UpperExp`] emit scientific notation (`1.5e0`
20//! / `1.5E0`). Trailing zeros in the mantissa are stripped.
21//!
22//! # Storage-level radix formats
23//!
24//! [`fmt::LowerHex`], [`fmt::UpperHex`], [`fmt::Octal`], and [`fmt::Binary`]
25//! format the **raw `i128` storage** (= `value * 10^SCALE`), not the decimal
26//! value. For example, `I128s12::ONE` (storage `10^12`) prints in lower-hex
27//! as `e8d4a51000`.
28//!
29//! # FromStr
30//!
31//! Parses canonical decimal literals. Accepted forms:
32//! - Integer-only: `42` parses as `42 * 10^SCALE`.
33//! - Decimal with up to `SCALE` fractional digits: `1.5`, `1.500`.
34//! - Optional sign prefix: `-` or `+`.
35//! - Bare zero: `0` or `0.0`.
36//!
37//! Rejected forms (with the corresponding [`ParseDecimalError`] variant):
38//! - Empty string: [`ParseDecimalError::Empty`].
39//! - Sign with no digits: [`ParseDecimalError::SignOnly`].
40//! - Redundant leading zeros (`01`, `00`): [`ParseDecimalError::LeadingZero`].
41//! - More than `SCALE` fractional digits: [`ParseDecimalError::OverlongFractional`].
42//! - Scientific notation (`1e3`): [`ParseDecimalError::ScientificNotation`].
43//! - Missing digits on either side of the point (`.5`, `5.`):
44//!   [`ParseDecimalError::MissingDigits`].
45//! - Non-digit, non-sign, non-dot characters: [`ParseDecimalError::InvalidChar`].
46//! - Magnitudes outside `[I128::MIN, I128::MAX]`: [`ParseDecimalError::OutOfRange`].
47
48use core::fmt;
49use core::str::FromStr;
50
51use crate::core_type::{ParseDecimalError, I128};
52
53#[cfg(feature = "alloc")]
54extern crate alloc;
55
56// ──────────────────────────────────────────────────────────────────────
57// Display -- canonical decimal string
58// ──────────────────────────────────────────────────────────────────────
59
60impl<const SCALE: u32> fmt::Display for I128<SCALE> {
61    /// Formats the value as a canonical decimal string.
62    ///
63    /// Always emits exactly `SCALE` fractional digits. The integer and
64    /// fractional parts are derived from integer division of the unsigned
65    /// magnitude, so `i128::MIN` (whose absolute value overflows `i128`)
66    /// is handled correctly via `unsigned_abs`.
67    ///
68    /// # Precision
69    ///
70    /// Strict: all arithmetic is integer-only; result is bit-exact.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use decimal_scaled::I128s12;
76    ///
77    /// let v = I128s12::from_bits(1_500_000_000_000);
78    /// assert_eq!(v.to_string(), "1.500000000000");
79    ///
80    /// let neg = I128s12::from_bits(-1_500_000_000_000);
81    /// assert_eq!(neg.to_string(), "-1.500000000000");
82    /// ```
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        let raw = self.0;
85        let negative = raw < 0;
86        // `unsigned_abs` is the only correct way to get |i128::MIN| as
87        // a u128; `i128::abs` would panic on MIN.
88        let mag: u128 = raw.unsigned_abs();
89        let multiplier: u128 = 10u128.pow(SCALE);
90        let int_part = mag / multiplier;
91        let frac_part = mag % multiplier;
92
93        if negative {
94            f.write_str("-")?;
95        }
96        // SCALE = 0: no fractional part and no decimal point.
97        if SCALE == 0 {
98            return write!(f, "{int_part}");
99        }
100        let scale_usize = SCALE as usize;
101        write!(f, "{int_part}.{frac_part:0>width$}", width = scale_usize)
102    }
103}
104
105// ──────────────────────────────────────────────────────────────────────
106// Debug -- Display + SCALE annotation
107// ──────────────────────────────────────────────────────────────────────
108
109impl<const SCALE: u32> fmt::Debug for I128<SCALE> {
110    /// Formats as `I128<SCALE>(<canonical decimal>)`.
111    ///
112    /// Delegates to [`fmt::Display`] so the output shows the human-readable
113    /// decimal value rather than the raw `i128` storage.
114    ///
115    /// # Precision
116    ///
117    /// Strict: all arithmetic is integer-only; result is bit-exact.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use decimal_scaled::I128s12;
123    ///
124    /// let v = I128s12::from_bits(1_500_000_000_000);
125    /// assert_eq!(format!("{v:?}"), "I128<12>(1.500000000000)");
126    /// ```
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "I128<{SCALE}>({self})")
129    }
130}
131
132// ──────────────────────────────────────────────────────────────────────
133// LowerExp / UpperExp -- scientific notation
134// ──────────────────────────────────────────────────────────────────────
135
136impl<const SCALE: u32> fmt::LowerExp for I128<SCALE> {
137    /// Formats the value in scientific notation with a lowercase `e`.
138    ///
139    /// Trailing zeros in the mantissa are stripped, so `1.500000000000`
140    /// formats as `1.5e0`. Zero formats as `0e0`.
141    ///
142    /// # Precision
143    ///
144    /// Strict: all arithmetic is integer-only; result is bit-exact.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use decimal_scaled::I128s12;
150    ///
151    /// let v = I128s12::from_bits(1_500_000_000_000);
152    /// assert_eq!(format!("{v:e}"), "1.5e0");
153    ///
154    /// let sub = I128s12::from_bits(1_500_000_000);
155    /// assert_eq!(format!("{sub:e}"), "1.5e-3");
156    /// ```
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        format_exp(self.0, SCALE, false, f)
159    }
160}
161
162impl<const SCALE: u32> fmt::UpperExp for I128<SCALE> {
163    /// Formats the value in scientific notation with an uppercase `E`.
164    ///
165    /// Identical to [`fmt::LowerExp`] except the exponent separator is `E`.
166    ///
167    /// # Precision
168    ///
169    /// Strict: all arithmetic is integer-only; result is bit-exact.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use decimal_scaled::I128s12;
175    ///
176    /// let v = I128s12::from_bits(1_500_000_000_000);
177    /// assert_eq!(format!("{v:E}"), "1.5E0");
178    /// ```
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        format_exp(self.0, SCALE, true, f)
181    }
182}
183
184/// Shared implementation for `LowerExp` and `UpperExp`.
185///
186/// Builds the decimal digit string in a fixed 40-byte stack buffer
187/// (a `u128` has at most 39 digits) so no heap allocation is needed.
188///
189/// # Precision
190///
191/// Strict: all arithmetic is integer-only; result is bit-exact.
192fn format_exp(raw: i128, scale: u32, upper: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193    let exp_char = if upper { 'E' } else { 'e' };
194    if raw == 0 {
195        return write!(f, "0{exp_char}0");
196    }
197    let negative = raw < 0;
198    let mag: u128 = raw.unsigned_abs();
199
200    // Collect decimal digits of `mag` LSB-first into the buffer,
201    // then reverse to get MSB-first order.
202    let mut buf = [0u8; 40];
203    let mut len = 0usize;
204    let mut n = mag;
205    while n > 0 {
206        let digit = (n % 10) as u8;
207        buf[len] = b'0' + digit;
208        len += 1;
209        n /= 10;
210    }
211    buf[..len].reverse();
212    let digits = &buf[..len];
213
214    // The decimal exponent for the leading digit is `(len - 1) - scale`.
215    let exp: i32 = (len as i32 - 1) - scale as i32;
216
217    // Strip trailing zeros from the mantissa digit string.
218    let mut frac_end = len;
219    while frac_end > 1 && digits[frac_end - 1] == b'0' {
220        frac_end -= 1;
221    }
222    let mantissa_int = digits[0] as char;
223    let mantissa_frac = &digits[1..frac_end];
224
225    if negative {
226        f.write_str("-")?;
227    }
228    if mantissa_frac.is_empty() {
229        // Single-digit mantissa: emit without a decimal point.
230        write!(f, "{mantissa_int}{exp_char}{exp}")
231    } else {
232        f.write_fmt(format_args!("{mantissa_int}."))?;
233        // mantissa_frac contains only ASCII digit bytes; from_utf8 cannot fail.
234        let frac_str = core::str::from_utf8(mantissa_frac).map_err(|_| fmt::Error)?;
235        write!(f, "{frac_str}{exp_char}{exp}")
236    }
237}
238
239// ──────────────────────────────────────────────────────────────────────
240// Storage hex / octal / binary -- delegate to i128
241// ──────────────────────────────────────────────────────────────────────
242//
243// These format the raw i128 storage (= value * 10^SCALE), not the
244// decimal value. Useful for inspecting the bit pattern of the storage.
245
246impl<const SCALE: u32> fmt::LowerHex for I128<SCALE> {
247    /// Formats the **raw `i128` storage** (= `value * 10^SCALE`) as lowercase hex.
248    ///
249    /// This is a bit-level view of the storage, not a hex encoding of the
250    /// decimal value. All standard format flags (`#`, `0`, width, precision)
251    /// are forwarded to the underlying `i128` formatter.
252    ///
253    /// # Precision
254    ///
255    /// Strict: all arithmetic is integer-only; result is bit-exact.
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// use decimal_scaled::I128s12;
261    ///
262    /// // Storage for 1.0 at SCALE=12 is 10^12 = 0xe8d4a51000.
263    /// assert_eq!(format!("{:x}", I128s12::ONE), "e8d4a51000");
264    /// ```
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        fmt::LowerHex::fmt(&self.0, f)
267    }
268}
269
270impl<const SCALE: u32> fmt::UpperHex for I128<SCALE> {
271    /// Formats the **raw `i128` storage** as uppercase hex.
272    ///
273    /// See [`fmt::LowerHex`] for the storage-versus-value distinction.
274    ///
275    /// # Precision
276    ///
277    /// Strict: all arithmetic is integer-only; result is bit-exact.
278    ///
279    /// # Examples
280    ///
281    /// ```
282    /// use decimal_scaled::I128s12;
283    ///
284    /// assert_eq!(format!("{:X}", I128s12::ONE), "E8D4A51000");
285    /// ```
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        fmt::UpperHex::fmt(&self.0, f)
288    }
289}
290
291impl<const SCALE: u32> fmt::Octal for I128<SCALE> {
292    /// Formats the **raw `i128` storage** in octal.
293    ///
294    /// See [`fmt::LowerHex`] for the storage-versus-value distinction.
295    ///
296    /// # Precision
297    ///
298    /// Strict: all arithmetic is integer-only; result is bit-exact.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// use decimal_scaled::I128s12;
304    ///
305    /// assert_eq!(format!("{:o}", I128s12::ZERO), "0");
306    /// ```
307    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308        fmt::Octal::fmt(&self.0, f)
309    }
310}
311
312impl<const SCALE: u32> fmt::Binary for I128<SCALE> {
313    /// Formats the **raw `i128` storage** in binary.
314    ///
315    /// See [`fmt::LowerHex`] for the storage-versus-value distinction.
316    ///
317    /// # Precision
318    ///
319    /// Strict: all arithmetic is integer-only; result is bit-exact.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use decimal_scaled::I128s12;
325    ///
326    /// // 10^12 in binary is a 40-bit value.
327    /// let s = format!("{:b}", I128s12::ONE);
328    /// assert_eq!(s, "1110100011010100101001010001000000000000");
329    /// ```
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        fmt::Binary::fmt(&self.0, f)
332    }
333}
334
335// ──────────────────────────────────────────────────────────────────────
336// ParseDecimalError -- Display + Error
337// ──────────────────────────────────────────────────────────────────────
338
339impl fmt::Display for ParseDecimalError {
340    /// Formats the error as a short human-readable message.
341    ///
342    /// # Precision
343    ///
344    /// Strict: all arithmetic is integer-only; result is bit-exact.
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        let msg = match self {
347            Self::Empty => "empty input",
348            Self::SignOnly => "sign with no digits",
349            Self::LeadingZero => "redundant leading zero in integer part",
350            Self::OverlongFractional => "fractional part exceeds SCALE digits",
351            Self::ScientificNotation => "scientific notation not accepted",
352            Self::InvalidChar => "invalid character",
353            Self::OutOfRange => "value out of representable range",
354            Self::MissingDigits => "decimal point with no adjacent digits",
355        };
356        f.write_str(msg)
357    }
358}
359
360#[cfg(feature = "std")]
361impl std::error::Error for ParseDecimalError {}
362
363// ──────────────────────────────────────────────────────────────────────
364// FromStr -- canonical decimal parser
365// ──────────────────────────────────────────────────────────────────────
366
367impl<const SCALE: u32> FromStr for I128<SCALE> {
368    type Err = ParseDecimalError;
369
370    /// Parses a canonical decimal literal into `I128<SCALE>`.
371    ///
372    /// Delegates to the internal parser. See the module-level docs for the
373    /// full list of accepted and rejected forms, and [`ParseDecimalError`]
374    /// for the failure variants.
375    ///
376    /// # Precision
377    ///
378    /// Strict: all arithmetic is integer-only; result is bit-exact.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use decimal_scaled::I128s12;
384    ///
385    /// let v: I128s12 = "1.5".parse().unwrap();
386    /// assert_eq!(v.to_bits(), 1_500_000_000_000);
387    ///
388    /// let neg: I128s12 = "-1.5".parse().unwrap();
389    /// assert_eq!(neg.to_bits(), -1_500_000_000_000);
390    /// ```
391    fn from_str(s: &str) -> Result<Self, Self::Err> {
392        parse_decimal::<SCALE>(s)
393    }
394}
395
396/// Core decimal string parser.
397///
398/// Extracted from the trait impl to keep `from_str` small and to centralise
399/// the sign / dot / digit state machine in one place.
400///
401/// # Precision
402///
403/// Strict: all arithmetic is integer-only; result is bit-exact.
404fn parse_decimal<const SCALE: u32>(s: &str) -> Result<I128<SCALE>, ParseDecimalError> {
405    if s.is_empty() {
406        return Err(ParseDecimalError::Empty);
407    }
408
409    let bytes = s.as_bytes();
410    let mut idx = 0usize;
411
412    // Consume an optional leading sign byte.
413    let negative = match bytes[0] {
414        b'-' => {
415            idx += 1;
416            true
417        }
418        b'+' => {
419            idx += 1;
420            false
421        }
422        _ => false,
423    };
424    if idx == bytes.len() {
425        // Sign byte with nothing following it.
426        return Err(ParseDecimalError::SignOnly);
427    }
428
429    // Single forward pass: locate the decimal point; reject scientific
430    // notation and invalid characters immediately.
431    let mut dot_pos: Option<usize> = None;
432    {
433        let mut i = idx;
434        while i < bytes.len() {
435            let c = bytes[i];
436            match c {
437                b'0'..=b'9' => {}
438                b'.' => {
439                    if dot_pos.is_some() {
440                        // A second dot is an invalid character, not a
441                        // missing-digit case.
442                        return Err(ParseDecimalError::InvalidChar);
443                    }
444                    dot_pos = Some(i);
445                }
446                b'e' | b'E' => {
447                    return Err(ParseDecimalError::ScientificNotation);
448                }
449                _ => return Err(ParseDecimalError::InvalidChar),
450            }
451            i += 1;
452        }
453    }
454
455    let (int_str, frac_str) = match dot_pos {
456        Some(p) => (&bytes[idx..p], &bytes[p + 1..]),
457        None => (&bytes[idx..], &[][..]),
458    };
459
460    if dot_pos.is_some() {
461        // Both sides of the dot must have at least one digit.
462        if int_str.is_empty() || frac_str.is_empty() {
463            return Err(ParseDecimalError::MissingDigits);
464        }
465    } else if int_str.is_empty() {
466        return Err(ParseDecimalError::SignOnly);
467    }
468
469    // Allow `0` and `0.x` but reject `00`, `01`, `01.5`.
470    if int_str.len() > 1 && int_str[0] == b'0' {
471        return Err(ParseDecimalError::LeadingZero);
472    }
473
474    // More than SCALE fractional digits would lose precision on round-trip.
475    if frac_str.len() > SCALE as usize {
476        return Err(ParseDecimalError::OverlongFractional);
477    }
478
479    // Accumulate the storage value as u128 (avoids the i128::MIN asymmetry)
480    // and apply the sign at the very end.
481    let multiplier: u128 = 10u128.pow(SCALE);
482
483    // Parse the integer part and scale it by 10^SCALE.
484    let mut int_value: u128 = 0;
485    for &b in int_str {
486        let digit = (b - b'0') as u128;
487        int_value = match int_value.checked_mul(10).and_then(|v| v.checked_add(digit)) {
488            Some(v) => v,
489            None => return Err(ParseDecimalError::OutOfRange),
490        };
491    }
492    let int_scaled = match int_value.checked_mul(multiplier) {
493        Some(v) => v,
494        None => return Err(ParseDecimalError::OutOfRange),
495    };
496
497    // Parse the fractional part, then pad to exactly SCALE digits by
498    // multiplying by 10^(SCALE - frac_len).
499    let mut frac_value: u128 = 0;
500    let frac_len = frac_str.len();
501    for &b in frac_str {
502        let digit = (b - b'0') as u128;
503        frac_value = match frac_value
504            .checked_mul(10)
505            .and_then(|v| v.checked_add(digit))
506        {
507            Some(v) => v,
508            None => return Err(ParseDecimalError::OutOfRange),
509        };
510    }
511    let pad = (SCALE as usize) - frac_len;
512    if pad > 0 {
513        let pad_factor: u128 = 10u128.pow(pad as u32);
514        frac_value = match frac_value.checked_mul(pad_factor) {
515            Some(v) => v,
516            None => return Err(ParseDecimalError::OutOfRange),
517        };
518    }
519
520    let combined = match int_scaled.checked_add(frac_value) {
521        Some(v) => v,
522        None => return Err(ParseDecimalError::OutOfRange),
523    };
524
525    // Convert to i128. The negative branch handles i128::MIN whose absolute
526    // value (i128::MAX + 1) is not representable as a positive i128.
527    let raw: i128 = if negative {
528        let neg_min_abs: u128 = (i128::MAX as u128) + 1;
529        if combined > neg_min_abs {
530            return Err(ParseDecimalError::OutOfRange);
531        }
532        if combined == neg_min_abs {
533            i128::MIN
534        } else {
535            -(combined as i128)
536        }
537    } else {
538        if combined > i128::MAX as u128 {
539            return Err(ParseDecimalError::OutOfRange);
540        }
541        combined as i128
542    };
543
544    Ok(I128::<SCALE>::from_bits(raw))
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use crate::core_type::{I128s12, I128};
551    #[cfg(feature = "alloc")]
552    use alloc::format;
553    #[cfg(feature = "alloc")]
554    use alloc::string::ToString;
555
556    // ── Display ──
557
558    /// ZERO renders as `0.000000000000` at SCALE = 12.
559    #[cfg(feature = "alloc")]
560    #[test]
561    fn display_zero_renders() {
562        assert_eq!(I128s12::ZERO.to_string(), "0.000000000000");
563    }
564
565    /// ONE renders as `1.000000000000` at SCALE = 12.
566    #[cfg(feature = "alloc")]
567    #[test]
568    fn display_one_renders() {
569        assert_eq!(I128s12::ONE.to_string(), "1.000000000000");
570    }
571
572    /// `1.5` renders with full SCALE fractional digits.
573    #[cfg(feature = "alloc")]
574    #[test]
575    fn display_one_point_five_renders() {
576        let v = I128s12::from_bits(1_500_000_000_000);
577        assert_eq!(v.to_string(), "1.500000000000");
578    }
579
580    /// Negative values get a leading `-`.
581    #[cfg(feature = "alloc")]
582    #[test]
583    fn display_negative_renders() {
584        let v = I128s12::from_bits(-1_500_000_000_000);
585        assert_eq!(v.to_string(), "-1.500000000000");
586    }
587
588    /// `0.001` (sub-unit positive) keeps leading-zero fractional.
589    #[cfg(feature = "alloc")]
590    #[test]
591    fn display_subunit_keeps_leading_zeros() {
592        // 0.001 = 1_000_000_000 at SCALE 12
593        let v = I128s12::from_bits(1_000_000_000);
594        assert_eq!(v.to_string(), "0.001000000000");
595    }
596
597    /// MAX renders without panicking. Spot-check the canonical form
598    /// at SCALE 12: `170141183460469231731687303.715884105727`.
599    #[cfg(feature = "alloc")]
600    #[test]
601    fn display_max_does_not_panic() {
602        let s = I128s12::MAX.to_string();
603        assert_eq!(s, "170141183460469231731687303.715884105727");
604    }
605
606    /// MIN renders without panicking. The unsigned-abs path handles
607    /// the i128::MIN special case (|MIN| = MAX + 1, so the trailing
608    /// digit is 8 not 7).
609    #[cfg(feature = "alloc")]
610    #[test]
611    fn display_min_does_not_panic() {
612        let s = I128s12::MIN.to_string();
613        assert_eq!(s, "-170141183460469231731687303.715884105728");
614    }
615
616    /// SCALE = 0 has no decimal point.
617    #[cfg(feature = "alloc")]
618    #[test]
619    fn display_scale_zero_no_dot() {
620        type D0 = I128<0>;
621        assert_eq!(D0::ONE.to_string(), "1");
622        assert_eq!(D0::ZERO.to_string(), "0");
623        assert_eq!(D0::from_bits(-42).to_string(), "-42");
624    }
625
626    // ── Debug ──
627
628    /// Debug delegates to Display + SCALE annotation.
629    #[cfg(feature = "alloc")]
630    #[test]
631    fn debug_includes_scale_and_value() {
632        let v = I128s12::from_bits(1_500_000_000_000);
633        let debug_str = format!("{v:?}");
634        assert_eq!(debug_str, "I128<12>(1.500000000000)");
635    }
636
637    /// Debug on ZERO at a non-12 scale.
638    #[cfg(feature = "alloc")]
639    #[test]
640    fn debug_other_scale() {
641        type D6 = I128<6>;
642        let v = D6::ZERO;
643        assert_eq!(format!("{v:?}"), "I128<6>(0.000000)");
644    }
645
646    // ── LowerExp / UpperExp ──
647
648    /// `1.0` -> `1e0` (single digit mantissa).
649    #[cfg(feature = "alloc")]
650    #[test]
651    fn lower_exp_one() {
652        let v = I128s12::ONE;
653        assert_eq!(format!("{v:e}"), "1e0");
654    }
655
656    /// `1.5` -> `1.5e0`.
657    #[cfg(feature = "alloc")]
658    #[test]
659    fn lower_exp_one_point_five() {
660        let v = I128s12::from_bits(1_500_000_000_000);
661        assert_eq!(format!("{v:e}"), "1.5e0");
662    }
663
664    /// `15.0` -> `1.5e1`.
665    #[cfg(feature = "alloc")]
666    #[test]
667    fn lower_exp_fifteen() {
668        let v = I128s12::from_bits(15_000_000_000_000);
669        assert_eq!(format!("{v:e}"), "1.5e1");
670    }
671
672    /// `0.0` -> `0e0`.
673    #[cfg(feature = "alloc")]
674    #[test]
675    fn lower_exp_zero() {
676        assert_eq!(format!("{:e}", I128s12::ZERO), "0e0");
677    }
678
679    /// Sub-unit value -> negative exponent. `0.0015 = 1.5e-3`.
680    #[cfg(feature = "alloc")]
681    #[test]
682    fn lower_exp_subunit_negative_exponent() {
683        // 0.0015 at SCALE 12 = 1_500_000_000
684        let v = I128s12::from_bits(1_500_000_000);
685        assert_eq!(format!("{v:e}"), "1.5e-3");
686    }
687
688    /// Negative value preserves sign.
689    #[cfg(feature = "alloc")]
690    #[test]
691    fn lower_exp_negative() {
692        let v = I128s12::from_bits(-1_500_000_000_000);
693        assert_eq!(format!("{v:e}"), "-1.5e0");
694    }
695
696    /// UpperExp uses `E`.
697    #[cfg(feature = "alloc")]
698    #[test]
699    fn upper_exp_uses_capital_e() {
700        let v = I128s12::from_bits(1_500_000_000_000);
701        assert_eq!(format!("{v:E}"), "1.5E0");
702    }
703
704    // ── LowerHex / UpperHex / Octal / Binary ──
705
706    /// LowerHex of I128s12::ONE is the hex of 10^12 (= 0xe8d4a51000),
707    /// NOT the hex of `1.0` formatted as a decimal in hex.
708    #[cfg(feature = "alloc")]
709    #[test]
710    fn lower_hex_is_storage() {
711        assert_eq!(format!("{:x}", I128s12::ONE), "e8d4a51000");
712    }
713
714    /// UpperHex of ONE: same digits in upper case.
715    #[cfg(feature = "alloc")]
716    #[test]
717    fn upper_hex_is_storage() {
718        assert_eq!(format!("{:X}", I128s12::ONE), "E8D4A51000");
719    }
720
721    /// Octal of ZERO is `0`.
722    #[cfg(feature = "alloc")]
723    #[test]
724    fn octal_zero() {
725        assert_eq!(format!("{:o}", I128s12::ZERO), "0");
726    }
727
728    /// Binary of ONE has the `10^12` bit pattern (40 bits).
729    #[cfg(feature = "alloc")]
730    #[test]
731    fn binary_one() {
732        // 10^12 in binary: 1110_1000_1101_0100_1010_0101_0001_0000_0000_0000
733        let s = format!("{:b}", I128s12::ONE);
734        assert_eq!(s, "1110100011010100101001010001000000000000");
735    }
736
737    // ── ParseDecimalError Display ──
738
739    #[cfg(feature = "alloc")]
740    #[test]
741    fn parse_error_display_messages() {
742        assert_eq!(ParseDecimalError::Empty.to_string(), "empty input");
743        assert_eq!(
744            ParseDecimalError::SignOnly.to_string(),
745            "sign with no digits"
746        );
747        assert_eq!(
748            ParseDecimalError::LeadingZero.to_string(),
749            "redundant leading zero in integer part"
750        );
751        assert_eq!(
752            ParseDecimalError::OverlongFractional.to_string(),
753            "fractional part exceeds SCALE digits"
754        );
755        assert_eq!(
756            ParseDecimalError::ScientificNotation.to_string(),
757            "scientific notation not accepted"
758        );
759        assert_eq!(
760            ParseDecimalError::InvalidChar.to_string(),
761            "invalid character"
762        );
763        assert_eq!(
764            ParseDecimalError::OutOfRange.to_string(),
765            "value out of representable range"
766        );
767        assert_eq!(
768            ParseDecimalError::MissingDigits.to_string(),
769            "decimal point with no adjacent digits"
770        );
771    }
772
773    // ── FromStr happy path ──
774
775    #[test]
776    fn from_str_zero() {
777        let v: I128s12 = "0".parse().unwrap();
778        assert_eq!(v, I128s12::ZERO);
779        let v: I128s12 = "0.0".parse().unwrap();
780        assert_eq!(v, I128s12::ZERO);
781    }
782
783    #[test]
784    fn from_str_one() {
785        let v: I128s12 = "1".parse().unwrap();
786        assert_eq!(v, I128s12::ONE);
787        let v: I128s12 = "1.0".parse().unwrap();
788        assert_eq!(v, I128s12::ONE);
789    }
790
791    /// Headline base-10 claim: `1.1` parses bit-exact.
792    #[test]
793    fn from_str_one_point_one_parses_exactly() {
794        let v: I128s12 = "1.1".parse().unwrap();
795        assert_eq!(v.to_bits(), 1_100_000_000_000);
796    }
797
798    /// Sign prefix.
799    #[test]
800    fn from_str_signs() {
801        let neg: I128s12 = "-1.5".parse().unwrap();
802        assert_eq!(neg.to_bits(), -1_500_000_000_000);
803
804        let pos: I128s12 = "+1.5".parse().unwrap();
805        assert_eq!(pos.to_bits(), 1_500_000_000_000);
806    }
807
808    /// Fractional with fewer digits than SCALE pads correctly.
809    #[test]
810    fn from_str_short_fractional_pads() {
811        // "0.5" at SCALE 12 -> 5_000_000_000 (= 0.5 * 10^12).
812        let v: I128s12 = "0.5".parse().unwrap();
813        assert_eq!(v.to_bits(), 500_000_000_000);
814    }
815
816    /// Fractional with exactly SCALE digits is the natural form.
817    #[test]
818    fn from_str_full_scale_fractional() {
819        let v: I128s12 = "1.500000000000".parse().unwrap();
820        assert_eq!(v.to_bits(), 1_500_000_000_000);
821    }
822
823    // ── FromStr error paths ──
824
825    #[test]
826    fn from_str_empty_is_err() {
827        let r: Result<I128s12, _> = "".parse();
828        assert_eq!(r, Err(ParseDecimalError::Empty));
829    }
830
831    #[test]
832    fn from_str_sign_only_is_err() {
833        assert_eq!("-".parse::<I128s12>(), Err(ParseDecimalError::SignOnly));
834        assert_eq!("+".parse::<I128s12>(), Err(ParseDecimalError::SignOnly));
835    }
836
837    #[test]
838    fn from_str_leading_zero_is_err() {
839        assert_eq!("01".parse::<I128s12>(), Err(ParseDecimalError::LeadingZero));
840        assert_eq!(
841            "01.5".parse::<I128s12>(),
842            Err(ParseDecimalError::LeadingZero)
843        );
844        assert_eq!("00".parse::<I128s12>(), Err(ParseDecimalError::LeadingZero));
845    }
846
847    #[test]
848    fn from_str_overlong_fractional_is_err() {
849        // SCALE 12, fractional length 13 -> reject.
850        let r: Result<I128s12, _> = "0.1234567890123".parse();
851        assert_eq!(r, Err(ParseDecimalError::OverlongFractional));
852    }
853
854    #[test]
855    fn from_str_scientific_notation_is_err() {
856        assert_eq!(
857            "1e3".parse::<I128s12>(),
858            Err(ParseDecimalError::ScientificNotation)
859        );
860        assert_eq!(
861            "1.5E2".parse::<I128s12>(),
862            Err(ParseDecimalError::ScientificNotation)
863        );
864    }
865
866    #[test]
867    fn from_str_invalid_char_is_err() {
868        assert_eq!(
869            "garbage".parse::<I128s12>(),
870            Err(ParseDecimalError::InvalidChar)
871        );
872        assert_eq!(
873            "1.2x".parse::<I128s12>(),
874            Err(ParseDecimalError::InvalidChar)
875        );
876        assert_eq!(
877            "1..2".parse::<I128s12>(),
878            Err(ParseDecimalError::InvalidChar)
879        );
880    }
881
882    #[test]
883    fn from_str_missing_digits_is_err() {
884        assert_eq!(
885            ".5".parse::<I128s12>(),
886            Err(ParseDecimalError::MissingDigits)
887        );
888        assert_eq!(
889            "5.".parse::<I128s12>(),
890            Err(ParseDecimalError::MissingDigits)
891        );
892        assert_eq!(
893            "-.5".parse::<I128s12>(),
894            Err(ParseDecimalError::MissingDigits)
895        );
896    }
897
898    #[test]
899    fn from_str_out_of_range_is_err() {
900        // 10^39 > i128::MAX (~1.7e38). At SCALE 12, the maximum
901        // integer part is i128::MAX / 10^12 ~= 1.7e26, so an integer
902        // part of 1e27 already overflows.
903        let r: Result<I128s12, _> = "1000000000000000000000000000".parse();
904        assert_eq!(r, Err(ParseDecimalError::OutOfRange));
905    }
906
907    /// Parse exactly at i128::MIN -- the asymmetric two's-complement
908    /// boundary. At SCALE 12:
909    /// `i128::MIN = -170141183460469231731687303715884105728`
910    /// which splits into integer `170141183460469231731687303` and
911    /// fractional `715884105728` (the negative form has the same
912    /// digits since |MIN| = MAX + 1).
913    #[test]
914    fn from_str_i128_min_boundary() {
915        let s = "-170141183460469231731687303.715884105728";
916        let v: I128s12 = s.parse().unwrap();
917        assert_eq!(v.to_bits(), i128::MIN);
918    }
919
920    /// Parse exactly at i128::MAX boundary. At SCALE 12 the canonical
921    /// form is `170141183460469231731687303.715884105727`.
922    #[test]
923    fn from_str_i128_max_boundary() {
924        let s = "170141183460469231731687303.715884105727";
925        let v: I128s12 = s.parse().unwrap();
926        assert_eq!(v.to_bits(), i128::MAX);
927    }
928
929    /// One-past-MAX positive overflows.
930    #[test]
931    fn from_str_just_above_max_overflows() {
932        // ...728 is one fractional LSB above i128::MAX.
933        let s = "170141183460469231731687303.715884105728";
934        let r: Result<I128s12, _> = s.parse();
935        assert_eq!(r, Err(ParseDecimalError::OutOfRange));
936    }
937
938    // ── Property tests: parse(value.to_string()) round-trip ──
939
940    /// Round-trip property for representative storage values.
941    /// Uses safe-decimal-test-values (no clippy approx_constant traps).
942    #[cfg(feature = "alloc")]
943    #[test]
944    fn round_trip_representative_values() {
945        let cases: &[i128] = &[
946            0,
947            1,
948            -1,
949            1_000_000_000_000, // 1.0
950            -1_000_000_000_000,
951            1_500_000_000_000, // 1.5
952            -1_500_000_000_000,
953            1_100_000_000_000, // 1.1 (the headline base-10 claim)
954            2_200_000_000_000, // 2.2
955            3_300_000_000_000, // 3.3
956            // Safe arbitrary-looking literal (avoids approx_constant
957            // triggers like 3.14, 2.718, 1.414 etc.):
958            1_234_567_890_123, // ~1.234567890123
959            -1_234_567_890_123,
960            4_567_891_234_567, // ~4.567891234567
961            7_890_123_456_789, // ~7.890123456789
962            i128::MAX,
963            i128::MIN,
964            i128::MAX / 2,
965            i128::MIN / 2,
966        ];
967        for &raw in cases {
968            let v = I128s12::from_bits(raw);
969            let s = v.to_string();
970            let parsed: I128s12 = s.parse().unwrap_or_else(|e| {
971                panic!("round-trip parse failed for raw={raw}, s={s:?}, err={e:?}")
972            });
973            assert_eq!(
974                parsed.to_bits(),
975                raw,
976                "round-trip mismatch: raw={raw}, s={s:?}, parsed_bits={}",
977                parsed.to_bits()
978            );
979        }
980    }
981
982    /// Round-trip property at SCALE = 6 to exercise the const-generic
983    /// path away from the v1 SCALE = 12.
984    #[cfg(feature = "alloc")]
985    #[test]
986    fn round_trip_other_scale() {
987        type D6 = I128<6>;
988        let cases: &[i128] = &[
989            0,
990            1,
991            -1,
992            1_000_000,
993            -1_000_000,
994            1_500_000,
995            i128::MAX,
996            i128::MIN,
997        ];
998        for &raw in cases {
999            let v = D6::from_bits(raw);
1000            let s = v.to_string();
1001            let parsed: D6 = s.parse().expect("round-trip parse");
1002            assert_eq!(
1003                parsed.to_bits(),
1004                raw,
1005                "round-trip mismatch at SCALE=6, raw={raw}"
1006            );
1007        }
1008    }
1009
1010    /// Round-trip at SCALE = 0 (integer-only) to exercise the
1011    /// no-decimal-point path.
1012    #[cfg(feature = "alloc")]
1013    #[test]
1014    fn round_trip_scale_zero() {
1015        type D0 = I128<0>;
1016        let cases: &[i128] = &[0, 1, -1, 42, -42, i128::MAX, i128::MIN];
1017        for &raw in cases {
1018            let v = D0::from_bits(raw);
1019            let s = v.to_string();
1020            let parsed: D0 = s.parse().expect("round-trip parse");
1021            assert_eq!(
1022                parsed.to_bits(),
1023                raw,
1024                "round-trip mismatch at SCALE=0, raw={raw}"
1025            );
1026        }
1027    }
1028}