devela/text/fmt/num_to_str/
mod.rs

1// devela::text::fmt::num_to_str
2//
3//! Defines the [`NumToStr`] trait.
4//
5
6crate::_use! {basic::from_utf8}
7#[allow(unused_imports, reason = "±unsafe")]
8use crate::_core::str::from_utf8_unchecked;
9
10#[cfg(test)]
11mod tests;
12
13/// Converts a number into a string representation, storing it into a byte slice.
14///
15/// # Features
16/// It makes use of the `unsafe_str` feature for faster unchecked conversion of
17/// the resulting bytes to a string slice, and of the `dep_simdutf8` dependency.
18///
19#[doc = crate::doc_!(vendor: "numtoa")]
20pub trait NumToStr<T> {
21    /// Given a base for encoding and a mutable byte slice, write the number
22    /// into the byte slice and return the indice where the inner string begins.
23    /// The inner string can be extracted by slicing the byte slice from
24    /// that indice.
25    ///
26    /// # Panics
27    /// If the supplied buffer is smaller than the number of bytes needed to
28    /// write the integer, this will panic. On debug builds, this function will
29    /// perform a check on base 10 conversions to ensure that the input array
30    /// is large enough to hold the largest possible value in digits.
31    ///
32    /// # Example
33    /// ```
34    /// use devela::NumToStr;
35    /// use std::io::{self, Write};
36    ///
37    /// let stdout = io::stdout();
38    /// let stdout = &mut io::stdout();
39    ///
40    /// // Allocate a buffer that will be reused in each iteration.
41    /// let mut buffer = [0u8; 20];
42    ///
43    /// let number = 15325;
44    /// let _ = stdout.write(number.to_bytes_base(10, &mut buffer));
45    ///
46    /// let number = 1241;
47    /// let _ = stdout.write(number.to_bytes_base(10, &mut buffer));
48    ///
49    /// assert_eq!(12345.to_bytes_base(10, &mut buffer), b"12345");
50    /// ```
51    fn to_bytes_base(self, base: T, string: &mut [u8]) -> &[u8];
52
53    /// Convenience method for quickly getting a string from the input's array buffer.
54    fn to_str_base(self, base: T, buf: &mut [u8]) -> &str;
55}
56
57// A lookup table to prevent the need for conditional branching
58// The value of the remainder of each step will be used as the index
59const LOOKUP: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
60
61// A lookup table optimized for decimal lookups.
62// Each two indices represents one possible number.
63const DEC_LOOKUP: &[u8; 200] = b"0001020304050607080910111213141516171819\
64                                 2021222324252627282930313233343536373839\
65                                 4041424344454647484950515253545556575859\
66                                 6061626364656667686970717273747576777879\
67                                 8081828384858687888990919293949596979899";
68
69macro_rules! base_10 {
70    ($number:ident, $index:ident, $string:ident) => {
71        // Decode four characters at the same time
72        while $number > 9999 {
73            let rem = ($number % 10000) as u16;
74            let (frst, scnd) = ((rem / 100) * 2, (rem % 100) * 2);
75            $string[$index - 3..$index - 1]
76                .copy_from_slice(&DEC_LOOKUP[frst as usize..frst as usize + 2]);
77            $string[$index - 1..$index + 1]
78                .copy_from_slice(&DEC_LOOKUP[scnd as usize..scnd as usize + 2]);
79            $index = $index.wrapping_sub(4);
80            $number /= 10000;
81        }
82
83        if $number > 999 {
84            let (frst, scnd) = (($number / 100) * 2, ($number % 100) * 2);
85            $string[$index - 3..$index - 1]
86                .copy_from_slice(&DEC_LOOKUP[frst as usize..frst as usize + 2]);
87            $string[$index - 1..$index + 1]
88                .copy_from_slice(&DEC_LOOKUP[scnd as usize..scnd as usize + 2]);
89            $index = $index.wrapping_sub(4);
90        } else if $number > 99 {
91            let section = ($number as u16 / 10) * 2;
92            $string[$index - 2..$index]
93                .copy_from_slice(&DEC_LOOKUP[section as usize..section as usize + 2]);
94            $string[$index] = LOOKUP[($number % 10) as usize];
95            $index = $index.wrapping_sub(3);
96        } else if $number > 9 {
97            $number *= 2;
98            $string[$index - 1..$index + 1]
99                .copy_from_slice(&DEC_LOOKUP[$number as usize..$number as usize + 2]);
100            $index = $index.wrapping_sub(2);
101        } else {
102            $string[$index] = LOOKUP[$number as usize];
103            $index = $index.wrapping_sub(1);
104        }
105    };
106}
107
108macro_rules! impl_primitive {
109    ( signed $($t:ty),+ ) => { $( impl_primitive![@signed $t]; )+ };
110    ( unsigned $($t:ty),+ ) => { $( impl_primitive![@unsigned $t]; )+ };
111    (@signed $t:ty) => {
112        impl NumToStr<$t> for $t {
113            fn to_bytes_base(mut self, base: $t, string: &mut [u8]) -> &[u8] {
114                if cfg!(debug_assertions) {
115                    if base == 10 {
116                        match size_of::<$t>() {
117                            2 => debug_assert![string.len() >= 6,
118                                "i16 base 10 conversions require at least 6 bytes"],
119                            4 => debug_assert![string.len() >= 11,
120                                "i32 base 10 conversions require at least 11 bytes"],
121                            8 => debug_assert![string.len() >= 20,
122                                "i64 base 10 conversions require at least 20 bytes"],
123                            _ => unreachable![],
124                        }
125                    }
126                }
127                let mut index = string.len() - 1;
128                let mut is_negative = false;
129                if self < 0 {
130                    is_negative = true;
131                    self = match self.checked_abs() {
132                        Some(value) => value,
133                        None => {
134                            let value = <$t>::MAX;
135                            string[index] = LOOKUP[((value % base + 1) % base) as usize];
136                            index -= 1;
137                            value / base + <$t>::from(value % base == base -1)
138                        }
139                    };
140                } else if self == 0 {
141                    string[index] = b'0';
142                    return &string[index..];
143                }
144                if base == 10 {
145                    // Convert using optimized base 10 algorithm
146                    base_10!(self, index, string);
147                } else {
148                    while self != 0 {
149                        let rem = self % base;
150                        string[index] = LOOKUP[rem as usize];
151                        index = index.wrapping_sub(1);
152                        self /= base;
153                    }
154                }
155                if is_negative {
156                    string[index] = b'-';
157                    index = index.wrapping_sub(1);
158                }
159                &string[index.wrapping_add(1)..]
160            }
161            fn to_str_base(self, base: $t, buf: &mut [u8]) -> &str {
162                #[cfg(any(feature = "safe_text", not(feature = "unsafe_str")))]
163                return from_utf8(self.to_bytes_base(base, buf)).unwrap();
164
165                #[cfg(all(not(feature = "safe_text"), feature = "unsafe_str"))]
166                // SAFETY: the bytes are valid utf-8
167                unsafe { from_utf8_unchecked(self.to_bytes_base(base, buf)) }
168            }
169        }
170    };
171    (@unsigned $t:ty) => {
172        impl NumToStr<$t> for $t {
173            fn to_bytes_base(mut self, base: $t, string: &mut [u8]) -> &[u8] {
174                // Check if the buffer is large enough and panic on debug builds if it isn't
175                if cfg!(debug_assertions) {
176                    if base == 10 {
177                        match size_of::<$t>() {
178                            2 => debug_assert![ string.len() >= 5,
179                                "u16 base 10 conversions require at least 5 bytes"],
180                            4 => debug_assert![ string.len() >= 10,
181                                "u32 base 10 conversions require at least 10 bytes"],
182                            8 => debug_assert![ string.len() >= 20,
183                                "u64 base 10 conversions require at least 20 bytes"],
184                            _ => unreachable![],
185                        }
186                    }
187                }
188                let mut index = string.len() - 1;
189                if self == 0 {
190                    string[index] = b'0';
191                    return &string[index..];
192                }
193                if base == 10 {
194                    // Convert using optimized base 10 algorithm
195                    base_10!(self, index, string);
196                } else {
197                    while self != 0 {
198                        let rem = self % base;
199                        string[index] = LOOKUP[rem as usize];
200                        index = index.wrapping_sub(1);
201                        self /= base;
202                    }
203                }
204                &string[index.wrapping_add(1)..]
205            }
206            fn to_str_base(self, base: $t, buf: &mut [u8]) -> &str {
207                #[cfg(any(feature = "safe_text", not(feature = "unsafe_str")))]
208                return from_utf8(self.to_bytes_base(base, buf)).unwrap();
209
210                #[cfg(all(not(feature = "safe_text"), feature = "unsafe_str"))]
211                // SAFETY: the bytes are valid utf-8
212                unsafe { from_utf8_unchecked(self.to_bytes_base(base, buf)) }
213            }
214        }
215    };
216}
217impl_primitive!(signed i16, i32, i64, isize);
218impl_primitive!(unsigned u16, u32, u64, usize);
219
220impl NumToStr<i8> for i8 {
221    fn to_bytes_base(mut self, base: i8, string: &mut [u8]) -> &[u8] {
222        if cfg!(debug_assertions) && base == 10 {
223            debug_assert!(string.len() >= 4, "i8 conversions need at least 4 bytes");
224        }
225        let mut index = string.len() - 1;
226        let mut is_negative = false;
227        #[allow(clippy::comparison_chain)]
228        if self < 0 {
229            is_negative = true;
230            self = if let Some(value) = self.checked_abs() {
231                value
232            } else {
233                let value = <i8>::MAX;
234                string[index] = LOOKUP[((value % base + 1) % base) as usize];
235                index -= 1;
236                value / base + ((value % base == base - 1) as i8)
237            };
238        } else if self == 0 {
239            string[index] = b'0';
240            return &string[index..];
241        }
242        if base == 10 {
243            if self > 99 {
244                let section = (self / 10) * 2;
245                string[index - 2..index]
246                    .copy_from_slice(&DEC_LOOKUP[section as usize..section as usize + 2]);
247                string[index] = LOOKUP[(self % 10) as usize];
248                index = index.wrapping_sub(3);
249            } else if self > 9 {
250                self *= 2;
251                string[index - 1..index + 1]
252                    .copy_from_slice(&DEC_LOOKUP[self as usize..self as usize + 2]);
253                index = index.wrapping_sub(2);
254            } else {
255                string[index] = LOOKUP[self as usize];
256                index = index.wrapping_sub(1);
257            }
258        } else {
259            while self != 0 {
260                let rem = self % base;
261                string[index] = LOOKUP[rem as usize];
262                index = index.wrapping_sub(1);
263                self /= base;
264            }
265        }
266        if is_negative {
267            string[index] = b'-';
268            index = index.wrapping_sub(1);
269        }
270        &string[index.wrapping_add(1)..]
271    }
272
273    fn to_str_base(self, base: Self, buf: &mut [u8]) -> &str {
274        #[cfg(any(feature = "safe_text", not(feature = "unsafe_str")))]
275        return from_utf8(self.to_bytes_base(base, buf)).unwrap();
276
277        #[cfg(all(not(feature = "safe_text"), feature = "unsafe_str"))]
278        // SAFETY: the bytes are valid utf-8
279        unsafe {
280            from_utf8_unchecked(self.to_bytes_base(base, buf))
281        }
282    }
283}
284
285impl NumToStr<u8> for u8 {
286    fn to_bytes_base(mut self, base: u8, string: &mut [u8]) -> &[u8] {
287        if cfg!(debug_assertions) && base == 10 {
288            debug_assert!(string.len() >= 3, "u8 conversions need at least 3 bytes");
289        }
290        let mut index = string.len() - 1;
291        if self == 0 {
292            string[index] = b'0';
293            return &string[index..];
294        }
295        if base == 10 {
296            if self > 99 {
297                let section = (self / 10) * 2;
298                string[index - 2..index]
299                    .copy_from_slice(&DEC_LOOKUP[section as usize..section as usize + 2]);
300                string[index] = LOOKUP[(self % 10) as usize];
301                index = index.wrapping_sub(3);
302            } else if self > 9 {
303                self *= 2;
304                string[index - 1..index + 1]
305                    .copy_from_slice(&DEC_LOOKUP[self as usize..self as usize + 2]);
306                index = index.wrapping_sub(2);
307            } else {
308                string[index] = LOOKUP[self as usize];
309                index = index.wrapping_sub(1);
310            }
311        } else {
312            while self != 0 {
313                let rem = self % base;
314                string[index] = LOOKUP[rem as usize];
315                index = index.wrapping_sub(1);
316                self /= base;
317            }
318        }
319        &string[index.wrapping_add(1)..]
320    }
321
322    fn to_str_base(self, base: Self, buf: &mut [u8]) -> &str {
323        #[cfg(any(feature = "safe_text", not(feature = "unsafe_str")))]
324        return from_utf8(self.to_bytes_base(base, buf)).unwrap();
325        #[cfg(all(not(feature = "safe_text"), feature = "unsafe_str"))]
326        // SAFETY: the bytes are valid utf-8
327        unsafe {
328            from_utf8_unchecked(self.to_bytes_base(base, buf))
329        }
330    }
331}