diff --git a/macros/CHANGELOG.md b/macros/CHANGELOG.md index d4fd699..fef6363 100644 --- a/macros/CHANGELOG.md +++ b/macros/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + +- added support for `{:b}`-style formatting arguments. must be used with `ufmt` 0.1.2+ + ## [v0.3.0] - 2022-08-10 ## Changed diff --git a/macros/src/lib.rs b/macros/src/lib.rs index dafc6c1..653b086 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -262,6 +262,18 @@ fn write(input: TokenStream, newline: bool) -> TokenStream { pad_length: #pad_length, ox_prefix: #prefix})?;)); } + Piece::Bin { + upper_case, + pad_char, + pad_length, + prefix, + } => { + exprs.push(quote!(ufmt::uDisplayBin::fmt_bin(#pat, f, ufmt::BinOptions{ + upper_case:#upper_case, + pad_char: #pad_char, + pad_length: #pad_length, + ob_prefix: #prefix})?;)); + } Piece::Str(_) => unreachable!(), } } @@ -327,6 +339,12 @@ enum Piece<'a> { pad_length: usize, prefix: bool, }, + Bin { + upper_case: bool, + pad_char: u8, + pad_length: usize, + prefix: bool, + }, } impl Piece<'_> { @@ -517,10 +535,30 @@ fn parse_colon(format: &str, span: Span) -> parse::Result<(Piece, &str)> { }, tail, )) + } else if let Some(tail) = format.strip_prefix("b}") { + Ok(( + Piece::Bin { + upper_case: false, + pad_char, + pad_length, + prefix, + }, + tail, + )) + } else if let Some(tail) = format.strip_prefix("B}") { + Ok(( + Piece::Bin { + upper_case: true, + pad_char, + pad_length, + prefix, + }, + tail, + )) } else { Err(parse::Error::new( span, - "invalid format string: expected `{{`, `{}`, `{:?}`, `{:#?}` or '{:x}'", + "invalid format string: expected `{{`, `{}`, `{:?}`, `{:#?}`, '{:x}' or {:b}", )) } } @@ -566,6 +604,16 @@ mod tests { }]), ); + assert_eq!( + super::parse("{:b}", span).ok(), + Some(vec![Piece::Bin { + upper_case: false, + pad_char: b' ', + pad_length: 0, + prefix: false + }]), + ); + assert_eq!( super::parse("{:9x}", span).ok(), Some(vec![Piece::Hex { diff --git a/src/impls.rs b/src/impls.rs index 5669f12..4faa4e5 100644 --- a/src/impls.rs +++ b/src/impls.rs @@ -1,4 +1,5 @@ mod array; +mod bin; mod core; mod hex; mod ixx; diff --git a/src/impls/bin.rs b/src/impls/bin.rs new file mode 100644 index 0000000..cbe9f48 --- /dev/null +++ b/src/impls/bin.rs @@ -0,0 +1,70 @@ +use crate::{uDisplayBin, uWrite, BinOptions, Formatter}; + +macro_rules! bin_format { + ($buf:expr, $val:expr, $options:expr) => {{ + let mut cursor = $buf.len(); + let mut val = $val; + if val <= 0 { + cursor -= 1; + $buf[cursor] = b'0'; + } else { + while val != 0 && cursor > 0 { + let rem = val & 0b1; + cursor -= 1; + $buf[cursor] = bin_digit(rem as u8, $options.upper_case); + val >>= 1; + } + } + unsafe { core::str::from_utf8_unchecked(&$buf[cursor..]) } + }}; +} + +macro_rules! bin_pattern { + ($itype: ty, $utype:ty) => { + impl uDisplayBin for $itype { + fn fmt_bin( + &self, + fmt: &mut Formatter<'_, W>, + options: BinOptions, + ) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let positive = if false && // the standard rust library doesn't format negative numbers with a minus sign + *self < 0 { + fmt.write_char('-')?; + ((!*self) as $utype).wrapping_add(1) + } else { + *self as $utype + }; + <$utype as uDisplayBin>::fmt_bin(&positive, fmt, options) + } + } + + impl uDisplayBin for $utype { + fn fmt_bin( + &self, + fmt: &mut Formatter<'_, W>, + options: BinOptions, + ) -> Result<(), W::Error> + where + W: uWrite + ?Sized, + { + let mut buffer = [b'0'; 2 * core::mem::size_of::<$utype>()]; + let bin_string = bin_format!(buffer, *self, options); + options.with_stuff(fmt, bin_string) + } + } + }; +} + +bin_pattern! {i8, u8} +bin_pattern! {i16, u16} +bin_pattern! {i32, u32} +bin_pattern! {i64, u64} +bin_pattern! {i128, u128} +bin_pattern! {isize, usize} + +fn bin_digit(val: u8, _upper_case: bool) -> u8 { + b'0' + val +} diff --git a/src/lib.rs b/src/lib.rs index d74072e..d41b1b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -337,6 +337,78 @@ pub trait uDisplayHex { W: uWrite + ?Sized; } +/// HEADS UP this is currently an implementation detail and not subject to semver guarantees. +/// do NOT use this outside the `ufmt` crate +// options for formatting binary numbers +#[doc(hidden)] +pub struct BinOptions { + /// does not apply to digits, but to the 0b prefix if used. should the 0b be upper case instead? + pub upper_case: bool, + /// when we are padding to a target length, what character should we pad using? + pub pad_char: u8, + /// when we are padding to a target length, how long should our string be? + pub pad_length: usize, + /// should we include a 0b prefix? (also controlled by upper_case) + pub ob_prefix: bool, +} + +impl BinOptions { + /// applies the various padding/prefix options while writing the `payload` string + pub fn with_stuff( + &self, + fmt: &mut Formatter<'_, W>, + payload: &str, + ) -> Result<(), ::Error> { + let pad_before = self.ob_prefix && self.pad_char == b' '; + + let pad = self.pad_length as isize + - (if self.ob_prefix { 2 } else { 0 } + payload.len()) as isize; + + let do_pad = |fmt: &mut Formatter<'_, W>, pad: isize| -> Result<(), ::Error> { + if pad > 0 { + for _ in 0..pad { + // miri considers the `write_char` defined in `ufmt-write` v0.1.0 unsound + // to workaround the issue we use `write_str` instead of `write_char` + fmt.write_str(unsafe { str::from_utf8_unchecked(&[self.pad_char]) })?; + } + } + Ok(()) + }; + + let do_prefix = |fmt: &mut Formatter<'_, W>, + go: bool, + upper_case: bool| + -> Result<(), ::Error> { + if go { + fmt.write_str(if upper_case { "0B" } else { "0b" }) + } else { + Ok(()) + } + }; + if pad_before { + do_pad(fmt, pad)?; + do_prefix(fmt, self.ob_prefix, self.upper_case)?; + } else { + do_prefix(fmt, self.ob_prefix, self.upper_case)?; + do_pad(fmt, pad)?; + } + + fmt.write_str(payload) + } +} + +/// HEADS UP this is currently an implementation detail and not subject to semver guarantees. +/// do NOT use this outside the `ufmt` crate +// just like std::fmt::LowerHex +#[doc(hidden)] +#[allow(non_camel_case_types)] +pub trait uDisplayBin { + /// Formats the value using the given formatter + fn fmt_bin(&self, _: &mut Formatter<'_, W>, options: BinOptions) -> Result<(), W::Error> + where + W: uWrite + ?Sized; +} + /// Configuration for formatting #[allow(non_camel_case_types)] pub struct Formatter<'w, W>