fmt_cmp/cmp/
mod.rs

1//! Stringy comparison utility.
2
3mod generic;
4#[cfg(fmt_cmp_semver_exempt)]
5mod spec;
6
7use std::cmp::Ordering;
8use std::fmt::{self, Debug, Display, Formatter};
9use std::hash::{Hash, Hasher};
10use std::mem;
11
12use super::{FmtEq, FmtOrd};
13
14#[cfg(not(fmt_cmp_semver_exempt))]
15use self::generic as imp;
16#[cfg(fmt_cmp_semver_exempt)]
17use self::spec as imp;
18
19/// A wrapper type that compares the inner value in its `Display` representation.
20///
21/// This implements [`Eq`][std::cmp::Eq], [`Ord`][std::cmp::Ord] and [`Hash`][std::hash::Hash]
22/// traits with [`eq`], [`cmp`] and [`hash`] functions.
23///
24/// ## Example
25///
26/// Wrapping `!FmtOrd` types:
27///
28/// ```
29/// assert_eq!(fmt_cmp::Cmp(f64::NAN), fmt_cmp::Cmp(f64::NAN));
30/// assert!(fmt_cmp::Cmp(42) > fmt_cmp::Cmp(240));
31/// ```
32///
33/// Sorting integers _lexicographically_:
34///
35#[cfg_attr(feature = "alloc", doc = " ```")]
36#[cfg_attr(not(feature = "alloc"), doc = " ```ignore")]
37/// # extern crate alloc as std;
38/// #
39/// use std::collections::BTreeSet;
40///
41/// let mut values: BTreeSet<fmt_cmp::Cmp<u32>> = (1..=10).map(fmt_cmp::Cmp).collect();
42/// assert!(values
43///    .into_iter()
44///    .map(|cmp| cmp.0)
45///    .eq([1, 10, 2, 3, 4, 5, 6, 7, 8, 9]));
46/// ```
47#[derive(Clone, Copy, Debug)]
48#[repr(transparent)]
49pub struct Cmp<T: ?Sized = dyn Display>(pub T);
50
51impl<T: Display + ?Sized> Cmp<T> {
52    /// Wraps a reference of type `T` as a reference of `Cmp<T>`.
53    #[must_use]
54    pub fn from_ref(value: &T) -> &Self {
55        fn inner<'a, T: ?Sized>(value: &'a T) -> &'a Cmp<T> {
56            // Safety:
57            // - The lifetime annotations ensure that the output does not outlive the input.
58            // - The `#[repr(transparent)]` attribute ensures that `Cmp<T>` has the same layout as
59            //   `T`.
60            unsafe { mem::transmute::<&'a T, &'a Cmp<T>>(value) }
61        }
62        inner(value)
63    }
64
65    /// Converts a `Box<T>` into `Box<Cmp<T>>`.
66    #[cfg(feature = "alloc")]
67    #[must_use]
68    pub fn from_boxed(boxed: alloc::boxed::Box<T>) -> alloc::boxed::Box<Self> {
69        let leaked: &mut Cmp<T> = Cmp::from_mut(alloc::boxed::Box::leak(boxed));
70        // Safety:
71        // - The `#[repr(transparent)]` attribute ensures that `Cmp<T>` has the same layout as `T`.
72        // - `leaked` points at a block of memory currently allocated via the `Global` allocator.
73        unsafe { alloc::boxed::Box::<Cmp<T>>::from_raw(leaked) }
74    }
75
76    /// Converts a `Box<Cmp<T>>` into a `Box<T>`.
77    #[cfg(feature = "alloc")]
78    #[must_use]
79    pub fn into_boxed_inner(self: alloc::boxed::Box<Self>) -> alloc::boxed::Box<T> {
80        let leaked: &mut T = &mut alloc::boxed::Box::leak(self).0;
81        // Safety:
82        // - The `#[repr(transparent)]` attribute ensures that `Cmp<T>` has the same layout as `T`.
83        // - `leaked` points at a block of memory currently allocated via the `Global` allocator.
84        unsafe { alloc::boxed::Box::<T>::from_raw(leaked) }
85    }
86
87    #[cfg(feature = "alloc")]
88    fn from_mut(value: &mut T) -> &mut Self {
89        fn inner<'a, T: ?Sized>(value: &'a mut T) -> &'a mut Cmp<T> {
90            // Safety:
91            // - The lifetime annotations ensure that the output does not outlive the input.
92            // - The `#[repr(transparent)]` attribute ensures that `Cmp<T>` has the same layout as
93            //   `T`.
94            unsafe { mem::transmute::<&'a mut T, &'a mut Cmp<T>>(value) }
95        }
96        inner(value)
97    }
98}
99
100impl<T> AsRef<T> for Cmp<T> {
101    fn as_ref(&self) -> &T {
102        &self.0
103    }
104}
105
106impl<T: Default + Display> Default for Cmp<T> {
107    fn default() -> Self {
108        Cmp(T::default())
109    }
110}
111
112// `AsRef<Cmp<T>> for T` cannot be implemented due to conflict with
113// `AsRef<U> for &T where T: AsRef<U>`.
114impl<'a, T: Display + ?Sized> From<&'a T> for &'a Cmp<T> {
115    fn from(t: &T) -> &Cmp<T> {
116        Cmp::from_ref(t)
117    }
118}
119
120#[cfg(feature = "alloc")]
121impl<T: Display + ?Sized> From<alloc::boxed::Box<T>> for alloc::boxed::Box<Cmp<T>> {
122    fn from(boxed: alloc::boxed::Box<T>) -> Self {
123        Cmp::from_boxed(boxed)
124    }
125}
126
127impl<T: Display + ?Sized> Display for Cmp<T> {
128    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
129        self.0.fmt(f)
130    }
131}
132
133// We _could_ implement more general `PartialEq<U>` here, but we cannot ensure symmetricity and
134// transitivity of such an impl.
135// e.g. `Cmp("hello") == "hello" && "hello" == CaseInsensitiveStr("HELLO")` would not necessarily
136// imply `Cmp("hello") == CaseInsensitiveStr("HELLO")`.
137impl<T: Display + ?Sized, U: Display + ?Sized> PartialEq<Cmp<U>> for Cmp<T> {
138    fn eq(&self, other: &Cmp<U>) -> bool {
139        eq(&self.0, &other.0)
140    }
141}
142
143impl<T: Display + ?Sized> Eq for Cmp<T> {}
144
145impl<T: Display + ?Sized, U: Display + ?Sized> PartialOrd<Cmp<U>> for Cmp<T> {
146    fn partial_cmp(&self, other: &Cmp<U>) -> Option<Ordering> {
147        Some(cmp(&self.0, &other.0))
148    }
149}
150
151impl<T: Display + ?Sized> Ord for Cmp<T> {
152    fn cmp(&self, other: &Self) -> Ordering {
153        cmp(&self.0, &other.0)
154    }
155}
156
157impl<T: Display + ?Sized> Hash for Cmp<T> {
158    fn hash<H: Hasher>(&self, state: &mut H) {
159        hash(&self.0, state)
160    }
161}
162
163impl<T: Display + ?Sized> FmtEq for Cmp<T> {}
164impl<T: Display + ?Sized> FmtOrd for Cmp<T> {}
165
166/// Tests two values for equality in their `Display` representations.
167///
168/// This yields the same result as `lhs.to_string() == rhs.to_string()` without heap allocation.
169///
170/// ## Note
171///
172/// This may call `Display::fmt` multiple times and if it emits different strings between the calls,
173/// the resulting value is unspecified.
174///
175/// Also, the `Display` implementations may not return error as described by the documentation of
176/// [`std::fmt`]. Doing so would result in an unspecified return value or might even cause
177/// a panic in a future version.
178///
179/// ## Examples
180///
181/// Comparing floating-point numbers:
182///
183/// ```
184/// assert!(fmt_cmp::eq(&f64::NAN, &f64::NAN)); // `"NaN" == "NaN"`
185/// assert!(!fmt_cmp::eq(&0.0, &-0.0)); // `"0" != "-0"`
186/// ```
187///
188/// Comparing values of different types:
189///
190/// ```
191/// assert!(fmt_cmp::eq(&format_args!("{:X}", 0x2A), "2A"));
192/// ```
193#[must_use]
194pub fn eq<T: Display + ?Sized, U: Display + ?Sized>(lhs: &T, rhs: &U) -> bool {
195    imp::eq(lhs, rhs)
196}
197
198/// Compares two values in their `Display` representations.
199///
200/// This yields the same result as `lhs.to_string().cmp(&rhs.to_string())` without heap allocation.
201///
202/// ## Note
203///
204/// This may call `Display::fmt` multiple times and if it emits different strings between the calls,
205/// the resulting `Ordering` value is unspecified.
206///
207/// Also, the `Display` implementations may not return error as described by the documentation of
208/// [`std::fmt`]. Doing so would result in an unspecified `Ordering` value or might even cause
209/// a panic in a future version.
210///
211/// ## Examples
212///
213/// Comparing digits of integers _lexicographically_:
214///
215/// ```
216/// assert!(fmt_cmp::cmp(&42, &240).is_gt());
217/// ```
218///
219/// Comparing `format_args!`:
220///
221/// ```
222/// assert!(fmt_cmp::cmp(&format_args!("{:X}", 0x2A), &format_args!("{:X}", 0x9)).is_le());
223/// ```
224#[must_use]
225pub fn cmp<T: Display + ?Sized, U: Display + ?Sized>(lhs: &T, rhs: &U) -> Ordering {
226    imp::cmp(lhs, rhs)
227}
228
229/// Hashes a value with respect to its `Display` representation.
230///
231/// This satisfies the same property as `hashee.to_string().hash(hasher)` without heap allocation,
232/// although the exact hash values are not guaranteed to match. In particular, the following variant
233/// of [`Hash` trait's property][hash-and-eq] holds:
234///
235/// ```text
236/// format!("{}", k1) == format!("{}", k2) -> hash(k1) == hash(k2)
237/// ```
238///
239/// ## Note
240///
241/// The `Display` implementation may not return error as described by the documentation of
242/// [`std::fmt`]. Doing so would result in an unspecified hash value or might even cause
243/// a panic in a future version.
244///
245/// [hash-and-eq]: Hash#hash-and-eq
246pub fn hash<T: Display + ?Sized, H: Hasher>(hashee: &T, hasher: &mut H) {
247    imp::hash(hashee, hasher)
248}
249
250#[cfg(test)]
251mod tests {
252    #[cfg(not(feature = "alloc"))]
253    extern crate alloc;
254
255    use alloc::string::ToString;
256    use std::fmt::{Debug, Formatter};
257
258    use super::*;
259
260    #[test]
261    fn fmt_cmp() {
262        #[derive(Debug)]
263        struct SplitFmt<'a>(&'a str, usize);
264        impl Display for SplitFmt<'_> {
265            fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
266                let SplitFmt(s, n) = *self;
267                let mut pos = 0;
268                s.split_inclusive(|_| {
269                    let ret = n == 0 || (pos != 0 && pos % n == 0);
270                    pos += 1;
271                    ret
272                })
273                .try_for_each(|s| f.write_str(s))
274            }
275        }
276
277        #[track_caller]
278        fn check<T: Debug + Display, U: Debug + Display>(x: T, y: U) {
279            let (x_str, y_str) = (x.to_string(), y.to_string());
280            let expected = x_str.cmp(&y_str);
281
282            assert_eq!(cmp(&x, &y), expected);
283            assert_eq!(cmp(&y, &x), expected.reverse(), "rev");
284            assert_eq!(generic::cmp(&x, &y), expected, "generic");
285            assert_eq!(generic::cmp(&y, &x), expected.reverse(), "generic,rev");
286
287            for s in [&*x_str, &*y_str] {
288                for n in 0..s.len() {
289                    let split = SplitFmt(s, n);
290                    assert_eq!(split.to_string(), s, "`{:?}` is broken", split);
291                }
292            }
293
294            for (nx, ny) in (0..x_str.len()).flat_map(|i| (0..y_str.len()).map(move |j| (i, j))) {
295                let (x, y) = (SplitFmt(&x_str, nx), SplitFmt(&y_str, ny));
296
297                assert_eq!(cmp(&x, &y), expected, "{:?}", (nx, ny));
298                assert_eq!(cmp(&y, &x), expected.reverse(), "{:?},rev", (nx, ny));
299                assert_eq!(generic::cmp(&x, &y), expected, "generic,{:?}", (nx, ny));
300                assert_eq!(
301                    generic::cmp(&y, &x),
302                    expected.reverse(),
303                    "generic,{:?},rev",
304                    (nx, ny)
305                );
306            }
307        }
308
309        // Empty inputs.
310        check("", "");
311
312        // Empty and non-empty inputs.
313        check("", 42);
314
315        // `lhs == rhs && lhs.to_string() == rhs.to_string()`
316        check("abracadabra", "abracadabra");
317
318        // `lhs == rhs && lhs.to_string() != rhs.to_string()`
319        check(0., -0.);
320
321        // `lhs != rhs && lhs.to_string() == rhs.to_string()`
322        check(f64::NAN, f64::NAN);
323
324        // `lhs < rhs && lhs.to_string() > rhs.to_string()`
325        // `lhs.to_string() > rhs.to_string() && lhs.to_string().len() < rhs.to_string().len()`
326        check(42, 240);
327
328        // `lhs > rhs && lhs.to_string() > rhs.to_string()`
329        // `lhs.to_string() > rhs.to_string() && lhs.to_string().len() > rhs.to_string().len()`
330        check(42, 2);
331
332        // One is a prefix of the other.
333        check("abracadabra", "abracad");
334
335        // Have a common prefix.
336        check("abracadabra", "abrabanana");
337    }
338
339    #[test]
340    fn soundness() {
341        let _ = &Cmp::from_ref(&1);
342        #[cfg(feature = "alloc")]
343        {
344            let _ = Cmp::from_boxed(alloc::boxed::Box::new(1)).into_boxed_inner();
345        }
346
347        // ZST
348        let _ = Cmp::from_ref(&std::fmt::Error);
349        #[cfg(feature = "alloc")]
350        {
351            let _ = Cmp::from_boxed(alloc::boxed::Box::new(std::fmt::Error)).into_boxed_inner();
352        }
353
354        // DST
355        let _ = Cmp::from_ref("hello");
356        #[cfg(feature = "alloc")]
357        {
358            let _ = Cmp::from_boxed(alloc::string::String::from("hello").into_boxed_str())
359                .into_boxed_inner();
360        }
361
362        // Trait object
363        let _ = <Cmp>::from_ref(&1);
364        #[cfg(feature = "alloc")]
365        {
366            let _ = <Cmp>::from_boxed(alloc::boxed::Box::new(1)).into_boxed_inner();
367        }
368    }
369}