string_utility/
lib.rs

1#![doc = include_str ! ("./../README.md")]
2#![forbid(unsafe_code)]
3
4pub mod prelude {
5    pub use crate::{
6        StringKeeperExt,
7        SubstringExt,
8        SubstringKeeperExt
9    };
10}
11
12pub trait SubstringExt {
13    fn substring<R: std::ops::RangeBounds<usize>>(&self, range: R) -> String;
14    fn try_substring<R: std::ops::RangeBounds<usize>>(&self, range: R) -> Option<String>;
15}
16
17pub trait StringKeeperExt<T> {
18    fn beginning_of_string(self) -> StringKeeper<T>;
19    fn end_of_string(self) -> StringKeeper<T>;
20    fn including_pattern(self) -> StringKeeper<T>;
21    fn excluding_pattern(self) -> StringKeeper<T>;
22    fn before_pattern(self) -> StringKeeper<T>;
23    fn after_pattern(self) -> StringKeeper<T>;
24}
25
26pub enum KeeperPeriod {
27    Start,
28    End,
29}
30
31pub enum KeeperCutoff {
32    After,
33    Before,
34}
35
36pub enum KeeperClusivity {
37    Including,
38    Excluding,
39}
40
41pub struct StringKeeper<T> {
42    pub to_parse: String,
43    pub pattern: T,
44    pub period: KeeperPeriod,
45    pub clusivity: KeeperClusivity,
46    pub cutoff: KeeperCutoff,
47}
48pub trait SubstringKeeperExt<T> {
49    fn keep(self, pattern: T) -> StringKeeper<T>;
50}
51
52impl SubstringKeeperExt<String> for String {
53    fn keep(self, pattern: String) -> StringKeeper<String> {
54        StringKeeper {
55            to_parse: self,
56            period: KeeperPeriod::Start,
57            cutoff: KeeperCutoff::After,
58            clusivity: KeeperClusivity::Including,
59            pattern,
60        }
61    }
62}
63
64impl SubstringKeeperExt<char> for String {
65    fn keep(self, pattern: char) -> StringKeeper<char> {
66        StringKeeper {
67            to_parse: self,
68            period: KeeperPeriod::Start,
69            cutoff: KeeperCutoff::After,
70            clusivity: KeeperClusivity::Including,
71            pattern,
72        }
73    }
74}
75
76impl StringKeeperExt<String> for StringKeeper<String> {
77    fn beginning_of_string(mut self) -> StringKeeper<String> {
78        self.period = KeeperPeriod::Start;
79        self
80    }
81
82    fn end_of_string(mut self) -> StringKeeper<String> {
83        self.period = KeeperPeriod::End;
84        self
85    }
86
87    fn including_pattern(mut self) -> StringKeeper<String> {
88        self.clusivity = KeeperClusivity::Including;
89        self
90    }
91
92    fn excluding_pattern(mut self) -> StringKeeper<String> {
93        self.clusivity = KeeperClusivity::Excluding;
94        self
95    }
96
97    fn before_pattern(mut self) -> StringKeeper<String> {
98        self.cutoff = KeeperCutoff::Before;
99        self
100    }
101
102    fn after_pattern(mut self) -> StringKeeper<String> {
103        self.cutoff = KeeperCutoff::After;
104        self
105    }
106}
107
108impl std::fmt::Display for StringKeeper<String> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        let try_find = match self.period {
111            KeeperPeriod::Start => self.to_parse.find(&self.pattern),
112            KeeperPeriod::End => self.to_parse.rfind(&self.pattern),
113        };
114
115        let range = match try_find {
116            None => usize::MIN..usize::MIN,
117            Some(pos) => match self.clusivity {
118                KeeperClusivity::Including => match self.cutoff {
119                    KeeperCutoff::After => pos..usize::MAX,
120                    KeeperCutoff::Before => {
121                        let offset = pos + self.pattern.chars().count();
122                        usize::MIN..offset
123                    }
124                },
125                KeeperClusivity::Excluding => match self.cutoff {
126                    KeeperCutoff::After => {
127                        let offset = pos + self.pattern.chars().count();
128                        offset..usize::MAX
129                    }
130                    KeeperCutoff::Before => usize::MIN..pos,
131                },
132            },
133        };
134
135        write!(f, "{}", self.to_parse.substring(range))
136    }
137}
138
139impl StringKeeperExt<char> for StringKeeper<char> {
140    fn beginning_of_string(mut self) -> Self {
141        self.period = KeeperPeriod::Start;
142        self
143    }
144
145    fn end_of_string(mut self) -> Self {
146        self.period = KeeperPeriod::End;
147        self
148    }
149
150    fn including_pattern(mut self) -> Self {
151        self.clusivity = KeeperClusivity::Including;
152        self
153    }
154
155    fn excluding_pattern(mut self) -> Self {
156        self.clusivity = KeeperClusivity::Excluding;
157        self
158    }
159
160    fn before_pattern(mut self) -> Self {
161        self.cutoff = KeeperCutoff::Before;
162        self
163    }
164
165    fn after_pattern(mut self) -> Self {
166        self.cutoff = KeeperCutoff::After;
167        self
168    }
169
170}
171
172impl std::fmt::Display for StringKeeper<char> {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        let try_find = match self.period {
175            KeeperPeriod::Start => self.to_parse.find(self.pattern),
176            KeeperPeriod::End => self.to_parse.rfind(self.pattern),
177        };
178
179        let range = match try_find {
180            None => usize::MIN..usize::MIN,
181            Some(pos) => match self.clusivity {
182                KeeperClusivity::Including => match self.cutoff {
183                    KeeperCutoff::After => pos..usize::MAX,
184                    KeeperCutoff::Before => usize::MIN..(pos).saturating_add(1),
185                },
186                KeeperClusivity::Excluding => match self.cutoff {
187                    KeeperCutoff::After => (pos).saturating_add(1)..usize::MAX,
188                    KeeperCutoff::Before => usize::MIN..pos,
189                },
190            },
191        };
192
193        write!(f, "{}", self.to_parse.substring(range))
194    }
195}
196
197impl SubstringExt for str {
198    fn substring<R: std::ops::RangeBounds<usize>>(&self, range: R) -> String {
199        self.try_substring(range).unwrap_or_else(|| "".to_string())
200    }
201
202    fn try_substring<R: std::ops::RangeBounds<usize>>(&self, range: R) -> Option<String> {
203        let start_idx = match range.start_bound() {
204            std::collections::Bound::Included(v) => *v,
205            std::collections::Bound::Excluded(v) => v.saturating_add(1),
206            std::collections::Bound::Unbounded => usize::MIN,
207        };
208
209        let end_idx = match range.end_bound() {
210            std::collections::Bound::Included(v) => v.saturating_add(1),
211            std::collections::Bound::Excluded(v) => *v,
212            std::collections::Bound::Unbounded => usize::MAX,
213        };
214
215        if end_idx <= start_idx {
216            return Some("".to_string());
217        }
218
219        end_idx
220            .checked_sub(start_idx)
221            .map(|take_count| self.chars().skip(start_idx).take(take_count).collect())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::prelude::*;
228
229    #[test]
230    fn try_substring() {
231        let some_text = "hello, world!";
232        let result = some_text.substring(7..12);
233        assert_eq!(result, "world");
234
235        let some_text = "42Hello, world!".to_string();
236        let result = some_text.try_substring(2..7).unwrap();
237        let expected = "Hello";
238        assert_eq!(result, expected);
239    }
240
241    #[test]
242    fn substring() {
243        let some_text = "42Hello, world!".to_string();
244
245        let result = some_text.substring(2..7);
246        let expected = "Hello";
247        assert_eq!(result, expected);
248
249        let result = some_text.substring(2..424242);
250        let expected = "Hello, world!";
251        assert_eq!(result, expected);
252    }
253
254    #[test]
255    fn test_substring() {
256        assert_eq!("foobar".substring(..3), "foo");
257    }
258
259    #[test]
260    fn test_out_of_bounds() {
261        assert_eq!("foobar".substring(..10), "foobar");
262        assert_eq!("foobar".substring(6..10), "");
263    }
264
265    #[test]
266    fn test_start_and_end_equal() {
267        assert_eq!("foobar".substring(3..3), "");
268    }
269
270    #[test]
271    fn test_multiple_byte_characters() {
272        assert_eq!("ã".substring(..1), "a"); // As opposed to "ã".
273        assert_eq!("ã".substring(1..2), "\u{0303}");
274        assert_eq!("fõøbα®".substring(2..5), "øbα");
275    }
276
277    #[test]
278    fn mozilla_substring_cases() {
279        let any_string = "Mozilla";
280        assert_eq!(any_string.substring(..1), "M");
281        assert_eq!(any_string.substring(1..), "ozilla");
282        assert_eq!(any_string.substring(..6), "Mozill");
283        assert_eq!(any_string.substring(4..), "lla");
284        assert_eq!(any_string.substring(4..7), "lla");
285        assert_eq!(any_string.substring(..7), "Mozilla");
286        assert_eq!(any_string.substring(..10), "Mozilla");
287        assert_eq!(any_string.substring(any_string.len() - 4..), "illa");
288        assert_eq!(any_string.substring(any_string.len() - 5..), "zilla");
289        assert_eq!(any_string.substring(2..5), "zil");
290        assert_eq!(any_string.substring(..2), "Mo");
291        assert_eq!(any_string.substring(..), "Mozilla");
292    }
293
294    #[test]
295    fn test_keep_after_include_string() {
296        assert_eq!(
297            "this is karøbα it was"
298                .to_string()
299                .keep("karøbα".to_string())
300                .beginning_of_string() // default
301                .after_pattern() // default
302                .including_pattern() // default
303                .to_string(),
304            "karøbα it was"
305        );
306        assert_eq!(
307            "this is karøbα it was"
308                .to_string()
309                .keep("karøbα".to_string())
310                .to_string(),
311            "karøbα it was"
312        );
313        assert_eq!(
314            "karøbα"
315                .to_string()
316                .keep("kar".to_string())
317                .after_pattern()
318                .including_pattern()
319                .to_string(),
320            "karøbα"
321        );
322    }
323
324    #[test]
325    fn test_keep_after_exclude_string() {
326        assert_eq!(
327            "this is karøbα it was"
328                .to_string()
329                .keep("karøbα".to_string())
330                .beginning_of_string()
331                .after_pattern()
332                .excluding_pattern()
333                .to_string(),
334            " it was"
335        );
336        assert_eq!(
337            "karøbα"
338                .to_string()
339                .keep("kar".to_string())
340                .after_pattern()
341                .excluding_pattern()
342                .to_string(),
343            "øbα"
344        );
345    }
346
347    #[test]
348    fn test_keep_after_include_char() {
349        assert_eq!(
350            "this is karøbα it was"
351                .to_string()
352                .keep('k')
353                .after_pattern()
354                .including_pattern()
355                .to_string(),
356            "karøbα it was"
357        );
358        assert_eq!(
359            "karøbα"
360                .to_string()
361                .keep('k')
362                .after_pattern()
363                .including_pattern()
364                .to_string(),
365            "karøbα"
366        );
367    }
368
369    #[test]
370    fn test_keep_after_exclude_char() {
371        assert_eq!(
372            "this is karøbα it was"
373                .to_string()
374                .keep('k')
375                .after_pattern()
376                .excluding_pattern()
377                .to_string(),
378            "arøbα it was"
379        );
380        assert_eq!(
381            "karøbα"
382                .to_string()
383                .keep('k')
384                .after_pattern()
385                .excluding_pattern()
386                .to_string(),
387            "arøbα"
388        );
389    }
390
391    #[test]
392    fn test_keep_before_include_string() {
393        assert_eq!(
394            "this is karøbα it was"
395                .to_string()
396                .keep("øbα".to_string())
397                .before_pattern()
398                .including_pattern()
399                .to_string(),
400            "this is karøbα"
401        );
402        assert_eq!(
403            "karøbα"
404                .to_string()
405                .keep("øbα".to_string())
406                .before_pattern()
407                .including_pattern()
408                .to_string(),
409            "karøbα"
410        );
411    }
412    #[test]
413    fn test_keep_before_include_char() {
414        assert_eq!(
415            "this is karøbα it was"
416                .to_string()
417                .keep('ø')
418                .before_pattern()
419                .including_pattern()
420                .to_string(),
421            "this is karø"
422        );
423        assert_eq!(
424            "karøbα"
425                .to_string()
426                .keep('ø')
427                .before_pattern()
428                .including_pattern()
429                .to_string(),
430            "karø"
431        );
432    }
433
434    #[test]
435    fn test_keep_before_exclude_string() {
436        assert_eq!(
437            "this is karøbα it was"
438                .to_string()
439                .keep("øbα".to_string())
440                .before_pattern()
441                .excluding_pattern()
442                .to_string(),
443            "this is kar"
444        );
445        assert_eq!(
446            "karøbα"
447                .to_string()
448                .keep("øbα".to_string())
449                .before_pattern()
450                .excluding_pattern()
451                .to_string(),
452            "kar"
453        );
454    }
455
456    #[test]
457    fn test_keep_before_exclude_char() {
458        assert_eq!(
459            "this is karøbα it was"
460                .to_string()
461                .keep('ø')
462                .before_pattern()
463                .excluding_pattern()
464                .to_string(),
465            "this is kar"
466        );
467        assert_eq!(
468            "karøbα"
469                .to_string()
470                .keep('ø')
471                .before_pattern()
472                .excluding_pattern()
473                .to_string(),
474            "kar"
475        );
476    }
477}