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