930-pangram-check — Pangram Check
Tutorial
The Problem
A pangram is a sentence containing every letter of the alphabet at least once. "The quick brown fox jumps over the lazy dog" is the famous example, used for font demonstrations and keyboard testing. Checking for a pangram is a set-membership problem: does the set of distinct lowercase letters in the sentence contain all 26 letters? Three approaches illuminate different algorithmic ideas: set-based (clear and general), bitflag (memory-minimal, no heap allocation), and recursive (OCaml-style demonstration). The bitflag approach — one bit per letter in a 32-bit integer — is a classic compact set representation for small fixed-universe sets.
🎯 Learning Outcomes
HashSet<char> to collect unique letters and check for 26 distinct entriesu32 as a 26-bit set — zero heap allocation1 << idx as the set-insert operation and (1 << 26) - 1 as the "all set" checkCode Example
#![allow(clippy::all)]
/// # Pangram Check
///
/// A pangram is a sentence using every letter of the alphabet at least once.
/// Demonstrates set-based string analysis.
use std::collections::HashSet;
/// Idiomatic Rust using HashSet and iterator chains.
/// The `collect()` into HashSet automatically deduplicates.
pub fn is_pangram(sentence: &str) -> bool {
// Filter to only alphabetic chars, lowercase them, collect unique into set
let unique_letters: HashSet<char> = sentence
.chars()
.filter(|c| c.is_ascii_alphabetic())
.map(|c| c.to_ascii_lowercase())
.collect();
unique_letters.len() == 26
}
/// Bitflag approach — uses a u32 as a compact set of 26 bits.
/// Each bit represents a letter: bit 0 = 'a', bit 1 = 'b', etc.
/// No heap allocation needed!
pub fn is_pangram_bitflag(sentence: &str) -> bool {
let mut seen: u32 = 0;
for c in sentence.chars() {
if c.is_ascii_alphabetic() {
let idx = c.to_ascii_lowercase() as u32 - 'a' as u32;
seen |= 1 << idx;
}
}
seen == (1 << 26) - 1
}
/// Recursive approach — checks if each letter 'a'..'z' exists in the string.
pub fn is_pangram_recursive(sentence: &str) -> bool {
fn has_all(sentence: &str, letter: u8) -> bool {
if letter > b'z' {
return true;
}
let lower = sentence.to_ascii_lowercase();
lower.contains(letter as char) && has_all(sentence, letter + 1)
}
has_all(sentence, b'a')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classic_pangram() {
assert!(is_pangram("The quick brown fox jumps over the lazy dog"));
}
#[test]
fn test_not_pangram() {
assert!(!is_pangram("Hello world"));
}
#[test]
fn test_empty_string() {
assert!(!is_pangram(""));
}
#[test]
fn test_missing_x() {
assert!(!is_pangram("The quick brown fo jumps over the lazy dog"));
}
#[test]
fn test_mixed_case() {
assert!(is_pangram("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"));
}
#[test]
fn test_with_numbers_and_punctuation() {
assert!(is_pangram(
"The 1 quick brown fox jumps! over the 2 lazy dogs."
));
}
#[test]
fn test_bitflag_version() {
assert!(is_pangram_bitflag(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_bitflag("Hello world"));
}
#[test]
fn test_recursive_version() {
assert!(is_pangram_recursive(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_recursive("abc"));
}
}Key Differences
HashSet<char> vs OCaml Hashtbl or List.sort_uniq — same semantics, different APIs.u32 bitfield uses safe arithmetic; OCaml's lsl on native integers can overflow (63-bit on 64-bit systems).u8 (letter codes) mirrors OCaml's style but requires explicit bounds checking.'a' as u32 numeric conversion; OCaml uses Char.code 'a'.OCaml Approach
OCaml is_pangram s = let letters = s |> String.lowercase_ascii |> String.to_seq |> Seq.filter (fun c -> c >= 'a' && c <= 'z') |> List.of_seq |> List.sort_uniq compare in List.length letters = 26. Or using a Hashtbl. The bitflag approach: let check_pangram s = let seen = ref 0 in String.iter (fun c -> if c >= 'a' && c <= 'z' then seen := !seen lor (1 lsl (Char.code c - Char.code 'a'))) s; !seen = (1 lsl 26) - 1. OCaml's lsl for left shift vs Rust's <<.
Full Source
#![allow(clippy::all)]
/// # Pangram Check
///
/// A pangram is a sentence using every letter of the alphabet at least once.
/// Demonstrates set-based string analysis.
use std::collections::HashSet;
/// Idiomatic Rust using HashSet and iterator chains.
/// The `collect()` into HashSet automatically deduplicates.
pub fn is_pangram(sentence: &str) -> bool {
// Filter to only alphabetic chars, lowercase them, collect unique into set
let unique_letters: HashSet<char> = sentence
.chars()
.filter(|c| c.is_ascii_alphabetic())
.map(|c| c.to_ascii_lowercase())
.collect();
unique_letters.len() == 26
}
/// Bitflag approach — uses a u32 as a compact set of 26 bits.
/// Each bit represents a letter: bit 0 = 'a', bit 1 = 'b', etc.
/// No heap allocation needed!
pub fn is_pangram_bitflag(sentence: &str) -> bool {
let mut seen: u32 = 0;
for c in sentence.chars() {
if c.is_ascii_alphabetic() {
let idx = c.to_ascii_lowercase() as u32 - 'a' as u32;
seen |= 1 << idx;
}
}
seen == (1 << 26) - 1
}
/// Recursive approach — checks if each letter 'a'..'z' exists in the string.
pub fn is_pangram_recursive(sentence: &str) -> bool {
fn has_all(sentence: &str, letter: u8) -> bool {
if letter > b'z' {
return true;
}
let lower = sentence.to_ascii_lowercase();
lower.contains(letter as char) && has_all(sentence, letter + 1)
}
has_all(sentence, b'a')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classic_pangram() {
assert!(is_pangram("The quick brown fox jumps over the lazy dog"));
}
#[test]
fn test_not_pangram() {
assert!(!is_pangram("Hello world"));
}
#[test]
fn test_empty_string() {
assert!(!is_pangram(""));
}
#[test]
fn test_missing_x() {
assert!(!is_pangram("The quick brown fo jumps over the lazy dog"));
}
#[test]
fn test_mixed_case() {
assert!(is_pangram("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"));
}
#[test]
fn test_with_numbers_and_punctuation() {
assert!(is_pangram(
"The 1 quick brown fox jumps! over the 2 lazy dogs."
));
}
#[test]
fn test_bitflag_version() {
assert!(is_pangram_bitflag(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_bitflag("Hello world"));
}
#[test]
fn test_recursive_version() {
assert!(is_pangram_recursive(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_recursive("abc"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classic_pangram() {
assert!(is_pangram("The quick brown fox jumps over the lazy dog"));
}
#[test]
fn test_not_pangram() {
assert!(!is_pangram("Hello world"));
}
#[test]
fn test_empty_string() {
assert!(!is_pangram(""));
}
#[test]
fn test_missing_x() {
assert!(!is_pangram("The quick brown fo jumps over the lazy dog"));
}
#[test]
fn test_mixed_case() {
assert!(is_pangram("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG"));
}
#[test]
fn test_with_numbers_and_punctuation() {
assert!(is_pangram(
"The 1 quick brown fox jumps! over the 2 lazy dogs."
));
}
#[test]
fn test_bitflag_version() {
assert!(is_pangram_bitflag(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_bitflag("Hello world"));
}
#[test]
fn test_recursive_version() {
assert!(is_pangram_recursive(
"The quick brown fox jumps over the lazy dog"
));
assert!(!is_pangram_recursive("abc"));
}
}
Deep Comparison
Pangram Check — OCaml vs Rust Comparison
Core Insight
Both languages excel at set operations on characters, but Rust offers a zero-allocation bitflag approach alongside the idiomatic HashSet version. OCaml's Set.Make functor creates a balanced tree set, while Rust's HashSet is hash-based.
OCaml Approach
Uses the Set.Make(Char) functor to create a character set module. Characters are filtered with Seq.filter, collected with CS.of_seq, and checked with CS.subset. The functor system requires declaring a module upfront.
Rust Approach
Iterator chain: .chars().filter().map().collect::<HashSet<_>>() — the type drives collect() to deduplicate automatically. The bitflag variant uses u32 as a 26-bit set with zero heap allocation, which has no direct OCaml equivalent without manual bit manipulation.
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Memory | Tree-based set (heap nodes) | HashSet (heap) or u32 bitflag (stack) |
| Null safety | N/A (bool result) | N/A (bool result) |
| Errors | Not applicable | Not applicable |
| Iteration | Seq.filter + CS.of_seq | .chars().filter().map().collect() |
| Set type | Set.Make functor (balanced tree) | HashSet (hash table) |
Things Rust Learners Should Notice
collect() is polymorphic** — collecting into HashSet vs Vec changes behavior (dedup vs preserve)is_ascii_alphabetic()** — Rust has rich character classification methods built inHashSet<char> works directly without declaring a moduleto_ascii_lowercase()** works on individual chars, not just stringsFurther Reading
Exercises
is_pangram to covers_all<T: Hash + Eq>(text: impl IntoIterator<Item=T>, required: impl IntoIterator<Item=T>) -> bool.pangram_missing_letters(s: &str) -> Vec<char> that returns the alphabetically sorted list of missing letters.shortest_pangram(words: &[&str]) -> Option<Vec<&str>> that finds the minimum-word-count subset that forms a pangram.