string_view/
lib.rs

1//! ## String View
2//! 
3//! #### Work with views into string slices. Safely extend, reduce without losing parent string size. 
4//! 
5//! #### Use in-place modifications to speed up your code
6//! 
7//! Example:
8//! 
9//! ```rust
10//! let program_text = r#"
11//! fn main() {
12//!     let text = "Hello World";
13//! }
14//! "#;
15//! use string_view::StrExt;
16//! 
17//! let mut view = program_text.view_part(0, 0);
18//! view.extend_while(char::is_alphabetic);
19//! view.extend_while(|ch| ch != ' ');
20//! // view.reduce_left_while(char::is_alphabetic); todo!()
21//! assert_eq!(view.as_str(), "fn");
22//! ```
23
24#![no_std]
25
26use core::error::Error;
27use core::fmt::{Debug, Display};
28use core::ops::{Deref, DerefMut};
29
30pub struct StringView<'a> {
31    pub base: &'a str,
32    /// byte idx of view start inside [`StringView::base`].
33    view_start: usize,
34    /// byte len of view inside [`StringView::base`].
35    view_len: usize,
36}
37
38impl<'a> StringView<'a> {
39    /// Creates [`StringView`] of a whole [`str`] slice.
40    ///
41    /// see [`StringView::new_part`] to view part of [`str`] slice.
42    pub fn new(base: &'a str) -> Self {
43        Self {
44            base,
45            view_start: 0,
46            view_len: base.len(),
47        }
48    }
49
50    /// Creates [`StringView`] of a part of [`str`] slice using 2 byte indexes.
51    ///
52    /// ```rust
53    /// use string_view::StringView;
54    ///
55    /// let text = "Hello World";
56    /// let view = StringView::new_part(text, 6, 11);
57    ///
58    /// assert_eq!(view.as_str(), "World");
59    /// ```
60    ///
61    /// Or using [`StrExt`] extension trait:
62    ///
63    /// ```rust
64    /// use string_view::StrExt;
65    ///
66    /// let text = "Hello World";
67    /// let view = text.view_part(6, 11);
68    ///
69    /// assert_eq!(view.as_str(), "World");
70    /// ```
71    pub fn new_part(base: &'a str, view_start: usize, view_end: usize) -> Self {
72        assert!(
73            view_end >= view_start,
74            "View end index cannot be less then start index"
75        );
76        Self {
77            base,
78            view_start,
79            view_len: view_end - view_start,
80        }
81    }
82
83    /// Byte index of view start inside [`StringView::base`].
84    pub fn start(&self) -> usize {
85        self.view_start
86    }
87
88    /// Byte index of view end inside [`StringView::base`].
89    pub fn end(&self) -> usize {
90        self.view_start + self.view_len
91    }
92
93    pub fn as_str(&self) -> &str {
94        &self.base[self.view_start..self.view_start + self.view_len]
95    }
96
97    /// Extend string view to the right by `n` characters.
98    ///
99    /// panics if there is not enough characters in base string to the right of this view.
100    ///
101    /// see [`StringView::try_extend`] for fallible version.
102    ///
103    /// ```rust
104    /// use string_view::StrExt;
105    ///
106    /// let text = "Hello World";
107    /// let mut view = text.view_part(0, 5);
108    /// assert_eq!(view.as_str(), "Hello");
109    ///
110    /// view.extend(6);
111    /// assert_eq!(view.as_str(), "Hello World");
112    /// ```
113    pub fn extend(&mut self, n: usize) {
114        self.try_extend(n)
115            .expect("Unable to extend string view to the right")
116    }
117
118    /// Try to extend string view to the right by `n` characters.
119    ///
120    /// returns [`Err`] if there is not enough characters in base string to the right of this view.
121    ///
122    /// ```rust
123    /// use string_view::StrExt;
124    ///
125    /// let text = "Hello World";
126    ///
127    /// let mut view = text.view_part(0, 5);
128    /// assert_eq!(view.as_str(), "Hello");
129    ///
130    /// let err = view.try_extend(4);
131    /// assert!(err.is_ok());
132    /// assert_eq!(view.as_str(), "Hello Wor");
133    ///
134    /// let err = view.try_extend(10);
135    /// assert!(matches!(err, Err(BaseStringIsTooShort)));
136    /// assert_eq!(view.as_str(), "Hello Wor");
137    /// ```
138    pub fn try_extend(&mut self, n: usize) -> Result<(), BaseStringIsTooShort<RIGHT>> {
139        let mut combined_len = 0;
140        let mut char_iter = self.base[self.end()..].chars();
141        for _ in 0..n {
142            combined_len += char_iter.next().ok_or(BaseStringIsTooShort)?.len_utf8();
143        }
144        self.view_len += combined_len;
145        Ok(())
146    }
147
148    /// Extend string view to the right while `func` returns `true`.
149    ///
150    /// ```rust
151    /// use string_view::StrExt;
152    ///
153    /// let text = "Hello World !!!";
154    ///
155    /// let mut view = text.view_part(0, 2);
156    /// assert_eq!(view.as_str(), "He");
157    ///
158    /// view.extend_while(|ch| ch != ' ');
159    /// assert_eq!(view.as_str(), "Hello");
160    ///
161    /// view.extend(1);
162    /// view.extend_while(|ch| ch != ' ');
163    /// assert_eq!(view.as_str(), "Hello World");
164    /// ```
165    pub fn extend_while<F>(&mut self, mut func: F)
166    where
167        F: FnMut(char) -> bool,
168    {
169        let mut combined_len = 0;
170
171        for ch in self.base[self.end()..].chars() {
172            if func(ch) {
173                combined_len += ch.len_utf8();
174            }
175            else {
176                break;
177            }
178        }
179        self.view_len += combined_len;
180    }
181
182    /// Reduce string view from the right by `n` characters.
183    ///
184    /// panics if there is not enough characters in current string view.
185    ///
186    /// ```rust
187    /// use string_view::StrExt;
188    ///
189    /// let text = "Hello World";
190    ///
191    /// let mut view = text.view();
192    /// assert_eq!(view.as_str(), "Hello World");
193    ///
194    /// view.reduce(6);
195    /// assert_eq!(view.as_str(), "Hello");
196    /// ```
197    pub fn reduce(&mut self, n: usize) {
198        self.try_reduce(n)
199            .expect("Unable to reduce string view from the right")
200    }
201
202    /// Try to reduce string view from the right by `n` characters.
203    ///
204    /// returns [`Err`] if there is not enough characters in current string view.
205    ///
206    /// ```rust
207    /// use string_view::StrExt;
208    ///
209    /// let text = "One and only Hello World";
210    ///
211    /// let mut view = text.view_part(13, 24);
212    /// assert_eq!(view.as_str(), "Hello World");
213    ///
214    /// let result = view.try_reduce(6);
215    /// assert!(result.is_ok());
216    /// assert_eq!(view.as_str(), "Hello");
217    ///
218    /// let result = view.try_reduce(10);
219    /// assert!(matches!(result, Err(ViewIsTooShort)));
220    /// assert_eq!(view.as_str(), "Hello");
221    /// ```
222    pub fn try_reduce(&mut self, n: usize) -> Result<(), ViewIsTooShort<RIGHT>> {
223        let mut combined_len = 0;
224        let mut char_iter = self.base[self.start()..self.end()].chars().rev();
225        for _ in 0..n {
226            combined_len += char_iter.next().ok_or(ViewIsTooShort)?.len_utf8();
227        }
228        self.view_len -= combined_len;
229        Ok(())
230    }
231
232    /// Reduce string view from the right while `func` returns `true`.
233    ///
234    /// ```rust
235    /// use string_view::StrExt;
236    ///
237    /// let text = "Hello World !!!";
238    ///
239    /// let mut view = text.view();
240    /// assert_eq!(view.as_str(), "Hello World !!!");
241    ///
242    /// view.reduce_while(|ch| ch != ' ');
243    /// assert_eq!(view.as_str(), "Hello World ");
244    ///
245    /// view.reduce(1);
246    /// view.reduce_while(|ch| ch != ' ');
247    /// assert_eq!(view.as_str(), "Hello ");
248    /// ```
249    pub fn reduce_while<F>(&mut self, mut func: F)
250    where
251        F: FnMut(char) -> bool,
252    {
253        let mut combined_len = 0;
254        for ch in self.base[self.start()..self.end()].chars().rev() {
255            if func(ch) {
256                combined_len += ch.len_utf8();
257            }
258            else {
259                break;
260            }
261        }
262        self.view_len -= combined_len;
263    }
264}
265
266impl Deref for StringView<'_> {
267    type Target = str;
268
269    fn deref(&self) -> &Self::Target {
270        self.as_str()
271    }
272}
273
274impl Debug for StringView<'_> {
275    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
276        Debug::fmt(self.as_str(), f)
277    }
278}
279
280impl Display for StringView<'_> {
281    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
282        Display::fmt(self.as_str(), f)
283    }
284}
285
286type Side = bool;
287const RIGHT: bool = true;
288const _LEFT: bool = false;
289
290/// The only error case in [`StringView::try_extend`].
291pub struct BaseStringIsTooShort<const SIDE: Side>;
292
293impl<const SIDE: Side> Debug for BaseStringIsTooShort<SIDE> {
294    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
295        write!(
296            f,
297            "Base String contains less characters than `n` to the {} of the view",
298            if SIDE == RIGHT { "right" } else { "left" }
299        )
300    }
301}
302
303impl<const SIDE: Side> Display for BaseStringIsTooShort<SIDE> {
304    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
305        Debug::fmt(&self, f)
306    }
307}
308
309impl<const SIDE: bool> Error for BaseStringIsTooShort<SIDE> {}
310
311/// The only error case in [`StringView::try_reduce`].
312pub struct ViewIsTooShort<const SIDE: Side>;
313
314impl<const SIDE: Side> Debug for ViewIsTooShort<SIDE> {
315    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316        write!(
317            f,
318            "View contains less characters than `n` to the {} of the view",
319            if SIDE == RIGHT { "right" } else { "left" }
320        )
321    }
322}
323
324impl<const SIDE: Side> Display for ViewIsTooShort<SIDE> {
325    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
326        Debug::fmt(&self, f)
327    }
328}
329
330impl<const SIDE: bool> Error for ViewIsTooShort<SIDE> {}
331
332/// In-place character representation inside str slice
333///
334/// Under the hood is &str with single character
335///
336/// ```rust
337/// use string_view::Char;
338///
339/// let ch = Char::new("A");
340/// ```
341///
342/// ```rust,should_panic
343/// use string_view::Char;
344///
345/// let ch = Char::new(""); // panics
346/// let ch = Char::new("Hello"); // panics
347/// ```
348pub struct Char<'a>(&'a str);
349
350impl Char<'_> {
351    pub fn new(ch: &str) -> Char<'_> {
352        let char_len = ch
353            .chars()
354            .next()
355            .expect("Unable to create Char from empty string")
356            .len_utf8();
357
358        assert_eq!(
359            char_len,
360            ch.len(),
361            "Char can only be constructed from single character string"
362        );
363
364        Char(ch)
365    }
366
367    pub fn char(&self) -> char {
368        self.0.chars().next().unwrap()
369    }
370
371    pub fn as_str(&self) -> &str {
372        self.0
373    }
374}
375
376impl<'a> Deref for Char<'a> {
377    type Target = str;
378
379    fn deref(&self) -> &str {
380        self.0
381    }
382}
383
384impl Debug for Char<'_> {
385    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
386        Debug::fmt(&self.0, f)
387    }
388}
389
390impl Display for Char<'_> {
391    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
392        Display::fmt(&self.0, f)
393    }
394}
395
396/// Iterator of chars in-place
397pub struct CharsInPlace<'a>(&'a str);
398
399impl<'a> Iterator for CharsInPlace<'a> {
400    type Item = Char<'a>;
401
402    fn next(&mut self) -> Option<Self::Item> {
403        let next_char_len = self.0.chars().next()?.len_utf8();
404
405        let (this, rest) = self.0.split_at(next_char_len);
406        self.0 = rest;
407
408        Some(Char(this))
409    }
410}
411
412/// In-place character representation inside mutable str slice
413///
414/// Convert to [`Char`] using [`CharMut::as_char`].
415pub struct CharMut<'a>(&'a mut str);
416
417impl CharMut<'_> {
418    pub fn new(ch: &mut str) -> CharMut<'_> {
419        let char_len = ch
420            .chars()
421            .next()
422            .expect("Unable to create CharMut from empty string")
423            .len_utf8();
424
425        assert_eq!(
426            char_len,
427            ch.len(),
428            "CharMut can only be constructed from single character string"
429        );
430
431        CharMut(ch)
432    }
433
434    pub fn char(&self) -> char {
435        self.0.chars().next().unwrap()
436    }
437
438    pub fn as_str(&self) -> &str {
439        self.0
440    }
441
442    pub fn as_str_mut(&mut self) -> &mut str {
443        self.0
444    }
445
446    pub fn as_char(&self) -> Char<'_> {
447        Char(&self.0)
448    }
449}
450
451impl<'a> Deref for CharMut<'a> {
452    type Target = str;
453
454    fn deref(&self) -> &str {
455        self.0
456    }
457}
458
459impl<'a> DerefMut for CharMut<'a> {
460    fn deref_mut(&mut self) -> &mut Self::Target {
461        self.0
462    }
463}
464
465impl Debug for CharMut<'_> {
466    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
467        Debug::fmt(&self.0, f)
468    }
469}
470
471impl Display for CharMut<'_> {
472    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
473        Display::fmt(&self.0, f)
474    }
475}
476
477/// Mutable iterator of chars in-place
478pub struct CharsInPlaceMut<'a>(&'a mut str);
479
480impl<'a> Iterator for CharsInPlaceMut<'a> {
481    type Item = CharMut<'a>;
482
483    fn next(&mut self) -> Option<Self::Item> {
484        let next_char_len = self.0.chars().next()?.len_utf8();
485
486        let this: &mut str = core::mem::take(&mut self.0);
487        let (this, rest) = this.split_at_mut(next_char_len);
488        self.0 = rest;
489
490        Some(CharMut(this))
491    }
492}
493
494pub trait StrExt {
495    fn view(&self) -> StringView<'_>;
496    fn view_part(&self, start_idx: usize, end_idx: usize) -> StringView<'_>;
497
498    /// Byte index of this [`Char`] start & end inside base [`str`].
499    ///
500    /// ```rust
501    /// use string_view::StrExt;
502    ///
503    /// let text = "Hello World";
504    /// let mut chars = text.chars_in_place();
505    ///
506    /// let first_char = chars.next().unwrap();
507    /// assert_eq!(first_char.as_str(), "H");
508    /// assert_eq!(text.char_idx(first_char), (0, 1));
509    ///
510    /// let second_char = chars.next().unwrap();
511    /// assert_eq!(second_char.as_str(), "e");
512    /// assert_eq!(text.char_idx(second_char), (1, 2));
513    /// ```
514    fn char_idx(&self, ch: Char) -> (usize, usize);
515
516    fn chars_in_place(&self) -> CharsInPlace<'_>;
517    fn chars_in_place_mut(&mut self) -> CharsInPlaceMut<'_>;
518}
519
520impl StrExt for str {
521    fn view(&self) -> StringView<'_> {
522        StringView::new(self)
523    }
524    fn view_part(&self, start_idx: usize, end_idx: usize) -> StringView<'_> {
525        StringView::new_part(self, start_idx, end_idx)
526    }
527    fn chars_in_place(&self) -> CharsInPlace<'_> {
528        CharsInPlace(self)
529    }
530    fn chars_in_place_mut(&mut self) -> CharsInPlaceMut<'_> {
531        CharsInPlaceMut(self)
532    }
533
534    /// Byte index of this [`Char`] start & end inside base [`str`].
535    ///
536    /// ```rust
537    /// use string_view::StrExt;
538    ///
539    /// let text = "Hello World";
540    /// let mut chars = text.chars_in_place();
541    ///
542    /// let first_char = chars.next().unwrap();
543    /// assert_eq!(first_char.as_str(), "H");
544    /// assert_eq!(text.char_idx(first_char), (0, 1));
545    ///
546    /// let second_char = chars.next().unwrap();
547    /// assert_eq!(second_char.as_str(), "e");
548    /// assert_eq!(text.char_idx(second_char), (1, 2));
549    /// ```
550    fn char_idx(&self, ch: Char) -> (usize, usize) {
551        let str_start = self.as_ptr() as usize;
552        let str_end = str_start + self.len();
553        let ch_start = ch.as_ptr() as usize;
554        let ch_end = ch_start + ch.len();
555
556        let range = str_start..str_end;
557        assert!(
558            range.contains(&ch_start),
559            "Char has to be inside this string to get its index"
560        );
561        assert!(
562            range.contains(&ch_end),
563            "Char has to be inside this string to get its index"
564        );
565
566        (ch_start - str_start, ch_end - str_start)
567    }
568}
569
570#[cfg(test)]
571mod test;