Skip to main content

dsp_fixedpoint/
format.rs

1use core::{fmt, fmt::Write, num::Wrapping};
2
3use crate::{AsFloat, Q};
4
5/// ```
6/// # use dsp_fixedpoint::Q8;
7/// let q = Q8::<4>::from_bits(7);
8/// assert_eq!(format!("{q} {q:e} {q:E}"), "0.4375 4.375e-1 4.375E-1");
9/// ```
10macro_rules! impl_fmt {
11    ($tr:path) => {
12        impl<T, A, const F: i8> $tr for Q<T, A, F>
13        where
14            T: AsFloat,
15        {
16            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17                <f64 as $tr>::fmt(&self.as_f64(), f)
18            }
19        }
20    };
21}
22impl_fmt!(fmt::Display);
23impl_fmt!(fmt::UpperExp);
24impl_fmt!(fmt::LowerExp);
25
26#[cfg(feature = "defmt")]
27impl<T, A, const F: i8> defmt::Format for Q<T, A, F>
28where
29    T: AsFloat,
30{
31    fn format(&self, fmt: defmt::Formatter<'_>) {
32        defmt::write!(fmt, "{=f32}", self.as_f32());
33    }
34}
35
36/// Binary, octal, and hexadecimal formatting always include the fixed-point radix point.
37/// Even whole-valued cases keep a trailing `.` to distinguish them from raw integer formatting.
38///
39/// ```
40/// # use dsp_fixedpoint::Q8;
41/// assert_eq!(format!("{:?}", Q8::<4>::from_bits(0x14)), "20");
42/// assert_eq!(
43///     format!("{:#b}", Q8::<3>::from_bits(0b01101001)),
44///     "0b1101.001"
45/// );
46/// assert_eq!(format!("{:x}", Q8::<-2>::from_bits(3)), "c.");
47/// assert_eq!(format!("{:x}", Q8::<4>::from_bits(-0x14)), "-1.4");
48/// ```
49impl<T, A, const F: i8> fmt::Debug for Q<T, A, F>
50where
51    T: fmt::Debug,
52{
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        self.inner.fmt(f)
55    }
56}
57
58trait RadixValue: Copy {
59    fn is_negative(self) -> bool;
60    fn magnitude(self) -> u64;
61}
62
63macro_rules! impl_unsigned_radix_value {
64    ($($ty:ty),* $(,)?) => {
65        $(
66            impl RadixValue for $ty {
67                #[inline]
68                fn is_negative(self) -> bool {
69                    false
70                }
71
72                #[inline]
73                fn magnitude(self) -> u64 {
74                    self as _
75                }
76            }
77        )*
78    };
79}
80
81macro_rules! impl_signed_radix_value {
82    ($($ty:ty),* $(,)?) => {
83        $(
84            impl RadixValue for $ty {
85                #[inline]
86                fn is_negative(self) -> bool {
87                    self.is_negative()
88                }
89
90                #[inline]
91                fn magnitude(self) -> u64 {
92                    self.unsigned_abs() as _
93                }
94            }
95        )*
96    };
97}
98
99macro_rules! impl_wrapping_unsigned_radix_value {
100    ($($ty:ty),* $(,)?) => {
101        $(
102            impl RadixValue for Wrapping<$ty> {
103                #[inline]
104                fn is_negative(self) -> bool {
105                    false
106                }
107
108                #[inline]
109                fn magnitude(self) -> u64 {
110                    self.0 as _
111                }
112            }
113        )*
114    };
115}
116
117macro_rules! impl_wrapping_signed_radix_value {
118    ($($ty:ty),* $(,)?) => {
119        $(
120            impl RadixValue for Wrapping<$ty> {
121                #[inline]
122                fn is_negative(self) -> bool {
123                    self.0.is_negative()
124                }
125
126                #[inline]
127                fn magnitude(self) -> u64 {
128                    self.0.unsigned_abs() as _
129                }
130            }
131        )*
132    };
133}
134
135impl_unsigned_radix_value!(u8, u16, u32, u64);
136impl_signed_radix_value!(i8, i16, i32, i64);
137impl_wrapping_unsigned_radix_value!(u8, u16, u32, u64);
138impl_wrapping_signed_radix_value!(i8, i16, i32, i64);
139
140#[derive(Copy, Clone)]
141struct Radix {
142    bits: u8,
143    table: &'static str,
144}
145
146impl Radix {
147    #[inline]
148    const fn ceil_digits(self, bits: usize) -> usize {
149        bits.div_ceil(self.bits as _)
150    }
151
152    #[inline]
153    const fn shifted_digit(self, magnitude: u64, shift: u8, index: usize) -> char {
154        let mask = (1u8 << self.bits) - 1;
155        let offset = index * self.bits as usize;
156        let value = if let Some(right) = offset.checked_sub(shift as usize) {
157            if right >= u64::BITS as usize {
158                0
159            } else {
160                ((magnitude >> right) & mask as u64) as u8
161            }
162        } else {
163            ((magnitude << (shift as usize - offset)) & mask as u64) as u8
164        };
165        self.table.as_bytes()[2 + value as usize] as char
166    }
167
168    fn format_fixed(
169        self,
170        negative: bool,
171        magnitude: u64,
172        frac_bits: i8,
173        f: &mut fmt::Formatter<'_>,
174    ) -> fmt::Result {
175        let magnitude_bits = (u64::BITS - magnitude.leading_zeros()) as usize;
176        let (frac_digits, zero_digits, shift) = if frac_bits > 0 {
177            let digits = self.ceil_digits(frac_bits as usize);
178            (
179                digits,
180                0,
181                (digits * self.bits as usize - frac_bits as usize) as u8,
182            )
183        } else {
184            let bits = frac_bits.unsigned_abs();
185            (0, (bits / self.bits) as usize, bits % self.bits)
186        };
187        let digits = if magnitude == 0 {
188            0
189        } else {
190            self.ceil_digits(magnitude_bits + shift as usize)
191        };
192        let body_len = if frac_digits > 0 {
193            digits.saturating_sub(frac_digits).max(1) + 1 + frac_digits
194        } else if magnitude == 0 {
195            2
196        } else {
197            digits + zero_digits + 1
198        };
199        let sign = if negative {
200            "-"
201        } else if f.sign_plus() {
202            "+"
203        } else {
204            ""
205        };
206        let prefix = if f.alternate() { &self.table[..2] } else { "" };
207        let total_len = sign.len() + prefix.len() + body_len;
208        let pad_len = f.width().unwrap_or_default().saturating_sub(total_len);
209        let zero_pad = if f.sign_aware_zero_pad() && f.align().is_none() {
210            pad_len
211        } else {
212            0
213        };
214        let align = f.align().unwrap_or(fmt::Alignment::Right);
215        let (left_pad, right_pad) = if zero_pad != 0 {
216            (0, 0)
217        } else {
218            match align {
219                fmt::Alignment::Left => (0, pad_len),
220                fmt::Alignment::Center => (pad_len / 2, pad_len - pad_len / 2),
221                fmt::Alignment::Right => (pad_len, 0),
222            }
223        };
224
225        for _ in 0..left_pad {
226            f.write_char(f.fill())?;
227        }
228        f.write_str(sign)?;
229        f.write_str(prefix)?;
230        for _ in 0..zero_pad {
231            f.write_char('0')?;
232        }
233
234        if frac_digits > 0 {
235            if digits <= frac_digits {
236                f.write_char('0')?;
237            } else {
238                for index in (frac_digits..digits).rev() {
239                    f.write_char(self.shifted_digit(magnitude, shift, index))?;
240                }
241            }
242            f.write_char('.')?;
243            for index in (0..frac_digits).rev() {
244                f.write_char(self.shifted_digit(magnitude, shift, index))?;
245            }
246        } else {
247            if magnitude == 0 {
248                f.write_char('0')?;
249            } else {
250                for index in (0..digits).rev() {
251                    f.write_char(self.shifted_digit(magnitude, shift, index))?;
252                }
253                for _ in 0..zero_digits {
254                    f.write_char('0')?;
255                }
256            }
257            f.write_char('.')?;
258        }
259
260        for _ in 0..right_pad {
261            f.write_char(f.fill())?;
262        }
263        Ok(())
264    }
265}
266
267macro_rules! impl_radix_fmt {
268    ($tr:path, $radix:expr) => {
269        impl<T: RadixValue, A, const F: i8> $tr for Q<T, A, F> {
270            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271                const {
272                    assert!(
273                        F != i8::MIN,
274                        "fractional bit count must not be i8::MIN for formatting"
275                    );
276                }
277                $radix.format_fixed(self.inner.is_negative(), self.inner.magnitude(), F, f)
278            }
279        }
280    };
281}
282
283const BINARY: Radix = Radix {
284    bits: 1,
285    table: "0b01",
286};
287const OCTAL: Radix = Radix {
288    bits: 3,
289    table: "0o01234567",
290};
291const LOWER_HEX: Radix = Radix {
292    bits: 4,
293    table: "0x0123456789abcdef",
294};
295const UPPER_HEX: Radix = Radix {
296    bits: 4,
297    table: "0X0123456789ABCDEF",
298};
299
300impl_radix_fmt!(fmt::Binary, BINARY);
301impl_radix_fmt!(fmt::Octal, OCTAL);
302impl_radix_fmt!(fmt::LowerHex, LOWER_HEX);
303impl_radix_fmt!(fmt::UpperHex, UPPER_HEX);
304
305#[cfg(test)]
306mod test {
307    #[cfg(feature = "defmt")]
308    #[test]
309    fn defmt_format_impls_exist() {
310        fn assert_defmt<T: defmt::Format>() {}
311
312        assert_defmt::<crate::Q8<4>>();
313        assert_defmt::<crate::P8<4>>();
314        assert_defmt::<crate::W8<4>>();
315        assert_defmt::<crate::V8<4>>();
316    }
317
318    #[cfg(feature = "std")]
319    #[test]
320    fn display() {
321        use crate::Q32;
322        use std::format;
323
324        assert_eq!(format!("{}", Q32::<9>::new(0x12345)), "145.634765625");
325        assert_eq!(format!("{}", Q32::<9>::from_int(99)), "99");
326    }
327
328    #[cfg(feature = "std")]
329    #[test]
330    fn float_accessors_cover_wrapping_types() {
331        use crate::{V8, W8};
332        use core::num::Wrapping;
333
334        assert_eq!(W8::<4>::new(Wrapping(-4)).as_f32(), -0.25);
335        assert_eq!(V8::<4>::new(Wrapping(4)).as_f64(), 0.25);
336    }
337
338    #[cfg(feature = "std")]
339    #[test]
340    fn radix_dot_examples() {
341        use crate::Q8;
342        use std::format;
343
344        assert_eq!(format!("{:#b}", Q8::<3>::new(0b01101001)), "0b1101.001");
345        assert_eq!(format!("{:x}", Q8::<3>::new(0b01101001)), "d.2");
346        assert_eq!(format!("{:o}", Q8::<5>::new(1)), "0.02");
347        assert_eq!(format!("{:x}", Q8::<-2>::new(3)), "c.");
348    }
349
350    #[cfg(feature = "std")]
351    #[test]
352    fn radix_dot_leading_zero_and_zero_value() {
353        use crate::Q8;
354        use std::format;
355
356        assert_eq!(format!("{:b}", Q8::<3>::new(1)), "0.001");
357        assert_eq!(format!("{:x}", Q8::<7>::new(1)), "0.02");
358        assert_eq!(format!("{:#x}", Q8::<7>::new(1)), "0x0.02");
359        assert_eq!(format!("{:b}", Q8::<5>::new(0)), "0.00000");
360        assert_eq!(format!("{:x}", Q8::<-5>::new(0)), "0.");
361    }
362
363    #[cfg(feature = "std")]
364    #[test]
365    fn radix_dot_signed_values_are_magnitude_based() {
366        use crate::{Q8, W8};
367        use core::num::Wrapping;
368        use std::format;
369
370        assert_eq!(format!("{:b}", Q8::<3>::new(-0x14)), "-10.100");
371        assert_eq!(format!("{:#x}", Q8::<4>::new(-0x14)), "-0x1.4");
372        assert_eq!(format!("{:o}", Q8::<0>::new(-1)), "-1.");
373        assert_eq!(format!("{:x}", Q8::<4>::new(i8::MIN)), "-8.0");
374        assert_eq!(format!("{:#b}", W8::<3>::new(Wrapping(-0x14))), "-0b10.100");
375    }
376
377    #[cfg(feature = "std")]
378    #[test]
379    fn radix_dot_unsigned_and_wrapping_unsigned() {
380        use crate::{P8, V8};
381        use core::num::Wrapping;
382        use std::format;
383
384        assert_eq!(format!("{:x}", P8::<4>::new(u8::MAX)), "f.f");
385        assert_eq!(
386            format!("{:b}", V8::<3>::new(Wrapping(0b1111_1111))),
387            "11111.111"
388        );
389    }
390
391    #[cfg(feature = "std")]
392    #[test]
393    fn radix_dot_handles_large_positive_and_negative_f() {
394        use crate::{Q8, Q64};
395        use std::format;
396
397        assert_eq!(format!("{:b}", Q8::<7>::new(i8::MAX)), "0.1111111");
398        assert_eq!(format!("{:b}", Q8::<-7>::new(1)), "10000000.");
399        assert_eq!(
400            format!("{:x}", Q64::<63>::new(i64::MAX)),
401            "0.fffffffffffffffe"
402        );
403        assert_eq!(format!("{:x}", Q64::<-63>::new(1)), "8000000000000000.");
404        assert_eq!(
405            format!("{:b}", Q64::<-63>::new(1)),
406            "1\
407000000000000000000000000000000000000000000000000000000000000000."
408        );
409    }
410
411    #[cfg(feature = "std")]
412    #[test]
413    fn radix_dot_handles_zero_fractional_bits() {
414        use crate::Q8;
415        use std::format;
416
417        assert_eq!(format!("{:b}", Q8::<0>::new(0b1010)), "1010.");
418        assert_eq!(format!("{:#x}", Q8::<0>::new(0x2a)), "0x2a.");
419    }
420
421    #[cfg(feature = "std")]
422    #[test]
423    fn radix_dot_respects_width_alignment_and_zero_fill() {
424        use crate::{Q8, Q32};
425        use std::format;
426
427        assert_eq!(format!("{:10x}", Q32::<12>::from_bits(4096)), "     1.000");
428        assert_eq!(format!("{:10o}", Q32::<12>::from_bits(4096)), "    1.0000");
429        assert_eq!(
430            format!("{:#010X}", Q32::<11>::from_bits(-2048)),
431            "-0X001.000"
432        );
433        assert_eq!(format!("{:>10x}", Q8::<4>::new(0x14)), "       1.4");
434        assert_eq!(format!("{:#010x}", Q8::<4>::new(0x14)), "0x000001.4");
435        assert_eq!(format!("{:#010x}", Q8::<4>::new(-0x14)), "-0x00001.4");
436        assert_eq!(format!("{:<010x}", Q8::<4>::new(0x14)), "1.4       ");
437        assert_eq!(format!("{:^010x}", Q8::<4>::new(0x14)), "   1.4    ");
438    }
439
440    #[cfg(feature = "std")]
441    #[test]
442    fn debug_stays_raw() {
443        use crate::Q8;
444        use std::format;
445
446        assert_eq!(format!("{:?}", Q8::<3>::new(-0x14)), "-20");
447        assert_eq!(format!("{:b}", Q8::<3>::new(-0x14)), "-10.100");
448    }
449}