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}