265: Caesar Cipher
Difficulty: 1 Level: Beginner Encrypt and decrypt text by shifting each letter a fixed number of positions in the alphabet, leaving non-letter characters unchanged.The Problem This Solves
You're learning character manipulation in Rust โ how to treat `char` as a number, apply arithmetic, and map transformations over strings. The Caesar cipher is the perfect vehicle: the logic is simple enough to understand immediately, but it forces you to work with character ranges, modular arithmetic, and string collection. Without range patterns and `as u8` casting, you'd write verbose `if/else` chains checking `c >= 'a' && c <= 'z'`. Without `.chars().map().collect()`, you'd iterate over a string building a new one character by character. The Caesar cipher shows you both idioms in under 10 lines. Decryption as `caesar(26 - n, s)` demonstrates a key functional insight: inverse operations can often be expressed as the original operation with a different parameter, avoiding duplicated logic.The Intuition
Map each letter to its shifted position in the alphabet using modular arithmetic; pass non-letter characters through unchanged.How It Works in Rust
fn shift_char(n: u8, c: char) -> char {
match c {
// Range pattern matches entire alphabet in one arm
'a'..='z' => ((c as u8 - b'a' + n) % 26 + b'a') as char,
'A'..='Z' => ((c as u8 - b'A' + n) % 26 + b'A') as char,
_ => c, // non-letters pass through unchanged
}
}
pub fn caesar(n: u8, s: &str) -> String {
s.chars() // iterate over Unicode code points
.map(|c| shift_char(n, c)) // shift each letter
.collect() // reassemble into a String
}
pub fn decrypt(n: u8, s: &str) -> String {
caesar(26 - (n % 26), s) // shift by the complement; n % 26 handles n=26
}
pub fn rot13(s: &str) -> String {
caesar(13, s) // ROT13 is its own inverse: rot13(rot13(s)) == s
}
The key arithmetic: `(c as u8 - b'a' + n) % 26 + b'a'`
1. Subtract `b'a'` โ 0-based index (0โ25)
2. Add shift `n`, take `% 26` โ stays in alphabet
3. Add `b'a'` โ back to ASCII code point
4. Cast to `char`
What This Unlocks
- Character arithmetic patterns: The `as u8` / `as char` dance applies to any char-to-number transformation.
- Range patterns: `'a'..='z'` in match arms replaces three separate comparisons โ reuse this anywhere.
- Functional string transformation: `.chars().map().collect()` is the idiomatic Rust `String.map` โ memorize it.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Char to int | `Char.code c` | `c as u8` | ||
| Int to char | `Char.chr n` | `n as char` | ||
| Alphabet check | `if c >= 'a' && c <= 'z'` | `'a'..='z'` range pattern | ||
| String mapping | `String.map (shift_char n) s` | `s.chars().map(\ | c\ | shift_char(n, c)).collect()` |
| Decryption | `let decrypt n = caesar (26 - n)` (partial app) | `fn decrypt(n, s) { caesar(26 - n % 26, s) }` |
/// Caesar Cipher โ Functional Encryption
fn shift_char(n: u8, c: char) -> char {
match c {
'a'..='z' => ((c as u8 - b'a' + n) % 26 + b'a') as char,
'A'..='Z' => ((c as u8 - b'A' + n) % 26 + b'A') as char,
_ => c,
}
}
pub fn caesar(n: u8, s: &str) -> String {
s.chars().map(|c| shift_char(n, c)).collect()
}
pub fn decrypt(n: u8, s: &str) -> String {
caesar(26 - (n % 26), s)
}
pub fn rot13(s: &str) -> String {
caesar(13, s)
}
fn main() {
let msg = "Hello World";
let enc = caesar(13, msg);
println!("Encrypted: {}", enc);
println!("Decrypted: {}", decrypt(13, &enc));
println!("ROT13 twice: {}", rot13(&rot13(msg)));
}
/* Output:
Encrypted: Uryyb Jbeyq
Decrypted: Hello World
ROT13 twice: Hello World
*/
let shift_char n c =
if c >= 'a' && c <= 'z' then
Char.chr ((Char.code c - Char.code 'a' + n) mod 26 + Char.code 'a')
else if c >= 'A' && c <= 'Z' then
Char.chr ((Char.code c - Char.code 'A' + n) mod 26 + Char.code 'A')
else c
let caesar n s = String.map (shift_char n) s
let decrypt n = caesar (26 - n)
let () =
let msg = "Hello World" in
let enc = caesar 13 msg in
assert (enc = "Uryyb Jbeyq");
assert (decrypt 13 enc = msg);
(* ROT13 is self-inverse *)
assert (caesar 13 (caesar 13 "test") = "test");
print_endline "ok"
๐ Detailed Comparison
OCaml vs Rust: Caesar Cipher โ Functional Encryption
Side-by-Side Code
OCaml
๐ช Show OCaml equivalent
let shift_char n c =
if c >= 'a' && c <= 'z' then
Char.chr ((Char.code c - Char.code 'a' + n) mod 26 + Char.code 'a')
else if c >= 'A' && c <= 'Z' then
Char.chr ((Char.code c - Char.code 'A' + n) mod 26 + Char.code 'A')
else c
let caesar n s = String.map (shift_char n) s
let decrypt n = caesar (26 - n)Rust (idiomatic)
fn shift_char(n: u8, c: char) -> char {
match c {
'a'..='z' => ((c as u8 - b'a' + n) % 26 + b'a') as char,
'A'..='Z' => ((c as u8 - b'A' + n) % 26 + b'A') as char,
_ => c,
}
}
pub fn caesar(n: u8, s: &str) -> String {
s.chars().map(|c| shift_char(n, c)).collect()
}
pub fn decrypt(n: u8, s: &str) -> String {
caesar(26 - (n % 26), s)
}Rust (iterator-based โ lazy)
pub fn caesar_iter(n: u8, s: &str) -> impl Iterator<Item = char> + '_ {
s.chars().map(move |c| shift_char(n, c))
}Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Shift char | `val shift_char : int -> char -> char` | `fn shift_char(n: u8, c: char) -> char` |
| Encrypt | `val caesar : int -> string -> string` | `fn caesar(n: u8, s: &str) -> String` |
| Decrypt | `val decrypt : int -> string -> string` | `fn decrypt(n: u8, s: &str) -> String` |
| Char type | `char` (8-bit) | `char` (32-bit Unicode scalar) |
| String type | `string` (byte sequence) | `&str` (UTF-8 borrowed) / `String` (owned) |
Key Insights
1. Range patterns are elegant: Rust's `'a'..='z'` in match arms is cleaner than OCaml's `if c >= 'a' && c <= 'z'` โ pattern matching at its best 2. Byte literals: Rust's `b'a'` is equivalent to OCaml's `Char.code 'a'` โ both give the numeric value of the character 3. String ownership: OCaml's `String.map` returns a new string (strings are mutable but `map` creates new); Rust borrows `&str` and returns owned `String` 4. Lazy iteration: Rust's `impl Iterator` approach delays computation; OCaml would need `Seq` for the same laziness 5. Type safety on shift: Rust uses `u8` for the shift amount, preventing negative shifts at the type level; OCaml uses `int` which could be negative (handled by `mod`)
When to Use Each Style
Use eager collection (`caesar`) when: you need the full encrypted string as a `String` for storage or further processing Use lazy iteration (`caesar_iter`) when: you're chaining with other transformations or writing to a stream character-by-character