🦀 Functional Rust

497: Case Conversion Patterns

Difficulty: 1 Level: Beginner Unicode-aware case conversion — plus snake_case, camelCase, and title case transformations.

The Problem This Solves

Python's `.upper()` and `.lower()` are Unicode-aware. JavaScript's `.toUpperCase()` and `.toLowerCase()` are too. Rust is also Unicode-aware for case conversion — but with a twist: `to_uppercase()` returns an iterator, not a `String`. This is because some characters expand when uppercased: the German `ß` becomes `SS` (two characters). Returning an iterator avoids the awkward question of whether to pre-allocate. For ASCII-only text where you know there are no special characters, Rust provides `to_ascii_uppercase()` and `to_ascii_lowercase()` which return a `String` directly and are faster — no Unicode table lookup. Beyond simple case flipping, case conversion often means transforming between naming conventions: `MyFunctionName` → `my_function_name` (snake_case), `my_function_name` → `myFunctionName` (camelCase). Rust's std library doesn't include these, but they're straightforward to implement with the character iterator tools you've already seen.

The Intuition

`to_uppercase()` returns `ToUppercase` — an iterator of `char`. To get a `String`, chain `.collect()`. This is different from Python and OCaml which return a string directly. The reason: Unicode case mappings aren't always 1-to-1 in character count. For ASCII text (English, numbers, common symbols), use `to_ascii_uppercase()` — it returns `String` directly and is faster because it only handles the ASCII range, no Unicode table needed. Case convention transformations (snake, camel, title) are just char-level iterator operations: detect uppercase chars, insert underscores, capitalize first letters. The pattern: `chars()` + `map`/`flat_map` + `collect()`.

How It Works in Rust

let s = "Hello, World! café";

// Unicode-aware (ß → SS, returns iterator)
let upper: String = s.chars().flat_map(|c| c.to_uppercase()).collect();
// Shorthand: s.to_uppercase() also works (collects for you)
println!("{}", s.to_uppercase());  // "HELLO, WORLD! CAFÉ"
println!("{}", s.to_lowercase());  // "hello, world! café"

// Special case: German ß
"straße".to_uppercase()   // "STRASSE" — expands to 7 chars from 6!

// ASCII-only (faster, no Unicode table lookup)
s.to_ascii_uppercase()   // returns String, only affects A-Z
s.to_ascii_lowercase()   // returns String, only affects a-z

// snake_case: detect uppercase, insert underscore before it
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()); // handles ß-style expansions
     } else {
         out.push(c);
     }
 }
 out
}
// "MyFunctionName" → "my_function_name"

// camelCase: split on _, capitalize first char of each word (except first)
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().collect::<String>())
             .unwrap_or_default();
         first + chars.as_str()
     }.chars().collect::<Vec<_>>()
 }).collect()
}
// "my_function_name" → "myFunctionName"

// Title case: capitalize first letter of each word
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(" ")
}
// "the quick brown fox" → "The Quick Brown Fox"

What This Unlocks

Key Differences

ConceptOCamlRust
Uppercase`String.map Char.uppercase_ascii s` (ASCII only)`s.to_uppercase()` (Unicode)
Lowercase`String.map Char.lowercase_ascii s` (ASCII only)`s.to_lowercase()` (Unicode)
Return type`string`Iterator — `.collect::<String>()` needed
ASCII-only fast pathDefault behavior`to_ascii_uppercase()` → `String`
ß expansionNot handled`"straße".to_uppercase()` → `"STRASSE"`
snake_caseManual with BufferManual with `chars()` + `push`
Title caseManual with BufferManual with `split_whitespace()`
// 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(" ")
}

fn main() {
    let s = "Hello, World! café";

    // Unicode-aware (ß → SS)
    println!("upper: {}", s.to_uppercase());
    println!("lower: {}", s.to_lowercase());

    // ASCII-only (faster)
    println!("ascii upper: {}", s.to_ascii_uppercase());
    println!("ascii lower: {}", s.to_ascii_lowercase());

    // Custom conversions
    println!("snake: {}", to_snake_case("MyFunctionName"));
    println!("camel: {}", to_camel_case("my_function_name"));
    println!("title: {}", to_title_case("the quick brown fox"));

    // German ß expands
    println!("ß→SS: {}", "straße".to_uppercase());
}

#[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"); }
}
(* 497. Case conversion – OCaml *)
let to_snake_case s =
  let buf = Buffer.create (String.length s) in
  String.iteri (fun i c ->
    if c >= 'A' && c <= 'Z' then begin
      if i > 0 then Buffer.add_char buf '_';
      Buffer.add_char buf (Char.lowercase_ascii c)
    end else Buffer.add_char buf c
  ) s;
  Buffer.contents buf

let to_title_case s =
  let words = String.split_on_char ' ' s in
  let cap w = if w = "" then w
              else String.make 1 (Char.uppercase_ascii w.[0]) ^
                   String.sub w 1 (String.length w - 1) in
  String.concat " " (List.map cap words)

let () =
  let s = "hello, World! café" in
  Printf.printf "upper: %s\n" (String.map Char.uppercase_ascii s);
  Printf.printf "lower: %s\n" (String.map Char.lowercase_ascii s);
  Printf.printf "snake: %s\n" (to_snake_case "MyFunctionName");
  Printf.printf "title: %s\n" (to_title_case "the quick brown fox")