char_classes/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5/// Like [`any()`], expand into [`matches`] for better performance
6///
7/// # Examples
8///
9/// ```
10/// use char_classes::any;
11///
12/// assert!(any!("ab",      'a'));
13/// assert!(any!("ab",      'b'));
14/// assert!(any!("ab",      'b'));
15/// assert!(any!("a-c",     'a'));
16/// assert!(any!("a-c",     'b'));
17/// assert!(any!("a-c",     'c'));
18/// assert!(any!(b"ab",    b'a'));
19/// assert!(any!(b"ab",    b'b'));
20/// ```
21///
22#[cfg(feature = "macros")]
23#[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
24pub use char_classes_proc_macro::any;
25
26#[doc = include_str!("../README.md")]
27pub trait MatchOne: FirstElem {
28    type Pattern: ?Sized;
29
30    /// Match one element or first element
31    ///
32    /// Match `'-'` Please write at the beginning or end, e.g `"a-z-"`
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// # use char_classes::MatchOne;
38    /// assert_eq!(Some('a'), 'a'.match_one("a-c"));
39    /// assert_eq!(Some('b'), 'b'.match_one("a-c"));
40    /// assert_eq!(None,      '-'.match_one("a-c"));
41    /// assert_eq!(Some('-'), '-'.match_one("a-"));
42    ///
43    /// assert_eq!(Some('a'), "ab".match_one("a"));
44    /// assert_eq!(None,      "ab".match_one("b"));
45    /// ```
46    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out>;
47
48    /// Call [`.match_one(pattern).is_some()`](#tymethod.match_one)
49    fn is_match_one(&self, pattern: &Self::Pattern) -> bool {
50        self.match_one(pattern).is_some()
51    }
52}
53
54impl<T: MatchOne> MatchOne for Option<T> {
55    type Pattern = T::Pattern;
56
57    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
58        self.as_ref().and_then(|val| val.match_one(pattern))
59    }
60}
61
62impl<T: MatchOne + ?Sized> MatchOne for &T {
63    type Pattern = T::Pattern;
64
65    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
66        (**self).match_one(pattern)
67    }
68}
69
70impl<T: MatchOne + ?Sized> MatchOne for &mut T {
71    type Pattern = T::Pattern;
72
73    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
74        (**self).match_one(pattern)
75    }
76}
77
78impl MatchOne for char {
79    type Pattern = str;
80
81    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
82        match_impl(pattern.chars(), self).then_some(*self)
83    }
84}
85
86impl MatchOne for u8 {
87    type Pattern = [u8];
88
89    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
90        match_impl(pattern.iter().copied(), self).then_some(*self)
91    }
92}
93
94impl MatchOne for str {
95    type Pattern = str;
96
97    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
98        self.chars().next().match_one(pattern)
99    }
100}
101
102impl MatchOne for () {
103    type Pattern = ();
104
105    fn match_one(&self, _pattern: &Self::Pattern) -> Option<Self::Out> {
106        None
107    }
108}
109
110impl<T: MatchOne> MatchOne for [T] {
111    type Pattern = T::Pattern;
112
113    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
114        self.first().match_one(pattern)
115    }
116}
117
118impl<T: MatchOne, const N: usize> MatchOne for [T; N] {
119    type Pattern = T::Pattern;
120
121    fn match_one(&self, pattern: &Self::Pattern) -> Option<Self::Out> {
122        self.first().match_one(pattern)
123    }
124}
125
126macro_rules! impl_first_elem_trivial {
127    ($($ty:ty),+ $(,)?) => {
128        $(
129            impl FirstElem for $ty {
130                type Out = $ty;
131
132                fn first_elem(&self) -> Option<Self::Out> {
133                    Some(*self)
134                }
135            }
136        )+
137    };
138}
139
140/// Get the first element
141pub trait FirstElem {
142    type Out;
143
144    fn first_elem(&self) -> Option<Self::Out>;
145}
146
147impl_first_elem_trivial! {
148    u8,
149    u16,
150    u32,
151    u64,
152    u128,
153    i8,
154    i16,
155    i32,
156    i64,
157    i128,
158    char,
159    f32,
160    f64,
161}
162
163impl FirstElem for () {
164    type Out = core::convert::Infallible;
165
166    fn first_elem(&self) -> Option<Self::Out> {
167        None
168    }
169}
170
171impl<T: FirstElem + ?Sized> FirstElem for &T {
172    type Out = T::Out;
173
174    fn first_elem(&self) -> Option<Self::Out> {
175        (**self).first_elem()
176    }
177}
178
179impl<T: FirstElem + ?Sized> FirstElem for &mut T {
180    type Out = T::Out;
181
182    fn first_elem(&self) -> Option<Self::Out> {
183        (**self).first_elem()
184    }
185}
186
187impl<T: FirstElem> FirstElem for Option<T> {
188    type Out = T::Out;
189
190    fn first_elem(&self) -> Option<Self::Out> {
191        self.as_ref().and_then(T::first_elem)
192    }
193}
194
195impl FirstElem for str {
196    type Out = char;
197
198    fn first_elem(&self) -> Option<Self::Out> {
199        self.chars().next()
200    }
201}
202
203impl<T: FirstElem> FirstElem for [T] {
204    type Out = T::Out;
205
206    fn first_elem(&self) -> Option<Self::Out> {
207        self.first().and_then(T::first_elem)
208    }
209}
210
211impl<T: FirstElem, const N: usize> FirstElem for [T; N] {
212    type Out = T::Out;
213
214    fn first_elem(&self) -> Option<Self::Out> {
215        self.first().and_then(T::first_elem)
216    }
217}
218
219/// Call [`MatchOne::is_match_one`]
220///
221/// Match `'-'` Please write at the beginning or end, e.g `"a-z-"`
222///
223/// # Examples
224///
225/// ```
226/// # use char_classes::any;
227/// assert!(any("ab",       'a'));
228/// assert!(any("ab",       'b'));
229/// assert!(any("a-c",      'a'));
230/// assert!(any("a-c",      'b'));
231/// assert!(any("a-ce-f",   'e'));
232/// assert!(any("a-ce-",    '-'));
233/// assert!(any("a-ce-",    'e'));
234/// assert!(any("a-c",      Some('b')));
235/// assert!(any("a-c",      ['b', 'd']));
236/// assert!(any("a-c",      "bd"));
237///
238/// assert!(! any("a-c",    '-'));
239/// assert!(! any("a-ce-",  'f'));
240///
241/// assert!(! any("a-c",    None::<char>));
242/// assert!(! any("a-c",    ['d', 'b']));
243/// assert!(! any("a-c",    "db"));
244/// ```
245pub fn any<T>(pattern: impl AsRef<T::Pattern>, val: T) -> bool
246where T: MatchOne,
247{
248    val.is_match_one(pattern.as_ref())
249}
250
251fn match_impl<I>(pattern: I, val: &I::Item) -> bool
252where I: IntoIterator,
253      I::Item: PartialOrd + From<u8> + Copy,
254{
255    let pat = &mut pattern.into_iter();
256    let dash = &I::Item::from(b'-');
257
258    let Some(mut first) = pat.next() else {
259        return false;
260    };
261
262    while let Some(ref cur) = pat.next() {
263        if first == *val {
264            return true;
265        }
266        if cur != dash {
267            first = *cur;
268        } else if let Some(to) = pat.next() {
269            if (first..=to).contains(val) {
270                return true;
271            }
272            if let Some(next) = pat.next() {
273                first = next;
274            } else {
275                return false;
276            }
277        } else {
278            first = *cur;
279        }
280    }
281
282    first == *val
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn basic_pattern() {
291        let datas = [
292            ("a", 'a'),
293            ("ab", 'a'),
294            ("ba", 'a'),
295            ("bac", 'a'),
296            ("bca", 'a'),
297            ("a-e", 'c'),
298            ("a-c", 'c'),
299            ("a-bc", 'c'),
300            ("a-bc", 'a'),
301            ("a-bc", 'b'),
302            ("你好", '你'),
303            ("你好", '好'),
304            ("a-", 'a'),
305            ("a-", '-'),
306            ("-a", '-'),
307            ("-", '-'),
308        ];
309
310        for (pat, val) in datas {
311            assert!(match_impl(pat.chars(), &val), "{pat:?}, {val:?}");
312        }
313    }
314
315    #[test]
316    fn basic_not_pattern() {
317        let datas = [
318            ("", 'a'),
319            ("a-b", '-'),
320            ("a-", 'c'),
321            ("a-", 'b'),
322        ];
323
324        for (pat, val) in datas {
325            assert!(! match_impl(pat.chars(), &val), "{pat:?}, {val:?}");
326        }
327    }
328
329    #[test]
330    fn first_pattern() {
331        let datas = [
332            ("a", "a"),
333            ("你", "你好"),
334            ("a-c", "a"),
335            ("a-c", "b"),
336        ];
337
338        for (pat, val) in datas {
339            assert!(any(pat, val));
340        }
341    }
342
343    #[test]
344    fn first_not_pattern_rest() {
345        let datas = [
346            ("a", "da"),
347            ("好", "你好"),
348            ("a-c", "da"),
349            ("a-c", "db"),
350            ("ab", "db"),
351        ];
352
353        for (pat, val) in datas {
354            assert!(! any(pat, val));
355        }
356    }
357
358    #[test]
359    fn first_not_pattern_empty() {
360        let datas = [
361            ("a", ""),
362            ("a-c", ""),
363        ];
364
365        for (pat, val) in datas {
366            assert!(! any(pat, val));
367        }
368    }
369
370    #[test]
371    fn dash_range() {
372        let datas = [
373            ("+--", "+"),
374            ("+--", ","),
375            ("+--", "-"),
376            ("+--a", "+"),
377            ("+--a", ","),
378            ("+--a", "-"),
379            ("+--a", "a"),
380            ("--0", "-"),
381            ("--0", "."),
382            ("--0", "/"),
383            ("--0", "0"),
384            ("--0a", "-"),
385            ("--0a", "."),
386            ("--0a", "/"),
387            ("--0a", "0"),
388            ("--0a", "a"),
389            ("a-c-e-g", "a"),
390            ("a-c-e-g", "b"),
391            ("a-c-e-g", "c"),
392            ("a-c-e-g", "e"),
393            ("a-c-e-g", "f"),
394            ("a-c-e-g", "g"),
395            ("a-c-e-g", "-"),
396        ];
397
398        for (pat, val) in datas {
399            assert!(any(pat, val), "{pat:?} cannot pattern {val:?}");
400        }
401    }
402
403    #[test]
404    fn dash_range_not_pat() {
405        let datas = [
406            ("+--a", "0"),
407            ("---a", "0"),
408            ("a-c-e-g", "d"),
409        ];
410
411        for (pat, val) in datas {
412            assert!(! any(pat, val));
413        }
414    }
415}