fn chars_match(left: char, right: char) -> bool {
left == right || left.to_lowercase().eq(right.to_lowercase())
}
#[must_use]
pub fn find_matches(text: &str, term: &str) -> Vec<usize> {
let characters: Vec<char> = text.chars().collect();
let term_characters: Vec<char> = term.chars().collect();
find_matches_in(&characters, &term_characters)
}
#[must_use]
pub fn find_matches_in(text: &[char], term: &[char]) -> Vec<usize> {
if term.is_empty() || term.len() > text.len() {
return Vec::new();
}
(0..=text.len() - term.len())
.filter(|&index| {
text.iter()
.skip(index)
.zip(term)
.all(|(&text_character, &term_character)| {
chars_match(text_character, term_character)
})
})
.collect()
}
#[must_use]
pub fn replace_range(text: &str, start: usize, length: usize, replacement: &str) -> String {
let characters: Vec<char> = text.chars().collect();
let head: String = characters.iter().take(start).collect();
let tail: String = characters.iter().skip(start + length).collect();
format!("{head}{replacement}{tail}")
}
#[must_use]
pub fn replace_all(text: &str, term: &str, replacement: &str) -> (String, usize) {
let characters: Vec<char> = text.chars().collect();
let length = term.chars().count();
let mut result = String::with_capacity(text.len());
let mut index = 0;
let mut replaced = 0;
for start in find_matches(text, term) {
if start < index {
continue;
}
result.extend(characters.iter().skip(index).take(start - index));
result.push_str(replacement);
index = start + length;
replaced += 1;
}
if replaced == 0 {
return (text.to_owned(), 0);
}
result.extend(characters.iter().skip(index));
(result, replaced)
}
#[cfg(test)]
mod tests {
use super::{find_matches, replace_all, replace_range};
#[test]
fn a_term_is_found_whatever_its_case() {
assert_eq!(find_matches("Foo foo FOO", "foo"), vec![0, 4, 8]);
}
#[test]
fn an_empty_term_matches_nowhere() {
assert!(find_matches("anything", "").is_empty());
}
#[test]
fn overlapping_occurrences_are_all_reported() {
assert_eq!(find_matches("aaaa", "aa"), vec![0, 1, 2]);
}
#[test]
fn replacing_a_range_keeps_both_sides() {
assert_eq!(replace_range("hello world", 6, 5, "there"), "hello there");
}
#[test]
fn replacing_all_reports_the_count_and_keeps_the_given_case() {
let (text, count) = replace_all("Foo and foo", "foo", "bar");
assert_eq!(text, "bar and bar");
assert_eq!(count, 2);
}
#[test]
fn a_replacement_containing_the_term_does_not_feed_itself() {
let (text, count) = replace_all("a a", "a", "aa");
assert_eq!(text, "aa aa");
assert_eq!(count, 2);
}
#[test]
fn overlapping_matches_are_replaced_without_reusing_characters() {
let (text, count) = replace_all("aaaa", "aa", "b");
assert_eq!(text, "bb");
assert_eq!(count, 2);
}
#[test]
fn an_odd_tail_survives_an_overlapping_replacement() {
let (text, count) = replace_all("aaa", "aa", "b");
assert_eq!(text, "ba");
assert_eq!(count, 1);
}
#[test]
fn replacing_all_of_a_missing_term_changes_nothing() {
let (text, count) = replace_all("hello", "xyz", "!");
assert_eq!(text, "hello");
assert_eq!(count, 0);
}
#[test]
fn characters_beyond_ascii_are_counted_as_one() {
let (text, count) = replace_all("größer größer", "größer", "kleiner");
assert_eq!(text, "kleiner kleiner");
assert_eq!(count, 2);
}
}