Skip to main content

fixed_bigint/heapless/
string_conversion.rs

1//! `Display`, `LowerHex`/`UpperHex`, `FromStr`, and `num_traits::Num` for
2//! `HeaplessBigInt<T, CAP, Nct>`.
3//!
4//! All Nct-only, mirroring `FixedUInt`: decimal/hex rendering and radix
5//! parsing walk limb content, which is not constant-time. `Display` and hex
6//! are feature-independent — digit extraction goes through the `MachineWord`
7//! `const_num_traits::ToPrimitive` supertrait, not `num_traits` — while
8//! `Num`/`FromStr` are gated behind `num-traits`.
9//!
10//! Rendering is over the value width (`self.len`), never `CAP`, so a value at
11//! `len = k` prints exactly what the same-width `FixedUInt<T, k>` prints.
12
13use super::HeaplessBigInt;
14use crate::MachineWord;
15use const_num_traits::{CarryingMul, Nct, ToPrimitive, Zero};
16use core::fmt::Write;
17
18impl<T, const CAP: usize> core::fmt::Display for HeaplessBigInt<T, CAP, Nct>
19where
20    T: MachineWord + CarryingMul<Unsigned = T, Output = T>,
21{
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        // A limb is at most u64 (the widest MachineWord), whose decimal form
24        // is <= 20 digits; `CAP` such blocks cover the widest value. (A
25        // per-limb `size_of::<T>() * 3` would need generic_const_exprs.)
26        const MAX_DIGITS: usize = 20;
27
28        if <Self as Zero>::is_zero(self) {
29            return f.write_char('0');
30        }
31
32        // Worst-case decimal length is bounded by the value width (`len` ≤
33        // `CAP`), so `CAP` blocks of `MAX_DIGITS` always suffice.
34        let mut digit_blocks = [[0u8; MAX_DIGITS]; CAP];
35        let mut digit_count = 0;
36        let mut number = *self;
37        let ten = Self::from(10u8);
38
39        while !<Self as Zero>::is_zero(&number) && digit_count < CAP * MAX_DIGITS {
40            let (quotient, remainder) = number.div_rem(&ten);
41            // remainder < 10, held in the low limb (zero-tail gives 0 at len 0).
42            let digit = <T as ToPrimitive>::to_u8(&remainder.limbs[0]).unwrap_or(0);
43            digit_blocks[digit_count / MAX_DIGITS][digit_count % MAX_DIGITS] = b'0' + digit;
44            digit_count += 1;
45            number = quotient;
46        }
47
48        for i in (0..digit_count).rev() {
49            f.write_char(digit_blocks[i / MAX_DIGITS][i % MAX_DIGITS] as char)?;
50        }
51        Ok(())
52    }
53}
54
55impl<T, const CAP: usize> HeaplessBigInt<T, CAP, Nct>
56where
57    T: MachineWord,
58{
59    // MSB-to-LSB over the value width, suppressing leading-zero nibbles. Zero
60    // renders empty (as `FixedUInt` does); callers that need "0" use `Display`.
61    // `to_be_bytes` gives each limb's bytes most-significant-first directly.
62    fn hex_fmt(
63        &self,
64        formatter: &mut core::fmt::Formatter<'_>,
65        uppercase: bool,
66    ) -> core::fmt::Result {
67        fn to_casedigit(byte: u8, uppercase: bool) -> Result<char, core::fmt::Error> {
68            let digit = core::char::from_digit(byte as u32, 16).ok_or(core::fmt::Error)?;
69            if uppercase {
70                digit.to_uppercase().next().ok_or(core::fmt::Error)
71            } else {
72                digit.to_lowercase().next().ok_or(core::fmt::Error)
73            }
74        }
75
76        let mut leading_zero = true;
77        let mut maybe_write = |nibble: char| -> core::fmt::Result {
78            leading_zero &= nibble == '0';
79            if !leading_zero {
80                formatter.write_char(nibble)?;
81            }
82            Ok(())
83        };
84
85        for index in (0..self.len as usize).rev() {
86            for &byte in self.limbs[index].to_be_bytes().as_ref() {
87                maybe_write(to_casedigit((byte & 0xf0) >> 4, uppercase)?)?;
88                maybe_write(to_casedigit(byte & 0x0f, uppercase)?)?;
89            }
90        }
91        Ok(())
92    }
93}
94
95impl<T, const CAP: usize> core::fmt::UpperHex for HeaplessBigInt<T, CAP, Nct>
96where
97    T: MachineWord,
98{
99    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100        self.hex_fmt(f, true)
101    }
102}
103
104impl<T, const CAP: usize> core::fmt::LowerHex for HeaplessBigInt<T, CAP, Nct>
105where
106    T: MachineWord,
107{
108    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109        self.hex_fmt(f, false)
110    }
111}
112
113#[cfg(feature = "num-traits")]
114impl<T, const CAP: usize> num_traits::Num for HeaplessBigInt<T, CAP, Nct>
115where
116    T: MachineWord + CarryingMul<Unsigned = T, Output = T>,
117{
118    type FromStrRadixErr = core::num::ParseIntError;
119
120    fn from_str_radix(input: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
121        use crate::fixeduint::{make_empty_error, make_overflow_err, make_parse_int_err};
122        use const_num_traits::{CheckedAdd, CheckedMul};
123
124        if input.is_empty() {
125            return Err(make_empty_error());
126        }
127        if !(2..=16).contains(&radix) {
128            return Err(make_overflow_err());
129        }
130
131        // Accumulate at the full CAP width, not the minimal width of the
132        // intermediate `from(digit)` values — otherwise `ret * radix + digit`
133        // would overflow at a single word instead of the carrier's capacity
134        // (matching `FixedUInt<T, CAP>`, whose parse width is its `N`).
135        let mut ret = <Self as Zero>::zero().widened(CAP as u16);
136        let range = match input.find(|c: char| c != '0') {
137            Some(x) => &input[x..],
138            _ => input,
139        };
140        let radix_val = Self::from(radix as u8);
141        for c in range.chars() {
142            let digit = c.to_digit(radix).ok_or_else(make_parse_int_err)?;
143            ret = CheckedMul::checked_mul(ret, radix_val).ok_or_else(make_overflow_err)?;
144            ret = CheckedAdd::checked_add(ret, Self::from(digit as u8))
145                .ok_or_else(make_overflow_err)?;
146        }
147        Ok(ret)
148    }
149}
150
151#[cfg(feature = "num-traits")]
152impl<T, const CAP: usize> core::str::FromStr for HeaplessBigInt<T, CAP, Nct>
153where
154    T: MachineWord + CarryingMul<Unsigned = T, Output = T>,
155{
156    type Err = core::num::ParseIntError;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        <Self as num_traits::Num>::from_str_radix(s, 10)
160    }
161}