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