diff --git a/Cargo.lock b/Cargo.lock index c28691b..1bbb311 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + [[package]] name = "bumpalo" version = "3.18.1" @@ -214,6 +220,13 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "cow-replace" +version = "0.1.1" +dependencies = [ + "ascii", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" diff --git a/Cargo.toml b/Cargo.toml index eef3aa4..7a59431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,4 @@ tracing-subscriber = "0.3.19" triomphe = "0.1.14" url = "2.5.4" bytes = "1.10.1" +ascii = "1.1.0" diff --git a/crates/bytes-str/Cargo.toml b/crates/bytes-str/Cargo.toml index 90e59f7..645a5ca 100644 --- a/crates/bytes-str/Cargo.toml +++ b/crates/bytes-str/Cargo.toml @@ -14,5 +14,5 @@ serde = ["dep:serde"] [dependencies] bytes = { workspace = true } -rkyv = { version = "0.8", optional = true } -serde = { version = "1", optional = true } +rkyv = { workspace = true, optional = true } +serde = { workspace = true, optional = true } diff --git a/crates/cow-replace/Cargo.toml b/crates/cow-replace/Cargo.toml new file mode 100644 index 0000000..a0ceb69 --- /dev/null +++ b/crates/cow-replace/Cargo.toml @@ -0,0 +1,14 @@ +[package] +authors = { workspace = true } +description = "String replace with Cow" +edition = { workspace = true } +include = ["Cargo.toml", "src/**/*.rs"] +license = { workspace = true } +name = "cow-replace" +repository = { workspace = true } +version = "0.1.1" + +[features] + +[dependencies] +ascii = { workspace = true } diff --git a/crates/cow-replace/src/lib.rs b/crates/cow-replace/src/lib.rs new file mode 100644 index 0000000..966bc29 --- /dev/null +++ b/crates/cow-replace/src/lib.rs @@ -0,0 +1,428 @@ +use std::borrow::Cow; + +use ascii::AsciiChar; + +// Helper functions for common operations +fn remove_ascii_from_str(s: &str, ch: AsciiChar) -> Option { + let target_byte = ch.as_byte(); + let bytes = s.as_bytes(); + + // Check if the character exists first + if !bytes.contains(&target_byte) { + return None; + } + + // Create new string without the target character + let mut result = String::with_capacity(s.len()); + for &byte in bytes { + if byte != target_byte { + result.push(byte as char); + } + } + + Some(result) +} + +fn replace_str_if_contains(s: &str, from: &str, to: &str) -> Option { + if from.is_empty() || !s.contains(from) { + return None; + } + + Some(s.replace(from, to)) +} + +/// Trait for string replacement operations that return a `Cow`. +/// +/// This trait provides methods for string manipulations that avoid unnecessary +/// allocations when no changes are needed, returning `Cow::Borrowed` for +/// unchanged strings and `Cow::Owned` for modified strings. +pub trait ReplaceString { + /// Removes all occurrences of the specified ASCII character from the + /// string. + /// + /// # Arguments + /// + /// * `ch` - The ASCII character to remove from the string + /// + /// # Returns + /// + /// * `Cow::Borrowed` - If the character is not found in the string (no + /// allocation needed) + /// * `Cow::Owned` - If the character is found and removed (new string + /// allocated) + /// + /// # Examples + /// + /// ``` + /// use cow_replace::ReplaceString; + /// use ascii::AsciiChar; + /// use std::borrow::Cow; + /// + /// let text = "hello world"; + /// let result = text.remove_all_ascii(AsciiChar::l); + /// assert_eq!(result, "heo word"); + /// + /// // No allocation when character not found + /// let result = text.remove_all_ascii(AsciiChar::z); + /// match result { + /// Cow::Borrowed(_) => println!("No allocation needed!"), + /// Cow::Owned(_) => unreachable!(), + /// } + /// ``` + fn remove_all_ascii(&self, ch: AsciiChar) -> Cow<'_, str>; + + /// Replaces all occurrences of a substring with another substring. + /// + /// # Arguments + /// + /// * `from` - The substring to search for and replace + /// * `to` - The replacement substring + /// + /// # Returns + /// + /// * `Cow::Borrowed` - If `from` is not found in the string (no allocation + /// needed) + /// * `Cow::Owned` - If replacements were made (new string allocated) + /// + /// # Examples + /// + /// ``` + /// use cow_replace::ReplaceString; + /// use std::borrow::Cow; + /// + /// let text = "hello world hello"; + /// let result = text.replace_all_str("hello", "hi"); + /// assert_eq!(result, "hi world hi"); + /// + /// // No allocation when substring not found + /// let result = text.replace_all_str("xyz", "abc"); + /// match result { + /// Cow::Borrowed(_) => println!("No allocation needed!"), + /// Cow::Owned(_) => unreachable!(), + /// } + /// ``` + fn replace_all_str(&self, from: &str, to: &str) -> Cow<'_, str>; +} + +/// Trait for in-place string replacement operations. +/// +/// This trait provides methods that modify the string directly without creating +/// new allocations when possible. These operations are more memory-efficient +/// but modify the original string. +pub trait ReplaceStringInPlace { + /// Removes all occurrences of the specified ASCII character from the string + /// in-place. + /// + /// This method modifies the string directly, potentially reducing its + /// length. For `Cow`, this may convert a borrowed string to an + /// owned string if modifications are needed. + /// + /// # Arguments + /// + /// * `ch` - The ASCII character to remove from the string + /// + /// # Examples + /// + /// ``` + /// use cow_replace::ReplaceStringInPlace; + /// use ascii::AsciiChar; + /// + /// let mut text = "hello world".to_string(); + /// text.remove_all_ascii_in_place(AsciiChar::l); + /// assert_eq!(text, "heo word"); + /// + /// // Works with empty results too + /// let mut text = "lllll".to_string(); + /// text.remove_all_ascii_in_place(AsciiChar::l); + /// assert_eq!(text, ""); + /// ``` + fn remove_all_ascii_in_place(&mut self, ch: AsciiChar); + + /// Replaces all occurrences of one ASCII character with another in-place. + /// + /// This method modifies the string directly by replacing bytes. Since both + /// characters are ASCII, the string length remains the same. + /// + /// # Arguments + /// + /// * `from` - The ASCII character to search for and replace + /// * `to` - The ASCII character to replace with + /// + /// # Examples + /// + /// ``` + /// use cow_replace::ReplaceStringInPlace; + /// use ascii::AsciiChar; + /// + /// let mut text = "hello world".to_string(); + /// text.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x); + /// assert_eq!(text, "hexxo worxd"); + /// + /// // No change if character not found + /// let mut text = "hello world".to_string(); + /// text.replace_all_ascii_in_place(AsciiChar::z, AsciiChar::x); + /// assert_eq!(text, "hello world"); + /// ``` + fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar); +} + +impl> ReplaceString for T { + fn remove_all_ascii(&self, ch: AsciiChar) -> Cow<'_, str> { + match remove_ascii_from_str(self.as_ref(), ch) { + Some(result) => Cow::Owned(result), + None => Cow::Borrowed(self.as_ref()), + } + } + + fn replace_all_str(&self, from: &str, to: &str) -> Cow<'_, str> { + match replace_str_if_contains(self.as_ref(), from, to) { + Some(result) => Cow::Owned(result), + None => Cow::Borrowed(self.as_ref()), + } + } +} +impl ReplaceStringInPlace for String { + fn remove_all_ascii_in_place(&mut self, ch: AsciiChar) { + let target_byte = ch.as_byte(); + let bytes = unsafe { self.as_bytes_mut() }; + + let mut write_pos = 0; + let mut read_pos = 0; + + while read_pos < bytes.len() { + if bytes[read_pos] != target_byte { + bytes[write_pos] = bytes[read_pos]; + write_pos += 1; + } + read_pos += 1; + } + + // Truncate to the new length + self.truncate(write_pos); + } + + fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar) { + let from_byte = from.as_byte(); + let to_byte = to.as_byte(); + let bytes = unsafe { self.as_bytes_mut() }; + + for byte in bytes { + if *byte == from_byte { + *byte = to_byte; + } + } + } +} + +impl ReplaceStringInPlace for Cow<'_, str> { + fn remove_all_ascii_in_place(&mut self, ch: AsciiChar) { + match self { + Cow::Borrowed(s) => { + if let Some(result) = remove_ascii_from_str(s, ch) { + *self = Cow::Owned(result); + } + } + Cow::Owned(s) => { + s.remove_all_ascii_in_place(ch); + } + } + } + + fn replace_all_ascii_in_place(&mut self, from: AsciiChar, to: AsciiChar) { + match self { + Cow::Borrowed(s) => { + let from_byte = from.as_byte(); + let bytes = s.as_bytes(); + + if !bytes.contains(&from_byte) { + return; // No changes needed + } + + // Convert to owned and replace + let mut owned = s.to_string(); + owned.replace_all_ascii_in_place(from, to); + *self = Cow::Owned(owned); + } + Cow::Owned(s) => { + s.replace_all_ascii_in_place(from, to); + } + } + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::*; + + #[test] + fn test_str_remove_all_ascii() { + let s = "hello world"; + let result = s.remove_all_ascii(AsciiChar::l); + assert_eq!(result, "heo word"); + + // Test with no occurrences + let s = "hello world"; + let result = s.remove_all_ascii(AsciiChar::z); + assert_eq!(result, "hello world"); + match result { + Cow::Borrowed(_) => {} + Cow::Owned(_) => panic!("Should return borrowed when no changes"), + } + } + + #[test] + fn test_str_replace_all_str() { + let s = "hello world hello"; + let result = s.replace_all_str("hello", "hi"); + assert_eq!(result, "hi world hi"); + + // Test with no occurrences + let s = "hello world"; + let result = s.replace_all_str("xyz", "abc"); + match result { + Cow::Borrowed(_) => {} + Cow::Owned(_) => panic!("Should return borrowed when no changes"), + } + assert_eq!(result, "hello world"); + } + + #[test] + fn test_string_remove_all_ascii() { + let s = "hello world".to_string(); + let result = s.remove_all_ascii(AsciiChar::l); + assert_eq!(result, "heo word"); + + // Test with no occurrences + let s = "hello world".to_string(); + let result = s.remove_all_ascii(AsciiChar::z); + assert_eq!(result, "hello world"); + match result { + Cow::Borrowed(_) => {} + Cow::Owned(_) => panic!("Should return borrowed when no changes"), + } + } + + #[test] + fn test_string_remove_all_ascii_in_place() { + let mut s = "hello world".to_string(); + s.remove_all_ascii_in_place(AsciiChar::l); + assert_eq!(s, "heo word"); + + let mut s = "aaaaaa".to_string(); + s.remove_all_ascii_in_place(AsciiChar::a); + assert_eq!(s, ""); + } + + #[test] + fn test_string_replace_all_ascii_in_place() { + let mut s = "hello world".to_string(); + s.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x); + assert_eq!(s, "hexxo worxd"); + + let mut s = "hello world".to_string(); + s.replace_all_ascii_in_place(AsciiChar::z, AsciiChar::x); + assert_eq!(s, "hello world"); + } + + #[test] + fn test_string_replace_all_str() { + let s = "hello world hello".to_string(); + let result = s.replace_all_str("hello", "hi"); + assert_eq!(result, "hi world hi"); + assert_eq!(s, "hello world hello"); // Original string should remain unchanged + + // Test with no occurrences + let s = "hello world".to_string(); + let result = s.replace_all_str("xyz", "abc"); + match result { + Cow::Borrowed(_) => {} + Cow::Owned(_) => panic!("Should return borrowed when no changes"), + } + assert_eq!(result, "hello world"); + assert_eq!(s, "hello world"); + } + + #[test] + fn test_cow_remove_all_ascii() { + let s: Cow<'_, str> = Cow::Borrowed("hello world"); + let result = s.remove_all_ascii(AsciiChar::l); + assert_eq!(result, "heo word"); + + let s: Cow<'_, str> = Cow::Owned("hello world".to_string()); + let result = s.remove_all_ascii(AsciiChar::l); + assert_eq!(result, "heo word"); + } + + #[test] + fn test_cow_remove_all_ascii_in_place() { + let mut s: Cow<'_, str> = Cow::Borrowed("hello world"); + s.remove_all_ascii_in_place(AsciiChar::l); + assert_eq!(s, "heo word"); + match s { + Cow::Owned(_) => {} + Cow::Borrowed(_) => panic!("Should be owned after modification"), + } + + let mut s: Cow<'_, str> = Cow::Owned("hello world".to_string()); + s.remove_all_ascii_in_place(AsciiChar::l); + assert_eq!(s, "heo word"); + } + + #[test] + fn test_cow_replace_all_ascii_in_place() { + let mut s: Cow<'_, str> = Cow::Borrowed("hello world"); + s.replace_all_ascii_in_place(AsciiChar::l, AsciiChar::x); + assert_eq!(s, "hexxo worxd"); + match s { + Cow::Owned(_) => {} + Cow::Borrowed(_) => panic!("Should be owned after modification"), + } + } + + #[test] + fn test_cow_replace_all_str() { + let s: Cow<'_, str> = Cow::Borrowed("hello world hello"); + let result = s.replace_all_str("hello", "hi"); + assert_eq!(result, "hi world hi"); + assert_eq!(s, "hello world hello"); // Original string should remain unchanged + + let s: Cow<'_, str> = Cow::Borrowed("hello world"); + let result = s.replace_all_str("xyz", "abc"); + match result { + Cow::Borrowed(_) => {} + Cow::Owned(_) => panic!("Should return borrowed when no changes"), + } + assert_eq!(result, "hello world"); + assert_eq!(s, "hello world"); + } + + #[test] + fn test_trait_separation() { + // Test that we can use both traits separately + fn use_replace_string(s: &T) -> Cow<'_, str> { + s.remove_all_ascii(AsciiChar::l) + } + + fn use_replace_string_in_place(s: &mut T) { + s.remove_all_ascii_in_place(AsciiChar::l); + } + + let s1 = "hello world"; + let result = use_replace_string(&s1); + assert_eq!(result, "heo word"); + + let s2 = "hello world".to_string(); + let result = use_replace_string(&s2); + assert_eq!(result, "heo word"); + + let mut s3 = "hello world".to_string(); + use_replace_string_in_place(&mut s3); + assert_eq!(s3, "heo word"); + + let mut s4: Cow<'_, str> = Cow::Borrowed("hello world"); + use_replace_string_in_place(&mut s4); + assert_eq!(s4, "heo word"); + } +}