#![expect(
clippy::module_name_repetitions,
reason = "type names intentionally include `Comparator` to avoid clashing \
with the core types."
)]
use core::{borrow::Borrow, cmp::Ordering, fmt};
pub trait Comparator<T: ?Sized> {
#[must_use]
fn compare(&self, a: &T, b: &T) -> Ordering;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct OrdComparator;
impl<T: Ord> Comparator<T> for OrdComparator {
#[inline]
fn compare(&self, a: &T, b: &T) -> Ordering {
a.cmp(b)
}
}
pub trait ComparatorKey<T, Q: ?Sized>: Comparator<T> {
#[must_use]
fn compare_key(&self, stored: &T, key: &Q) -> Ordering;
}
impl<T, Q> ComparatorKey<T, Q> for OrdComparator
where
T: Ord + Borrow<Q>,
Q: Ord + ?Sized,
{
#[inline]
fn compare_key(&self, stored: &T, key: &Q) -> Ordering {
stored.borrow().cmp(key)
}
}
#[derive(Clone, Copy)]
#[expect(
clippy::exhaustive_structs,
reason = "`FnComparator<F>(pub F)` must remain a tuple struct so callers can \
write `FnComparator(|a, b| …)`; `#[non_exhaustive]` would prevent \
construction outside this crate"
)]
pub struct FnComparator<F>(pub F);
impl<F> fmt::Debug for FnComparator<F> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FnComparator").finish_non_exhaustive()
}
}
impl<T, F: Fn(&T, &T) -> Ordering> Comparator<T> for FnComparator<F> {
#[inline]
fn compare(&self, a: &T, b: &T) -> Ordering {
(self.0)(a, b)
}
}
impl<T, F: Fn(&T, &T) -> Ordering> ComparatorKey<T, T> for FnComparator<F> {
#[inline]
fn compare_key(&self, stored: &T, key: &T) -> Ordering {
(self.0)(stored, key)
}
}
#[cfg(feature = "partial-ord")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PartialOrdComparator;
#[cfg(feature = "partial-ord")]
impl<T: PartialOrd> ComparatorKey<T, T> for PartialOrdComparator {
#[inline]
fn compare_key(&self, stored: &T, key: &T) -> Ordering {
self.compare(stored, key)
}
}
#[cfg(feature = "partial-ord")]
impl<T: PartialOrd> Comparator<T> for PartialOrdComparator {
#[inline]
#[expect(
clippy::expect_used,
reason = "panicking on incomparable values is the documented and intentional \
behaviour of `PartialOrdComparator`"
)]
fn compare(&self, a: &T, b: &T) -> Ordering {
a.partial_cmp(b)
.expect("comparison returned None: values are not comparable")
}
}
#[cfg(test)]
mod tests {
use core::cmp::Ordering;
use pretty_assertions::assert_eq;
use super::{Comparator, FnComparator, OrdComparator};
#[test]
fn ord_comparator_less() {
assert_eq!(OrdComparator.compare(&1_i32, &2), Ordering::Less);
}
#[test]
fn ord_comparator_equal() {
assert_eq!(OrdComparator.compare(&42_i32, &42), Ordering::Equal);
}
#[test]
fn ord_comparator_greater() {
assert_eq!(OrdComparator.compare(&3_i32, &2), Ordering::Greater);
}
#[test]
fn ord_comparator_strings() {
assert_eq!(OrdComparator.compare(&"apple", &"banana"), Ordering::Less);
}
#[test]
fn fn_comparator_natural_order() {
let cmp = FnComparator(|a: &i32, b: &i32| a.cmp(b));
assert_eq!(cmp.compare(&1, &2), Ordering::Less);
assert_eq!(cmp.compare(&2, &2), Ordering::Equal);
assert_eq!(cmp.compare(&3, &2), Ordering::Greater);
}
#[test]
fn fn_comparator_reverse_order() {
let cmp = FnComparator(|a: &i32, b: &i32| b.cmp(a));
assert_eq!(cmp.compare(&3, &1), Ordering::Less);
assert_eq!(cmp.compare(&1, &1), Ordering::Equal);
assert_eq!(cmp.compare(&1, &3), Ordering::Greater);
}
#[test]
fn fn_comparator_by_string_length() {
let cmp = FnComparator(|a: &&str, b: &&str| a.len().cmp(&b.len()));
assert_eq!(cmp.compare(&"hi", &"hello"), Ordering::Less);
assert_eq!(cmp.compare(&"abc", &"xyz"), Ordering::Equal);
}
#[cfg(feature = "partial-ord")]
mod partial_ord {
use core::cmp::Ordering;
use pretty_assertions::assert_eq;
use super::super::{Comparator, PartialOrdComparator};
#[test]
fn partial_ord_less() {
assert_eq!(PartialOrdComparator.compare(&1.0_f64, &2.0), Ordering::Less);
}
#[test]
fn partial_ord_equal() {
assert_eq!(
PartialOrdComparator.compare(&1.5_f64, &1.5),
Ordering::Equal
);
}
#[test]
fn partial_ord_greater() {
assert_eq!(
PartialOrdComparator.compare(&3.0_f64, &2.0),
Ordering::Greater
);
}
#[test]
#[should_panic(expected = "comparison returned None")]
fn partial_ord_nan_panics() {
_ = PartialOrdComparator.compare(&f64::NAN, &1.0);
}
}
}