diff --git a/utils/writeable/src/cmp.rs b/utils/writeable/src/cmp.rs index 5f0050e2684..2586283acf6 100644 --- a/utils/writeable/src/cmp.rs +++ b/utils/writeable/src/cmp.rs @@ -97,6 +97,31 @@ 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 `writeable::concatenate!` to make an efficient +/// string-substring comparison: +/// +/// ``` +/// 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") +/// ); +/// assert_eq!( +/// Ordering::Equal, +/// writeable::cmp_str(&writeable::concatenate!(en, '-', us), "en-US") +/// ); +/// assert_eq!( +/// Ordering::Greater, +/// writeable::cmp_str(&writeable::concatenate!(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..b90367d1c28 --- /dev/null +++ b/utils/writeable/src/concat.rs @@ -0,0 +1,148 @@ +// 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 [`concatenate!`] macro for a convenient way to make one of these. +/// +/// # Examples +/// +/// ``` +/// use writeable::adapters::Concatenate; +/// use writeable::assert_writeable_eq; +/// +/// assert_writeable_eq!(Concatenate("Number: ", 25), "Number: 25"); +/// ``` +/// +/// With [`TryWriteable`]: +/// +/// ``` +/// use writeable::adapters::Concatenate; +/// 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!( +/// Concatenate(writeable0, writeable1), +/// "error message", +/// Err("error message"), +/// ) +/// ``` +#[derive(Debug)] +#[allow(clippy::exhaustive_structs)] // designed for nesting +pub struct Concatenate(pub A, pub B); + +impl Writeable for Concatenate +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 Concatenate +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 Concatenate +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`] concatenating any number of [`Writeable`]s. +/// +/// The macro resolves to a nested [`Concatenate`]. +/// +/// # Examples +/// +/// ``` +/// use writeable::assert_writeable_eq; +/// +/// let concatenated = writeable::concatenate!( +/// "Health: ", +/// 5, +/// '/', +/// 8 +/// ); +/// +/// assert_writeable_eq!(concatenated, "Health: 5/8"); +/// ``` +#[macro_export] +#[doc(hidden)] // macro +macro_rules! __concatenate { + // Base case: + ($x:expr) => ($x); + // `$x` followed by at least one `$y,` + ($x:expr, $($y:expr),+) => ( + // Call `concatenate!` recursively on the tail `$y` + $crate::adapters::Concatenate($x, $crate::concatenate!($($y),+)) + ) +} +#[doc(inline)] +pub use __concatenate as concatenate; diff --git a/utils/writeable/src/lib.rs b/utils/writeable/src/lib.rs index cfddd29135c..a2165cc3798 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::concatenate; 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::Concatenate; pub use parts_write_adapter::CoreWriteAsPartsWrite; pub use parts_write_adapter::WithPart; pub use try_writeable::TryWriteableInfallibleAsWriteable;