1fn chars_match(left: char, right: char) -> bool {
9 left == right || left.to_lowercase().eq(right.to_lowercase())
10}
11
12#[must_use]
15pub fn find_matches(text: &str, term: &str) -> Vec<usize> {
16 let characters: Vec<char> = text.chars().collect();
17 let term_characters: Vec<char> = term.chars().collect();
18 find_matches_in(&characters, &term_characters)
19}
20
21#[must_use]
24pub fn find_matches_in(text: &[char], term: &[char]) -> Vec<usize> {
25 if term.is_empty() || term.len() > text.len() {
26 return Vec::new();
27 }
28 (0..=text.len() - term.len())
29 .filter(|&index| {
30 text.iter()
31 .skip(index)
32 .zip(term)
33 .all(|(&text_character, &term_character)| {
34 chars_match(text_character, term_character)
35 })
36 })
37 .collect()
38}
39
40#[must_use]
42pub fn replace_range(text: &str, start: usize, length: usize, replacement: &str) -> String {
43 let characters: Vec<char> = text.chars().collect();
44 let head: String = characters.iter().take(start).collect();
45 let tail: String = characters.iter().skip(start + length).collect();
46 format!("{head}{replacement}{tail}")
47}
48
49#[must_use]
57pub fn replace_all(text: &str, term: &str, replacement: &str) -> (String, usize) {
58 let characters: Vec<char> = text.chars().collect();
59 let length = term.chars().count();
60 let mut result = String::with_capacity(text.len());
61 let mut index = 0;
62 let mut replaced = 0;
63 for start in find_matches(text, term) {
64 if start < index {
65 continue;
66 }
67 result.extend(characters.iter().skip(index).take(start - index));
68 result.push_str(replacement);
69 index = start + length;
70 replaced += 1;
71 }
72 if replaced == 0 {
73 return (text.to_owned(), 0);
74 }
75 result.extend(characters.iter().skip(index));
76 (result, replaced)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::{find_matches, replace_all, replace_range};
82
83 #[test]
84 fn a_term_is_found_whatever_its_case() {
85 assert_eq!(find_matches("Foo foo FOO", "foo"), vec![0, 4, 8]);
86 }
87
88 #[test]
89 fn an_empty_term_matches_nowhere() {
90 assert!(find_matches("anything", "").is_empty());
91 }
92
93 #[test]
94 fn overlapping_occurrences_are_all_reported() {
95 assert_eq!(find_matches("aaaa", "aa"), vec![0, 1, 2]);
96 }
97
98 #[test]
99 fn replacing_a_range_keeps_both_sides() {
100 assert_eq!(replace_range("hello world", 6, 5, "there"), "hello there");
101 }
102
103 #[test]
104 fn replacing_all_reports_the_count_and_keeps_the_given_case() {
105 let (text, count) = replace_all("Foo and foo", "foo", "bar");
106 assert_eq!(text, "bar and bar");
107 assert_eq!(count, 2);
108 }
109
110 #[test]
111 fn a_replacement_containing_the_term_does_not_feed_itself() {
112 let (text, count) = replace_all("a a", "a", "aa");
113 assert_eq!(text, "aa aa");
114 assert_eq!(count, 2);
115 }
116
117 #[test]
118 fn overlapping_matches_are_replaced_without_reusing_characters() {
119 let (text, count) = replace_all("aaaa", "aa", "b");
120 assert_eq!(text, "bb");
121 assert_eq!(count, 2);
122 }
123
124 #[test]
125 fn an_odd_tail_survives_an_overlapping_replacement() {
126 let (text, count) = replace_all("aaa", "aa", "b");
127 assert_eq!(text, "ba");
128 assert_eq!(count, 1);
129 }
130
131 #[test]
132 fn replacing_all_of_a_missing_term_changes_nothing() {
133 let (text, count) = replace_all("hello", "xyz", "!");
134 assert_eq!(text, "hello");
135 assert_eq!(count, 0);
136 }
137
138 #[test]
139 fn characters_beyond_ascii_are_counted_as_one() {
140 let (text, count) = replace_all("größer größer", "größer", "kleiner");
141 assert_eq!(text, "kleiner kleiner");
142 assert_eq!(count, 2);
143 }
144}