From 1b18dd502671b8c7071051aa6d7a53f9636ab495 Mon Sep 17 00:00:00 2001 From: ede1998 Date: Tue, 30 May 2023 19:34:17 +0200 Subject: [PATCH 1/5] add format macro implementation --- src/lib.rs | 2 +- src/string.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 90c37e2080..3641ce9416 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,7 +97,7 @@ mod histbuf; mod indexmap; mod indexset; mod linear_map; -mod string; +pub mod string; mod vec; #[cfg(feature = "serde")] diff --git a/src/string.rs b/src/string.rs index 0d5ae59cb4..8616b777a1 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,7 +1,9 @@ +//! A fixed capacity [`String`](https://doc.rust-lang.org/std/string/struct.String.html) + use core::{ cmp::Ordering, fmt, - fmt::Write, + fmt::{Arguments, Write}, hash, iter, ops, str::{self, Utf8Error}, }; @@ -570,6 +572,53 @@ impl Ord for String { } } +/// Equivalent to [`format`](https://doc.rust-lang.org/std/fmt/fn.format.html). +/// +/// Please note that using [`format!`] might be preferable. +/// +/// [`format!`]: crate::format! +pub fn format(args: Arguments<'_>) -> String { + fn format_inner(args: Arguments<'_>) -> String { + let mut output = String::new(); + output + .write_fmt(args) + // cannot differentiate between these error cases because fmt::Error is empty + .expect("capacity exceeded or a formatting trait implementation returned an error"); + output + } + + args.as_str() + .map_or_else(|| format_inner(args), |s| s.try_into().expect("capacity exceeded")) +} + +/// Macro that creates a fixed capacity [`String`]. Equivalent to [`format!`](https://doc.rust-lang.org/std/macro.format.html). +/// +/// The first argument is the capacity of the `String`. The following arguments work in the same way as the regular macro. +/// +/// # Panics +/// +/// `format!` panics if the formatted `String` would exceeded its capacity. +/// `format!` also panics if a formatting trait implementation returns an error (same as the regular macro). +/// +/// # Examples +/// +/// ``` +/// use heapless::format; +/// +/// format!(4, "test"); +/// format!(15, "hello {}", "world!"); +/// format!(20, "x = {}, y = {y}", 10, y = 30); +/// let (x, y) = (1, 2); +/// format!(12, "{x} + {y} = 3"); +/// ``` +#[macro_export] +macro_rules! format { + ($max:literal, $($arg:tt)*) => {{ + let res = $crate::string::format::<$max>(core::format_args!($($arg)*)); + res + }} +} + macro_rules! impl_try_from_num { ($num:ty, $size:expr) => { impl core::convert::TryFrom<$num> for String { @@ -831,4 +880,25 @@ mod tests { assert_eq!(s.remove(2), '\u{0301}'); assert_eq!(s.as_str(), "hey"); } + + #[test] + fn format() { + let number = 5; + let float = 3.12; + let formatted = format!(15, "{:0>3} plus {float}", number); + assert_eq!(formatted, "005 plus 3.12") + } + + #[test] + #[should_panic] + fn format_overflow() { + let i = 1234567; + format!(4, "13{}", i); + } + + #[test] + #[should_panic] + fn format_plain_string_overflow() { + format!(2, "123"); + } } From 5e1aff379ff7170f649c7274a7b830cf997e3167 Mon Sep 17 00:00:00 2001 From: ede1998 Date: Wed, 1 Nov 2023 19:43:09 +0100 Subject: [PATCH 2/5] return Result from format --- src/string.rs | 57 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/string.rs b/src/string.rs index 8616b777a1..ea25ac6e7a 100644 --- a/src/string.rs +++ b/src/string.rs @@ -576,40 +576,55 @@ impl Ord for String { /// /// Please note that using [`format!`] might be preferable. /// +/// # Errors +/// +/// There are two possible error cases. Both return the unit type [`core::fmt::Error`]. +/// +/// - In case the formatting exceeds the string's capacity. This error does not exist in +/// the standard library as the string would just grow. +/// - If a formatting trait implementation returns an error. The standard library panics +/// in this case. +/// /// [`format!`]: crate::format! -pub fn format(args: Arguments<'_>) -> String { - fn format_inner(args: Arguments<'_>) -> String { +pub fn format(args: Arguments<'_>) -> Result, fmt::Error> { + fn format_inner(args: Arguments<'_>) -> Result, fmt::Error> { let mut output = String::new(); - output - .write_fmt(args) - // cannot differentiate between these error cases because fmt::Error is empty - .expect("capacity exceeded or a formatting trait implementation returned an error"); - output + output.write_fmt(args)?; + Ok(output) } - args.as_str() - .map_or_else(|| format_inner(args), |s| s.try_into().expect("capacity exceeded")) + args.as_str().map_or_else( + || format_inner(args), + |s| s.try_into().map_err(|_| fmt::Error), + ) } /// Macro that creates a fixed capacity [`String`]. Equivalent to [`format!`](https://doc.rust-lang.org/std/macro.format.html). /// /// The first argument is the capacity of the `String`. The following arguments work in the same way as the regular macro. /// -/// # Panics +/// # Errors /// -/// `format!` panics if the formatted `String` would exceeded its capacity. -/// `format!` also panics if a formatting trait implementation returns an error (same as the regular macro). +/// There are two possible error cases. Both return the unit type [`core::fmt::Error`]. +/// +/// - In case the formatting exceeds the string's capacity. This error does not exist in +/// the standard library as the string would just grow. +/// - If a formatting trait implementation returns an error. The standard library panics +/// in this case. /// /// # Examples /// /// ``` +/// # fn main() -> Result<(), core::fmt::Error> { /// use heapless::format; /// -/// format!(4, "test"); -/// format!(15, "hello {}", "world!"); -/// format!(20, "x = {}, y = {y}", 10, y = 30); +/// format!(4, "test")?; +/// format!(15, "hello {}", "world!")?; +/// format!(20, "x = {}, y = {y}", 10, y = 30)?; /// let (x, y) = (1, 2); -/// format!(12, "{x} + {y} = 3"); +/// format!(12, "{x} + {y} = 3")?; +/// # Ok(()) +/// # } /// ``` #[macro_export] macro_rules! format { @@ -885,20 +900,20 @@ mod tests { fn format() { let number = 5; let float = 3.12; - let formatted = format!(15, "{:0>3} plus {float}", number); + let formatted = format!(15, "{:0>3} plus {float}", number).unwrap(); assert_eq!(formatted, "005 plus 3.12") } #[test] - #[should_panic] fn format_overflow() { let i = 1234567; - format!(4, "13{}", i); + let formatted = format!(4, "13{}", i); + assert_eq!(formatted, Err(core::fmt::Error)) } #[test] - #[should_panic] fn format_plain_string_overflow() { - format!(2, "123"); + let formatted = format!(2, "123"); + assert_eq!(formatted, Err(core::fmt::Error)) } } From 8184759182ac454e8075a8bacc9fcf1de0676ba6 Mon Sep 17 00:00:00 2001 From: ede1998 Date: Wed, 1 Nov 2023 20:37:13 +0100 Subject: [PATCH 3/5] make capacity inferrable --- src/string.rs | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/string.rs b/src/string.rs index ea25ac6e7a..7e8f8662b3 100644 --- a/src/string.rs +++ b/src/string.rs @@ -601,7 +601,10 @@ pub fn format(args: Arguments<'_>) -> Result, fmt::Err /// Macro that creates a fixed capacity [`String`]. Equivalent to [`format!`](https://doc.rust-lang.org/std/macro.format.html). /// -/// The first argument is the capacity of the `String`. The following arguments work in the same way as the regular macro. +/// The macro's arguments work in the same way as the regular macro. +/// +/// It is possible to explicitly specify the capacity of the returned string as the first argument. +/// In this case it is necessary to disambiguate by separating the capacity with a semicolon. /// /// # Errors /// @@ -616,22 +619,31 @@ pub fn format(args: Arguments<'_>) -> Result, fmt::Err /// /// ``` /// # fn main() -> Result<(), core::fmt::Error> { -/// use heapless::format; +/// use heapless::{format, String}; /// -/// format!(4, "test")?; -/// format!(15, "hello {}", "world!")?; -/// format!(20, "x = {}, y = {y}", 10, y = 30)?; +/// // Notice semicolon instead of comma! +/// format!(4; "test")?; +/// format!(15; "hello {}", "world!")?; +/// format!(20; "x = {}, y = {y}", 10, y = 30)?; /// let (x, y) = (1, 2); -/// format!(12, "{x} + {y} = 3")?; +/// format!(12; "{x} + {y} = 3")?; +/// +/// let implicit: String<10> = format!("speed = {}", 7)?; /// # Ok(()) /// # } /// ``` #[macro_export] macro_rules! format { - ($max:literal, $($arg:tt)*) => {{ + // Without semicolon as separator to disambiguate between arms, Rust just + // chooses the first so that the format string would land in $max. + ($max:expr; $($arg:tt)*) => {{ let res = $crate::string::format::<$max>(core::format_args!($($arg)*)); res - }} + }}; + ($($arg:tt)*) => {{ + let res = $crate::string::format(core::format_args!($($arg)*)); + res + }}; } macro_rules! impl_try_from_num { @@ -900,20 +912,27 @@ mod tests { fn format() { let number = 5; let float = 3.12; - let formatted = format!(15, "{:0>3} plus {float}", number).unwrap(); + let formatted = format!(15; "{:0>3} plus {float}", number).unwrap(); + assert_eq!(formatted, "005 plus 3.12") + } + #[test] + fn format_inferred_capacity() { + let number = 5; + let float = 3.12; + let formatted: String<15> = format!("{:0>3} plus {float}", number).unwrap(); assert_eq!(formatted, "005 plus 3.12") } #[test] fn format_overflow() { let i = 1234567; - let formatted = format!(4, "13{}", i); + let formatted = format!(4; "13{}", i); assert_eq!(formatted, Err(core::fmt::Error)) } #[test] fn format_plain_string_overflow() { - let formatted = format!(2, "123"); + let formatted = format!(2; "123"); assert_eq!(formatted, Err(core::fmt::Error)) } } From 1e270a9cba4f639015d7a93d151b5b71915e0f7f Mon Sep 17 00:00:00 2001 From: ede1998 Date: Wed, 1 Nov 2023 20:43:37 +0100 Subject: [PATCH 4/5] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index accf16bf82..4e9598f43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + +- Added `format` macro. + ### Changed - Changed `stable_deref_trait` to a platform-dependent dependency. From 0602c6627196f4a472996eb2a4142230ba6fe728 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Mon, 20 Nov 2023 02:57:34 +0100 Subject: [PATCH 5/5] Don't make string mod public, make a hidden `_export` module instead. --- src/lib.rs | 9 ++++++++- src/string.rs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3641ce9416..0d47fe7b3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,7 +97,7 @@ mod histbuf; mod indexmap; mod indexset; mod linear_map; -pub mod string; +mod string; mod vec; #[cfg(feature = "serde")] @@ -134,3 +134,10 @@ pub mod spsc; mod ufmt; mod sealed; + +/// Implementation details for macros. +/// Do not use. Used for macros only. Not covered by semver guarantees. +#[doc(hidden)] +pub mod _export { + pub use crate::string::format; +} diff --git a/src/string.rs b/src/string.rs index 7e8f8662b3..01a02521fc 100644 --- a/src/string.rs +++ b/src/string.rs @@ -637,11 +637,11 @@ macro_rules! format { // Without semicolon as separator to disambiguate between arms, Rust just // chooses the first so that the format string would land in $max. ($max:expr; $($arg:tt)*) => {{ - let res = $crate::string::format::<$max>(core::format_args!($($arg)*)); + let res = $crate::_export::format::<$max>(core::format_args!($($arg)*)); res }}; ($($arg:tt)*) => {{ - let res = $crate::string::format(core::format_args!($($arg)*)); + let res = $crate::_export::format(core::format_args!($($arg)*)); res }}; }