Skip to main content

nexus_decimal/
format.rs

1//! Display, Debug, FromStr, and `write_to_buf` for `Decimal`.
2
3use core::fmt;
4use core::str::FromStr;
5
6use crate::Decimal;
7use crate::error::ParseError;
8
9/// Lookup table for two-digit formatting (Alexandrescu/itoa pattern).
10const DIGIT_PAIRS: &[u8; 200] = b"\
11    00010203040506070809\
12    10111213141516171819\
13    20212223242526272829\
14    30313233343536373839\
15    40414243444546474849\
16    50515253545556575859\
17    60616263646566676869\
18    70717273747576777879\
19    80818283848586878889\
20    90919293949596979899";
21
22macro_rules! impl_decimal_format {
23    ($backing:ty, $unsigned:ty) => {
24        impl<const D: u8> Decimal<$backing, D> {
25            /// Write decimal representation to a byte buffer.
26            ///
27            /// Returns the number of bytes written. Buffer must be at
28            /// least 64 bytes. Useful for wire protocol encoding without
29            /// `fmt` overhead.
30            pub fn write_to_buf(&self, buf: &mut [u8]) -> usize {
31                debug_assert!(buf.len() >= 64);
32
33                if self.value == 0 {
34                    buf[0] = b'0';
35                    return 1;
36                }
37
38                let negative = self.value < 0;
39                let abs = if negative {
40                    self.value.unsigned_abs()
41                } else {
42                    self.value as $unsigned
43                };
44
45                let scale = Self::SCALE as $unsigned;
46                let integer = abs / scale;
47                let frac = abs % scale;
48
49                let mut pos = 0;
50
51                // Sign
52                if negative {
53                    buf[pos] = b'-';
54                    pos += 1;
55                }
56
57                // Integer part → stack buffer using digit pairs, then copy
58                let mut int_buf = [0u8; 40];
59                let mut int_pos = 40;
60                let mut val = integer;
61                while val >= 100 {
62                    int_pos -= 2;
63                    let d = (val % 100) as usize * 2;
64                    int_buf[int_pos] = DIGIT_PAIRS[d];
65                    int_buf[int_pos + 1] = DIGIT_PAIRS[d + 1];
66                    val /= 100;
67                }
68                if val >= 10 {
69                    int_pos -= 2;
70                    let d = val as usize * 2;
71                    int_buf[int_pos] = DIGIT_PAIRS[d];
72                    int_buf[int_pos + 1] = DIGIT_PAIRS[d + 1];
73                } else {
74                    int_pos -= 1;
75                    int_buf[int_pos] = b'0' + val as u8;
76                }
77                let int_len = 40 - int_pos;
78                buf[pos..pos + int_len].copy_from_slice(&int_buf[int_pos..40]);
79                pos += int_len;
80
81                // Fractional part
82                if frac > 0 {
83                    buf[pos] = b'.';
84                    pos += 1;
85
86                    let mut frac_buf = [b'0'; 40];
87                    let mut frac_val = frac;
88                    let mut frac_pos = D as usize;
89
90                    while frac_val >= 100 && frac_pos >= 2 {
91                        frac_pos -= 2;
92                        let d = (frac_val % 100) as usize * 2;
93                        frac_buf[frac_pos] = DIGIT_PAIRS[d];
94                        frac_buf[frac_pos + 1] = DIGIT_PAIRS[d + 1];
95                        frac_val /= 100;
96                    }
97                    while frac_val > 0 && frac_pos > 0 {
98                        frac_pos -= 1;
99                        frac_buf[frac_pos] = b'0' + (frac_val % 10) as u8;
100                        frac_val /= 10;
101                    }
102
103                    // Strip trailing zeros
104                    let mut end = D as usize;
105                    while end > 0 && frac_buf[end - 1] == b'0' {
106                        end -= 1;
107                    }
108
109                    buf[pos..pos + end].copy_from_slice(&frac_buf[..end]);
110                    pos += end;
111                }
112
113                pos
114            }
115        }
116
117        impl<const D: u8> fmt::Display for Decimal<$backing, D> {
118            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119                let mut buf = [0u8; 64];
120                let len = self.write_to_buf(&mut buf);
121                // SAFETY: write_to_buf only writes ASCII digits, '-', and '.'
122                let s = unsafe { core::str::from_utf8_unchecked(&buf[..len]) };
123                f.write_str(s)
124            }
125        }
126
127        impl<const D: u8> fmt::Debug for Decimal<$backing, D> {
128            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129                if f.alternate() {
130                    f.debug_struct("Decimal")
131                        .field("value", &self.value)
132                        .finish()
133                } else {
134                    write!(f, "{self}")
135                }
136            }
137        }
138
139        impl<const D: u8> FromStr for Decimal<$backing, D> {
140            type Err = ParseError;
141
142            #[inline]
143            fn from_str(s: &str) -> Result<Self, ParseError> {
144                Self::from_str_exact(s)
145            }
146        }
147    };
148}
149
150impl_decimal_format!(i32, u32);
151impl_decimal_format!(i64, u64);
152impl_decimal_format!(i128, u128);