Skip to main content

str_utils/
pattern.rs

1use alloc::{borrow::Cow, string::String};
2
3mod sealed {
4    pub trait Sealed {}
5
6    impl Sealed for char {}
7    impl Sealed for &str {}
8    impl Sealed for &&str {}
9    impl Sealed for &[char] {}
10
11    impl<const N: usize> Sealed for [char; N] {}
12    impl<const N: usize> Sealed for &[char; N] {}
13
14    impl<F> Sealed for F where F: FnMut(char) -> bool {}
15}
16
17#[inline]
18fn char_eq_str(c: char, s: &str) -> bool {
19    let mut buffer = [0; 4];
20
21    c.encode_utf8(&mut buffer) == s
22}
23
24fn remove_matches<'a, I>(s: &'a str, matches: I) -> Cow<'a, str>
25where
26    I: Iterator<Item = (usize, usize)>, {
27    let mut borrowed_start = 0;
28    let mut pending: Option<(usize, usize)> = None;
29    let mut new_s: Option<String> = None;
30    let mut start = 0;
31
32    for (match_start, match_end) in matches {
33        if let Some(new_s) = new_s.as_mut() {
34            new_s.push_str(&s[start..match_start]);
35            start = match_end;
36
37            continue;
38        }
39
40        if let Some((pending_start, pending_end)) = pending.as_mut() {
41            if match_start == *pending_end {
42                *pending_end = match_end;
43
44                continue;
45            }
46
47            // A later match proves that the pending removed range is inside the remaining text.
48            let mut owned = String::with_capacity(s.len());
49
50            owned.push_str(&s[borrowed_start..*pending_start]);
51            owned.push_str(&s[*pending_end..match_start]);
52
53            new_s = Some(owned);
54            start = match_end;
55
56            continue;
57        }
58
59        if match_start == borrowed_start {
60            borrowed_start = match_end;
61        } else {
62            pending = Some((match_start, match_end));
63        }
64    }
65
66    if let Some(mut new_s) = new_s {
67        new_s.push_str(&s[start..]);
68
69        return Cow::Owned(new_s);
70    }
71
72    match pending {
73        Some((pending_start, pending_end)) if pending_end == s.len() => {
74            Cow::Borrowed(&s[borrowed_start..pending_start])
75        },
76        Some((pending_start, pending_end)) => {
77            let mut new_s = String::with_capacity(s.len());
78
79            new_s.push_str(&s[borrowed_start..pending_start]);
80            new_s.push_str(&s[pending_end..]);
81
82            Cow::Owned(new_s)
83        },
84        None => Cow::Borrowed(&s[borrowed_start..]),
85    }
86}
87
88fn replace_chars<'a, F>(s: &'a str, mut from: F, to: &str, mut count: usize) -> Cow<'a, str>
89where
90    F: FnMut(char) -> bool, {
91    if count == 0 {
92        return Cow::Borrowed(s);
93    }
94
95    if to.is_empty() {
96        let matches =
97            s.char_indices().filter_map(
98                |(p, c)| {
99                    if from(c) {
100                        Some((p, p + c.len_utf8()))
101                    } else {
102                        None
103                    }
104                },
105            );
106
107        return remove_matches(s, matches.take(count));
108    }
109
110    let mut new_s: Option<String> = None;
111    let mut start = 0;
112
113    for (p, c) in s.char_indices() {
114        if count == 0 {
115            break;
116        }
117
118        if from(c) {
119            let next = p + c.len_utf8();
120
121            if char_eq_str(c, to) {
122                count -= 1;
123
124                continue;
125            }
126
127            if let Some(new_s) = new_s.as_mut() {
128                new_s.push_str(&s[start..p]);
129                new_s.push_str(to);
130            } else {
131                let mut owned = String::with_capacity(s.len());
132
133                owned.push_str(&s[..p]);
134                owned.push_str(to);
135
136                new_s = Some(owned);
137            }
138
139            start = next;
140            count -= 1;
141        }
142    }
143
144    match new_s {
145        Some(mut new_s) => {
146            new_s.push_str(&s[start..]);
147
148            Cow::Owned(new_s)
149        },
150        None => Cow::Borrowed(s),
151    }
152}
153
154#[inline]
155fn replace_str<'a>(s: &'a str, from: &str, to: &str) -> Cow<'a, str> {
156    if from == to {
157        Cow::Borrowed(s)
158    } else if to.is_empty() {
159        remove_matches(s, s.match_indices(from).map(|(p, from)| (p, p + from.len())))
160    } else if !s.contains(from) {
161        Cow::Borrowed(s)
162    } else {
163        Cow::Owned(s.replace(from, to))
164    }
165}
166
167#[inline]
168fn replacen_str<'a>(s: &'a str, from: &str, to: &str, count: usize) -> Cow<'a, str> {
169    if count == 0 || from == to {
170        Cow::Borrowed(s)
171    } else if to.is_empty() {
172        remove_matches(s, s.match_indices(from).map(|(p, from)| (p, p + from.len())).take(count))
173    } else if !s.contains(from) {
174        Cow::Borrowed(s)
175    } else {
176        Cow::Owned(s.replacen(from, to, count))
177    }
178}
179
180/// A stable pattern type that can be used by replacement and trimming methods.
181///
182/// This trait is local to this crate because the standard library `Pattern` trait is still unstable to name in public APIs.
183pub trait Pattern: sealed::Sealed {
184    /// Returns a `Cow<str>` after replacing all matches in the given string.
185    #[doc(hidden)]
186    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str>;
187
188    /// Returns a `Cow<str>` after replacing at most `count` matches in the given string.
189    #[doc(hidden)]
190    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str>;
191
192    /// Returns a string slice after removing all matching prefixes and suffixes.
193    #[doc(hidden)]
194    fn trim_matches_from(self, s: &str) -> &str;
195
196    /// Returns a string slice after removing all matching prefixes.
197    #[doc(hidden)]
198    fn trim_start_matches_from(self, s: &str) -> &str;
199
200    /// Returns a string slice after removing all matching suffixes.
201    #[doc(hidden)]
202    fn trim_end_matches_from(self, s: &str) -> &str;
203}
204
205impl Pattern for char {
206    #[inline]
207    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
208        replace_chars(s, |c| c == self, to, usize::MAX)
209    }
210
211    #[inline]
212    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
213        replace_chars(s, |c| c == self, to, count)
214    }
215
216    #[inline]
217    fn trim_matches_from(self, s: &str) -> &str {
218        s.trim_matches(self)
219    }
220
221    #[inline]
222    fn trim_start_matches_from(self, s: &str) -> &str {
223        s.trim_start_matches(self)
224    }
225
226    #[inline]
227    fn trim_end_matches_from(self, s: &str) -> &str {
228        s.trim_end_matches(self)
229    }
230}
231
232impl Pattern for &str {
233    #[inline]
234    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
235        replace_str(s, self, to)
236    }
237
238    #[inline]
239    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
240        replacen_str(s, self, to, count)
241    }
242
243    #[inline]
244    fn trim_matches_from(self, s: &str) -> &str {
245        s.trim_start_matches(self).trim_end_matches(self)
246    }
247
248    #[inline]
249    fn trim_start_matches_from(self, s: &str) -> &str {
250        s.trim_start_matches(self)
251    }
252
253    #[inline]
254    fn trim_end_matches_from(self, s: &str) -> &str {
255        s.trim_end_matches(self)
256    }
257}
258
259impl Pattern for &&str {
260    #[inline]
261    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
262        (*self).replace_from(s, to)
263    }
264
265    #[inline]
266    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
267        (*self).replacen_from(s, to, count)
268    }
269
270    #[inline]
271    fn trim_matches_from(self, s: &str) -> &str {
272        (*self).trim_matches_from(s)
273    }
274
275    #[inline]
276    fn trim_start_matches_from(self, s: &str) -> &str {
277        (*self).trim_start_matches_from(s)
278    }
279
280    #[inline]
281    fn trim_end_matches_from(self, s: &str) -> &str {
282        (*self).trim_end_matches_from(s)
283    }
284}
285
286impl Pattern for &[char] {
287    #[inline]
288    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
289        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
290    }
291
292    #[inline]
293    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
294        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
295    }
296
297    #[inline]
298    fn trim_matches_from(self, s: &str) -> &str {
299        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
300    }
301
302    #[inline]
303    fn trim_start_matches_from(self, s: &str) -> &str {
304        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
305    }
306
307    #[inline]
308    fn trim_end_matches_from(self, s: &str) -> &str {
309        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
310    }
311}
312
313impl<const N: usize> Pattern for [char; N] {
314    #[inline]
315    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
316        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
317    }
318
319    #[inline]
320    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
321        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
322    }
323
324    #[inline]
325    fn trim_matches_from(self, s: &str) -> &str {
326        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
327    }
328
329    #[inline]
330    fn trim_start_matches_from(self, s: &str) -> &str {
331        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
332    }
333
334    #[inline]
335    fn trim_end_matches_from(self, s: &str) -> &str {
336        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
337    }
338}
339
340impl<const N: usize> Pattern for &[char; N] {
341    #[inline]
342    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
343        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
344    }
345
346    #[inline]
347    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
348        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
349    }
350
351    #[inline]
352    fn trim_matches_from(self, s: &str) -> &str {
353        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
354    }
355
356    #[inline]
357    fn trim_start_matches_from(self, s: &str) -> &str {
358        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
359    }
360
361    #[inline]
362    fn trim_end_matches_from(self, s: &str) -> &str {
363        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
364    }
365}
366
367impl<F> Pattern for F
368where
369    F: FnMut(char) -> bool,
370{
371    #[inline]
372    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
373        replace_chars(s, self, to, usize::MAX)
374    }
375
376    #[inline]
377    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
378        replace_chars(s, self, to, count)
379    }
380
381    #[inline]
382    fn trim_matches_from(self, s: &str) -> &str {
383        s.trim_matches(self)
384    }
385
386    #[inline]
387    fn trim_start_matches_from(self, s: &str) -> &str {
388        s.trim_start_matches(self)
389    }
390
391    #[inline]
392    fn trim_end_matches_from(self, s: &str) -> &str {
393        s.trim_end_matches(self)
394    }
395}