1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
use std::cmp::Ordering;

#[cfg(test)]
use std::collections::BTreeSet;

fn cmp_ignore_ascii_case(a: &str, b: &str) -> Ordering {
    let (mut a, mut b) = (a.chars(), b.chars());
    loop {
        match (a.next(), b.next()) {
            (Some(a), Some(b)) => match a.to_ascii_uppercase().cmp(&b.to_ascii_uppercase()) {
                Ordering::Greater => return Ordering::Greater,
                Ordering::Less => return Ordering::Less,
                Ordering::Equal => (),
            }
            (None, Some(_)) => return Ordering::Less,
            (Some(_), None) => return Ordering::Greater,
            (None, None) => return Ordering::Equal,
        }
    }
}

/// An ASCII-case-insensitive wrapper for strings.
#[derive(Clone, Copy, Debug)]
pub struct Caseless<'a>(pub &'a str);

impl PartialEq<Caseless<'_>> for Caseless<'_> {
    fn eq(&self, other: &Caseless<'_>) -> bool {
        self.0.eq_ignore_ascii_case(other.0)
    }
}
impl Eq for Caseless<'_> {}

impl Ord for Caseless<'_> {
    fn cmp(&self, other: &Self) -> Ordering {
        cmp_ignore_ascii_case(self.0, other.0)
    }
}
impl PartialOrd<Caseless<'_>> for Caseless<'_> {
    fn partial_cmp(&self, other: &Caseless<'_>) -> Option<Ordering> {
        Some(cmp_ignore_ascii_case(self.0, other.0))
    }
}

#[test]
fn test_caseless() {
    assert_eq!(Caseless("hello"), Caseless("hello"));
    assert_eq!(Caseless("HeLLo"), Caseless("hello"));

    assert_ne!(Caseless("hello"), Caseless("hellod"));
    assert_ne!(Caseless("HeLLo"), Caseless("hellod"));

    assert!(Caseless("abc") < Caseless("BCD"));
    assert!(Caseless("ABC") < Caseless("bcd"));

    assert!(Caseless("ABC") < Caseless("abcd"));
    assert!(Caseless("abc") < Caseless("ABCD"));

    assert!(Caseless("ABCDE") > Caseless("abcd"));
    assert!(Caseless("abcde") > Caseless("ABCD"));

    assert!(Caseless("DQ") != Caseless("DX"));
    assert!(Caseless("DQ") < Caseless("DX"));

    {
        let mut s = BTreeSet::new();
        s.insert(Caseless("hello"));
        assert!(s.contains(&Caseless("HellO")));
        assert!(!s.contains(&Caseless("HellfO")));
        assert!(!s.insert(Caseless("hEllo")));
    }
}