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 replace_chars<'a, F>(s: &'a str, mut from: F, to: &str, mut count: usize) -> Cow<'a, str>
25where
26    F: FnMut(char) -> bool, {
27    if count == 0 {
28        return Cow::Borrowed(s);
29    }
30
31    let mut new_s: Option<String> = None;
32    let mut start = 0;
33
34    for (p, c) in s.char_indices() {
35        if count == 0 {
36            break;
37        }
38
39        if from(c) {
40            let next = p + c.len_utf8();
41
42            if char_eq_str(c, to) {
43                count -= 1;
44
45                continue;
46            }
47
48            if let Some(new_s) = new_s.as_mut() {
49                new_s.push_str(&s[start..p]);
50                new_s.push_str(to);
51            } else {
52                let mut owned = String::with_capacity(s.len());
53
54                owned.push_str(&s[..p]);
55                owned.push_str(to);
56
57                new_s = Some(owned);
58            }
59
60            start = next;
61            count -= 1;
62        }
63    }
64
65    match new_s {
66        Some(mut new_s) => {
67            new_s.push_str(&s[start..]);
68
69            Cow::Owned(new_s)
70        },
71        None => Cow::Borrowed(s),
72    }
73}
74
75#[inline]
76fn replace_str<'a>(s: &'a str, from: &str, to: &str) -> Cow<'a, str> {
77    if from == to || !s.contains(from) {
78        Cow::Borrowed(s)
79    } else {
80        Cow::Owned(s.replace(from, to))
81    }
82}
83
84#[inline]
85fn replacen_str<'a>(s: &'a str, from: &str, to: &str, count: usize) -> Cow<'a, str> {
86    if count == 0 || from == to || !s.contains(from) {
87        Cow::Borrowed(s)
88    } else {
89        Cow::Owned(s.replacen(from, to, count))
90    }
91}
92
93/// A stable pattern type that can be used by replacement and trimming methods.
94///
95/// This trait is local to this crate because the standard library `Pattern` trait is still unstable to name in public APIs.
96pub trait Pattern: sealed::Sealed {
97    /// Returns a `Cow<str>` after replacing all matches in the given string.
98    #[doc(hidden)]
99    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str>;
100
101    /// Returns a `Cow<str>` after replacing at most `count` matches in the given string.
102    #[doc(hidden)]
103    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str>;
104
105    /// Returns a string slice after removing all matching prefixes and suffixes.
106    #[doc(hidden)]
107    fn trim_matches_from(self, s: &str) -> &str;
108
109    /// Returns a string slice after removing all matching prefixes.
110    #[doc(hidden)]
111    fn trim_start_matches_from(self, s: &str) -> &str;
112
113    /// Returns a string slice after removing all matching suffixes.
114    #[doc(hidden)]
115    fn trim_end_matches_from(self, s: &str) -> &str;
116}
117
118impl Pattern for char {
119    #[inline]
120    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
121        replace_chars(s, |c| c == self, to, usize::MAX)
122    }
123
124    #[inline]
125    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
126        replace_chars(s, |c| c == self, to, count)
127    }
128
129    #[inline]
130    fn trim_matches_from(self, s: &str) -> &str {
131        s.trim_matches(self)
132    }
133
134    #[inline]
135    fn trim_start_matches_from(self, s: &str) -> &str {
136        s.trim_start_matches(self)
137    }
138
139    #[inline]
140    fn trim_end_matches_from(self, s: &str) -> &str {
141        s.trim_end_matches(self)
142    }
143}
144
145impl Pattern for &str {
146    #[inline]
147    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
148        replace_str(s, self, to)
149    }
150
151    #[inline]
152    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
153        replacen_str(s, self, to, count)
154    }
155
156    #[inline]
157    fn trim_matches_from(self, s: &str) -> &str {
158        s.trim_start_matches(self).trim_end_matches(self)
159    }
160
161    #[inline]
162    fn trim_start_matches_from(self, s: &str) -> &str {
163        s.trim_start_matches(self)
164    }
165
166    #[inline]
167    fn trim_end_matches_from(self, s: &str) -> &str {
168        s.trim_end_matches(self)
169    }
170}
171
172impl Pattern for &&str {
173    #[inline]
174    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
175        (*self).replace_from(s, to)
176    }
177
178    #[inline]
179    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
180        (*self).replacen_from(s, to, count)
181    }
182
183    #[inline]
184    fn trim_matches_from(self, s: &str) -> &str {
185        (*self).trim_matches_from(s)
186    }
187
188    #[inline]
189    fn trim_start_matches_from(self, s: &str) -> &str {
190        (*self).trim_start_matches_from(s)
191    }
192
193    #[inline]
194    fn trim_end_matches_from(self, s: &str) -> &str {
195        (*self).trim_end_matches_from(s)
196    }
197}
198
199impl Pattern for &[char] {
200    #[inline]
201    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
202        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
203    }
204
205    #[inline]
206    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
207        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
208    }
209
210    #[inline]
211    fn trim_matches_from(self, s: &str) -> &str {
212        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
213    }
214
215    #[inline]
216    fn trim_start_matches_from(self, s: &str) -> &str {
217        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
218    }
219
220    #[inline]
221    fn trim_end_matches_from(self, s: &str) -> &str {
222        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
223    }
224}
225
226impl<const N: usize> Pattern for [char; N] {
227    #[inline]
228    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
229        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
230    }
231
232    #[inline]
233    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
234        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
235    }
236
237    #[inline]
238    fn trim_matches_from(self, s: &str) -> &str {
239        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
240    }
241
242    #[inline]
243    fn trim_start_matches_from(self, s: &str) -> &str {
244        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
245    }
246
247    #[inline]
248    fn trim_end_matches_from(self, s: &str) -> &str {
249        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
250    }
251}
252
253impl<const N: usize> Pattern for &[char; N] {
254    #[inline]
255    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
256        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, usize::MAX)
257    }
258
259    #[inline]
260    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
261        replace_chars(s, |c| self.iter().any(|from| c.eq(from)), to, count)
262    }
263
264    #[inline]
265    fn trim_matches_from(self, s: &str) -> &str {
266        s.trim_matches(|c: char| self.iter().any(|from| c.eq(from)))
267    }
268
269    #[inline]
270    fn trim_start_matches_from(self, s: &str) -> &str {
271        s.trim_start_matches(|c: char| self.iter().any(|from| c.eq(from)))
272    }
273
274    #[inline]
275    fn trim_end_matches_from(self, s: &str) -> &str {
276        s.trim_end_matches(|c: char| self.iter().any(|from| c.eq(from)))
277    }
278}
279
280impl<F> Pattern for F
281where
282    F: FnMut(char) -> bool,
283{
284    #[inline]
285    fn replace_from<'a>(self, s: &'a str, to: &str) -> Cow<'a, str> {
286        replace_chars(s, self, to, usize::MAX)
287    }
288
289    #[inline]
290    fn replacen_from<'a>(self, s: &'a str, to: &str, count: usize) -> Cow<'a, str> {
291        replace_chars(s, self, to, count)
292    }
293
294    #[inline]
295    fn trim_matches_from(self, s: &str) -> &str {
296        s.trim_matches(self)
297    }
298
299    #[inline]
300    fn trim_start_matches_from(self, s: &str) -> &str {
301        s.trim_start_matches(self)
302    }
303
304    #[inline]
305    fn trim_end_matches_from(self, s: &str) -> &str {
306        s.trim_end_matches(self)
307    }
308}