Skip to main content

string_patterns/
pattern_capture.rs

1use regex::{Captures, Match};
2
3use crate::pattern_cache::with_cached_regex;
4use crate::utils::build_whole_word_pattern;
5
6/// Set of methods to capture groups or match objects derived from Regex::captures.
7pub trait PatternCapture<'a> {
8
9  /// Yields an option with Regex::Captures as returned from re.captures, Accepts a boolean case_insensitive flag
10  fn pattern_captures(&'a self, pattern: &str, case_insensitive: bool) -> Option<Captures<'a>>;
11
12  /// Yields a vector of Match objects with two modes, outer will whole groups only, otherwise uniqe matched groups and subgroups
13  /// Use either pattern_matches_vec or pattern_matches_outer
14  fn pattern_matches_as_vec(&'a self, pattern: &str, case_insensitive: bool, outer: bool) -> Vec<Match<'a>>;
15
16  /// Yields a vector of Match objects with start and end index + the captured string. Accepts a boolean case_insensitive flag
17  /// Unlike pattern_captures, this method will only return unique matches including subgroups
18  fn pattern_matches_vec(&'a self, pattern: &str, case_insensitive: bool) -> Vec<Match<'a>> {
19    self.pattern_matches_as_vec(pattern, case_insensitive, false)
20  }
21
22   /// Yields a vector of Match objects with start and end index + the captured string. Accepts a boolean case_insensitive flag
23  /// Unlike pattern_captures, this method will only outer matches for whole pattern
24  fn pattern_matches_outer(&'a self, pattern: &str, case_insensitive: bool) -> Vec<Match<'a>> {
25    self.pattern_matches_as_vec(pattern, case_insensitive, true)
26  }
27
28  /// Yields an option with first match object if available with a boolean case_insensitive flag
29  /// As this uses re.find it will be fast than the matching last_match method
30  fn pattern_first_match(&'a self, pattern: &str, case_insensitive: bool) -> Option<Match<'a>>;
31
32 /// Yields an option with last match object if available with a boolean case_insensitive flag
33 fn pattern_last_match(&'a self, pattern: &str, case_insensitive: bool) -> Option<Match<'a>> {
34   let matched_segments = self.pattern_matches_vec(pattern, case_insensitive);
35   matched_segments.last().map(|m| *m)
36 }
37
38 /// returns an option with a pair of match objects
39 /// If there is only one match the match objects will have the same indices
40 fn pattern_first_last_matches(&'a self, pattern: &str, case_insensitive: bool) -> Option<(Match<'a>, Match<'a>)> {
41   let matched_segments = self.pattern_matches_vec(pattern, case_insensitive);
42   if let Some(first) = matched_segments.get(0) {
43     if let Some(last) = matched_segments.last() {
44       return Some((*first, *last));
45     }
46   }
47   None
48 }
49
50 /// Yields an option with an unsigned integer for the index of the start of the last match
51 /// with a boolean case_insensitive flag
52 fn pattern_first_index(&'a self, pattern: &str, case_insensitive: bool) -> Option<usize> {
53   if let Some(first) = self.pattern_first_match(pattern, case_insensitive) {
54     Some(first.start())
55   } else {
56     None
57   }
58 }
59
60 /// Yields an option with an unsigned integer for the index of the end of the first match
61 /// with a boolean case_insensitive flag
62 fn pattern_first_end_index(&'a self, pattern: &str, case_insensitive: bool) -> Option<usize> {
63   if let Some(first) = self.pattern_first_match(pattern, case_insensitive) {
64     Some(first.end())
65   } else {
66     None
67   }
68 }
69
70 /// Yields an option with an unsigned integer for the index of the start of the last match
71 /// with a boolean case_insensitive flag
72 fn pattern_last_start_index(&'a self, pattern: &str, case_insensitive: bool) -> Option<usize> {
73   if let Some(first) = self.pattern_first_match(pattern, case_insensitive) {
74     Some(first.start())
75   } else {
76     None
77   }
78 }
79
80 // Yields an option with an unsigned integer for the index of the end of the last match
81 /// with a boolean case_insensitive flag
82 fn pattern_last_index(&'a self, pattern: &str, case_insensitive: bool) -> Option<usize> {
83   if let Some(first) = self.pattern_first_match(pattern, case_insensitive) {
84     Some(first.end())
85   } else {
86     None
87   }
88 }
89
90 // Counts the number of matches with a boolean case_insensitive flag
91 fn count_pattern(&'a self, pattern: &'a str, case_insensitive: bool) -> usize {
92   self.pattern_matches_vec(pattern, case_insensitive).len()
93 }
94
95 // Counts the number of matches with a boolean case_insensitive flag
96 fn count_word(&'a self, word: &'a str, case_insensitive: bool) -> usize {
97   let pattern = build_whole_word_pattern(word);
98   self.pattern_matches_vec(&pattern, case_insensitive).len()
99 }
100}
101
102
103/// This function is the basis for both pattern_matches_vec() and pattern_matches_outer()
104/// and will be used with string-patterns-extras to replicate look-ahead and look-behind behaviour
105/// It returns a flattened vector of Match objects
106/// The outer options limits the matches to the whole matched sequence and excludes inner groups
107pub fn find_matches_within_haystack<'a>(haystack: &'a str, pattern: &str, case_insensitive: bool, outer: bool) -> Vec<Match<'a>> {
108  with_cached_regex(pattern, case_insensitive, |re| {
109    let mut matched_items: Vec<Match<'a>> = Vec::new();
110    let mut item_keys: Vec<(&str, usize, usize)> = Vec::new();
111    for inner_captures in re.captures_iter(haystack) {
112      for capture_opt in inner_captures.iter() {
113        if let Some(matched_item) = capture_opt {
114          let item_str = matched_item.as_str();
115
116          let item_key = (item_str, matched_item.start(), matched_item.end());
117          let is_matched = if outer {
118            true
119          } else {
120            item_keys.contains(&item_key) == false
121          };
122          if is_matched {
123            matched_items.push(matched_item.to_owned());
124            if !outer {
125              item_keys.push(item_key);
126            }
127          }
128          // if only capturing the first group of outer matches, break the inner loop here and move onto the next outer group
129          if outer {
130            break;
131          }
132        }
133      }
134    }
135    matched_items
136  })
137  .unwrap_or_default()
138}
139
140/// Implementation for any string-like type (&str, String, &String, `Cow<str>`, etc.)
141impl<'a, T: AsRef<str>> PatternCapture<'a> for T {
142
143  /// Yields an option with Regex::Captures as returned from re.captures, Accepts a boolean case_insensitive flag
144  fn pattern_captures(&'a self, pattern: &str, case_insensitive: bool) -> Option<Captures<'a>> {
145    with_cached_regex(pattern, case_insensitive, |re| re.captures(self.as_ref()))
146      .ok()
147      .flatten()
148  }
149
150  /// Returns vector of match objects. The outer options excludes inner match groups.
151  fn pattern_matches_as_vec(&'a self, pattern: &str, case_insensitive: bool, outer: bool) -> Vec<Match<'a>> {
152    find_matches_within_haystack(self.as_ref(), pattern, case_insensitive, outer)
153  }
154
155  /// Yields an option with first match object if available with a boolean case_insensitive flag
156  /// As this uses re.find it will be fast than the matching last_match method
157  /// Implemented here to shortcut the larger find_matches_within_haystack function
158  fn pattern_first_match(&'a self, pattern: &str, case_insensitive: bool) -> Option<Match<'a>> {
159    with_cached_regex(pattern, case_insensitive, |re| re.find(self.as_ref()))
160      .ok()
161      .flatten()
162  }
163
164}