1pub(super) fn next_scalar_value(index: usize, string: &str) -> usize {
2 if let Some((offset, _)) = string[index..].char_indices().nth(1) {
3 index + offset
4 } else {
5 string.len()
6 }
7}
8
9pub(super) fn previous_scalar_value(index: usize, string: &str) -> usize {
10 if let Some((new_index, _)) = string[..index].char_indices().next_back() {
11 new_index
12 } else {
13 0
14 }
15}
16
17pub(super) fn next_word(pivot: usize, string: &str) -> usize {
18 let end = string.len();
19 if pivot == end {
20 pivot
21 } else {
22 unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
23 .find(|pair| {
24 pair.0 > pivot && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
25 })
26 .map_or(string.len(), |pair| pair.0)
27 }
28}
29
30pub(super) fn previous_word(pivot: usize, string: &str) -> usize {
31 if pivot == 0 {
32 pivot
33 } else {
34 unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
35 .rfind(|pair| {
36 pair.0 < pivot && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
37 })
38 .map_or(0, |pair| pair.0)
39 }
40}
41
42pub(super) fn previous_word_end(pivot: usize, string: &str) -> usize {
43 if pivot == 0 {
44 pivot
45 } else {
46 unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
47 .rfind(|pair| {
48 pair.0 + pair.1.len() < pivot
49 && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
50 })
51 .map_or(0, |pair| pair.0 + pair.1.len())
52 }
53}
54
55#[allow(clippy::non_ascii_literal)]
57#[cfg(test)]
58mod test {
59 #[derive(Copy, Clone)]
60 enum Direction {
61 Forward,
62 Backward,
63 }
64
65 impl Direction {
66 pub(super) fn start_for(self, scenario: &str) -> usize {
67 match self {
68 Direction::Forward => 0,
69 Direction::Backward => scenario.len(),
70 }
71 }
72 }
73
74 struct Tester {
75 direction: Direction,
76 scenarios: [&'static str; 10],
77 }
78
79 impl Tester {
80 pub(super) fn prepare(direction: Direction) -> Self {
81 Self {
82 direction,
83 scenarios: [
84 "",
85 " \t ",
86 "AddZ \t ",
87 " \t AddZ",
88 " \t AddZ \t ",
89 "AddZ AdZ AZ \t O AZ AdZ AddZ",
90 "AddZ AdZ AZ \t π AZ AdZ AddZ",
91 "AddZ AdZ AZπAZ \t O AZ AdZ AddZ",
92 "AddZ AdZ AZ \t π§π· AZ AdZ AddZ",
93 "AddZ AdZ AZπ§π·AZ \t O AZ AdZ AddZ",
94 ],
95 }
96 }
97
98 pub(super) fn test<F, V>(&self, uut: F, validator: V)
99 where
100 F: Fn(usize, &str) -> usize,
101 V: Fn(usize, &str) -> bool,
102 {
103 for scenario in &self.scenarios {
104 test_scenario(
105 &uut,
106 &validator,
107 self.direction,
108 &scenario,
109 self.direction.start_for(&scenario),
110 0,
111 )
112 }
113 }
114 }
115
116 fn test_scenario<F, V>(
117 uut: F,
118 validator: V,
119 direction: Direction,
120 scenario: &str,
121 start: usize,
122 iteration: usize,
123 ) where
124 F: Fn(usize, &str) -> usize,
125 V: Fn(usize, &str) -> bool,
126 {
127 let pivot = uut(start, scenario);
128
129 match direction {
130 Direction::Forward => {
131 if pivot == scenario.len() {
132 return;
133 }
134 assert!(pivot > start);
135 }
136 Direction::Backward => {
137 if pivot == 0 {
138 return;
139 }
140 assert!(pivot < start);
141 }
142 }
143
144 assert!(
145 validator(pivot, scenario),
146 "failed on iteration {} at index {} for \"{}\"",
147 iteration,
148 pivot,
149 scenario
150 );
151 test_scenario(uut, validator, direction, scenario, pivot, iteration + 1);
152 }
153
154 #[test]
155 fn next_word() {
156 let tester = Tester::prepare(Direction::Forward);
157 tester.test(super::next_word, |pivot, string| {
158 let c = string[pivot..].chars().next().unwrap();
159 c == 'A' || c == 'O' || c == 'π' || c == 'π§'
160 });
161 }
162
163 #[test]
164 fn previous_word() {
165 let tester = Tester::prepare(Direction::Backward);
166 tester.test(super::previous_word, |pivot, string| {
167 let c = string[pivot..].chars().next().unwrap();
168 c == 'A' || c == 'O' || c == 'π' || c == 'π§'
169 });
170 }
171
172 #[test]
173 fn previous_word_end() {
174 let tester = Tester::prepare(Direction::Backward);
175 tester.test(super::previous_word_end, |pivot, string| {
176 let c = string[..pivot].chars().next_back().unwrap();
177 c == 'Z' || c == 'O' || c == 'π' || c == 'π·'
178 });
179 }
180
181 #[test]
182 fn within_multiple_unicode_scalar_values() {
183 let string = "ab π§π· cd";
184 let pivot = "ab π§π·".len() - 4;
185
186 assert_eq!(super::next_word(pivot, string), "ab π§π· ".len());
187 assert_eq!(super::previous_word(pivot, string), "ab ".len());
188 assert_eq!(super::previous_word_end(pivot, string), "ab".len());
189 }
190
191 #[test]
192 fn next_scalar_value() {
193 use super::next_scalar_value;
194
195 let string = "aπΓΈΓ©π§π·";
196 let mut index = 0;
197 index = next_scalar_value(index, string);
198 assert_eq!(string[index..].chars().next().unwrap(), 'π');
199 index = next_scalar_value(index, string);
200 assert_eq!(string[index..].chars().next().unwrap(), 'ΓΈ');
201 index = next_scalar_value(index, string);
202 assert_eq!(string[index..].chars().next().unwrap(), 'Γ©');
203 index = next_scalar_value(index, string);
204 assert_eq!(
205 string[index..].chars().next().unwrap(),
206 "π§π·".chars().next().unwrap()
207 );
208 index = next_scalar_value(index, string);
209 assert_eq!(
210 string[index..].chars().next().unwrap(),
211 "π§π·".chars().nth(1).unwrap()
212 );
213 index = next_scalar_value(index, string);
214 assert_eq!(index, string.len());
215 index = next_scalar_value(index, string);
216 assert_eq!(index, string.len());
217 }
218
219 #[test]
220 fn previous_scalar_value() {
221 use super::previous_scalar_value;
222
223 let string = "aπΓΈΓ©π§π·";
224 let mut index = string.len();
225 index = previous_scalar_value(index, string);
226 assert_eq!(
227 string[index..].chars().next().unwrap(),
228 "π§π·".chars().nth(1).unwrap()
229 );
230 index = previous_scalar_value(index, string);
231 assert_eq!(
232 string[index..].chars().next().unwrap(),
233 "π§π·".chars().next().unwrap()
234 );
235 index = previous_scalar_value(index, string);
236 assert_eq!(string[index..].chars().next().unwrap(), 'Γ©');
237 index = previous_scalar_value(index, string);
238 assert_eq!(string[index..].chars().next().unwrap(), 'ΓΈ');
239 index = previous_scalar_value(index, string);
240 assert_eq!(string[index..].chars().next().unwrap(), 'π');
241 index = previous_scalar_value(index, string);
242 assert_eq!(index, 0);
243 assert_eq!(string[index..].chars().next().unwrap(), 'a');
244 index = previous_scalar_value(index, string);
245 assert_eq!(index, 0);
246 }
247}