decancer/string.rs
1use crate::{util::merge_ranges, Matcher};
2#[cfg(feature = "serde")]
3use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
4use std::{
5 fmt::{self, Debug, Display, Formatter},
6 ops::{Deref, Range},
7};
8
9/// A small wrapper around the [`String`] data type for comparison purposes.
10///
11/// This is used because imperfections from translations can happen, thus this is used to provide comparison functions that are not as strict and can detect similar-looking characters (e.g: `i` and `l`)
12#[derive(Clone, Eq, Hash)]
13pub struct CuredString(pub(crate) String);
14
15impl CuredString {
16 /// Iterates throughout this string and yields every similar-looking match.
17 ///
18 /// If you plan on using this method with an array of strings, use [`find_multiple`][CuredString::find_multiple].
19 ///
20 /// This comparison is case-insensitive.
21 ///
22 /// ```rust
23 /// let cured = decancer::cure!("wow hello wow heellllo!").unwrap();
24 /// let mut matcher = cured.find("hello");
25 ///
26 /// assert_eq!(matcher.next(), Some(4..9));
27 /// assert_eq!(matcher.next(), Some(14..22));
28 /// assert_eq!(matcher.next(), None);
29 /// ```
30 #[inline(always)]
31 pub fn find<'a, 'b>(&'a self, other: &'b str) -> Matcher<'a, 'b> {
32 Matcher::new(self, other)
33 }
34
35 /// Iterates throughout this string and returns a [`Vec`] of every similar-looking match. Unlike [`find`][CuredString::find], this method also takes note of overlapping matches and merges them together.
36 ///
37 /// This comparison is case-insensitive.
38 ///
39 /// ```rust
40 /// let cured = decancer::cure!("hꡩ𝔏┕⊕𝚑ᅠΎ⫕ᣲ𑀜").unwrap();
41 /// let matches = cured.find_multiple(["hello", "oh yeah"]);
42 ///
43 /// assert_eq!(matches, [0..11]);
44 /// ```
45 ///
46 /// Usage with the [`censor`](https://docs.rs/censor) crate:
47 ///
48 /// ```rust
49 /// let censor = censor::Standard + censor::Sex;
50 ///
51 /// let cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
52 /// let matches = cured.find_multiple(censor.set());
53 ///
54 /// assert_eq!(matches, [0..10]);
55 /// ```
56 pub fn find_multiple<S, O>(&self, other: O) -> Vec<Range<usize>>
57 where
58 S: AsRef<str>,
59 O: IntoIterator<Item = S>,
60 {
61 let other = other.into_iter();
62 let mut ranges = Vec::with_capacity(other.size_hint().0);
63
64 for o in other {
65 ranges.extend(self.find(o.as_ref()));
66 }
67
68 merge_ranges(&mut ranges);
69 ranges
70 }
71
72 fn censor_inner<I>(&mut self, original: &str, matches: I, with: char)
73 where
74 I: IntoIterator<Item = Range<usize>>,
75 {
76 let mut with_str = String::new();
77 let mut char_diff = 0isize;
78
79 for mat in matches {
80 let cap = original[mat.clone()].chars().count() * with.len_utf8();
81
82 with_str.reserve_exact(cap);
83
84 for _ in (with_str.len()..cap).step_by(with.len_utf8()) {
85 with_str.push(with);
86 }
87
88 self.0.replace_range(
89 (mat.start as isize + char_diff) as usize..(mat.end as isize + char_diff) as _,
90 &with_str[..cap],
91 );
92
93 char_diff += cap as isize - mat.len() as isize;
94 }
95 }
96
97 /// Censors every match of a string with a repetition of a character in-place.
98 ///
99 /// If you plan on using this method with an array of strings, use [`censor_multiple`][CuredString::censor_multiple].
100 ///
101 /// This comparison is case-insensitive.
102 ///
103 /// ```rust
104 /// let mut cured = decancer::cure!("wow heellllo wow hello wow!").unwrap();
105 /// cured.censor("hello", '*');
106 ///
107 /// assert_eq!(cured, "wow ******** wow ***** wow!");
108 /// ```
109 pub fn censor(&mut self, other: &str, with: char) {
110 let original = self.clone();
111
112 self.censor_inner(&original, original.find(other), with);
113 }
114
115 /// Censors every matches from an array of strings with a repetition of a character in-place.
116 ///
117 /// This comparison is case-insensitive.
118 ///
119 /// ```rust
120 /// let mut cured = decancer::cure!("ꀡৎレレ⌽ⴙᅠ𝓎ȩ㆟ҥ").unwrap();
121 /// cured.censor_multiple(["hello", "oh yeah"], '*');
122 ///
123 /// assert_eq!(cured, "***********");
124 /// ```
125 ///
126 /// Usage with the [`censor`](https://docs.rs/censor) crate:
127 ///
128 /// ```rust
129 /// let censor = censor::Standard + censor::Sex;
130 ///
131 /// let mut cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
132 /// cured.censor_multiple(censor.set(), '*');
133 ///
134 /// assert_eq!(cured, "**********");
135 /// ```
136 pub fn censor_multiple<S, O>(&mut self, other: O, with: char)
137 where
138 S: AsRef<str>,
139 O: IntoIterator<Item = S>,
140 {
141 let original = self.clone();
142
143 self.censor_inner(&original, original.find_multiple(other), with);
144 }
145
146 fn replace_inner<I>(&mut self, matches: I, with: &str)
147 where
148 I: IntoIterator<Item = Range<usize>>,
149 {
150 let mut char_diff = 0isize;
151
152 for mat in matches {
153 self.0.replace_range(
154 (mat.start as isize + char_diff) as usize..(mat.end as isize + char_diff) as _,
155 with,
156 );
157
158 char_diff += with.len() as isize - mat.len() as isize;
159 }
160 }
161
162 /// Replaces every match of a string with another string in-place.
163 ///
164 /// If you plan on using this method with an array of strings, use [`replace_multiple`][CuredString::replace_multiple].
165 ///
166 /// This comparison is case-insensitive.
167 ///
168 /// ```rust
169 /// let mut cured = decancer::cure!("wow hello wow heellllo!").unwrap();
170 /// cured.replace("hello", "world");
171 ///
172 /// assert_eq!(cured, "wow world wow world!");
173 /// ```
174 #[inline(always)]
175 pub fn replace(&mut self, other: &str, with: &str) {
176 self.replace_inner(self.clone().find(other), with);
177 }
178
179 /// Replaces every matches from an array of strings with another string in-place.
180 ///
181 /// This comparison is case-insensitive.
182 ///
183 /// ```rust
184 /// let mut cured = decancer::cure!("ꀡৎレレ⌽ⴙᅠ𝓎ȩ㆟ҥ").unwrap();
185 /// cured.replace_multiple(["hello", "oh yeah"], "world");
186 ///
187 /// assert_eq!(cured, "world");
188 /// ```
189 ///
190 /// Usage with the [`censor`](https://docs.rs/censor) crate:
191 ///
192 /// ```rust
193 /// let censor = censor::Standard + censor::Sex;
194 ///
195 /// let mut cured = decancer::cure!("𝑺ꡘ꡶イ↥⢗ㄒ❘⋶ᔚ").unwrap();
196 /// cured.replace_multiple(censor.set(), "no :)");
197 ///
198 /// assert_eq!(cured, "no :)");
199 /// ```
200 #[inline(always)]
201 pub fn replace_multiple<S, O>(&mut self, other: O, with: &str)
202 where
203 S: AsRef<str>,
204 O: IntoIterator<Item = S>,
205 {
206 self.replace_inner(self.clone().find_multiple(other), with);
207 }
208
209 /// Checks if this cured string similarly starts with another string.
210 ///
211 /// This comparison is case-insensitive.
212 pub fn starts_with(&self, other: &str) -> bool {
213 let mut iter = self.find(other);
214 let Some(mat) = iter.next() else {
215 return false;
216 };
217
218 mat.start == 0
219 }
220
221 /// Checks if this cured string similarly ends with another string.
222 ///
223 /// This comparison is case-insensitive.
224 pub fn ends_with(&self, other: &str) -> bool {
225 let Some(last) = self.find(other).last() else {
226 return false;
227 };
228
229 last.end == self.len()
230 }
231
232 /// Checks if this cured string similarly contains another string.
233 ///
234 /// This comparison is case-insensitive.
235 pub fn contains(&self, other: &str) -> bool {
236 let mut iter = self.find(other);
237
238 iter.next().is_some()
239 }
240}
241
242/// Coerces this cured string to a [`String`].
243///
244/// **NOTE:** It's highly **NOT** recommended to use Rust's comparison methods after calling this, and since the string output is laid out in memory the same way as it were to be displayed graphically, displaying it **may not display correctly** since some right-to-left characters are reversed.
245impl From<CuredString> for String {
246 #[inline(always)]
247 fn from(val: CuredString) -> Self {
248 val.0
249 }
250}
251
252impl AsRef<str> for CuredString {
253 #[inline(always)]
254 fn as_ref(&self) -> &str {
255 &self.0
256 }
257}
258
259/// Checks if this cured string is similar with another string.
260///
261/// This comparison is case-insensitive.
262impl<S> PartialEq<S> for CuredString
263where
264 S: AsRef<str> + ?Sized,
265{
266 #[inline(always)]
267 fn eq(&self, other: &S) -> bool {
268 Matcher::is_equal(self, other.as_ref())
269 }
270}
271
272impl Debug for CuredString {
273 #[inline(always)]
274 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
275 Debug::fmt(&self.0, f)
276 }
277}
278
279impl Display for CuredString {
280 #[inline(always)]
281 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
282 Display::fmt(&self.0, f)
283 }
284}
285
286impl Deref for CuredString {
287 type Target = String;
288
289 #[inline(always)]
290 fn deref(&self) -> &Self::Target {
291 &self.0
292 }
293}
294
295#[cfg(feature = "serde")]
296#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
297impl Serialize for CuredString {
298 #[inline(always)]
299 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
300 where
301 S: Serializer,
302 {
303 serializer.serialize_str(self)
304 }
305}
306
307#[cfg(feature = "serde")]
308#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
309impl<'de> Deserialize<'de> for CuredString {
310 #[inline(always)]
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: Deserializer<'de>,
314 {
315 Deserialize::deserialize(deserializer)
316 .and_then(|s: &str| crate::cure!(s).map_err(de::Error::custom))
317 }
318}