From b9e074ee164050aab6efdf2f018becbbb2b8aac0 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 8 Sep 2025 20:47:49 -0700 Subject: [PATCH 1/7] Add writeable::adapters::Concat and writeable::concat! --- utils/writeable/src/cmp.rs | 20 +++++ utils/writeable/src/concat.rs | 147 ++++++++++++++++++++++++++++++++++ utils/writeable/src/lib.rs | 3 + 3 files changed, 170 insertions(+) create mode 100644 utils/writeable/src/concat.rs diff --git a/utils/writeable/src/cmp.rs b/utils/writeable/src/cmp.rs index 5f0050e2684..1c1f6662ba4 100644 --- a/utils/writeable/src/cmp.rs +++ b/utils/writeable/src/cmp.rs @@ -97,6 +97,26 @@ pub fn cmp_utf8(writeable: &impl Writeable, other: &[u8]) -> Ordering { /// assert_eq!(Ordering::Less, writeable::cmp_str(&message, "Hello, Bob!")); /// assert_eq!(Ordering::Less, (*message_str).cmp("Hello, Bob!")); /// ``` +/// +/// This function can be combined with the Writeable impl on tuples to make an efficient +/// string-substring comparison: +/// +/// ``` +/// use core::cmp::Ordering; +/// +/// assert_eq!( +/// Ordering::Less, +/// writeable::cmp_str(&writeable::concat!("en", '-', "AU"), "en-US") +/// ); +/// assert_eq!( +/// Ordering::Equal, +/// writeable::cmp_str(&writeable::concat!("en", '-', "US"), "en-US") +/// ); +/// assert_eq!( +/// Ordering::Greater, +/// writeable::cmp_str(&writeable::concat!("fr", '-', "US"), "en-US") +/// ); +/// ``` #[inline] pub fn cmp_str(writeable: &impl Writeable, other: &str) -> Ordering { cmp_utf8(writeable, other.as_bytes()) diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs new file mode 100644 index 00000000000..7b6ac0d323b --- /dev/null +++ b/utils/writeable/src/concat.rs @@ -0,0 +1,147 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use crate::TryWriteable; +use crate::Writeable; +use core::fmt; + +/// A [`Writeable`] that efficiently concatenates two other [`Writeable`]s. +/// +/// See the [`concat!`] macro for a convenient way to make one of these. +/// +/// # Examples +/// +/// ``` +/// use writeable::adapters::Concat; +/// use writeable::assert_writeable_eq; +/// +/// assert_writeable_eq!(Concat("Number: ", 25), "Number: 25"); +/// ``` +/// +/// With [`TryWriteable`]: +/// +/// ``` +/// use writeable::adapters::Concat; +/// use writeable::TryWriteable; +/// use writeable::assert_try_writeable_eq; +/// +/// struct AlwaysPanic; +/// +/// impl TryWriteable for AlwaysPanic { +/// type Error = &'static str; +/// fn try_write_to_parts(&self, sink: &mut W) -> Result, core::fmt::Error> { +/// // Unreachable panic: the first Writeable errors, +/// // so the second Writeable is not evaluated. +/// panic!() +/// } +/// } +/// +/// let writeable0: Result<&str, &str> = Err("error message"); +/// let writeable1 = AlwaysPanic; +/// +/// assert_try_writeable_eq!( +/// Concat(writeable0, writeable1), +/// "error message", +/// Err("error message"), +/// ) +/// ``` +#[derive(Debug)] +pub struct Concat(pub A, pub B); + +impl Writeable for Concat +where + A: Writeable, + B: Writeable, +{ + #[inline] + fn write_to(&self, sink: &mut W) -> fmt::Result { + self.0.write_to(sink)?; + self.1.write_to(sink) + } + #[inline] + fn write_to_parts(&self, sink: &mut S) -> fmt::Result { + self.0.write_to_parts(sink)?; + self.1.write_to_parts(sink) + } + #[inline] + fn writeable_length_hint(&self) -> crate::LengthHint { + self.0.writeable_length_hint() + self.1.writeable_length_hint() + } +} + +impl TryWriteable for Concat +where + A: TryWriteable, + B: TryWriteable, +{ + type Error = E; + #[inline] + fn try_write_to( + &self, + sink: &mut W, + ) -> Result, fmt::Error> { + if let Err(e) = self.0.try_write_to(sink)? { + return Ok(Err(e)); + } + self.1.try_write_to(sink) + } + #[inline] + fn try_write_to_parts( + &self, + sink: &mut S, + ) -> Result, fmt::Error> { + if let Err(e) = self.0.try_write_to_parts(sink)? { + return Ok(Err(e)); + } + self.1.try_write_to_parts(sink) + } + #[inline] + fn writeable_length_hint(&self) -> crate::LengthHint { + self.0.writeable_length_hint() + self.1.writeable_length_hint() + } +} + +impl fmt::Display for Concat +where + A: Writeable, + B: Writeable, +{ + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.write_to(f)?; + self.1.write_to(f) + } +} + +/// Returns a [`Writeable`] concatinating any number of [`Writeable`]s. +/// +/// The macro resolves to a nested [`Concat`]. +/// +/// # Examples +/// +/// ``` +/// use writeable::assert_writeable_eq; +/// +/// let concatenated = writeable::concat!( +/// "Health: ", +/// 5, +/// '/', +/// 8 +/// ); +/// +/// assert_writeable_eq!(concatenated, "Health: 5/8"); +/// ``` +#[macro_export] +#[doc(hidden)] // macro +macro_rules! __concat { + // Base case: + ($x:expr) => ($x); + // `$x` followed by at least one `$y,` + ($x:expr, $($y:expr),+) => ( + // Call `find_min!` on the tail `$y` + $crate::adapters::Concat($x, $crate::concat!($($y),+)) + ) +} +#[doc(inline)] +pub use __concat as concat; diff --git a/utils/writeable/src/lib.rs b/utils/writeable/src/lib.rs index cfddd29135c..14d1f5b049e 100644 --- a/utils/writeable/src/lib.rs +++ b/utils/writeable/src/lib.rs @@ -79,6 +79,7 @@ extern crate alloc; mod cmp; +mod concat; #[cfg(feature = "either")] mod either; mod impls; @@ -93,6 +94,7 @@ use alloc::string::String; use core::fmt; pub use cmp::{cmp_str, cmp_utf8}; +pub use concat::concat; pub use to_string_or_borrow::to_string_or_borrow; pub use try_writeable::TryWriteable; @@ -100,6 +102,7 @@ pub use try_writeable::TryWriteable; pub mod adapters { use super::*; + pub use concat::Concat; pub use parts_write_adapter::CoreWriteAsPartsWrite; pub use parts_write_adapter::WithPart; pub use try_writeable::TryWriteableInfallibleAsWriteable; From 1634bbb802cf8d754ffcc666c139cedd340865cb Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 8 Sep 2025 20:49:39 -0700 Subject: [PATCH 2/7] clippy --- utils/writeable/src/concat.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs index 7b6ac0d323b..5338eb4324d 100644 --- a/utils/writeable/src/concat.rs +++ b/utils/writeable/src/concat.rs @@ -47,6 +47,7 @@ use core::fmt; /// ) /// ``` #[derive(Debug)] +#[allow(clippy::exhaustive_structs)] // designed for nesting pub struct Concat(pub A, pub B); impl Writeable for Concat From 6b7b485364afe19598eab95a4955ff42570bfc6e Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 8 Sep 2025 20:51:55 -0700 Subject: [PATCH 3/7] use concatenate to avoid conflicting the std concat --- utils/writeable/src/cmp.rs | 6 +++--- utils/writeable/src/concat.rs | 10 +++++----- utils/writeable/src/lib.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/utils/writeable/src/cmp.rs b/utils/writeable/src/cmp.rs index 1c1f6662ba4..37ba64b5be3 100644 --- a/utils/writeable/src/cmp.rs +++ b/utils/writeable/src/cmp.rs @@ -106,15 +106,15 @@ pub fn cmp_utf8(writeable: &impl Writeable, other: &[u8]) -> Ordering { /// /// assert_eq!( /// Ordering::Less, -/// writeable::cmp_str(&writeable::concat!("en", '-', "AU"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!("en", '-', "AU"), "en-US") /// ); /// assert_eq!( /// Ordering::Equal, -/// writeable::cmp_str(&writeable::concat!("en", '-', "US"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!("en", '-', "US"), "en-US") /// ); /// assert_eq!( /// Ordering::Greater, -/// writeable::cmp_str(&writeable::concat!("fr", '-', "US"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!("fr", '-', "US"), "en-US") /// ); /// ``` #[inline] diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs index 5338eb4324d..a67638bc418 100644 --- a/utils/writeable/src/concat.rs +++ b/utils/writeable/src/concat.rs @@ -8,7 +8,7 @@ use core::fmt; /// A [`Writeable`] that efficiently concatenates two other [`Writeable`]s. /// -/// See the [`concat!`] macro for a convenient way to make one of these. +/// See the [`concatenate!`] macro for a convenient way to make one of these. /// /// # Examples /// @@ -124,7 +124,7 @@ where /// ``` /// use writeable::assert_writeable_eq; /// -/// let concatenated = writeable::concat!( +/// let concatenated = writeable::concatenate!( /// "Health: ", /// 5, /// '/', @@ -135,14 +135,14 @@ where /// ``` #[macro_export] #[doc(hidden)] // macro -macro_rules! __concat { +macro_rules! __concatenate { // Base case: ($x:expr) => ($x); // `$x` followed by at least one `$y,` ($x:expr, $($y:expr),+) => ( // Call `find_min!` on the tail `$y` - $crate::adapters::Concat($x, $crate::concat!($($y),+)) + $crate::adapters::Concat($x, $crate::concatenate!($($y),+)) ) } #[doc(inline)] -pub use __concat as concat; +pub use __concatenate as concatenate; diff --git a/utils/writeable/src/lib.rs b/utils/writeable/src/lib.rs index 14d1f5b049e..7f446f8e410 100644 --- a/utils/writeable/src/lib.rs +++ b/utils/writeable/src/lib.rs @@ -94,7 +94,7 @@ use alloc::string::String; use core::fmt; pub use cmp::{cmp_str, cmp_utf8}; -pub use concat::concat; +pub use concat::concatenate; pub use to_string_or_borrow::to_string_or_borrow; pub use try_writeable::TryWriteable; From 1087867ae0b926357fb998c35a70e59b0b4d05bb Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 8 Sep 2025 20:53:44 -0700 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- utils/writeable/src/concat.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs index a67638bc418..0109c8bca5c 100644 --- a/utils/writeable/src/concat.rs +++ b/utils/writeable/src/concat.rs @@ -115,7 +115,7 @@ where } } -/// Returns a [`Writeable`] concatinating any number of [`Writeable`]s. +/// Returns a [`Writeable`] concatenating any number of [`Writeable`]s. /// /// The macro resolves to a nested [`Concat`]. /// @@ -140,7 +140,7 @@ macro_rules! __concatenate { ($x:expr) => ($x); // `$x` followed by at least one `$y,` ($x:expr, $($y:expr),+) => ( - // Call `find_min!` on the tail `$y` + // Call `concatenate!` recursively on the tail `$y` $crate::adapters::Concat($x, $crate::concatenate!($($y),+)) ) } From 5feaebb35c7f97ed1e90aba33fd21066746d1375 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Mon, 8 Sep 2025 20:56:18 -0700 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- utils/writeable/src/cmp.rs | 2 +- utils/writeable/src/concat.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/writeable/src/cmp.rs b/utils/writeable/src/cmp.rs index 37ba64b5be3..2f660bedad0 100644 --- a/utils/writeable/src/cmp.rs +++ b/utils/writeable/src/cmp.rs @@ -98,7 +98,7 @@ pub fn cmp_utf8(writeable: &impl Writeable, other: &[u8]) -> Ordering { /// assert_eq!(Ordering::Less, (*message_str).cmp("Hello, Bob!")); /// ``` /// -/// This function can be combined with the Writeable impl on tuples to make an efficient +/// This function can be combined with `writeable::concatenate!` to make an efficient /// string-substring comparison: /// /// ``` diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs index 0109c8bca5c..f1914eb0d58 100644 --- a/utils/writeable/src/concat.rs +++ b/utils/writeable/src/concat.rs @@ -30,7 +30,7 @@ use core::fmt; /// /// impl TryWriteable for AlwaysPanic { /// type Error = &'static str; -/// fn try_write_to_parts(&self, sink: &mut W) -> Result, core::fmt::Error> { +/// fn try_write_to_parts(&self, _sink: &mut W) -> Result, core::fmt::Error> { /// // Unreachable panic: the first Writeable errors, /// // so the second Writeable is not evaluated. /// panic!() From 274aad9c3a63c099e291071b44e27bf70c0440da Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Tue, 9 Sep 2025 13:46:19 -0700 Subject: [PATCH 6/7] Feedback on test --- utils/writeable/src/cmp.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/utils/writeable/src/cmp.rs b/utils/writeable/src/cmp.rs index 2f660bedad0..2586283acf6 100644 --- a/utils/writeable/src/cmp.rs +++ b/utils/writeable/src/cmp.rs @@ -104,17 +104,22 @@ pub fn cmp_utf8(writeable: &impl Writeable, other: &[u8]) -> Ordering { /// ``` /// use core::cmp::Ordering; /// +/// let en = "en"; +/// let fr = "fr"; +/// let au = "AU"; +/// let us = "US"; +/// /// assert_eq!( /// Ordering::Less, -/// writeable::cmp_str(&writeable::concatenate!("en", '-', "AU"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!(en, '-', au), "en-US") /// ); /// assert_eq!( /// Ordering::Equal, -/// writeable::cmp_str(&writeable::concatenate!("en", '-', "US"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!(en, '-', us), "en-US") /// ); /// assert_eq!( /// Ordering::Greater, -/// writeable::cmp_str(&writeable::concatenate!("fr", '-', "US"), "en-US") +/// writeable::cmp_str(&writeable::concatenate!(fr, '-', us), "en-US") /// ); /// ``` #[inline] From 26b4533818db9e194eb13166e6a4ce8f2c63a226 Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Tue, 9 Sep 2025 13:48:36 -0700 Subject: [PATCH 7/7] Concat -> Concatenate --- utils/writeable/src/concat.rs | 20 ++++++++++---------- utils/writeable/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/utils/writeable/src/concat.rs b/utils/writeable/src/concat.rs index f1914eb0d58..b90367d1c28 100644 --- a/utils/writeable/src/concat.rs +++ b/utils/writeable/src/concat.rs @@ -13,16 +13,16 @@ use core::fmt; /// # Examples /// /// ``` -/// use writeable::adapters::Concat; +/// use writeable::adapters::Concatenate; /// use writeable::assert_writeable_eq; /// -/// assert_writeable_eq!(Concat("Number: ", 25), "Number: 25"); +/// assert_writeable_eq!(Concatenate("Number: ", 25), "Number: 25"); /// ``` /// /// With [`TryWriteable`]: /// /// ``` -/// use writeable::adapters::Concat; +/// use writeable::adapters::Concatenate; /// use writeable::TryWriteable; /// use writeable::assert_try_writeable_eq; /// @@ -41,16 +41,16 @@ use core::fmt; /// let writeable1 = AlwaysPanic; /// /// assert_try_writeable_eq!( -/// Concat(writeable0, writeable1), +/// Concatenate(writeable0, writeable1), /// "error message", /// Err("error message"), /// ) /// ``` #[derive(Debug)] #[allow(clippy::exhaustive_structs)] // designed for nesting -pub struct Concat(pub A, pub B); +pub struct Concatenate(pub A, pub B); -impl Writeable for Concat +impl Writeable for Concatenate where A: Writeable, B: Writeable, @@ -71,7 +71,7 @@ where } } -impl TryWriteable for Concat +impl TryWriteable for Concatenate where A: TryWriteable, B: TryWriteable, @@ -103,7 +103,7 @@ where } } -impl fmt::Display for Concat +impl fmt::Display for Concatenate where A: Writeable, B: Writeable, @@ -117,7 +117,7 @@ where /// Returns a [`Writeable`] concatenating any number of [`Writeable`]s. /// -/// The macro resolves to a nested [`Concat`]. +/// The macro resolves to a nested [`Concatenate`]. /// /// # Examples /// @@ -141,7 +141,7 @@ macro_rules! __concatenate { // `$x` followed by at least one `$y,` ($x:expr, $($y:expr),+) => ( // Call `concatenate!` recursively on the tail `$y` - $crate::adapters::Concat($x, $crate::concatenate!($($y),+)) + $crate::adapters::Concatenate($x, $crate::concatenate!($($y),+)) ) } #[doc(inline)] diff --git a/utils/writeable/src/lib.rs b/utils/writeable/src/lib.rs index 7f446f8e410..a2165cc3798 100644 --- a/utils/writeable/src/lib.rs +++ b/utils/writeable/src/lib.rs @@ -102,7 +102,7 @@ pub use try_writeable::TryWriteable; pub mod adapters { use super::*; - pub use concat::Concat; + pub use concat::Concatenate; pub use parts_write_adapter::CoreWriteAsPartsWrite; pub use parts_write_adapter::WithPart; pub use try_writeable::TryWriteableInfallibleAsWriteable;