1use crate::Case;
2
3#[cfg(feature = "random")]
4use rand::prelude::*;
5
6pub(super) struct Words {
7 words: Vec<String>,
8}
9
10impl 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)]
347mod 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