/rust/registry/src/index.crates.io-1949cf8c6b5b557f/writeable-0.6.4/src/replace.rs
Line | Count | Source |
1 | | // This file is part of ICU4X. For terms of use, please see the file |
2 | | // called LICENSE at the top level of the ICU4X source tree |
3 | | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
4 | | |
5 | | use crate::{impl_display_with_writeable, LengthHint, Writeable}; |
6 | | use core::fmt; |
7 | | |
8 | | /// A [`Writeable`] adapter that replaces occurrences of a needle with a replacement. |
9 | | /// |
10 | | /// This adapter performs the replacement in a streaming fashion during `write_to`, |
11 | | /// requiring zero allocations. |
12 | | /// |
13 | | /// # Examples |
14 | | /// |
15 | | /// ``` |
16 | | /// use writeable::adapters::Replace; |
17 | | /// use writeable::assert_writeable_eq; |
18 | | /// use writeable::concat_writeable; |
19 | | /// |
20 | | /// let source = concat_writeable!("I 💖 🦀", " and 🦀 loves me!"); |
21 | | /// let replace = Replace { |
22 | | /// source, |
23 | | /// needle: "🦀", |
24 | | /// replacement: "Rust", |
25 | | /// }; |
26 | | /// |
27 | | /// assert_writeable_eq!(replace, "I 💖 Rust and Rust loves me!"); |
28 | | /// ``` |
29 | | #[derive(Debug)] |
30 | | #[allow(clippy::exhaustive_structs)] // designed for nesting |
31 | | pub struct Replace<A, B, C> { |
32 | | /// The source writeable. |
33 | | pub source: A, |
34 | | /// The needle to search for. |
35 | | pub needle: B, |
36 | | /// The replacement writeable. |
37 | | pub replacement: C, |
38 | | } |
39 | | |
40 | | // Computes the Knuth-Morris-Pratt (KMP) prefix function (failure function) value |
41 | | // for the character prefix ending at byte index `matched_bytes` in `needle`. |
42 | | // |
43 | | // Returns the byte length of the longest proper prefix of `needle[0..matched_bytes]` |
44 | | // that is also a suffix of `needle[0..matched_bytes]`. |
45 | | // |
46 | | // This is computed on the fly without allocation by iterating over char boundaries. |
47 | 0 | fn get_pi_bytes(needle: &str, matched_bytes: usize) -> usize { |
48 | 0 | let s = match needle.get(0..matched_bytes) { |
49 | 0 | Some(s) => s, |
50 | 0 | None => return 0, |
51 | | }; |
52 | | // char_indices() gives us the byte offsets of character starts. |
53 | | // These offsets correspond to the byte lengths of all possible prefixes. |
54 | | // We want to iterate them in reverse order, excluding the first one (0) |
55 | | // because we want proper prefixes. |
56 | 0 | for k in s |
57 | 0 | .char_indices() |
58 | 0 | .map(|(idx, _)| idx) |
59 | 0 | .rev() |
60 | 0 | .filter(|&idx| idx > 0) |
61 | | { |
62 | | // Compare the prefix of length `k` with the suffix of length `k`. |
63 | 0 | if let Some(suffix) = s.as_bytes().get(s.len() - k..) { |
64 | 0 | if s.as_bytes().starts_with(suffix) { |
65 | 0 | return k; |
66 | 0 | } |
67 | 0 | } |
68 | | } |
69 | 0 | 0 |
70 | 0 | } |
71 | | |
72 | | // A writer wrapper that performs streaming replacement. |
73 | | // It intercepts characters written to it, matches them against `needle` using KMP |
74 | | // (tracking progress by storing the remaining unmatched suffix of the needle), |
75 | | // and writes `replacement` when a full match is found, or the original characters otherwise. |
76 | | struct ReplaceWriter<'a, W: ?Sized, C> { |
77 | | // The underlying sink to write to. |
78 | | sink: &'a mut W, |
79 | | // The needle we are searching for. |
80 | | needle: &'a str, |
81 | | // The replacement to write when the needle is matched. |
82 | | replacement: &'a C, |
83 | | // The remaining unmatched suffix of the needle. |
84 | | // This is always a suffix of `needle` starting at a character boundary. |
85 | | remaining_needle: &'a str, |
86 | | } |
87 | | |
88 | | impl<'a, W, C> ReplaceWriter<'a, W, C> |
89 | | where |
90 | | W: fmt::Write + ?Sized, |
91 | | C: Writeable, |
92 | | { |
93 | 0 | fn new(sink: &'a mut W, needle: &'a str, replacement: &'a C) -> Self { |
94 | 0 | Self { |
95 | 0 | sink, |
96 | 0 | needle, |
97 | 0 | replacement, |
98 | 0 | remaining_needle: needle, |
99 | 0 | } |
100 | 0 | } |
101 | | |
102 | | // Helper to get the length of the prefix matched so far. |
103 | 0 | fn matched_len(&self) -> usize { |
104 | 0 | self.needle.len() - self.remaining_needle.len() |
105 | 0 | } |
106 | | |
107 | | // Finalizes the writer, flushing any partially matched prefix to the sink. |
108 | 0 | fn finalize(&mut self) -> fmt::Result { |
109 | 0 | let matched = self.matched_len(); |
110 | 0 | if matched > 0 { |
111 | 0 | let slice = self.needle.get(0..matched).ok_or(fmt::Error)?; |
112 | 0 | self.sink.write_str(slice)?; |
113 | 0 | self.remaining_needle = self.needle; |
114 | 0 | } |
115 | 0 | Ok(()) |
116 | 0 | } |
117 | | } |
118 | | |
119 | | impl<'a, W, C> fmt::Write for ReplaceWriter<'a, W, C> |
120 | | where |
121 | | W: fmt::Write + ?Sized, |
122 | | C: Writeable, |
123 | | { |
124 | 0 | fn write_str(&mut self, s: &str) -> fmt::Result { |
125 | 0 | for c in s.chars() { |
126 | 0 | self.write_char(c)?; |
127 | | } |
128 | 0 | Ok(()) |
129 | 0 | } |
130 | | |
131 | 0 | fn write_char(&mut self, c: char) -> fmt::Result { |
132 | | // If the needle is empty, we just pass through the characters. |
133 | 0 | if self.needle.is_empty() { |
134 | 0 | return self.sink.write_char(c); |
135 | 0 | } |
136 | | |
137 | 0 | let mut matched = self.matched_len(); |
138 | | // KMP State Transition: |
139 | | // While we have a mismatch and we are not at the start of the needle, |
140 | | // backtrack using the prefix function. |
141 | 0 | while matched > 0 && !self.remaining_needle.starts_with(c) { |
142 | 0 | let old_j = matched; |
143 | 0 | matched = get_pi_bytes(self.needle, old_j); |
144 | | // Since we backtracked, the prefix of length `old_j - j` is no longer |
145 | | // part of the potential match. We write it to the sink as a single slice. |
146 | 0 | let slice = self.needle.get(0..(old_j - matched)).ok_or(fmt::Error)?; |
147 | 0 | self.sink.write_str(slice)?; |
148 | | // Update remaining_needle to reflect the new matched length. |
149 | 0 | self.remaining_needle = self.needle.get(matched..).ok_or(fmt::Error)?; |
150 | | } |
151 | | |
152 | | // If the character matches the next character in the needle, advance the match state. |
153 | 0 | if self.remaining_needle.starts_with(c) { |
154 | | // Advance remaining_needle by the matched character. |
155 | 0 | self.remaining_needle = self |
156 | 0 | .remaining_needle |
157 | 0 | .get(c.len_utf8()..) |
158 | 0 | .ok_or(fmt::Error)?; |
159 | 0 | if self.remaining_needle.is_empty() { |
160 | | // Full match found! Write the replacement instead of the needle. |
161 | 0 | self.replacement.write_to(self.sink)?; |
162 | | // Reset match state. |
163 | 0 | self.remaining_needle = self.needle; |
164 | 0 | } |
165 | | } else { |
166 | | // Mismatch at the very beginning of the needle. Write the character as is. |
167 | 0 | self.sink.write_char(c)?; |
168 | | } |
169 | 0 | Ok(()) |
170 | 0 | } |
171 | | } |
172 | | |
173 | | impl<A, C> Writeable for Replace<A, &str, C> |
174 | | where |
175 | | A: Writeable, |
176 | | C: Writeable, |
177 | | { |
178 | | // We do not implement writeable_borrow because it is meant to be a constant-time O(1) |
179 | | // operation, but determining if a replacement occurred would require O(N) scanning. |
180 | 0 | fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result { |
181 | 0 | let mut writer = ReplaceWriter::new(sink, self.needle, &self.replacement); |
182 | 0 | self.source.write_to(&mut writer)?; |
183 | 0 | writer.finalize() |
184 | 0 | } |
185 | | |
186 | 0 | fn writeable_length_hint(&self) -> LengthHint { |
187 | 0 | let source_hint = self.source.writeable_length_hint(); |
188 | 0 | let needle_len = self.needle.len(); |
189 | 0 | let replacement_hint = self.replacement.writeable_length_hint(); |
190 | | |
191 | | // If needle and replacement have same exact length, length is unchanged. |
192 | 0 | if let Some(r_upper) = replacement_hint.1 { |
193 | 0 | if replacement_hint.0 == r_upper && needle_len == r_upper { |
194 | 0 | return source_hint; |
195 | 0 | } |
196 | 0 | } |
197 | | |
198 | 0 | let mut lower = 0; |
199 | 0 | let mut upper = None; |
200 | | |
201 | | // If replacement is always larger than or equal to needle: |
202 | | // New length is at least the source length. |
203 | 0 | if replacement_hint.0 >= needle_len { |
204 | 0 | lower = source_hint.0; |
205 | 0 | } |
206 | | |
207 | | // If replacement is always smaller than or equal to needle: |
208 | | // New length is at most the source length. |
209 | 0 | if let Some(r_upper) = replacement_hint.1 { |
210 | 0 | if r_upper <= needle_len { |
211 | 0 | upper = source_hint.1; |
212 | 0 | } |
213 | 0 | } |
214 | | |
215 | 0 | LengthHint(lower, upper) |
216 | 0 | } |
217 | | } |
218 | | |
219 | | impl_display_with_writeable!(Replace<A, &'a str, C>, #[cfg(feature = "alloc")], where 'a, A: Writeable, C: Writeable); |
220 | | |
221 | | #[test] |
222 | | fn test_replace() { |
223 | | use crate::assert_writeable_eq; |
224 | | use crate::concat::Concat; |
225 | | |
226 | | // Basic replacement |
227 | | let replace1 = Replace { |
228 | | source: Concat("Hello", " 10 22 1101 33"), |
229 | | needle: "10", |
230 | | replacement: Concat("4", "4"), |
231 | | }; |
232 | | assert_writeable_eq!(replace1, "Hello 44 22 1441 33"); |
233 | | |
234 | | // Empty needle (should just write source) |
235 | | let replace2 = Replace { |
236 | | source: "Hello World", |
237 | | needle: "", |
238 | | replacement: "X", |
239 | | }; |
240 | | assert_writeable_eq!(replace2, "Hello World"); |
241 | | |
242 | | // Empty replacement |
243 | | let replace3 = Replace { |
244 | | source: "Hello 10 World 10", |
245 | | needle: "10", |
246 | | replacement: "", |
247 | | }; |
248 | | assert_writeable_eq!(replace3, "Hello World "); |
249 | | |
250 | | // Needle not found |
251 | | let replace4 = Replace { |
252 | | source: "Hello World", |
253 | | needle: "10", |
254 | | replacement: "X", |
255 | | }; |
256 | | assert_writeable_eq!(replace4, "Hello World"); |
257 | | |
258 | | // Needle at the beginning |
259 | | let replace5 = Replace { |
260 | | source: "10 Hello World", |
261 | | needle: "10", |
262 | | replacement: "X", |
263 | | }; |
264 | | assert_writeable_eq!(replace5, "X Hello World"); |
265 | | |
266 | | // Needle at the end |
267 | | let replace6 = Replace { |
268 | | source: "Hello World 10", |
269 | | needle: "10", |
270 | | replacement: "X", |
271 | | }; |
272 | | assert_writeable_eq!(replace6, "Hello World X"); |
273 | | |
274 | | // Overlapping needles (should consume and not match again) |
275 | | let replace7 = Replace { |
276 | | source: "ababa", |
277 | | needle: "aba", |
278 | | replacement: "X", |
279 | | }; |
280 | | assert_writeable_eq!(replace7, "Xba"); |
281 | | |
282 | | // Self-overlap but no match |
283 | | let replace8 = Replace { |
284 | | source: "aab", |
285 | | needle: "aac", |
286 | | replacement: "X", |
287 | | }; |
288 | | assert_writeable_eq!(replace8, "aab"); |
289 | | |
290 | | // Multi-byte UTF-8 |
291 | | let replace9 = Replace { |
292 | | source: "🚀 🛸 🚀🚀 🚁", |
293 | | needle: "🚀", |
294 | | replacement: "星", |
295 | | }; |
296 | | assert_writeable_eq!(replace9, "星 🛸 星星 🚁"); |
297 | | |
298 | | // Multi-byte UTF-8 with partial match |
299 | | let replace10 = Replace { |
300 | | source: "🚀🚁", |
301 | | needle: "🚀🛸", |
302 | | replacement: "星", |
303 | | }; |
304 | | assert_writeable_eq!(replace10, "🚀🚁"); |
305 | | |
306 | | // Multi-byte UTF-8 with backtracking (no match) |
307 | | let replace11 = Replace { |
308 | | source: "🚀🚀🚁", |
309 | | needle: "🚀🚀🛸", |
310 | | replacement: "星", |
311 | | }; |
312 | | assert_writeable_eq!(replace11, "🚀🚀🚁"); |
313 | | |
314 | | // Multi-byte UTF-8 with backtracking (match) |
315 | | let replace12 = Replace { |
316 | | source: "🚀🚀🚀🛸", |
317 | | needle: "🚀🚀🛸", |
318 | | replacement: "星", |
319 | | }; |
320 | | assert_writeable_eq!(replace12, "🚀星"); |
321 | | } |