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}