1 | use crate::Case; |
2 | |
3 | #[cfg (feature = "random" )] |
4 | use rand::prelude::*; |
5 | |
6 | pub(super) struct Words { |
7 | words: Vec<String>, |
8 | } |
9 | |
10 | impl Words { |
11 | pub fn new(name: &str) -> Self { |
12 | let words = name |
13 | .split(|c| "-_ " .contains(c)) |
14 | .flat_map(Self::split_camel) |
15 | .filter(|s| !s.is_empty()) |
16 | .collect(); |
17 | Words { words } |
18 | } |
19 | |
20 | pub fn from_casing(name: &str, case: Case) -> Self { |
21 | use Case::*; |
22 | let words = match case { |
23 | Title | Upper | Lower | Toggle | Alternating => name |
24 | .split_ascii_whitespace() |
25 | .map(ToString::to_string) |
26 | .collect(), |
27 | Kebab | Cobol | Train => name |
28 | .split('-' ) |
29 | .map(ToString::to_string) |
30 | .filter(|s| !s.is_empty()) |
31 | .collect(), |
32 | Snake | UpperSnake | ScreamingSnake => name |
33 | .split('_' ) |
34 | .map(ToString::to_string) |
35 | .filter(|s| !s.is_empty()) |
36 | .collect(), |
37 | Pascal | Camel | UpperCamel => Self::split_camel(name), |
38 | Flat | UpperFlat => vec![name.to_string()], |
39 | |
40 | // Same behavior as title, upper, etc. |
41 | #[cfg (feature = "random" )] |
42 | Random | PseudoRandom => name |
43 | .split_ascii_whitespace() |
44 | .map(ToString::to_string) |
45 | .collect(), |
46 | }; |
47 | Self { words } |
48 | } |
49 | |
50 | fn split_camel(name: &str) -> Vec<String> { |
51 | let left_iter = name.chars(); |
52 | let mid_iter = name.chars().skip(1); |
53 | let right_iter = name.chars().skip(2); |
54 | |
55 | let mut split_indices = left_iter |
56 | .zip(mid_iter) |
57 | .zip(right_iter) |
58 | .enumerate() |
59 | .filter(|(_, ((f, s), t))| Self::three_char_is_boundary(*f, *s, *t)) |
60 | .map(|(i, _)| i + 1) |
61 | .collect::<Vec<usize>>(); |
62 | |
63 | // Check for boundary in the last two characters |
64 | // Can be rewritten nicer (use fold) |
65 | let mut backwards_seek = name.chars().rev(); |
66 | let last = backwards_seek.next(); |
67 | let second_last = backwards_seek.next(); |
68 | if let (Some(a), Some(b)) = (second_last, last) { |
69 | if Self::two_char_is_boundary(a, b) { |
70 | split_indices.push(name.len() - 1); |
71 | } |
72 | } |
73 | |
74 | Self::split_at_indices(name, split_indices) |
75 | } |
76 | |
77 | /// Allowed boundaries are (lower upper) (digit (!digit and !punc)) ((!digit and !punc) digit). |
78 | fn two_char_is_boundary(f: char, s: char) -> bool { |
79 | (f.is_lowercase() && s.is_uppercase()) |
80 | || (f.is_ascii_digit() && !(s.is_ascii_digit() || s.is_ascii_punctuation())) |
81 | || (!(f.is_ascii_digit() || f.is_ascii_punctuation()) && s.is_ascii_digit()) |
82 | } |
83 | |
84 | /// Checks if three characters are the end of an acronym, otherwise |
85 | /// calls `two_char_is_boundary`. |
86 | fn three_char_is_boundary(f: char, s: char, t: char) -> bool { |
87 | (f.is_uppercase() && s.is_uppercase() && t.is_lowercase()) |
88 | || Self::two_char_is_boundary(f, s) |
89 | } |
90 | |
91 | fn split_at_indices(name: &str, indices: Vec<usize>) -> Vec<String> { |
92 | let mut words = Vec::new(); |
93 | |
94 | let mut first = name; |
95 | let mut second; |
96 | for &x in indices.iter().rev() { |
97 | let pair = first.split_at(x); |
98 | first = pair.0; |
99 | second = pair.1; |
100 | words.push(second); |
101 | } |
102 | words.push(first); |
103 | |
104 | words.iter().rev().map(ToString::to_string).collect() |
105 | } |
106 | |
107 | pub fn into_case(mut self, case: Case) -> String { |
108 | use Case::*; |
109 | match case { |
110 | Camel => { |
111 | self.make_camel_case(); |
112 | self.join("" ) |
113 | } |
114 | Title => { |
115 | self.capitalize_first_letter(); |
116 | self.join(" " ) |
117 | } |
118 | Pascal | UpperCamel => { |
119 | self.capitalize_first_letter(); |
120 | self.join("" ) |
121 | } |
122 | Toggle => { |
123 | self.lower_first_letter(); |
124 | self.join(" " ) |
125 | } |
126 | Snake => { |
127 | self.make_lowercase(); |
128 | self.join("_" ) |
129 | } |
130 | Cobol => { |
131 | self.make_uppercase(); |
132 | self.join("-" ) |
133 | } |
134 | Kebab => { |
135 | self.make_lowercase(); |
136 | self.join("-" ) |
137 | } |
138 | UpperSnake | ScreamingSnake => { |
139 | self.make_uppercase(); |
140 | self.join("_" ) |
141 | } |
142 | Lower => { |
143 | self.make_lowercase(); |
144 | self.join(" " ) |
145 | } |
146 | Upper => { |
147 | self.make_uppercase(); |
148 | self.join(" " ) |
149 | } |
150 | Flat => { |
151 | self.make_lowercase(); |
152 | self.join("" ) |
153 | } |
154 | Train => { |
155 | self.capitalize_first_letter(); |
156 | self.join("-" ) |
157 | } |
158 | UpperFlat => { |
159 | self.make_uppercase(); |
160 | self.join("" ) |
161 | } |
162 | Alternating => { |
163 | self.make_alternating(); |
164 | self.join(" " ) |
165 | } |
166 | #[cfg (feature = "random" )] |
167 | Random => { |
168 | self.randomize(); |
169 | self.join(" " ) |
170 | } |
171 | #[cfg (feature = "random" )] |
172 | PseudoRandom => { |
173 | self.pseudo_randomize(); |
174 | self.join(" " ) |
175 | } |
176 | } |
177 | } |
178 | |
179 | // Randomly picks whether to be upper case or lower case |
180 | #[cfg (feature = "random" )] |
181 | fn randomize(&mut self) { |
182 | let mut rng = rand::thread_rng(); |
183 | self.words = self |
184 | .words |
185 | .iter() |
186 | .map(|word| { |
187 | word.chars() |
188 | .map(|letter| { |
189 | if rng.gen::<f32>() > 0.5 { |
190 | letter.to_uppercase().to_string() |
191 | } else { |
192 | letter.to_lowercase().to_string() |
193 | } |
194 | }) |
195 | .collect() |
196 | }) |
197 | .collect(); |
198 | } |
199 | |
200 | // Randomly selects patterns: [upper, lower] or [lower, upper] |
201 | // for a more random feeling pattern. |
202 | #[cfg (feature = "random" )] |
203 | fn pseudo_randomize(&mut self) { |
204 | let mut rng = rand::thread_rng(); |
205 | |
206 | // Keeps track of when to alternate |
207 | let mut alt: Option<bool> = None; |
208 | self.words = self |
209 | .words |
210 | .iter() |
211 | .map(|word| { |
212 | word.chars() |
213 | .map(|letter| { |
214 | match alt { |
215 | // No existing pattern, start one |
216 | None => { |
217 | if rng.gen::<f32>() > 0.5 { |
218 | alt = Some(false); // Make the next char lower |
219 | letter.to_uppercase().to_string() |
220 | } else { |
221 | alt = Some(true); // Make the next char upper |
222 | letter.to_lowercase().to_string() |
223 | } |
224 | } |
225 | // Existing pattern, do what it says |
226 | Some(upper) => { |
227 | alt = None; |
228 | if upper { |
229 | letter.to_uppercase().to_string() |
230 | } else { |
231 | letter.to_lowercase().to_string() |
232 | } |
233 | } |
234 | } |
235 | }) |
236 | .collect() |
237 | }) |
238 | .collect(); |
239 | } |
240 | |
241 | fn make_camel_case(&mut self) { |
242 | self.words = self |
243 | .words |
244 | .iter() |
245 | .enumerate() |
246 | .map(|(i, word)| { |
247 | if i != 0 { |
248 | let mut chars = word.chars(); |
249 | if let Some(a) = chars.next() { |
250 | a.to_uppercase() |
251 | .chain(chars.as_str().to_lowercase().chars()) |
252 | .collect() |
253 | } else { |
254 | String::new() |
255 | } |
256 | } else { |
257 | word.to_lowercase() |
258 | } |
259 | }) |
260 | .collect(); |
261 | } |
262 | |
263 | fn make_alternating(&mut self) { |
264 | let mut upper = false; |
265 | self.words = self |
266 | .words |
267 | .iter() |
268 | .map(|word| { |
269 | word.chars() |
270 | .map(|letter| { |
271 | if letter.is_uppercase() || letter.is_lowercase() { |
272 | if upper { |
273 | upper = false; |
274 | letter.to_uppercase().to_string() |
275 | } else { |
276 | upper = true; |
277 | letter.to_lowercase().to_string() |
278 | } |
279 | } else { |
280 | letter.to_string() |
281 | } |
282 | }) |
283 | .collect() |
284 | }) |
285 | .collect(); |
286 | } |
287 | |
288 | fn make_uppercase(&mut self) { |
289 | self.words = self.words.iter().map(|word| word.to_uppercase()).collect(); |
290 | } |
291 | |
292 | fn make_lowercase(&mut self) { |
293 | self.words = self.words.iter().map(|word| word.to_lowercase()).collect(); |
294 | } |
295 | |
296 | fn capitalize_first_letter(&mut self) { |
297 | self.words = self |
298 | .words |
299 | .iter() |
300 | .map(|word| { |
301 | let mut chars = word.chars(); |
302 | if let Some(a) = chars.next() { |
303 | a.to_uppercase() |
304 | .chain(chars.as_str().to_lowercase().chars()) |
305 | .collect() |
306 | } else { |
307 | String::new() |
308 | } |
309 | }) |
310 | .collect(); |
311 | } |
312 | |
313 | fn lower_first_letter(&mut self) { |
314 | self.words = self |
315 | .words |
316 | .iter() |
317 | .map(|word| { |
318 | let mut chars = word.chars(); |
319 | if let Some(a) = chars.next() { |
320 | a.to_lowercase() |
321 | .chain(chars.as_str().to_uppercase().chars()) |
322 | .collect() |
323 | } else { |
324 | String::new() |
325 | } |
326 | }) |
327 | .collect(); |
328 | } |
329 | |
330 | // Alternative: construct [my, -, variable, -, name] then collect |
331 | fn join(self, delim: &str) -> String { |
332 | self.words |
333 | .iter() |
334 | .enumerate() |
335 | .map(|(i, val)| { |
336 | if i == 0 { |
337 | val.to_owned() |
338 | } else { |
339 | delim.to_owned() + val |
340 | } |
341 | }) |
342 | .collect() |
343 | } |
344 | } |
345 | |
346 | #[cfg (test)] |
347 | mod test { |
348 | use super::*; |
349 | |
350 | #[test ] |
351 | fn correct_two_char_boundaries() { |
352 | assert!(!Words::two_char_is_boundary('a' , 'a' )); |
353 | assert!(Words::two_char_is_boundary('a' , 'A' )); |
354 | assert!(Words::two_char_is_boundary('a' , '5' )); |
355 | assert!(!Words::two_char_is_boundary('a' , ',' )); |
356 | assert!(!Words::two_char_is_boundary('A' , 'A' )); |
357 | assert!(!Words::two_char_is_boundary('A' , 'a' )); |
358 | assert!(Words::two_char_is_boundary('A' , '5' )); |
359 | assert!(!Words::two_char_is_boundary('A' , ',' )); |
360 | assert!(Words::two_char_is_boundary('5' , 'a' )); |
361 | assert!(Words::two_char_is_boundary('5' , 'A' )); |
362 | assert!(!Words::two_char_is_boundary('5' , '5' )); |
363 | assert!(!Words::two_char_is_boundary('5' , ',' )); |
364 | assert!(!Words::two_char_is_boundary(',' , 'a' )); |
365 | assert!(!Words::two_char_is_boundary(',' , 'A' )); |
366 | assert!(!Words::two_char_is_boundary(',' , '5' )); |
367 | assert!(!Words::two_char_is_boundary(',' , ',' )); |
368 | } |
369 | |
370 | #[test ] |
371 | fn correct_three_char_boundaries() { |
372 | assert!(Words::three_char_is_boundary('A' , 'A' , 'a' )); |
373 | assert!(!Words::three_char_is_boundary('A' , 'a' , 'a' )); |
374 | assert!(!Words::three_char_is_boundary('A' , 'a' , 'A' )); |
375 | assert!(!Words::three_char_is_boundary('A' , 'A' , '3' )); |
376 | } |
377 | } |
378 | |