string_replace_all/lib.rs
1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4use regex::Regex;
5
6/// A trait that provides a `replace_all` method for `String` and `str` types,
7/// enabling both exact string and regex-based replacements.
8pub trait StringReplaceAll {
9 /// Replaces all occurrences of a pattern with the given replacement.
10 ///
11 /// This method supports:
12 /// - Exact string replacements (`&str`)
13 /// - Regular expression-based replacements (`Regex`)
14 ///
15 /// # Arguments
16 /// * `pattern` - The pattern to search for, which can be either:
17 /// - A string slice (`&str`) for simple replacements.
18 /// - A compiled regular expression (`Regex`) for pattern-based replacements.
19 /// * `replacement` - The string that will replace occurrences of the pattern.
20 ///
21 /// # Returns
22 /// A new `String` with all occurrences replaced.
23 ///
24 /// # Examples
25 /// ## Using an exact string match
26 /// ```
27 /// use string_replace_all::StringReplaceAll;
28 ///
29 /// let text = "I think Ruth's dog is cuter than your dog!";
30 /// let result = text.replace_all("dog", "monkey");
31 /// assert_eq!(result, "I think Ruth's monkey is cuter than your monkey!");
32 /// ```
33 ///
34 /// ## Using a regular expression match
35 /// ```
36 /// use regex::Regex;
37 /// use string_replace_all::StringReplaceAll;
38 ///
39 /// let text = "I think Ruth's dog is cuter than your dog!";
40 /// let regex = Regex::new("(?i)Dog").unwrap(); // Case-insensitive regex
41 ///
42 /// let result = text.replace_all(®ex, "ferret");
43 /// assert_eq!(result, "I think Ruth's ferret is cuter than your ferret!");
44 /// ```
45 fn replace_all<'a, P: Into<Pattern<'a>>>(&self, pattern: P, replacement: &str) -> String;
46}
47
48/// Implementation of `StringReplaceAll` for `String`.
49///
50/// This allows direct use of `.replace_all()` on `String` instances.
51impl StringReplaceAll for String {
52 /// Replaces all occurrences of the given pattern in a `String`.
53 ///
54 /// # See also
55 /// - [`replace_all`](StringReplaceAll::replace_all) for details on arguments and behavior.
56 fn replace_all<'a, P: Into<Pattern<'a>>>(&self, pattern: P, replacement: &str) -> String {
57 match pattern.into() {
58 Pattern::Str(s) => self.replace(s, replacement),
59 Pattern::Regex(r) => r.replace_all(self, replacement).to_string(),
60 }
61 }
62}
63
64/// Implementation of `StringReplaceAll` for string slices (`str`).
65///
66/// This allows direct use of `.replace_all()` on `&str` instances.
67impl StringReplaceAll for str {
68 /// Replaces all occurrences of the given pattern in a `&str`, returning a `String`.
69 ///
70 /// This implementation converts the string slice into a `String` and calls
71 /// [`replace_all`](StringReplaceAll::replace_all) on it.
72 ///
73 /// # See also
74 /// - [`replace_all`](StringReplaceAll::replace_all) for details on arguments and behavior.
75 fn replace_all<'a, P: Into<Pattern<'a>>>(&self, pattern: P, replacement: &str) -> String {
76 self.to_string().replace_all(pattern, replacement)
77 }
78}
79
80/// Replaces all occurrences of `from` with `to` in `input`, supporting both exact string and regex replacements.
81///
82/// This function works as follows:
83/// - If `from` is a **string slice (`&str`)**, it performs a simple `.replace()` on the input.
84/// - If `from` is a **regex (`Regex`)**, it applies `.replace_all()` to match the pattern.
85/// - The original input string remains **unchanged** and a new `String` is returned.
86/// - Consecutive occurrences of `to` are collapsed into a single instance to avoid unintended duplication.
87///
88/// # Arguments
89/// * `input` - The original string.
90/// * `from` - The pattern to replace (either a string slice or a regex).
91/// * `to` - The replacement string.
92///
93/// # Returns
94/// A new `String` with all occurrences replaced. Consecutive duplicates of `to` are merged.
95///
96/// # Examples
97/// ```
98/// use string_replace_all::string_replace_all;
99///
100/// let text = "Hello world! This is Rust.";
101/// let result = string_replace_all(text, "world", "RustLang");
102/// assert_eq!(result, "Hello RustLang! This is Rust.");
103/// ```
104///
105/// ```
106/// use string_replace_all::string_replace_all;
107///
108/// let text = "A B C D";
109/// let result = string_replace_all(text, "B", "X");
110/// assert_eq!(result, "A X C D"); // Spaces are properly collapsed
111/// ```
112///
113/// ```
114/// use string_replace_all::string_replace_all;
115///
116/// let text = "Some special characters like * & % !";
117/// let result = string_replace_all(text, "*", "[STAR]");
118/// assert_eq!(result, "Some special characters like [STAR] & % !");
119/// ```
120///
121/// ```
122/// use regex::Regex;
123/// use string_replace_all::string_replace_all;
124///
125/// let text = "I think Ruth's dog is cuter than your dog!";
126/// let regex = Regex::new("(?i)Dog").unwrap(); // Case-insensitive regex
127///
128/// let result = string_replace_all(text, ®ex, "ferret");
129/// assert_eq!(result, "I think Ruth's ferret is cuter than your ferret!");
130/// ```
131pub fn string_replace_all<'a, P: Into<Pattern<'a>>>(
132 input: &str,
133 pattern: P,
134 replacement: &str,
135) -> String {
136 let mut result = match pattern.into() {
137 Pattern::Str(s) => {
138 if s == replacement || s.is_empty() {
139 return input.to_string();
140 }
141 input.replace(s, replacement)
142 }
143 Pattern::Regex(r) => r.replace_all(input, replacement).to_string(),
144 };
145
146 if !replacement.is_empty() {
147 let cleanup_pattern = Regex::new(&format!("(?:{})+", regex::escape(replacement))).unwrap();
148 result = cleanup_pattern
149 .replace_all(&result, replacement)
150 .to_string();
151 }
152
153 result
154}
155
156/// Allows both `&str` and `Regex` as input for `from`.
157pub enum Pattern<'a> {
158 Str(&'a str),
159 Regex(Regex),
160}
161
162impl<'a> From<&'a str> for Pattern<'a> {
163 fn from(s: &'a str) -> Self {
164 Pattern::Str(s)
165 }
166}
167
168impl<'a> From<&'a Regex> for Pattern<'a> {
169 fn from(r: &'a Regex) -> Self {
170 Pattern::Regex(r.clone())
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::string_replace_all;
177
178 #[test]
179 fn test_basic_replacement() {
180 let input = "Hello world! Hello Rust!";
181 let result = string_replace_all(input, "Hello", "Hi");
182 assert_eq!(result, "Hi world! Hi Rust!");
183 }
184
185 #[test]
186 fn test_no_occurrences() {
187 let input = "Hello world!";
188 let result = string_replace_all(input, "Goodbye", "Hi");
189 assert_eq!(result, "Hello world!"); // Should remain unchanged
190 }
191
192 #[test]
193 fn test_replace_multiple_spaces() {
194 let input = "Hello world! This is Rust.";
195 let result = string_replace_all(input, " ", " "); // Collapse spaces
196 assert_eq!(result, "Hello world! This is Rust.");
197 }
198
199 #[test]
200 fn test_replace_multiple_spaces_doubled() {
201 let input = "Hello world! This is Rust.";
202 let result = string_replace_all(input, " ", " "); // Collapse to double-spaces
203 assert_eq!(result, "Hello world! This is Rust.");
204 }
205
206 #[test]
207 fn test_replace_entire_string() {
208 let input = "Hello";
209 let result = string_replace_all(input, "Hello", "Hi");
210 assert_eq!(result, "Hi");
211 }
212
213 #[test]
214 fn test_replace_with_empty_string() {
215 let input = "Hello world!";
216 let result = string_replace_all(input, "world!", "");
217 assert_eq!(result, "Hello ");
218 }
219
220 #[test]
221 fn test_replace_empty_string() {
222 let input = "Hello world!";
223 let result = string_replace_all(input, "", "X");
224 assert_eq!(result, "Hello world!"); // Should not change
225 }
226
227 #[test]
228 fn test_multi_line() {
229 let input = r#"Hello (line 1)
230 world! (line 2)"#;
231
232 let result = {
233 let result = string_replace_all(input, "\n", ""); // Remove newlines
234 let result = string_replace_all(&result, "\r", ""); // Remove carriage returns
235 let result = string_replace_all(&result, " (line 1)", ""); // Remove labels
236 let result = string_replace_all(&result, " (line 2)", "");
237 string_replace_all(&result, " ", " ") // Normalize spaces
238 };
239
240 assert_eq!(result, "Hello world!");
241 }
242
243 #[test]
244 fn test_replace_with_special_characters() {
245 let input = "Regex test with $pecial characters!";
246 let result = string_replace_all(input, "$pecial", "special");
247 assert_eq!(result, "Regex test with special characters!");
248 }
249
250 #[test]
251 fn test_replace_newlines() {
252 let input = "Line1\nLine2\nLine3";
253 let result = string_replace_all(input, "\n", " | ");
254 assert_eq!(result, "Line1 | Line2 | Line3");
255 }
256
257 #[test]
258 fn test_replace_unicode() {
259 let input = "Привет мир! こんにちは世界!";
260 let result = string_replace_all(input, "мир", "Rust");
261 assert_eq!(result, "Привет Rust! こんにちは世界!");
262 }
263
264 #[test]
265 fn test_regex_replacement() {
266 let text = "I think Ruth's dog is cuter than your dog!";
267 let regex = regex::Regex::new("(?i)Dog").unwrap(); // Case-insensitive regex
268
269 let result = string_replace_all(text, ®ex, "ferret");
270 assert_eq!(result, "I think Ruth's ferret is cuter than your ferret!");
271 }
272}
273
274#[cfg(test)]
275mod trait_tests {
276 use super::StringReplaceAll;
277 use regex::Regex;
278
279 #[test]
280 fn test_string_replace_all() {
281 let input = "Hello world!".to_string();
282 let result = input.replace_all("world", "Rust");
283
284 assert_eq!(result, "Hello Rust!");
285 }
286
287 #[test]
288 fn test_str_replace_all() {
289 let input = "Hello world!";
290 let result = input.replace_all("world", "Rust");
291
292 assert_eq!(result, "Hello Rust!");
293 }
294
295 #[test]
296 fn test_regex_replace_all() {
297 let input = "I love RustLang and rust programming!".to_string();
298 let regex = Regex::new("(?i)rust").unwrap(); // Case-insensitive
299
300 let result = input.replace_all(®ex, "Go");
301
302 assert_eq!(result, "I love GoLang and Go programming!");
303 }
304
305 #[test]
306 fn test_replace_special_characters() {
307 let input = "Replace * special ** characters!".to_string();
308 let result = input.replace_all("*", "-");
309
310 assert_eq!(result, "Replace - special -- characters!");
311 }
312
313 #[test]
314 fn test_replace_entire_string() {
315 let input = "Completely replace this".to_string();
316 let result = input.replace_all("Completely replace this", "Done");
317
318 assert_eq!(result, "Done");
319 }
320}