084 — Phone Number Validation
Tutorial Video
Text description (accessibility)
This video demonstrates the "084 — Phone Number Validation" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Parse and validate a North American phone number from a free-form string (`(223) 456-7890`, `+1 223-456-7890`, etc.). Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Parse and validate a North American phone number from a free-form string ((223) 456-7890, +1 223-456-7890, etc.). Strip non-digit characters, normalise 11-digit numbers with a leading 1, then validate the area code and exchange codes. Implement both an imperative and an and_then-chain version, comparing with OCaml's Result.bind pipeline.
🎯 Learning Outcomes
.chars().filter(…).collect() to extract digits from a stringand_then on Result to build a validation pipeline without nested matchd.as_bytes()[0]) for single-byte ASCII comparisons&'static str as the error type for string literalsand_then to OCaml's Result.bind operatorCode Example
#![allow(clippy::all)]
/// Phone Number Parser — Validation Pipeline
///
/// Ownership: Input is borrowed &str. Result returns owned String on success.
/// The and_then chain mirrors OCaml's Result.bind pipeline.
/// Extract only digits from input
fn digits_only(s: &str) -> String {
s.chars().filter(|c| c.is_ascii_digit()).collect()
}
/// Validate a phone number using Result chaining
pub fn validate(s: &str) -> Result<String, &'static str> {
let d = digits_only(s);
// Normalize 11-digit numbers starting with 1
let d = if d.len() == 11 && d.starts_with('1') {
d[1..].to_string()
} else if d.len() == 10 {
d
} else {
return Err("wrong number of digits");
};
// Validate area code
let area = d.as_bytes()[0];
if area == b'0' || area == b'1' {
return Err("invalid area code");
}
// Validate exchange
let exchange = d.as_bytes()[3];
if exchange == b'0' || exchange == b'1' {
return Err("invalid exchange");
}
Ok(d)
}
/// Version 2: Using and_then chain (more functional)
pub fn validate_chain(s: &str) -> Result<String, &'static str> {
let d = digits_only(s);
normalize_length(d)
.and_then(check_area_code)
.and_then(check_exchange)
}
fn normalize_length(d: String) -> Result<String, &'static str> {
match d.len() {
11 if d.starts_with('1') => Ok(d[1..].to_string()),
10 => Ok(d),
_ => Err("wrong number of digits"),
}
}
fn check_area_code(d: String) -> Result<String, &'static str> {
if d.as_bytes()[0] == b'0' || d.as_bytes()[0] == b'1' {
Err("invalid area code")
} else {
Ok(d)
}
}
fn check_exchange(d: String) -> Result<String, &'static str> {
if d.as_bytes()[3] == b'0' || d.as_bytes()[3] == b'1' {
Err("invalid exchange")
} else {
Ok(d)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_10_digit() {
assert_eq!(validate("(223) 456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_valid_11_digit() {
assert_eq!(validate("1-223-456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_invalid_area_code() {
assert_eq!(validate("(023) 456-7890"), Err("invalid area code"));
}
#[test]
fn test_invalid_exchange() {
assert_eq!(validate("(223) 056-7890"), Err("invalid exchange"));
}
#[test]
fn test_wrong_length() {
assert_eq!(validate("123"), Err("wrong number of digits"));
}
#[test]
fn test_chain_version() {
assert_eq!(validate_chain("(223) 456-7890"), Ok("2234567890".into()));
assert_eq!(validate_chain("(023) 456-7890"), Err("invalid area code"));
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Error type | &'static str | string |
| Chaining | .and_then(f) | |> Result.bind (fun d -> ...) |
| Digit filter | .chars().filter(…).collect() | String.to_seq |> Seq.filter |> String.of_seq |
| Byte indexing | d.as_bytes()[0] | d.[0] (char indexing) |
| Early return | return Err(…) | Pattern on prev Result |
| Pipeline style | Method chain | Pipe operator |> |
The validation pipeline pattern — normalise, then check each rule in sequence — is a common functional idiom. Using and_then/Result.bind keeps each check independent and composable, unlike nested match or if/else chains that tangle logic across rules.
OCaml Approach
OCaml's Result.bind (|> Result.bind (fun d -> ...)) chains validation steps. String.to_seq, Seq.filter, and String.of_seq extract digits. Individual checks use if … then Error "…" else Ok d — identical logic to the Rust version. The |> pipe makes the chain read left-to-right, equivalent to Rust's method chain. Both versions express: "start with the string, normalise, check area code, check exchange — fail at the first error."
Full Source
#![allow(clippy::all)]
/// Phone Number Parser — Validation Pipeline
///
/// Ownership: Input is borrowed &str. Result returns owned String on success.
/// The and_then chain mirrors OCaml's Result.bind pipeline.
/// Extract only digits from input
fn digits_only(s: &str) -> String {
s.chars().filter(|c| c.is_ascii_digit()).collect()
}
/// Validate a phone number using Result chaining
pub fn validate(s: &str) -> Result<String, &'static str> {
let d = digits_only(s);
// Normalize 11-digit numbers starting with 1
let d = if d.len() == 11 && d.starts_with('1') {
d[1..].to_string()
} else if d.len() == 10 {
d
} else {
return Err("wrong number of digits");
};
// Validate area code
let area = d.as_bytes()[0];
if area == b'0' || area == b'1' {
return Err("invalid area code");
}
// Validate exchange
let exchange = d.as_bytes()[3];
if exchange == b'0' || exchange == b'1' {
return Err("invalid exchange");
}
Ok(d)
}
/// Version 2: Using and_then chain (more functional)
pub fn validate_chain(s: &str) -> Result<String, &'static str> {
let d = digits_only(s);
normalize_length(d)
.and_then(check_area_code)
.and_then(check_exchange)
}
fn normalize_length(d: String) -> Result<String, &'static str> {
match d.len() {
11 if d.starts_with('1') => Ok(d[1..].to_string()),
10 => Ok(d),
_ => Err("wrong number of digits"),
}
}
fn check_area_code(d: String) -> Result<String, &'static str> {
if d.as_bytes()[0] == b'0' || d.as_bytes()[0] == b'1' {
Err("invalid area code")
} else {
Ok(d)
}
}
fn check_exchange(d: String) -> Result<String, &'static str> {
if d.as_bytes()[3] == b'0' || d.as_bytes()[3] == b'1' {
Err("invalid exchange")
} else {
Ok(d)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_10_digit() {
assert_eq!(validate("(223) 456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_valid_11_digit() {
assert_eq!(validate("1-223-456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_invalid_area_code() {
assert_eq!(validate("(023) 456-7890"), Err("invalid area code"));
}
#[test]
fn test_invalid_exchange() {
assert_eq!(validate("(223) 056-7890"), Err("invalid exchange"));
}
#[test]
fn test_wrong_length() {
assert_eq!(validate("123"), Err("wrong number of digits"));
}
#[test]
fn test_chain_version() {
assert_eq!(validate_chain("(223) 456-7890"), Ok("2234567890".into()));
assert_eq!(validate_chain("(023) 456-7890"), Err("invalid area code"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_10_digit() {
assert_eq!(validate("(223) 456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_valid_11_digit() {
assert_eq!(validate("1-223-456-7890"), Ok("2234567890".into()));
}
#[test]
fn test_invalid_area_code() {
assert_eq!(validate("(023) 456-7890"), Err("invalid area code"));
}
#[test]
fn test_invalid_exchange() {
assert_eq!(validate("(223) 056-7890"), Err("invalid exchange"));
}
#[test]
fn test_wrong_length() {
assert_eq!(validate("123"), Err("wrong number of digits"));
}
#[test]
fn test_chain_version() {
assert_eq!(validate_chain("(223) 456-7890"), Ok("2234567890".into()));
assert_eq!(validate_chain("(023) 456-7890"), Err("invalid area code"));
}
}
Deep Comparison
Phone Number Parser — Comparison
Core Insight
Validation pipelines demonstrate monadic chaining with Result. Both OCaml (Result.bind) and Rust (.and_then()) thread a value through a series of checks, short-circuiting on the first error.
OCaml Approach
Result.bind chains validations: Ok x |> Result.bind f1 |> Result.bind f2String.to_seq |> Seq.filter |> String.of_seq for digit extractiond.[0] for character accessError "message" for error variantsRust Approach
.and_then(f) chains validations (same as Result.bind).chars().filter().collect() for digit extractiond.as_bytes()[0] for byte access, d.starts_with() for prefixErr("message") with static string slicesComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Bind | Result.bind | .and_then() |
| Filter chars | Seq.filter | .chars().filter() |
| String access | s.[i] (char) | s.as_bytes()[i] (u8) |
| Error type | string | &'static str |
| Substring | String.sub d 1 10 | d[1..].to_string() |
Learner Notes
? operator is syntactic sugar for and_then in many casesand_then passes ownership of the Ok value to the next function|> pipe and Rust's method chaining serve the same purposeExercises
format_number(s: &str) -> Result<String, &'static str> function that returns the number formatted as (NXX) NXX-XXXX.111-111-1111).Result<String, Vec<&'static str>> and accumulating failures.? in a helper function instead of and_then chaining, and compare readability.let* syntax (monadic bind with ppx_let or Result.bind desugared) to write the validation in do-notation style.