Skip to main content

rstring/
contains.rs

1//! Containment check utilities.
2//!
3//! This module provides functions for checking whether strings contain
4//! specific characters or substrings, including case-insensitive variants
5//! and checks for character sets.
6//!
7//! # Usage
8//!
9//! Import the [`StringContains`] trait to use methods directly on strings:
10//!
11//! ```
12//! use rstring::StringContains;
13//!
14//! assert!("zzabyycdxx".contains_any_char(&['z', 'a']));
15//! assert!("abab".contains_only("abc"));
16//! assert!("abc".contains_ignore_case("A"));
17//! ```
18
19use crate::guard_empty;
20
21/// Extension trait for containment check methods.
22///
23/// This trait is implemented for `str`, allowing you to call containment
24/// check methods directly on `&str`, `String`, and other string types.
25///
26/// # Examples
27///
28/// ```
29/// use rstring::StringContains;
30///
31/// // Works with &str
32/// assert!("hello world".contains_whitespace());
33///
34/// // Works with String
35/// assert!(String::from("abc").contains_any_char(&['a', 'x']));
36/// ```
37pub trait StringContains {
38    /// Checks if the string contains any of the specified characters.
39    ///
40    /// Returns `false` if the string is empty or the character array is empty.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use rstring::StringContains;
46    ///
47    /// assert!(!"".contains_any_char(&['a', 'b']));
48    /// assert!(!"abc".contains_any_char(&[]));
49    /// assert!("zzabyycdxx".contains_any_char(&['z', 'a']));
50    /// assert!("zzabyycdxx".contains_any_char(&['b', 'y']));
51    /// assert!("zzabyycdxx".contains_any_char(&['z', 'y']));
52    /// assert!(!"aba".contains_any_char(&['z']));
53    /// ```
54    #[must_use]
55    fn contains_any_char(&self, search_chars: &[char]) -> bool;
56
57    /// Checks if the string contains any character from the search string.
58    ///
59    /// Returns `false` if either string is empty.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use rstring::StringContains;
65    ///
66    /// assert!(!"".contains_any_in("ab"));
67    /// assert!(!"abc".contains_any_in(""));
68    /// assert!("zzabyycdxx".contains_any_in("za"));
69    /// assert!("zzabyycdxx".contains_any_in("by"));
70    /// assert!(!"aba".contains_any_in("z"));
71    /// ```
72    #[must_use]
73    fn contains_any_in(&self, search_chars: &str) -> bool;
74
75    /// Checks if the string contains any of the specified substrings.
76    ///
77    /// Returns `false` if the string is empty or the search array is empty.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use rstring::StringContains;
83    ///
84    /// assert!(!"".contains_any(&["ab", "cd"]));
85    /// assert!(!"abc".contains_any(&[]));
86    /// assert!("abcd".contains_any(&["ab", "cd"]));
87    /// assert!("abcd".contains_any(&["ab", "xy"]));
88    /// assert!(!"abcd".contains_any(&["xy", "zz"]));
89    /// ```
90    #[must_use]
91    fn contains_any(&self, search: &[&str]) -> bool;
92
93    /// Checks if the string contains the search string, ignoring case.
94    ///
95    /// Returns `false` if either string is empty.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use rstring::StringContains;
101    ///
102    /// assert!(!"".contains_ignore_case("a"));
103    /// assert!(!"abc".contains_ignore_case(""));
104    /// assert!("abc".contains_ignore_case("A"));
105    /// assert!("abc".contains_ignore_case("ABC"));
106    /// assert!("ABC".contains_ignore_case("abc"));
107    /// assert!(!"abc".contains_ignore_case("Z"));
108    /// ```
109    #[must_use]
110    fn contains_ignore_case(&self, search: &str) -> bool;
111
112    /// Checks if the string contains any of the search strings, ignoring case.
113    ///
114    /// Returns `false` if the string is empty or the search array is empty.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use rstring::StringContains;
120    ///
121    /// assert!(!"".contains_any_ignore_case(&["ab"]));
122    /// assert!(!"abc".contains_any_ignore_case(&[]));
123    /// assert!("abcd".contains_any_ignore_case(&["AB", "cd"]));
124    /// assert!("abc".contains_any_ignore_case(&["D", "ABC"]));
125    /// assert!(!"abc".contains_any_ignore_case(&["D", "XYZ"]));
126    /// ```
127    #[must_use]
128    fn contains_any_ignore_case(&self, search: &[&str]) -> bool;
129
130    /// Checks if the string does NOT contain any of the specified characters.
131    ///
132    /// Returns `true` if the string is empty.
133    /// Returns `true` if the invalid characters array is empty.
134    ///
135    /// # Examples
136    ///
137    /// ```
138    /// use rstring::StringContains;
139    ///
140    /// assert!("".contains_none_char(&['a', 'b']));
141    /// assert!("abc".contains_none_char(&[]));
142    /// assert!("abab".contains_none_char(&['x', 'y', 'z']));
143    /// assert!(!"abab".contains_none_char(&['a', 'b', 'z']));
144    /// assert!(!"abab".contains_none_char(&['x', 'y', 'z', 'a']));
145    /// ```
146    #[must_use]
147    fn contains_none_char(&self, invalid_chars: &[char]) -> bool;
148
149    /// Checks if the string does NOT contain any character from the invalid string.
150    ///
151    /// Returns `true` if the string is empty.
152    /// Returns `true` if the invalid characters string is empty.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use rstring::StringContains;
158    ///
159    /// assert!("".contains_none("ab"));
160    /// assert!("abc".contains_none(""));
161    /// assert!("abab".contains_none("xyz"));
162    /// assert!(!"abab".contains_none("abz"));
163    /// assert!(!"abz".contains_none("xyz"));
164    /// ```
165    #[must_use]
166    fn contains_none(&self, invalid_chars: &str) -> bool;
167
168    /// Checks if the string contains ONLY the specified characters.
169    ///
170    /// Returns `false` if the string is empty.
171    /// Returns `false` if the valid characters array is empty.
172    /// Returns `true` for an empty string when valid chars are provided.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use rstring::StringContains;
178    ///
179    /// assert!(!"abc".contains_only_char(&[]));
180    /// assert!("ab".contains_only_char(&['a', 'b', 'c']));
181    /// assert!("abab".contains_only_char(&['a', 'b', 'c']));
182    /// assert!(!"ab1".contains_only_char(&['a', 'b', 'c']));
183    /// assert!(!"abz".contains_only_char(&['a', 'b', 'c']));
184    /// ```
185    #[must_use]
186    fn contains_only_char(&self, valid_chars: &[char]) -> bool;
187
188    /// Checks if the string contains ONLY characters from the valid string.
189    ///
190    /// Returns `false` if the string is empty.
191    /// Returns `false` if the valid characters string is empty.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use rstring::StringContains;
197    ///
198    /// assert!(!"abc".contains_only(""));
199    /// assert!("ab".contains_only("abc"));
200    /// assert!("abab".contains_only("abc"));
201    /// assert!(!"ab1".contains_only("abc"));
202    /// assert!(!"abz".contains_only("abc"));
203    /// ```
204    #[must_use]
205    fn contains_only(&self, valid_chars: &str) -> bool;
206
207    /// Checks if the string contains any whitespace characters.
208    ///
209    /// Whitespace is defined by [`char::is_whitespace`].
210    ///
211    /// Returns `false` if the string is empty.
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use rstring::StringContains;
217    ///
218    /// assert!(!"".contains_whitespace());
219    /// assert!("  ".contains_whitespace());
220    /// assert!(" ab".contains_whitespace());
221    /// assert!("ab ".contains_whitespace());
222    /// assert!("a b".contains_whitespace());
223    /// assert!("a\tb".contains_whitespace());
224    /// assert!("a\nb".contains_whitespace());
225    /// assert!(!"abc".contains_whitespace());
226    /// ```
227    #[must_use]
228    fn contains_whitespace(&self) -> bool;
229}
230
231impl StringContains for str {
232    fn contains_any_char(&self, search_chars: &[char]) -> bool {
233        guard_empty!(self, search_chars, false);
234        self.chars().any(|c| search_chars.contains(&c))
235    }
236
237    fn contains_any_in(&self, search_chars: &str) -> bool {
238        guard_empty!(self, search_chars, false);
239        self.chars().any(|c| search_chars.chars().any(|sc| sc == c))
240    }
241
242    fn contains_any(&self, search: &[&str]) -> bool {
243        guard_empty!(self, search, false);
244        search.iter().any(|s| !s.is_empty() && self.contains(s))
245    }
246
247    fn contains_ignore_case(&self, search: &str) -> bool {
248        guard_empty!(self, search, false);
249        self.to_lowercase().contains(&search.to_lowercase())
250    }
251
252    fn contains_any_ignore_case(&self, search: &[&str]) -> bool {
253        guard_empty!(self, search, false);
254        let lower = self.to_lowercase();
255        search
256            .iter()
257            .any(|s| !s.is_empty() && lower.contains(&s.to_lowercase()))
258    }
259
260    fn contains_none_char(&self, invalid_chars: &[char]) -> bool {
261        guard_empty!(self, invalid_chars, true);
262        !self.chars().any(|c| invalid_chars.contains(&c))
263    }
264
265    fn contains_none(&self, invalid_chars: &str) -> bool {
266        guard_empty!(self, invalid_chars, true);
267        !self
268            .chars()
269            .any(|c| invalid_chars.chars().any(|ic| ic == c))
270    }
271
272    fn contains_only_char(&self, valid_chars: &[char]) -> bool {
273        guard_empty!(self, valid_chars, false);
274        self.chars().all(|c| valid_chars.contains(&c))
275    }
276
277    fn contains_only(&self, valid_chars: &str) -> bool {
278        guard_empty!(self, valid_chars, false);
279        self.chars().all(|c| valid_chars.chars().any(|vc| vc == c))
280    }
281
282    fn contains_whitespace(&self) -> bool {
283        self.chars().any(|c| c.is_whitespace())
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    mod contains_any_char {
292        use super::*;
293
294        #[test]
295        fn empty_string() {
296            assert!(!"".contains_any_char(&['a', 'b']));
297        }
298
299        #[test]
300        fn empty_search() {
301            assert!(!"abc".contains_any_char(&[]));
302        }
303
304        #[test]
305        fn both_empty() {
306            assert!(!"".contains_any_char(&[]));
307        }
308
309        #[test]
310        fn found_first_char() {
311            assert!("zzabyycdxx".contains_any_char(&['z', 'a']));
312        }
313
314        #[test]
315        fn found_middle_char() {
316            assert!("zzabyycdxx".contains_any_char(&['b', 'y']));
317        }
318
319        #[test]
320        fn found_multiple() {
321            assert!("zzabyycdxx".contains_any_char(&['z', 'y']));
322        }
323
324        #[test]
325        fn not_found() {
326            assert!(!"aba".contains_any_char(&['z']));
327        }
328
329        #[test]
330        fn unicode() {
331            assert!("日本語".contains_any_char(&['本', 'x']));
332            assert!(!"日本語".contains_any_char(&['x', 'y']));
333        }
334    }
335
336    mod contains_any_in {
337        use super::*;
338
339        #[test]
340        fn empty_string() {
341            assert!(!"".contains_any_in("ab"));
342        }
343
344        #[test]
345        fn empty_search() {
346            assert!(!"abc".contains_any_in(""));
347        }
348
349        #[test]
350        fn both_empty() {
351            assert!(!"".contains_any_in(""));
352        }
353
354        #[test]
355        fn found() {
356            assert!("zzabyycdxx".contains_any_in("za"));
357        }
358
359        #[test]
360        fn found_middle() {
361            assert!("zzabyycdxx".contains_any_in("by"));
362        }
363
364        #[test]
365        fn not_found() {
366            assert!(!"aba".contains_any_in("z"));
367        }
368
369        #[test]
370        fn unicode() {
371            assert!("日本語".contains_any_in("本x"));
372            assert!(!"日本語".contains_any_in("xy"));
373        }
374    }
375
376    mod contains_any {
377        use super::*;
378
379        #[test]
380        fn empty_string() {
381            assert!(!"".contains_any(&["ab", "cd"]));
382        }
383
384        #[test]
385        fn empty_search() {
386            assert!(!"abc".contains_any(&[]));
387        }
388
389        #[test]
390        fn both_empty() {
391            assert!(!"".contains_any(&[]));
392        }
393
394        #[test]
395        fn found_first() {
396            assert!("abcd".contains_any(&["ab", "cd"]));
397        }
398
399        #[test]
400        fn found_second() {
401            assert!("abcd".contains_any(&["xy", "cd"]));
402        }
403
404        #[test]
405        fn not_found() {
406            assert!(!"abcd".contains_any(&["xy", "zz"]));
407        }
408
409        #[test]
410        fn empty_search_string() {
411            assert!(!"abc".contains_any(&[""]));
412            assert!("abc".contains_any(&["", "b"]));
413        }
414
415        #[test]
416        fn unicode() {
417            assert!("日本語".contains_any(&["本", "x"]));
418            assert!(!"日本語".contains_any(&["x", "y"]));
419        }
420
421        #[test]
422        fn with_empty_string_in_array() {
423            assert!("abcd".contains_any(&["ab", ""]));
424        }
425
426        #[test]
427        fn case_sensitive() {
428            assert!(!"hello, goodbye".contains_any(&["Hello", "Goodbye"]));
429        }
430    }
431
432    mod contains_ignore_case {
433        use super::*;
434
435        #[test]
436        fn empty_string() {
437            assert!(!"".contains_ignore_case("a"));
438        }
439
440        #[test]
441        fn empty_search() {
442            assert!(!"abc".contains_ignore_case(""));
443        }
444
445        #[test]
446        fn both_empty() {
447            assert!(!"".contains_ignore_case(""));
448        }
449
450        #[test]
451        fn found_lower_in_lower() {
452            assert!("abc".contains_ignore_case("a"));
453        }
454
455        #[test]
456        fn found_upper_in_lower() {
457            assert!("abc".contains_ignore_case("A"));
458        }
459
460        #[test]
461        fn found_lower_in_upper() {
462            assert!("ABC".contains_ignore_case("a"));
463        }
464
465        #[test]
466        fn found_mixed() {
467            assert!("abc".contains_ignore_case("ABC"));
468            assert!("ABC".contains_ignore_case("abc"));
469            assert!("AbCdEf".contains_ignore_case("cDe"));
470        }
471
472        #[test]
473        fn not_found() {
474            assert!(!"abc".contains_ignore_case("Z"));
475        }
476
477        #[test]
478        fn unicode() {
479            assert!("Héllo".contains_ignore_case("héllo"));
480            assert!("HÉLLO".contains_ignore_case("héllo"));
481        }
482    }
483
484    mod contains_any_ignore_case {
485        use super::*;
486
487        #[test]
488        fn empty_string() {
489            assert!(!"".contains_any_ignore_case(&["ab"]));
490        }
491
492        #[test]
493        fn empty_search() {
494            assert!(!"abc".contains_any_ignore_case(&[]));
495        }
496
497        #[test]
498        fn found() {
499            assert!("abcd".contains_any_ignore_case(&["AB", "cd"]));
500        }
501
502        #[test]
503        fn found_second() {
504            assert!("abc".contains_any_ignore_case(&["D", "ABC"]));
505        }
506
507        #[test]
508        fn not_found() {
509            assert!(!"abc".contains_any_ignore_case(&["D", "XYZ"]));
510        }
511
512        #[test]
513        fn empty_search_string() {
514            assert!(!"abc".contains_any_ignore_case(&[""]));
515        }
516
517        #[test]
518        fn hello_goodbye_both_lowercase() {
519            assert!("hello, goodbye".contains_any_ignore_case(&["hello", "goodbye"]));
520        }
521
522        #[test]
523        fn hello_goodbye_mixed_case() {
524            assert!("hello, goodbye".contains_any_ignore_case(&["hello", "Goodbye"]));
525        }
526
527        #[test]
528        fn hello_goodbye_both_capitalized() {
529            assert!("hello, goodbye".contains_any_ignore_case(&["Hello", "Goodbye"]));
530        }
531    }
532
533    mod contains_none_char {
534        use super::*;
535
536        #[test]
537        fn empty_string() {
538            assert!("".contains_none_char(&['a', 'b']));
539        }
540
541        #[test]
542        fn empty_invalid() {
543            assert!("abc".contains_none_char(&[]));
544        }
545
546        #[test]
547        fn both_empty() {
548            assert!("".contains_none_char(&[]));
549        }
550
551        #[test]
552        fn none_found() {
553            assert!("abab".contains_none_char(&['x', 'y', 'z']));
554        }
555
556        #[test]
557        fn found_first() {
558            assert!(!"abab".contains_none_char(&['a', 'b', 'z']));
559        }
560
561        #[test]
562        fn found_last() {
563            assert!(!"abab".contains_none_char(&['x', 'y', 'z', 'a']));
564        }
565
566        #[test]
567        fn unicode() {
568            assert!("日本語".contains_none_char(&['x', 'y']));
569            assert!(!"日本語".contains_none_char(&['本', 'x']));
570        }
571    }
572
573    mod contains_none {
574        use super::*;
575
576        #[test]
577        fn empty_string() {
578            assert!("".contains_none("ab"));
579        }
580
581        #[test]
582        fn empty_invalid() {
583            assert!("abc".contains_none(""));
584        }
585
586        #[test]
587        fn both_empty() {
588            assert!("".contains_none(""));
589        }
590
591        #[test]
592        fn none_found() {
593            assert!("abab".contains_none("xyz"));
594        }
595
596        #[test]
597        fn found() {
598            assert!(!"abab".contains_none("abz"));
599        }
600
601        #[test]
602        fn found_partial() {
603            assert!(!"abz".contains_none("xyz"));
604        }
605
606        #[test]
607        fn unicode() {
608            assert!("日本語".contains_none("xy"));
609            assert!(!"日本語".contains_none("本x"));
610        }
611
612        #[test]
613        fn single_none() {
614            assert!("a".contains_none("b"));
615        }
616
617        #[test]
618        fn single_contains() {
619            assert!(!"a".contains_none("a"));
620        }
621
622        #[test]
623        fn single_contains_2() {
624            assert!(!"b".contains_none("b"));
625        }
626
627        #[test]
628        fn first_char_contained() {
629            assert!(!"ab".contains_none("a"));
630        }
631
632        #[test]
633        fn second_char_contained() {
634            assert!(!"ab".contains_none("b"));
635        }
636    }
637
638    mod contains_only_char {
639        use super::*;
640
641        #[test]
642        fn empty_string() {
643            assert!(!"".contains_only_char(&['a', 'b']));
644        }
645
646        #[test]
647        fn empty_valid() {
648            assert!(!"abc".contains_only_char(&[]));
649        }
650
651        #[test]
652        fn both_empty() {
653            assert!(!"".contains_only_char(&[]));
654        }
655
656        #[test]
657        fn valid() {
658            assert!("ab".contains_only_char(&['a', 'b', 'c']));
659        }
660
661        #[test]
662        fn valid_repeated() {
663            assert!("abab".contains_only_char(&['a', 'b', 'c']));
664        }
665
666        #[test]
667        fn invalid_digit() {
668            assert!(!"ab1".contains_only_char(&['a', 'b', 'c']));
669        }
670
671        #[test]
672        fn invalid_char() {
673            assert!(!"abz".contains_only_char(&['a', 'b', 'c']));
674        }
675
676        #[test]
677        fn unicode() {
678            assert!("日本".contains_only_char(&['日', '本', '語']));
679            assert!(!"日本x".contains_only_char(&['日', '本', '語']));
680        }
681    }
682
683    mod contains_only {
684        use super::*;
685
686        #[test]
687        fn empty_string() {
688            assert!(!"".contains_only("abc"));
689        }
690
691        #[test]
692        fn empty_valid() {
693            assert!(!"abc".contains_only(""));
694        }
695
696        #[test]
697        fn both_empty() {
698            assert!(!"".contains_only(""));
699        }
700
701        #[test]
702        fn valid() {
703            assert!("ab".contains_only("abc"));
704        }
705
706        #[test]
707        fn valid_repeated() {
708            assert!("abab".contains_only("abc"));
709        }
710
711        #[test]
712        fn invalid_digit() {
713            assert!(!"ab1".contains_only("abc"));
714        }
715
716        #[test]
717        fn invalid_char() {
718            assert!(!"abz".contains_only("abc"));
719        }
720
721        #[test]
722        fn unicode() {
723            assert!("日本".contains_only("日本語"));
724            assert!(!"日本x".contains_only("日本語"));
725        }
726
727        #[test]
728        fn does_contain_any() {
729            assert!(!"a".contains_only("b"));
730        }
731
732        #[test]
733        fn contains_single() {
734            assert!("a".contains_only("a"));
735        }
736
737        #[test]
738        fn contains_single_and_ignored() {
739            assert!("a".contains_only("ab"));
740        }
741
742        #[test]
743        fn equals() {
744            assert!("ab".contains_only("ab"));
745        }
746    }
747
748    mod contains_whitespace {
749        use super::*;
750
751        #[test]
752        fn empty_string() {
753            assert!(!"".contains_whitespace());
754        }
755
756        #[test]
757        fn only_spaces() {
758            assert!("  ".contains_whitespace());
759        }
760
761        #[test]
762        fn leading_space() {
763            assert!(" ab".contains_whitespace());
764        }
765
766        #[test]
767        fn trailing_space() {
768            assert!("ab ".contains_whitespace());
769        }
770
771        #[test]
772        fn middle_space() {
773            assert!("a b".contains_whitespace());
774        }
775
776        #[test]
777        fn tab() {
778            assert!("a\tb".contains_whitespace());
779        }
780
781        #[test]
782        fn newline() {
783            assert!("a\nb".contains_whitespace());
784        }
785
786        #[test]
787        fn carriage_return() {
788            assert!("a\rb".contains_whitespace());
789        }
790
791        #[test]
792        fn no_whitespace() {
793            assert!(!"abc".contains_whitespace());
794        }
795
796        #[test]
797        fn single_char_no_whitespace() {
798            assert!(!"a".contains_whitespace());
799        }
800
801        #[test]
802        fn trailing_tab() {
803            assert!("a\t".contains_whitespace());
804        }
805
806        #[test]
807        fn only_newline() {
808            assert!("\n".contains_whitespace());
809        }
810
811        #[test]
812        fn unicode_space() {
813            // Non-breaking space
814            assert!("a\u{00A0}b".contains_whitespace());
815        }
816    }
817
818    mod string_types {
819        use super::*;
820
821        #[test]
822        fn string_type() {
823            assert!(String::from("abc").contains_any_char(&['a']));
824        }
825
826        #[test]
827        fn string_ref() {
828            let s = String::from("abc");
829            assert_eq!(s.contains_whitespace(), false);
830        }
831
832        #[test]
833        fn boxed_str() {
834            let s: Box<str> = "a b".into();
835            assert!(s.contains_whitespace());
836        }
837    }
838}