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}