1#[must_use]
3pub fn snake_case(name: &str) -> String {
4 let characters: Vec<char> = name.chars().collect();
5 let mut snake = String::with_capacity(name.len() + 4);
6 for (index, &character) in characters.iter().enumerate() {
7 if index > 0 && starts_word(&characters, index) {
8 snake.push('_');
9 }
10 snake.extend(character.to_lowercase());
11 }
12 snake
13}
14
15fn starts_word(characters: &[char], index: usize) -> bool {
16 let current = characters[index];
17 if !current.is_uppercase() {
18 return false;
19 }
20 let previous = characters[index - 1];
21 let next_is_lowercase = characters
22 .get(index + 1)
23 .is_some_and(|next| next.is_lowercase());
24 previous.is_lowercase()
25 || previous.is_ascii_digit()
26 || (previous.is_uppercase() && next_is_lowercase)
27}
28
29#[cfg(test)]
30mod tests {
31 use super::snake_case;
32
33 #[test]
34 fn an_acronym_is_one_word() {
35 assert_eq!(snake_case("HTTPStatus"), "http_status");
36 assert_eq!(snake_case("OrderLine"), "order_line");
37 assert_eq!(snake_case("Line2Item"), "line2_item");
38 }
39}