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 ).
45use crate::{impl_display_with_writeable, LengthHint, Writeable};
6use core::fmt;
78/// 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(#[automatically_derived]
#[allow(clippy::exhaustive_structs)]
impl<A: ::core::fmt::Debug, B: ::core::fmt::Debug, C: ::core::fmt::Debug>
::core::fmt::Debug for Replace<A, B, C> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field3_finish(f, "Replace",
"source", &self.source, "needle", &self.needle, "replacement",
&&self.replacement)
}
}Debug)]
30#[allow(clippy::exhaustive_structs)] // designed for nesting
31pub struct Replace<A, B, C> {
32/// The source writeable.
33pub source: A,
34/// The needle to search for.
35pub needle: B,
36/// The replacement writeable.
37pub replacement: C,
38}
3940// 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.
47fn get_pi_bytes(needle: &str, matched_bytes: usize) -> usize {
48let s = match needle.get(0..matched_bytes) {
49Some(s) => s,
50None => 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.
56for k in s
57 .char_indices()
58 .map(|(idx, _)| idx)
59 .rev()
60 .filter(|&idx| idx > 0)
61 {
62// Compare the prefix of length `k` with the suffix of length `k`.
63if let Some(suffix) = s.as_bytes().get(s.len() - k..) {
64if s.as_bytes().starts_with(suffix) {
65return k;
66 }
67 }
68 }
690
70}
7172// 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.
76struct ReplaceWriter<'a, W: ?Sized, C> {
77// The underlying sink to write to.
78sink: &'a mut W,
79// The needle we are searching for.
80needle: &'a str,
81// The replacement to write when the needle is matched.
82replacement: &'a C,
83// The remaining unmatched suffix of the needle.
84 // This is always a suffix of `needle` starting at a character boundary.
85remaining_needle: &'a str,
86}
8788impl<'a, W, C> ReplaceWriter<'a, W, C>
89where
90W: fmt::Write + ?Sized,
91 C: Writeable,
92{
93fn new(sink: &'a mut W, needle: &'a str, replacement: &'a C) -> Self {
94Self {
95sink,
96needle,
97replacement,
98 remaining_needle: needle,
99 }
100 }
101102// Helper to get the length of the prefix matched so far.
103fn matched_len(&self) -> usize {
104self.needle.len() - self.remaining_needle.len()
105 }
106107// Finalizes the writer, flushing any partially matched prefix to the sink.
108fn finalize(&mut self) -> fmt::Result {
109let matched = self.matched_len();
110if matched > 0 {
111let slice = self.needle.get(0..matched).ok_or(fmt::Error)?;
112self.sink.write_str(slice)?;
113self.remaining_needle = self.needle;
114 }
115Ok(())
116 }
117}
118119impl<'a, W, C> fmt::Writefor ReplaceWriter<'a, W, C>
120where
121W: fmt::Write + ?Sized,
122 C: Writeable,
123{
124fn write_str(&mut self, s: &str) -> fmt::Result {
125for c in s.chars() {
126self.write_char(c)?;
127 }
128Ok(())
129 }
130131fn write_char(&mut self, c: char) -> fmt::Result {
132// If the needle is empty, we just pass through the characters.
133if self.needle.is_empty() {
134return self.sink.write_char(c);
135 }
136137let 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.
141while matched > 0 && !self.remaining_needle.starts_with(c) {
142let old_j = matched;
143 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.
146let slice = self.needle.get(0..(old_j - matched)).ok_or(fmt::Error)?;
147self.sink.write_str(slice)?;
148// Update remaining_needle to reflect the new matched length.
149self.remaining_needle = self.needle.get(matched..).ok_or(fmt::Error)?;
150 }
151152// If the character matches the next character in the needle, advance the match state.
153if self.remaining_needle.starts_with(c) {
154// Advance remaining_needle by the matched character.
155self.remaining_needle = self
156.remaining_needle
157 .get(c.len_utf8()..)
158 .ok_or(fmt::Error)?;
159if self.remaining_needle.is_empty() {
160// Full match found! Write the replacement instead of the needle.
161self.replacement.write_to(self.sink)?;
162// Reset match state.
163self.remaining_needle = self.needle;
164 }
165 } else {
166// Mismatch at the very beginning of the needle. Write the character as is.
167self.sink.write_char(c)?;
168 }
169Ok(())
170 }
171}
172173impl<A, C> Writeablefor Replace<A, &str, C>
174where
175A: 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.
180fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
181let mut writer = ReplaceWriter::new(sink, self.needle, &self.replacement);
182self.source.write_to(&mut writer)?;
183writer.finalize()
184 }
185186fn writeable_length_hint(&self) -> LengthHint {
187let source_hint = self.source.writeable_length_hint();
188let needle_len = self.needle.len();
189let replacement_hint = self.replacement.writeable_length_hint();
190191// If needle and replacement have same exact length, length is unchanged.
192if let Some(r_upper) = replacement_hint.1 {
193if replacement_hint.0 == r_upper && needle_len == r_upper {
194return source_hint;
195 }
196 }
197198let mut lower = 0;
199let mut upper = None;
200201// If replacement is always larger than or equal to needle:
202 // New length is at least the source length.
203if replacement_hint.0 >= needle_len {
204lower = source_hint.0;
205 }
206207// If replacement is always smaller than or equal to needle:
208 // New length is at most the source length.
209if let Some(r_upper) = replacement_hint.1 {
210if r_upper <= needle_len {
211upper = source_hint.1;
212 }
213 }
214215LengthHint(lower, upper)
216 }
217}
218219/// This trait is implemented for compatibility with [`fmt!`](core::fmt).
/// To create a string, [`Writeable::write_to_string`] is usually more efficient.
impl<'a, A: Writeable, C: Writeable> core::fmt::Display for
Replace<A, &'a str, C> {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
crate::Writeable::write_to(&self, f)
}
}impl_display_with_writeable!(Replace<A, &'a str, C>, #[cfg(feature = "alloc")], where 'a, A: Writeable, C: Writeable);
220221#[test]
222fn test_replace() {
223use crate::assert_writeable_eq;
224use crate::concat::Concat;
225226// Basic replacement
227let replace1 = Replace {
228 source: Concat("Hello", " 10 22 1101 33"),
229 needle: "10",
230 replacement: Concat("4", "4"),
231 };
232assert_writeable_eq!(replace1, "Hello 44 22 1441 33");
233234// Empty needle (should just write source)
235let replace2 = Replace {
236 source: "Hello World",
237 needle: "",
238 replacement: "X",
239 };
240assert_writeable_eq!(replace2, "Hello World");
241242// Empty replacement
243let replace3 = Replace {
244 source: "Hello 10 World 10",
245 needle: "10",
246 replacement: "",
247 };
248assert_writeable_eq!(replace3, "Hello World ");
249250// Needle not found
251let replace4 = Replace {
252 source: "Hello World",
253 needle: "10",
254 replacement: "X",
255 };
256assert_writeable_eq!(replace4, "Hello World");
257258// Needle at the beginning
259let replace5 = Replace {
260 source: "10 Hello World",
261 needle: "10",
262 replacement: "X",
263 };
264assert_writeable_eq!(replace5, "X Hello World");
265266// Needle at the end
267let replace6 = Replace {
268 source: "Hello World 10",
269 needle: "10",
270 replacement: "X",
271 };
272assert_writeable_eq!(replace6, "Hello World X");
273274// Overlapping needles (should consume and not match again)
275let replace7 = Replace {
276 source: "ababa",
277 needle: "aba",
278 replacement: "X",
279 };
280assert_writeable_eq!(replace7, "Xba");
281282// Self-overlap but no match
283let replace8 = Replace {
284 source: "aab",
285 needle: "aac",
286 replacement: "X",
287 };
288assert_writeable_eq!(replace8, "aab");
289290// Multi-byte UTF-8
291let replace9 = Replace {
292 source: "🚀 🛸 🚀🚀 🚁",
293 needle: "🚀",
294 replacement: "星",
295 };
296assert_writeable_eq!(replace9, "星 🛸 星星 🚁");
297298// Multi-byte UTF-8 with partial match
299let replace10 = Replace {
300 source: "🚀🚁",
301 needle: "🚀🛸",
302 replacement: "星",
303 };
304assert_writeable_eq!(replace10, "🚀🚁");
305306// Multi-byte UTF-8 with backtracking (no match)
307let replace11 = Replace {
308 source: "🚀🚀🚁",
309 needle: "🚀🚀🛸",
310 replacement: "星",
311 };
312assert_writeable_eq!(replace11, "🚀🚀🚁");
313314// Multi-byte UTF-8 with backtracking (match)
315let replace12 = Replace {
316 source: "🚀🚀🚀🛸",
317 needle: "🚀🚀🛸",
318 replacement: "星",
319 };
320assert_writeable_eq!(replace12, "🚀星");
321}