pub fn snake_case(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
let mut out = String::new();
for (index, ¤t) in chars.iter().enumerate() {
if current.is_uppercase() && index > 0 {
let previous = chars[index - 1];
let next_lower = chars.get(index + 1).is_some_and(|c| c.is_lowercase());
if previous.is_lowercase()
|| previous.is_numeric()
|| (previous.is_uppercase() && next_lower)
{
out.push('_');
}
}
out.extend(current.to_lowercase());
}
out
}
pub fn camel_case(name: &str) -> String {
name.split('_')
.filter(|segment| !segment.is_empty())
.map(|segment| {
let mut chars = segment.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{camel_case, snake_case};
#[test]
fn camel_case_pins_the_outputs_its_docstring_names() {
assert_eq!(camel_case("httpServer"), "HttpServer");
assert_eq!(camel_case("HTTPServer"), "HTTPServer");
assert_eq!(camel_case("foo_bar"), "FooBar");
}
#[test]
fn camel_case_is_not_injective_in_two_ways() {
assert_eq!(camel_case("foo_bar"), camel_case("fooBar"));
assert_eq!(camel_case("fooBar"), camel_case("FooBar"));
}
#[test]
fn the_two_transforms_are_incomparable() {
assert_eq!(camel_case("XY"), camel_case("x_y"));
assert_ne!(snake_case("XY"), snake_case("x_y"));
assert_ne!(camel_case("HTTPServer"), camel_case("httpServer"));
assert_eq!(snake_case("HTTPServer"), snake_case("httpServer"));
}
#[test]
fn an_acronym_stays_one_word() {
assert_eq!(snake_case("getVIN"), "get_vin");
assert_eq!(snake_case("ABC"), "abc");
}
#[test]
fn an_acronym_followed_by_a_word_splits() {
assert_eq!(snake_case("HTTPServer"), "http_server");
assert_eq!(snake_case("IOError"), "io_error");
assert_eq!(snake_case("parseHTTPResponse"), "parse_http_response");
}
#[test]
fn a_camel_case_name_splits_on_every_boundary() {
assert_eq!(snake_case("currentSpeed"), "current_speed");
assert_eq!(snake_case("speed2Target"), "speed2_target");
assert_eq!(snake_case("aB"), "a_b");
}
#[test]
fn an_underscore_already_present_is_kept() {
assert_eq!(snake_case("already_snake"), "already_snake");
assert_eq!(snake_case("mixed_CaseName"), "mixed_case_name");
}
#[test]
fn the_transform_is_idempotent() {
for name in [
"getVIN",
"HTTPServer",
"currentSpeed",
"mixed_CaseName",
"a1B2",
] {
let once = snake_case(name);
assert_eq!(snake_case(&once), once, "not idempotent on `{name}`");
}
}
}