1pub fn is_snake_case(name: &str) -> bool {
5 wolf_project::is_snake_case(name)
6}
7
8pub fn is_pascal_case(name: &str) -> bool {
10 name.starts_with(|c: char| c.is_ascii_uppercase())
11 && name.chars().all(|c| c.is_ascii_alphanumeric())
12}
13
14pub(crate) fn to_snake(name: &str) -> String {
16 let mut out = String::new();
17 for (i, c) in name.chars().enumerate() {
18 if c.is_ascii_uppercase() {
19 if i > 0 && !out.ends_with('_') {
20 out.push('_');
21 }
22 out.push(c.to_ascii_lowercase());
23 } else if c == '_' || c == '-' {
24 if !out.is_empty() && !out.ends_with('_') {
25 out.push('_');
26 }
27 } else {
28 out.push(c);
29 }
30 }
31 out.trim_matches('_').to_string()
32}
33
34pub(crate) fn to_pascal(name: &str) -> String {
36 name.split(['_', '-'])
37 .filter(|p| !p.is_empty())
38 .map(|p| {
39 let mut chars = p.chars();
40 match chars.next() {
41 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
42 None => String::new(),
43 }
44 })
45 .collect()
46}
47
48pub fn suggest<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
51 let limit = (name.chars().count() / 3).max(1);
52 let mut best: Option<(usize, &str)> = None;
53 for candidate in candidates {
54 if candidate == name {
55 continue;
56 }
57 let d = distance(name, candidate);
58 if d > limit {
59 continue;
60 }
61 best = match best {
62 Some((bd, bc)) if (bd, bc) <= (d, candidate) => Some((bd, bc)),
63 _ => Some((d, candidate)),
64 };
65 }
66 best.map(|(_, c)| c.to_string())
67}
68
69fn distance(a: &str, b: &str) -> usize {
71 let b: Vec<char> = b.chars().collect();
72 let mut row: Vec<usize> = (0..=b.len()).collect();
73 for (i, ca) in a.chars().enumerate() {
74 let mut prev = row.first().copied().unwrap_or(0);
75 if let Some(first) = row.first_mut() {
76 *first = i + 1;
77 }
78 for (j, cb) in b.iter().enumerate() {
79 let cost = usize::from(ca != *cb);
80 let above = row.get(j + 1).copied().unwrap_or(0);
81 let left = row.get(j).copied().unwrap_or(0);
82 let value = (above + 1).min(left + 1).min(prev + cost);
83 prev = above;
84 if let Some(cell) = row.get_mut(j + 1) {
85 *cell = value;
86 }
87 }
88 }
89 row.last().copied().unwrap_or(0)
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn styles() {
98 assert!(is_pascal_case("FileStorage"));
99 assert!(!is_pascal_case("fileStorage"));
100 assert!(!is_pascal_case("File_Storage"));
101 assert_eq!(to_snake("findUser"), "find_user");
102 assert_eq!(to_snake("FileStorage"), "file_storage");
103 assert_eq!(to_pascal("file_storage"), "FileStorage");
104 }
105
106 #[test]
107 fn suggestions() {
108 let names = ["print", "panic", "price"];
109 assert_eq!(
110 suggest("prnt", names.iter().copied()).as_deref(),
111 Some("print")
112 );
113 assert_eq!(suggest("zzz", names.iter().copied()), None);
114 }
115}