string_replace_all/
lib.rs

1#[cfg(doctest)]
2doc_comment::doctest!("../README.md");
3
4use regex::Regex;
5
6/// Replaces all occurrences of `from` with `to` in `input`, supporting both exact string and regex replacements.
7///
8/// This function works as follows:
9/// - If `from` is a **string slice (`&str`)**, it performs a simple `.replace()` on the input.
10/// - If `from` is a **regex (`Regex`)**, it applies `.replace_all()` to match the pattern.
11/// - The original input string remains **unchanged** and a new `String` is returned.
12/// - Consecutive occurrences of `to` are collapsed into a single instance to avoid unintended duplication.
13///
14/// # Arguments
15/// * `input` - The original string.
16/// * `from` - The pattern to replace (either a string slice or a regex).
17/// * `to` - The replacement string.
18///
19/// # Returns
20/// A new `String` with all occurrences replaced. Consecutive duplicates of `to` are merged.
21///
22/// # Examples
23/// ```
24/// use string_replace_all::string_replace_all;
25///
26/// let text = "Hello world! This is Rust.";
27/// let result = string_replace_all(text, "world", "RustLang");
28/// assert_eq!(result, "Hello RustLang! This is Rust.");
29/// ```
30///
31/// ```
32/// use string_replace_all::string_replace_all;
33///
34/// let text = "A B C D";
35/// let result = string_replace_all(text, "B", "X");
36/// assert_eq!(result, "A X C D"); // Spaces are properly collapsed
37/// ```
38///
39/// ```
40/// use string_replace_all::string_replace_all;
41///
42/// let text = "Some special characters like * & % !";
43/// let result = string_replace_all(text, "*", "[STAR]");
44/// assert_eq!(result, "Some special characters like [STAR] & % !");
45/// ```
46///
47/// ```
48/// use regex::Regex;
49/// use string_replace_all::string_replace_all;
50///
51/// let text = "I think Ruth's dog is cuter than your dog!";
52/// let regex = Regex::new("(?i)Dog").unwrap(); // Case-insensitive regex
53///
54/// let result = string_replace_all(text, &regex, "ferret");
55/// assert_eq!(result, "I think Ruth's ferret is cuter than your ferret!");
56/// ```
57pub fn string_replace_all<'a, P: Into<Pattern<'a>>>(
58    input: &str,
59    pattern: P,
60    replacement: &str,
61) -> String {
62    let mut result = match pattern.into() {
63        Pattern::Str(s) => {
64            if s == replacement || s.is_empty() {
65                return input.to_string();
66            }
67            input.replace(s, replacement)
68        }
69        Pattern::Regex(r) => r.replace_all(input, replacement).to_string(),
70    };
71
72    if !replacement.is_empty() {
73        let cleanup_pattern = Regex::new(&format!("(?:{})+", regex::escape(replacement))).unwrap();
74        result = cleanup_pattern
75            .replace_all(&result, replacement)
76            .to_string();
77    }
78
79    result
80}
81
82/// Allows both `&str` and `Regex` as input for `from`.
83pub enum Pattern<'a> {
84    Str(&'a str),
85    Regex(Regex),
86}
87
88impl<'a> From<&'a str> for Pattern<'a> {
89    fn from(s: &'a str) -> Self {
90        Pattern::Str(s)
91    }
92}
93
94impl<'a> From<&'a Regex> for Pattern<'a> {
95    fn from(r: &'a Regex) -> Self {
96        Pattern::Regex(r.clone())
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::string_replace_all;
103
104    #[test]
105    fn test_basic_replacement() {
106        let input = "Hello world! Hello Rust!";
107        let result = string_replace_all(input, "Hello", "Hi");
108        assert_eq!(result, "Hi world! Hi Rust!");
109    }
110
111    #[test]
112    fn test_no_occurrences() {
113        let input = "Hello world!";
114        let result = string_replace_all(input, "Goodbye", "Hi");
115        assert_eq!(result, "Hello world!"); // Should remain unchanged
116    }
117
118    #[test]
119    fn test_replace_multiple_spaces() {
120        let input = "Hello        world!     This      is       Rust.";
121        let result = string_replace_all(input, "  ", " "); // Collapse spaces
122        assert_eq!(result, "Hello world! This is Rust.");
123    }
124
125    #[test]
126    fn test_replace_multiple_spaces_doubled() {
127        let input = "Hello    world!    This    is    Rust.";
128        let result = string_replace_all(input, "    ", "  "); // Collapse to double-spaces
129        assert_eq!(result, "Hello  world!  This  is  Rust.");
130    }
131
132    #[test]
133    fn test_replace_entire_string() {
134        let input = "Hello";
135        let result = string_replace_all(input, "Hello", "Hi");
136        assert_eq!(result, "Hi");
137    }
138
139    #[test]
140    fn test_replace_with_empty_string() {
141        let input = "Hello world!";
142        let result = string_replace_all(input, "world!", "");
143        assert_eq!(result, "Hello ");
144    }
145
146    #[test]
147    fn test_replace_empty_string() {
148        let input = "Hello world!";
149        let result = string_replace_all(input, "", "X");
150        assert_eq!(result, "Hello world!"); // Should not change
151    }
152
153    #[test]
154    fn test_multi_line() {
155        let input = r#"Hello (line 1)
156        world! (line 2)"#;
157
158        let result = {
159            let result = string_replace_all(input, "\n", ""); // Remove newlines
160            let result = string_replace_all(&result, "\r", ""); // Remove carriage returns
161            let result = string_replace_all(&result, " (line 1)", ""); // Remove labels
162            let result = string_replace_all(&result, " (line 2)", "");
163            string_replace_all(&result, "  ", " ") // Normalize spaces
164        };
165
166        assert_eq!(result, "Hello world!");
167    }
168
169    #[test]
170    fn test_replace_with_special_characters() {
171        let input = "Regex test with $pecial characters!";
172        let result = string_replace_all(input, "$pecial", "special");
173        assert_eq!(result, "Regex test with special characters!");
174    }
175
176    #[test]
177    fn test_replace_newlines() {
178        let input = "Line1\nLine2\nLine3";
179        let result = string_replace_all(input, "\n", " | ");
180        assert_eq!(result, "Line1 | Line2 | Line3");
181    }
182
183    #[test]
184    fn test_replace_unicode() {
185        let input = "Привет мир! こんにちは世界!";
186        let result = string_replace_all(input, "мир", "Rust");
187        assert_eq!(result, "Привет Rust! こんにちは世界!");
188    }
189
190    #[test]
191    fn test_regex_replacement() {
192        let text = "I think Ruth's dog is cuter than your dog!";
193        let regex = regex::Regex::new("(?i)Dog").unwrap(); // Case-insensitive regex
194
195        let result = string_replace_all(text, &regex, "ferret");
196        assert_eq!(result, "I think Ruth's ferret is cuter than your ferret!");
197    }
198}