String Case Conversion
Functional Programming
Tutorial
The Problem
Case conversion appears in: code generation (struct names in camelCase, field names in snake_case), API normalisation (HTTP headers are case-insensitive), URL slugs (lowercase-with-hyphens), and display formatting (title case for headings). str::to_uppercase handles the simple case but does not perform format conversion between naming conventions. These require splitting on boundaries (_, uppercase chars, spaces) and reassembling with different rules.
🎯 Learning Outcomes
.to_uppercase() and .to_lowercase() for Unicode-correct case conversionsnake_case conversion by inserting _ before uppercase charscamelCase by capitalising the first char of each _-delimited wordTitle Case by capitalising the first char of each whitespace-delimited wordchar_indices, flat_map, and split for case transformation pipelinesCode Example
#![allow(clippy::all)]
// 497. Case conversion patterns
fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
out.push('_');
}
out.extend(c.to_lowercase());
} else {
out.push(c);
}
}
out
}
fn to_camel_case(s: &str) -> String {
s.split('_')
.enumerate()
.flat_map(|(i, word)| {
let mut chars = word.chars();
if i == 0 {
chars.map(|c| c).collect::<String>()
} else {
let first = chars
.next()
.map(|c| c.to_uppercase().to_string())
.unwrap_or_default();
let rest: String = chars.collect();
first + &rest
}
.chars()
.collect::<Vec<_>>()
})
.collect()
}
fn to_title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut cs = word.chars();
cs.next()
.map(|c| c.to_uppercase().collect::<String>() + cs.as_str())
.unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_upper() {
assert_eq!("hello".to_uppercase(), "HELLO");
}
#[test]
fn test_lower() {
assert_eq!("HELLO".to_lowercase(), "hello");
}
#[test]
fn test_snake() {
assert_eq!(to_snake_case("MyFunc"), "my_func");
}
#[test]
fn test_camel() {
assert_eq!(to_camel_case("my_func_name"), "myFuncName");
}
#[test]
fn test_title() {
assert_eq!(to_title_case("hello world"), "Hello World");
}
}Key Differences
to_uppercase/to_lowercase are Unicode-correct ('ß'.to_uppercase() == "SS"); OCaml's String.uppercase_ascii is ASCII-only.flat_map + collect for camelCase is idiomatic; OCaml requires Buffer + imperative loops for the same transformation.char.to_lowercase()**: Returns a ToLowercase iterator (because one char can expand to multiple chars, e.g. 'ß'); OCaml's Char.lowercase_ascii returns a single char.heck crate for all naming convention conversions; OCaml needs stringext or manual code.OCaml Approach
(* Simple upper/lower via standard library *)
String.uppercase_ascii "hello" (* "HELLO" *)
String.lowercase_ascii "HELLO" (* "hello" *)
(* snake_case — manual loop *)
let to_snake_case s =
let buf = Buffer.create (String.length s) in
String.iteri (fun i c ->
if Char.uppercase_ascii c = c && c <> ' ' && i > 0
then (Buffer.add_char buf '_'; Buffer.add_char buf (Char.lowercase_ascii c))
else Buffer.add_char buf c) s;
Buffer.contents buf
The stringext library provides String.split_on_string and helpers; snake-case and camelCase conversions are common in code generation libraries like ppx_deriving.
Full Source
#![allow(clippy::all)]
// 497. Case conversion patterns
fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
out.push('_');
}
out.extend(c.to_lowercase());
} else {
out.push(c);
}
}
out
}
fn to_camel_case(s: &str) -> String {
s.split('_')
.enumerate()
.flat_map(|(i, word)| {
let mut chars = word.chars();
if i == 0 {
chars.map(|c| c).collect::<String>()
} else {
let first = chars
.next()
.map(|c| c.to_uppercase().to_string())
.unwrap_or_default();
let rest: String = chars.collect();
first + &rest
}
.chars()
.collect::<Vec<_>>()
})
.collect()
}
fn to_title_case(s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut cs = word.chars();
cs.next()
.map(|c| c.to_uppercase().collect::<String>() + cs.as_str())
.unwrap_or_default()
})
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_upper() {
assert_eq!("hello".to_uppercase(), "HELLO");
}
#[test]
fn test_lower() {
assert_eq!("HELLO".to_lowercase(), "hello");
}
#[test]
fn test_snake() {
assert_eq!(to_snake_case("MyFunc"), "my_func");
}
#[test]
fn test_camel() {
assert_eq!(to_camel_case("my_func_name"), "myFuncName");
}
#[test]
fn test_title() {
assert_eq!(to_title_case("hello world"), "Hello World");
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_upper() {
assert_eq!("hello".to_uppercase(), "HELLO");
}
#[test]
fn test_lower() {
assert_eq!("HELLO".to_lowercase(), "hello");
}
#[test]
fn test_snake() {
assert_eq!(to_snake_case("MyFunc"), "my_func");
}
#[test]
fn test_camel() {
assert_eq!(to_camel_case("my_func_name"), "myFuncName");
}
#[test]
fn test_title() {
assert_eq!(to_title_case("hello world"), "Hello World");
}
}
Exercises
to_kebab_case(s: &str) -> String that converts CamelCase or snake_case to kebab-case.to_screaming_snake(s: &str) -> String for constant naming (MAX_VALUE).proptest) that verifies camelCase → snake_case → camelCase is an identity for ASCII-alphabetic identifiers.