cow_replace/
lib.rs

1use std::borrow::Cow;
2
3use ascii::AsciiChar;
4
5// Helper functions for common operations
6fn remove_ascii_from_str(s: &str, ch: AsciiChar) -> Option<String> {
7    let target_byte = ch.as_byte();
8    let bytes = s.as_bytes();
9
10    // Check if the character exists first
11    if !bytes.contains(&target_byte) {
12        return None;
13    }
14
15    // Create new string without the target character
16    let mut result = String::with_capacity(s.len());
17    for &byte in bytes {
18        if byte != target_byte {
19            result.push(byte as char);
20        }
21    }
22
23    Some(result)
24}
25
26fn replace_str_if_contains(s: &str, from: &str, to: &str) -> Option<String> {
27    if from.is_empty() || !s.contains(from) {
28        return None;
29    }
30
31    Some(s.replace(from, to))
32}
33
34/// Trait for string replacement operations that return a `Cow<str>`.
35///
36/// This trait provides methods for string manipulations that avoid unnecessary
37/// allocations when no changes are needed, returning `Cow::Borrowed` for
38/// unchanged strings and `Cow::Owned` for modified strings.
39pub trait ReplaceString {
40    /// Removes all occurrences of the specified ASCII character from the
41    /// string.
42    ///
43    /// # Arguments
44    ///
45    /// * `ch` - The ASCII character to remove from the string
46    ///
47    /// # Returns
48    ///
49    /// * `Cow::Borrowed` - If the character is not found in the string (no
50    ///   allocation needed)
51    /// * `Cow::Owned` - If the character is found and removed (new string
52    ///   allocated)
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use cow_replace::ReplaceString;
58    /// use ascii::AsciiChar;
59    /// use std::borrow::Cow;
60    ///
61    /// let text = "hello world";
62    /// let result = text.remove_all_ascii(AsciiChar::l);
63    /// assert_eq!(result, "heo word");
64    ///
65    /// // No allocation when character not found
66    /// let result = text.remove_all_ascii(AsciiChar::z);
67    /// match result {
68    ///     Cow::Borrowed(_) => println!("No allocation needed!"),
69    ///     Cow::Owned(_) => unreachable!(),
70    /// }
71    /// ```
72    fn remove_all_ascii(&self, ch: AsciiChar) -> Cow<'_, str>;
73
74    /// Replaces all occurrences of a substring with another substring.
75    ///
76    /// # Arguments
77    ///
78    /// * `from` - The substring to search for and replace
79    /// * `to` - The replacement substring
80    ///
81    /// # Returns
82    ///
83    /// * `Cow::Borrowed` - If `from` is not found in the string (no allocation
84    ///   needed)
85    /// * `Cow::Owned` - If replacements were made (new string allocated)
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use cow_replace::ReplaceString;
91    /// use std::borrow::Cow;
92    ///
93    /// let text = "hello world hello";
94    /// let result = text.replace_all_str("hello", "hi");
95    /// assert_eq!(result, "hi world hi");
96    ///
97    /// // No allocation when substring not found
98    /// let result = text.replace_all_str("xyz", "abc");
99    /// match result {
100    ///     Cow::Borrowed(_) => println!("No allocation needed!"),
101    ///     Cow::Owned(_) => unreachable!(),
102    /// }
103    /// ```
104    fn replace_all_str(&self, from: &str, to: &str) -> Cow<'_, str>;
105}
106
107/// Trait for in-place string replacement operations.
108///
109/// This trait provides methods that modify the string directly without creating
110/// new allocations when possible. These operations are more memory-efficient
111/// but modify the original string.
112pub trait ReplaceStringInPlace {
113    /// Removes all occurrences of the specified ASCII character from the string
114    /// in-place.
115    ///
116    /// This method modifies the string directly, potentially reducing its
117    /// length. For `Cow<str>`, this may convert a borrowed string to an
118    /// owned string if modifications are needed.
119    ///
120    /// # Arguments
121    ///
122    /// * `ch` - The ASCII character to remove from the string
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use cow_replace::ReplaceStringInPlace;
128    /// use ascii::AsciiChar;
129    ///
130    /// let mut text = "hello world".to_string();
131    /// text.remove_all_ascii_in_place(AsciiChar::l);
132    /// assert_eq!(text, "heo word");
133    ///
134    /// // Works with empty results too
135    /// let mut text = "lllll".to_string();
136    /// text.remove_all_ascii_in_place(AsciiChar::l);
137    /// assert_eq!(text, "");
138    /// ```
139    fn remove_all_ascii_in_place(&mut self, ch: AsciiChar);
140
141    /// Replaces all occurrences of one ASCII character with another in-place.
142    ///
143    /// This method modifies the string directly by replacing bytes. Since both
144    /// characters are ASCII, the string length remains the same.
145    ///
146    /// # Arguments
147    ///
148    /// * `from` - The ASCII character to search for and replace
149    /// * `to` - The ASCII character to replace with
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use cow_replace::ReplaceStringInPlace;
155    /// use ascii::AsciiChar;
156    ///
157    /// let mut text = "hello world".to_string();
158    /// text.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x);
159    /// assert_eq!(text, "hexxo worxd");
160    ///
161    /// // No change if character not found
162    /// let mut text = "hello world".to_string();
163    /// text.replace_all_ascii_in_place(AsciiChar::z, AsciiChar::x);
164    /// assert_eq!(text, "hello world");
165    /// ```
166    fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar);
167}
168
169impl<T: AsRef<str>> ReplaceString for T {
170    fn remove_all_ascii(&self, ch: AsciiChar) -> Cow<'_, str> {
171        match remove_ascii_from_str(self.as_ref(), ch) {
172            Some(result) => Cow::Owned(result),
173            None => Cow::Borrowed(self.as_ref()),
174        }
175    }
176
177    fn replace_all_str(&self, from: &str, to: &str) -> Cow<'_, str> {
178        match replace_str_if_contains(self.as_ref(), from, to) {
179            Some(result) => Cow::Owned(result),
180            None => Cow::Borrowed(self.as_ref()),
181        }
182    }
183}
184impl ReplaceStringInPlace for String {
185    fn remove_all_ascii_in_place(&mut self, ch: AsciiChar) {
186        let target_byte = ch.as_byte();
187        let bytes = unsafe { self.as_bytes_mut() };
188
189        let mut write_pos = 0;
190        let mut read_pos = 0;
191
192        while read_pos < bytes.len() {
193            if bytes[read_pos] != target_byte {
194                bytes[write_pos] = bytes[read_pos];
195                write_pos += 1;
196            }
197            read_pos += 1;
198        }
199
200        // Truncate to the new length
201        self.truncate(write_pos);
202    }
203
204    fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar) {
205        let from_byte = from.as_byte();
206        let to_byte = to.as_byte();
207        let bytes = unsafe { self.as_bytes_mut() };
208
209        for byte in bytes {
210            if *byte == from_byte {
211                *byte = to_byte;
212            }
213        }
214    }
215}
216
217impl ReplaceStringInPlace for Cow<'_, str> {
218    fn remove_all_ascii_in_place(&mut self, ch: AsciiChar) {
219        match self {
220            Cow::Borrowed(s) => {
221                if let Some(result) = remove_ascii_from_str(s, ch) {
222                    *self = Cow::Owned(result);
223                }
224            }
225            Cow::Owned(s) => {
226                s.remove_all_ascii_in_place(ch);
227            }
228        }
229    }
230
231    fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar) {
232        match self {
233            Cow::Borrowed(s) => {
234                let from_byte = from.as_byte();
235                let bytes = s.as_bytes();
236
237                if !bytes.contains(&from_byte) {
238                    return; // No changes needed
239                }
240
241                // Convert to owned and replace
242                let mut owned = s.to_string();
243                owned.replace_all_ascii_in_place(from, to);
244                *self = Cow::Owned(owned);
245            }
246            Cow::Owned(s) => {
247                s.replace_all_ascii_in_place(from, to);
248            }
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::borrow::Cow;
256
257    use super::*;
258
259    #[test]
260    fn test_str_remove_all_ascii() {
261        let s = "hello world";
262        let result = s.remove_all_ascii(AsciiChar::l);
263        assert_eq!(result, "heo word");
264
265        // Test with no occurrences
266        let s = "hello world";
267        let result = s.remove_all_ascii(AsciiChar::z);
268        assert_eq!(result, "hello world");
269        match result {
270            Cow::Borrowed(_) => {}
271            Cow::Owned(_) => panic!("Should return borrowed when no changes"),
272        }
273    }
274
275    #[test]
276    fn test_str_replace_all_str() {
277        let s = "hello world hello";
278        let result = s.replace_all_str("hello", "hi");
279        assert_eq!(result, "hi world hi");
280
281        // Test with no occurrences
282        let s = "hello world";
283        let result = s.replace_all_str("xyz", "abc");
284        match result {
285            Cow::Borrowed(_) => {}
286            Cow::Owned(_) => panic!("Should return borrowed when no changes"),
287        }
288        assert_eq!(result, "hello world");
289    }
290
291    #[test]
292    fn test_string_remove_all_ascii() {
293        let s = "hello world".to_string();
294        let result = s.remove_all_ascii(AsciiChar::l);
295        assert_eq!(result, "heo word");
296
297        // Test with no occurrences
298        let s = "hello world".to_string();
299        let result = s.remove_all_ascii(AsciiChar::z);
300        assert_eq!(result, "hello world");
301        match result {
302            Cow::Borrowed(_) => {}
303            Cow::Owned(_) => panic!("Should return borrowed when no changes"),
304        }
305    }
306
307    #[test]
308    fn test_string_remove_all_ascii_in_place() {
309        let mut s = "hello world".to_string();
310        s.remove_all_ascii_in_place(AsciiChar::l);
311        assert_eq!(s, "heo word");
312
313        let mut s = "aaaaaa".to_string();
314        s.remove_all_ascii_in_place(AsciiChar::a);
315        assert_eq!(s, "");
316    }
317
318    #[test]
319    fn test_string_replace_all_ascii_in_place() {
320        let mut s = "hello world".to_string();
321        s.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x);
322        assert_eq!(s, "hexxo worxd");
323
324        let mut s = "hello world".to_string();
325        s.replace_all_ascii_in_place(AsciiChar::z, AsciiChar::x);
326        assert_eq!(s, "hello world");
327    }
328
329    #[test]
330    fn test_string_replace_all_str() {
331        let s = "hello world hello".to_string();
332        let result = s.replace_all_str("hello", "hi");
333        assert_eq!(result, "hi world hi");
334        assert_eq!(s, "hello world hello"); // Original string should remain unchanged
335
336        // Test with no occurrences
337        let s = "hello world".to_string();
338        let result = s.replace_all_str("xyz", "abc");
339        match result {
340            Cow::Borrowed(_) => {}
341            Cow::Owned(_) => panic!("Should return borrowed when no changes"),
342        }
343        assert_eq!(result, "hello world");
344        assert_eq!(s, "hello world");
345    }
346
347    #[test]
348    fn test_cow_remove_all_ascii() {
349        let s: Cow<'_, str> = Cow::Borrowed("hello world");
350        let result = s.remove_all_ascii(AsciiChar::l);
351        assert_eq!(result, "heo word");
352
353        let s: Cow<'_, str> = Cow::Owned("hello world".to_string());
354        let result = s.remove_all_ascii(AsciiChar::l);
355        assert_eq!(result, "heo word");
356    }
357
358    #[test]
359    fn test_cow_remove_all_ascii_in_place() {
360        let mut s: Cow<'_, str> = Cow::Borrowed("hello world");
361        s.remove_all_ascii_in_place(AsciiChar::l);
362        assert_eq!(s, "heo word");
363        match s {
364            Cow::Owned(_) => {}
365            Cow::Borrowed(_) => panic!("Should be owned after modification"),
366        }
367
368        let mut s: Cow<'_, str> = Cow::Owned("hello world".to_string());
369        s.remove_all_ascii_in_place(AsciiChar::l);
370        assert_eq!(s, "heo word");
371    }
372
373    #[test]
374    fn test_cow_replace_all_ascii_in_place() {
375        let mut s: Cow<'_, str> = Cow::Borrowed("hello world");
376        s.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x);
377        assert_eq!(s, "hexxo worxd");
378        match s {
379            Cow::Owned(_) => {}
380            Cow::Borrowed(_) => panic!("Should be owned after modification"),
381        }
382    }
383
384    #[test]
385    fn test_cow_replace_all_str() {
386        let s: Cow<'_, str> = Cow::Borrowed("hello world hello");
387        let result = s.replace_all_str("hello", "hi");
388        assert_eq!(result, "hi world hi");
389        assert_eq!(s, "hello world hello"); // Original string should remain unchanged
390
391        let s: Cow<'_, str> = Cow::Borrowed("hello world");
392        let result = s.replace_all_str("xyz", "abc");
393        match result {
394            Cow::Borrowed(_) => {}
395            Cow::Owned(_) => panic!("Should return borrowed when no changes"),
396        }
397        assert_eq!(result, "hello world");
398        assert_eq!(s, "hello world");
399    }
400
401    #[test]
402    fn test_trait_separation() {
403        // Test that we can use both traits separately
404        fn use_replace_string<T: ReplaceString>(s: &T) -> Cow<'_, str> {
405            s.remove_all_ascii(AsciiChar::l)
406        }
407
408        fn use_replace_string_in_place<T: ReplaceStringInPlace>(s: &mut T) {
409            s.remove_all_ascii_in_place(AsciiChar::l);
410        }
411
412        let s1 = "hello world";
413        let result = use_replace_string(&s1);
414        assert_eq!(result, "heo word");
415
416        let s2 = "hello world".to_string();
417        let result = use_replace_string(&s2);
418        assert_eq!(result, "heo word");
419
420        let mut s3 = "hello world".to_string();
421        use_replace_string_in_place(&mut s3);
422        assert_eq!(s3, "heo word");
423
424        let mut s4: Cow<'_, str> = Cow::Borrowed("hello world");
425        use_replace_string_in_place(&mut s4);
426        assert_eq!(s4, "heo word");
427    }
428}