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(&regex, "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, &regex, "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, &regex, "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(&regex, "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}