mut_str/
slice.rs

1use core::ops::{Bound, RangeBounds};
2
3#[must_use]
4/// Slice a [`prim@str`] in units of UTF-8 characters.
5///
6/// ```
7/// use mut_str::char_slice;
8///
9/// let s = "Hello, World!";
10///
11/// let hello = char_slice(s, ..5).unwrap();
12/// assert_eq!(hello, "Hello");
13///
14/// let world = char_slice(s, 7..12).unwrap();
15/// assert_eq!(world, "World");
16/// ```
17pub fn char_slice<R: RangeBounds<usize>>(s: &str, range: R) -> Option<&str> {
18    let start_inclusive = match range.start_bound() {
19        Bound::Included(i) => *i,
20        Bound::Excluded(i) => i.checked_add(1)?,
21        Bound::Unbounded => 0,
22    };
23
24    // This will not exclude all out of bounds ranges but is a quick
25    // check that will eliminate extreme bounds.
26    if start_inclusive >= s.len() {
27        return None;
28    }
29
30    let mut iter = s.char_indices();
31
32    // Get the first char in the slice and its position.
33    let start_char = iter.nth(start_inclusive)?;
34    let start_index = start_char.0;
35
36    Some(
37        if let Some(end_inclusive) = match range.end_bound() {
38            Bound::Included(i) => Some(*i),
39            Bound::Excluded(i) => {
40                if let Some(index) = i.checked_sub(1) {
41                    Some(index)
42                } else {
43                    return (start_inclusive == 0).then(||
44                        // SAFETY:
45                        // 0 is guaranteed to be a char boundary and cannot exceed
46                        // the length.
47                        unsafe { s.get_unchecked(0..0) });
48                }
49            }
50            Bound::Unbounded => None,
51        } {
52            // This will not exclude all out of bounds ranges but is a quick
53            // check that will eliminate extreme bounds.
54            if end_inclusive >= s.len() {
55                return None;
56            }
57
58            // Get the number of characters the last character is after the
59            // first character.
60            let offset = end_inclusive.checked_sub(start_inclusive)?;
61
62            // Get the last character and its position.
63            let end_char = if let Some(n) = offset.checked_sub(1) {
64                iter.nth(n)?
65            } else {
66                start_char
67            };
68            let end_index = end_char.0 + end_char.1.len_utf8();
69
70            // SAFETY:
71            // `start_index` is from a `CharIndices` iterator, so its position
72            // is valid. `end_index` is from `CharIndices` + the char length, so
73            // `end_index` is a byte after a char boundary.
74            unsafe { s.get_unchecked(start_index..end_index) }
75        } else {
76            // SAFETY:
77            // `start_index` is from a `CharIndices` iterator, so its position
78            // is valid.
79            unsafe { s.get_unchecked(start_index..) }
80        },
81    )
82}
83
84#[must_use]
85/// Slice a mutable [`prim@str`] in units of UTF-8 characters.
86///
87/// ```
88/// use mut_str::char_slice_mut;
89///
90/// let mut owned_s = Box::<str>::from("Hello, World!");
91///
92/// let hello = char_slice_mut(&mut *owned_s, ..5).unwrap();
93/// assert_eq!(hello, "Hello");
94///
95/// let world = char_slice_mut(&mut *owned_s, 7..12).unwrap();
96/// assert_eq!(world, "World");
97/// ```
98pub fn char_slice_mut<R: RangeBounds<usize>>(s: &mut str, range: R) -> Option<&mut str> {
99    let start_inclusive = match range.start_bound() {
100        Bound::Included(i) => *i,
101        Bound::Excluded(i) => i.checked_add(1)?,
102        Bound::Unbounded => 0,
103    };
104
105    // This will not exclude all out of bounds ranges but is a quick
106    // check that will eliminate extreme bounds.
107    if start_inclusive >= s.len() {
108        return None;
109    }
110
111    let mut iter = s.char_indices();
112
113    // Get the first char in the slice and its position.
114    let start_char = iter.nth(start_inclusive)?;
115    let start_index = start_char.0;
116
117    Some(
118        if let Some(end_inclusive) = match range.end_bound() {
119            Bound::Included(i) => Some(*i),
120            Bound::Excluded(i) => {
121                if let Some(index) = i.checked_sub(1) {
122                    Some(index)
123                } else {
124                    return (start_inclusive == 0).then(||
125                        // SAFETY:
126                        // 0 is guaranteed to be a char boundary and cannot exceed
127                        // the length.
128                        unsafe { s.get_unchecked_mut(0..0) });
129                }
130            }
131            Bound::Unbounded => None,
132        } {
133            // This will not exclude all out of bounds ranges but is a quick
134            // check that will eliminate extreme bounds.
135            if end_inclusive >= s.len() {
136                return None;
137            }
138
139            // Get the number of characters the last character is after the
140            // first character.
141            let offset = end_inclusive.checked_sub(start_inclusive)?;
142
143            // Get the last character and its position.
144            let end_char = if let Some(n) = offset.checked_sub(1) {
145                iter.nth(n)?
146            } else {
147                start_char
148            };
149            let end_index = end_char.0 + end_char.1.len_utf8();
150
151            // SAFETY:
152            // `start_index` is from a `CharIndices` iterator, so its position
153            // is valid. `end_index` is from `CharIndices` + the char length, so
154            // `end_index` is a byte after a char boundary.
155            unsafe { s.get_unchecked_mut(start_index..end_index) }
156        } else {
157            // SAFETY:
158            // `start_index` is from a `CharIndices` iterator, so its position
159            // is valid.
160            unsafe { s.get_unchecked_mut(start_index..) }
161        },
162    )
163}