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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
use std::cmp::Ordering;

pub trait CaseInsensitiveStr<'a> {
    fn eq_ci<S: AsRef<str>>(&self, other: S) -> bool;

    fn cmp_ci<S: AsRef<str>>(&self, other: S) -> Ordering;

    fn in_ci<S: AsRef<str>>(&self, haystack: &[S]) -> bool;

    fn starts_with_ci<S: AsRef<str>>(&self, prefix: S) -> bool;

    fn ends_with_ci<S: AsRef<str>>(&self, suffix: S) -> bool;

    fn strip_prefix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str>;

    fn strip_suffix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str>;
}

impl<'a, T: AsRef<str>> CaseInsensitiveStr<'a> for T {
    fn eq_ci<S: AsRef<str>>(&self, other: S) -> bool {
        let (a, b) = (self.as_ref(), other.as_ref());

        a == b
            || (a.len() == b.len())
                && a.chars().zip(b.chars()).all(|(a, b)| {
                    a == b
                        || !(!a.is_alphabetic()
                            || !b.is_alphabetic()
                            || a.is_lowercase() == b.is_lowercase()
                            || !a.to_lowercase().eq(b.to_lowercase()))
                })
    }

    fn cmp_ci<S: AsRef<str>>(&self, other: S) -> Ordering {
        let (a, b) = (self.as_ref(), other.as_ref());

        if a == b {
            Ordering::Equal
        } else {
            a.chars()
                .zip(b.chars())
                .find_map(|(a, b)| {
                    match if a == b {
                        Ordering::Equal
                    } else if a.is_uppercase() || b.is_uppercase() {
                        a.to_lowercase().cmp(b.to_lowercase())
                    } else {
                        a.cmp(&b)
                    } {
                        Ordering::Equal => None,
                        o => Some(o),
                    }
                })
                .unwrap_or_else(|| a.len().cmp(&b.len()))
        }
    }

    fn in_ci<S: AsRef<str>>(&self, haystack: &[S]) -> bool {
        let needle = self.as_ref();
        haystack.iter().any(|s| s.eq_ci(needle))
    }

    fn starts_with_ci<S: AsRef<str>>(&self, prefix: S) -> bool {
        let (subject, prefix) = (self.as_ref(), prefix.as_ref());

        if let Some(start) = subject.get(..prefix.len()) {
            start.eq_ci(prefix)
        } else {
            false
        }
    }

    fn ends_with_ci<S: AsRef<str>>(&self, suffix: S) -> bool {
        let (subject, suffix) = (self.as_ref(), suffix.as_ref());

        if let Some(end) = subject
            .len()
            .checked_sub(suffix.len())
            .and_then(|i| subject.get(i..))
        {
            end.eq_ci(suffix)
        } else {
            false
        }
    }

    fn strip_prefix_ci<S: AsRef<str>>(&'a self, prefix: S) -> Option<&'a str> {
        let prefix = prefix.as_ref();

        if self.starts_with_ci(prefix) {
            self.as_ref().get(prefix.len()..)
        } else {
            None
        }
    }

    fn strip_suffix_ci<S: AsRef<str>>(&'a self, suffix: S) -> Option<&'a str> {
        let suffix = suffix.as_ref();

        if self.ends_with_ci(suffix) {
            let subject = self.as_ref();

            subject
                .len()
                .checked_sub(suffix.len())
                .and_then(|i| subject.get(..i))
        } else {
            None
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn eq_ci_test() {
        assert!("".eq_ci(""));
        assert!("abc".eq_ci("abc"));
        assert!("abc".eq_ci("abC"));
        assert!("!@#".eq_ci("!@#"));
        assert!("p🥔tat🥔".eq_ci("P🥔TAT🥔"));

        assert!(!"abcd".eq_ci("abc"));
        assert!(!"abc".eq_ci("abcd"));
        assert!(!"".eq_ci("🥔"));
        assert!(!"🥔".eq_ci(""));
        assert!(!"🥔".eq_ci("potato"));
        assert!(!"potato".eq_ci("🥔"));
        assert!(!"SS".eq_ci("ß"));
        assert!(!"ß".eq_ci("S"));
        assert!(!"S".eq_ci("ß"));
    }

    #[test]
    #[ignore]
    fn eq_ci_test_failing() {
        // This is a known limitation. Documenting here for posteritity.
        assert!("ß".eq_ci("SS"));
    }

    #[test]
    fn starts_ends_with_ci_test() {
        assert!("AbC".starts_with_ci("aB"));
        assert!("AbC".ends_with_ci("Bc"));
        assert!("AbC".starts_with_ci(""));
        assert!("AbC".ends_with_ci(""));

        assert!(!"🥔".starts_with_ci("a"));
        assert!(!"🥔".ends_with_ci("a"));
        assert!(!"abc".starts_with_ci("abcd"));
        assert!(!"abc".ends_with_ci("abcd"));
    }

    #[test]
    fn strip_prefix_suffix_ci_test() {
        assert_eq!(Some("aBC"), "aBCXYz".strip_suffix_ci("xYz"));
        assert_eq!(Some("XYz"), "aBCXYz".strip_prefix_ci("aBc"));
        assert_eq!(Some("p🥔tat"), "p🥔tat🥔".strip_suffix_ci("🥔"));

        assert_eq!(Some(""), "".strip_prefix_ci(""));
    }

    #[test]
    fn cmp_ci_test() {
        let mut data = vec![
            "ddd", "aaa", "!", "aaaa", "aAA", "", "aaa", "CCC", "🥔", "Bbb",
        ];

        data.sort_by(|a, b| a.cmp_ci(b));

        assert_eq!(
            vec!["", "!", "aaa", "aAA", "aaa", "aaaa", "Bbb", "CCC", "ddd", "🥔"],
            data,
        );
    }

    #[test]
    fn in_ci_test() {
        assert!("B".in_ci(&["a", "b", "c"]));
        assert!(!"d".in_ci(&["a", "b", "c"]));
    }
}