1mod sealed {
2 pub trait Sealed {}
3}
4
5pub trait PatternRef: sealed::Sealed {
12 fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str;
13 fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str;
14}
15
16macro_rules! impl_pattern_ref {
17 ([$($gen:tt)*], $ty:ty, $to_pattern:expr) => {
18 impl<$($gen)*> sealed::Sealed for $ty {}
19
20 impl<$($gen)*> PatternRef for $ty {
21 fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str {
22 s.trim_start_matches(($to_pattern)(self))
23 }
24
25 fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str {
26 s.trim_end_matches(($to_pattern)(self))
27 }
28 }
29 };
30}
31
32impl_pattern_ref!([], char, Clone::clone);
34impl_pattern_ref!([const N: usize], [char; N], Clone::clone);
35impl_pattern_ref!([const N: usize], &[char; N], Clone::clone);
36impl_pattern_ref!([], &[char], Clone::clone);
37impl_pattern_ref!([], &str, Clone::clone);
38impl_pattern_ref!([], &&str, Clone::clone);
39impl_pattern_ref!([F: FnMut(char) -> bool], F, std::convert::identity);
40
41pub trait TrimExt {
42 fn trim_matches_in_place<P: PatternRef>(&mut self, pattern: P);
45
46 fn trim_start_matches_in_place<P: PatternRef>(&mut self, pattern: P);
49
50 fn trim_end_matches_in_place<P: PatternRef>(&mut self, pattern: P);
53
54 fn trim_in_place(&mut self) {
56 self.trim_matches_in_place(char::is_whitespace);
57 }
58
59 fn trim_start_in_place(&mut self) {
62 self.trim_start_matches_in_place(char::is_whitespace);
63 }
64
65 fn trim_end_in_place(&mut self) {
68 self.trim_end_matches_in_place(char::is_whitespace);
69 }
70}
71
72fn trim_start_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
73 s.drain(..s.len() - pattern.trim_start_matches(s).len());
74}
75
76fn trim_end_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
77 s.truncate(pattern.trim_end_matches(s).len());
78}
79
80impl TrimExt for String {
81 fn trim_start_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
82 trim_start_matches_in_place(self, &mut pattern);
83 }
84
85 fn trim_end_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
86 trim_end_matches_in_place(self, &mut pattern);
87 }
88
89 fn trim_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
90 trim_start_matches_in_place(self, &mut pattern);
91 trim_end_matches_in_place(self, &mut pattern);
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use proptest::prelude::*;
98 use rstest::rstest;
99
100 use super::TrimExt;
101
102 fn trimmed(input: &str, pattern: impl super::PatternRef) -> String {
104 let mut string = input.to_string();
105 string.trim_matches_in_place(pattern);
106 string
107 }
108
109 #[rstest]
110 #[case::both_ends("xxhixx", "hi")]
111 #[case::leading_only("xxhi", "hi")]
112 #[case::trailing_only("hixx", "hi")]
113 #[case::interior_kept("xhixhix", "hixhi")]
114 #[case::no_match("hi", "hi")]
115 #[case::all_pattern("xxxx", "")]
116 #[case::empty("", "")]
117 #[case::single_char("x", "")]
118 fn trims_a_char_pattern(#[case] input: &str, #[case] expected: &str) {
119 assert_eq!(trimmed(input, 'x'), expected);
120 }
121
122 #[rstest]
123 #[case::blank_lines_and_spaces("\n\n hi there \n\n", "hi there")]
124 #[case::mixed_run(" \n \n hi", "hi")]
125 #[case::interior_newline_kept("\none\ntwo\n", "one\ntwo")]
126 fn trims_a_char_array_pattern(#[case] input: &str, #[case] expected: &str) {
127 assert_eq!(trimmed(input, ['\n', ' ']), expected);
128 assert_eq!(trimmed(input, &['\n', ' ']), expected);
129 assert_eq!(trimmed(input, &['\n', ' '][..]), expected);
131 }
132
133 #[rstest]
134 #[case::str_pattern("abcXabc", "abc", "X")]
135 #[case::repeated_str_pattern("abcabcXabcabc", "abc", "X")]
136 #[case::partial_match_kept("abXab", "abc", "abXab")]
137 fn trims_a_str_pattern(#[case] input: &str, #[case] pattern: &str, #[case] expected: &str) {
138 assert_eq!(trimmed(input, pattern), expected);
139 assert_eq!(trimmed(input, &pattern), expected);
140 }
141
142 #[rstest]
143 #[case::digits("123hi456", "hi")]
144 #[case::only_digits("123", "")]
145 #[case::interior_digits_kept("1h2i3", "h2i")]
146 #[case::nothing_to_trim("hi", "hi")]
147 fn trims_a_closure_pattern(#[case] input: &str, #[case] expected: &str) {
148 assert_eq!(trimmed(input, |c: char| c.is_ascii_digit()), expected);
149 }
150
151 #[rstest]
152 #[case::multibyte_pattern("——hi——", '—', "hi")]
153 #[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界")]
154 #[case::multibyte_content_all_trimmed("🦀🦀", '🦀', "")]
155 fn handles_multibyte_characters(
156 #[case] input: &str,
157 #[case] pattern: char,
158 #[case] expected: &str,
159 ) {
160 assert_eq!(trimmed(input, pattern), expected);
161 }
162
163 #[rstest]
164 #[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi")]
165 #[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi")]
166 #[case::interior_kept(" a b ", "a b")]
167 #[case::nothing_to_trim("hi", "hi")]
168 fn trim_in_place_matches_str_trim(#[case] input: &str, #[case] expected: &str) {
169 let mut string = input.to_string();
170 string.trim_in_place();
171 assert_eq!(string, expected);
172 assert_eq!(string, input.trim());
173 }
174
175 fn trimmed_start(input: &str, pattern: impl super::PatternRef) -> String {
183 let mut string = input.to_string();
184 string.trim_start_matches_in_place(pattern);
185 string
186 }
187
188 fn trimmed_end(input: &str, pattern: impl super::PatternRef) -> String {
189 let mut string = input.to_string();
190 string.trim_end_matches_in_place(pattern);
191 string
192 }
193
194 #[rstest]
195 #[case::both_ends("\n\nhi\n\n", "hi\n\n", "\n\nhi")]
196 #[case::leading_only("\n\nhi", "hi", "\n\nhi")]
197 #[case::trailing_only("hi\n\n", "hi\n\n", "hi")]
198 #[case::interior_kept("\none\ntwo\n", "one\ntwo\n", "\none\ntwo")]
199 #[case::all_pattern("\n\n", "", "")]
200 #[case::empty("", "", "")]
201 #[case::nothing_to_trim("hi", "hi", "hi")]
202 fn trims_only_the_requested_end(
203 #[case] input: &str,
204 #[case] start_trimmed: &str,
205 #[case] end_trimmed: &str,
206 ) {
207 assert_eq!(trimmed_start(input, '\n'), start_trimmed);
208 assert_eq!(trimmed_end(input, '\n'), end_trimmed);
209 }
210
211 #[rstest]
212 #[case::multibyte_pattern("——hi——", '—', "hi——", "——hi")]
213 #[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界xx", "xx🦀 世界")]
214 fn one_sided_trims_handle_multibyte_characters(
215 #[case] input: &str,
216 #[case] pattern: char,
217 #[case] start_trimmed: &str,
218 #[case] end_trimmed: &str,
219 ) {
220 assert_eq!(trimmed_start(input, pattern), start_trimmed);
221 assert_eq!(trimmed_end(input, pattern), end_trimmed);
222 }
223
224 #[rstest]
225 #[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi \t\r\n", " \t\r\nhi")]
226 #[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi\u{3000}", "\u{3000}hi")]
227 #[case::nothing_to_trim("hi", "hi", "hi")]
228 fn one_sided_whitespace_trims_match_str(
229 #[case] input: &str,
230 #[case] start_trimmed: &str,
231 #[case] end_trimmed: &str,
232 ) {
233 let mut start = input.to_string();
234 start.trim_start_in_place();
235 assert_eq!(start, start_trimmed);
236 assert_eq!(start, input.trim_start());
237
238 let mut end = input.to_string();
239 end.trim_end_in_place();
240 assert_eq!(end, end_trimmed);
241 assert_eq!(end, input.trim_end());
242 }
243
244 #[rstest]
245 fn a_stateful_pattern_is_reused_rather_than_consumed() {
246 let mut string = "abhixy".to_string();
248 let mut chars: std::collections::HashSet<char> = string.chars().collect();
249 string.trim_matches_in_place(|c| {
250 assert!(chars.remove(&c), "matcher called on nonexistent char, or same char twice");
251 !c.is_ascii_alphabetic() || "abxy".contains(c)
252 });
253 assert_eq!(string, "hi");
254 assert!(chars.is_empty(), "matcher not called on every char");
255 }
256
257 proptest! {
258 #[rstest]
261 fn agrees_with_str_trim_matches(input in ".{0,64}", pattern in prop::char::range('a', 'e')) {
262 let mut string = input.clone();
263 string.trim_matches_in_place(pattern);
264 prop_assert_eq!(string, input.trim_matches(pattern));
265 }
266
267 #[rstest]
268 fn agrees_with_str_trim(input in ".{0,64}") {
269 let mut string = input.clone();
270 string.trim_in_place();
271 prop_assert_eq!(string, input.trim());
272 }
273
274 #[rstest]
275 fn agrees_with_str_trim_start_matches(
276 input in ".{0,64}",
277 pattern in prop::char::range('a', 'e'),
278 ) {
279 let mut string = input.clone();
280 string.trim_start_matches_in_place(pattern);
281 prop_assert_eq!(string, input.trim_start_matches(pattern));
282 }
283
284 #[rstest]
285 fn agrees_with_str_trim_end_matches(
286 input in ".{0,64}",
287 pattern in prop::char::range('a', 'e'),
288 ) {
289 let mut string = input.clone();
290 string.trim_end_matches_in_place(pattern);
291 prop_assert_eq!(string, input.trim_end_matches(pattern));
292 }
293
294 #[rstest]
295 fn agrees_with_str_trim_start(input in ".{0,64}") {
296 let mut string = input.clone();
297 string.trim_start_in_place();
298 prop_assert_eq!(string, input.trim_start());
299 }
300
301 #[rstest]
302 fn agrees_with_str_trim_end(input in ".{0,64}") {
303 let mut string = input.clone();
304 string.trim_end_in_place();
305 prop_assert_eq!(string, input.trim_end());
306 }
307
308 #[rstest]
310 fn both_ends_is_the_two_one_sided_trims(
311 input in ".{0,64}",
312 pattern in prop::char::range('a', 'e'),
313 ) {
314 let mut both = input.clone();
315 both.trim_matches_in_place(pattern);
316
317 let mut start_then_end = input.clone();
318 start_then_end.trim_start_matches_in_place(pattern);
319 start_then_end.trim_end_matches_in_place(pattern);
320
321 let mut end_then_start = input;
322 end_then_start.trim_end_matches_in_place(pattern);
323 end_then_start.trim_start_matches_in_place(pattern);
324
325 prop_assert_eq!(&both, &start_then_end);
326 prop_assert_eq!(&both, &end_then_start);
327 }
328 }
329}