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