Skip to main content

writeable/
cmp.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use crate::Writeable;
6use core::cmp::Ordering;
7use core::fmt;
8
9struct WriteComparator<'a> {
10    code_units: &'a [u8],
11    result: Ordering,
12}
13
14/// This is an infallible impl. Functions always return Ok, not Err.
15impl fmt::Write for WriteComparator<'_> {
16    #[inline]
17    fn write_str(&mut self, other: &str) -> fmt::Result {
18        if self.result != Ordering::Equal {
19            return Ok(());
20        }
21        let (this, remainder) = self
22            .code_units
23            .split_at_checked(other.len())
24            .unwrap_or((self.code_units, &[]));
25        self.code_units = remainder;
26        self.result = this.cmp(other.as_bytes());
27        Ok(())
28    }
29}
30
31impl<'a> WriteComparator<'a> {
32    #[inline]
33    fn new(code_units: &'a [u8]) -> Self {
34        Self {
35            code_units,
36            result: Ordering::Equal,
37        }
38    }
39
40    #[inline]
41    fn finish(self) -> Ordering {
42        if #[allow(non_exhaustive_omitted_patterns)] match self.result {
    Ordering::Equal => true,
    _ => false,
}matches!(self.result, Ordering::Equal) && !self.code_units.is_empty() {
43            // Self is longer than Other
44            Ordering::Greater
45        } else {
46            self.result
47        }
48    }
49}
50
51/// Compares the contents of a [`Writeable`] to the given UTF-8 bytes without allocating memory.
52///
53/// For more details, see: [`cmp_str`]
54pub fn cmp_utf8(writeable: &impl Writeable, other: &[u8]) -> Ordering {
55    let mut wc = WriteComparator::new(other);
56    let _ = writeable.write_to(&mut wc);
57    wc.finish().reverse()
58}
59
60/// Compares the contents of a `Writeable` to the given bytes
61/// without allocating a String to hold the `Writeable` contents.
62///
63/// This returns a lexicographical comparison, the same as if the Writeable
64/// were first converted to a String and then compared with `Ord`. For a
65/// string ordering suitable for display to end users, use a localized
66/// collation crate, such as `icu_collator`.
67///
68/// # Examples
69///
70/// ```
71/// use core::cmp::Ordering;
72/// use core::fmt;
73/// use writeable::Writeable;
74///
75/// struct WelcomeMessage<'s> {
76///     pub name: &'s str,
77/// }
78///
79/// impl<'s> Writeable for WelcomeMessage<'s> {
80///     // see impl in Writeable docs
81/// #    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
82/// #        sink.write_str("Hello, ")?;
83/// #        sink.write_str(self.name)?;
84/// #        sink.write_char('!')?;
85/// #        Ok(())
86/// #    }
87/// }
88///
89/// let message = WelcomeMessage { name: "Alice" };
90/// let message_str = message.write_to_string();
91///
92/// assert_eq!(Ordering::Equal, writeable::cmp_str(&message, "Hello, Alice!"));
93///
94/// assert_eq!(Ordering::Greater, writeable::cmp_str(&message, "Alice!"));
95/// assert_eq!(Ordering::Greater, (*message_str).cmp("Alice!"));
96///
97/// assert_eq!(Ordering::Less, writeable::cmp_str(&message, "Hello, Bob!"));
98/// assert_eq!(Ordering::Less, (*message_str).cmp("Hello, Bob!"));
99/// ```
100///
101/// This function can be combined with `writeable::concat_writeable!` to make an efficient
102/// comparison between a string and a sequence of substrings:
103///
104/// ```
105/// use core::cmp::Ordering;
106///
107/// assert_eq!(
108///     Ordering::Less,
109///     writeable::cmp_str(&writeable::concat_writeable!("loop", 1), "loop12")
110/// );
111/// assert_eq!(
112///     Ordering::Equal,
113///     writeable::cmp_str(&writeable::concat_writeable!("loop", 12), "loop12")
114/// );
115/// assert_eq!(
116///     Ordering::Greater,
117///     writeable::cmp_str(&writeable::concat_writeable!("loop", 2), "loop12")
118/// );
119/// ```
120#[inline]
121pub fn cmp_str(writeable: &impl Writeable, other: &str) -> Ordering {
122    cmp_utf8(writeable, other.as_bytes())
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use core::fmt::Write;
129
130    mod data {
131        include!("../tests/data/data.rs");
132    }
133
134    #[test]
135    fn test_write_char() {
136        for a in data::KEBAB_CASE_STRINGS {
137            for b in data::KEBAB_CASE_STRINGS {
138                let mut wc = WriteComparator::new(a.as_bytes());
139                for ch in b.chars() {
140                    wc.write_char(ch).unwrap();
141                }
142                assert_eq!(a.cmp(b), wc.finish(), "{a} <=> {b}");
143            }
144        }
145    }
146
147    #[test]
148    fn test_write_str() {
149        for a in data::KEBAB_CASE_STRINGS {
150            for b in data::KEBAB_CASE_STRINGS {
151                let mut wc = WriteComparator::new(a.as_bytes());
152                wc.write_str(b).unwrap();
153                assert_eq!(a.cmp(b), wc.finish(), "{a} <=> {b}");
154            }
155        }
156    }
157
158    #[test]
159    fn test_mixed() {
160        for a in data::KEBAB_CASE_STRINGS {
161            for b in data::KEBAB_CASE_STRINGS {
162                let mut wc = WriteComparator::new(a.as_bytes());
163                let mut first = true;
164                for substr in b.split('-') {
165                    if first {
166                        first = false;
167                    } else {
168                        wc.write_char('-').unwrap();
169                    }
170                    wc.write_str(substr).unwrap();
171                }
172                assert_eq!(a.cmp(b), wc.finish(), "{a} <=> {b}");
173            }
174        }
175    }
176}