ExamplesBy LevelBy TopicLearning Paths
084 Fundamental

084 — Phone Number Validation

Functional Programming

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

  • • Use .chars().filter(…).collect() to extract digits from a string
  • • Chain and_then on Result to build a validation pipeline without nested match
  • • Use byte indexing (d.as_bytes()[0]) for single-byte ASCII comparisons
  • • Understand &'static str as the error type for string literals
  • • Compose small single-purpose validation functions for testability
  • • Map Rust's and_then to OCaml's Result.bind operator
  • Code 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

    AspectRustOCaml
    Error type&'static strstring
    Chaining.and_then(f)|> Result.bind (fun d -> ...)
    Digit filter.chars().filter(…).collect()String.to_seq |> Seq.filter |> String.of_seq
    Byte indexingd.as_bytes()[0]d.[0] (char indexing)
    Early returnreturn Err(…)Pattern on prev Result
    Pipeline styleMethod chainPipe 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"));
        }
    }
    ✓ Tests Rust test suite
    #[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 f2
  • String.to_seq |> Seq.filter |> String.of_seq for digit extraction
  • d.[0] for character access
  • Error "message" for error variants
  • Rust Approach

  • .and_then(f) chains validations (same as Result.bind)
  • .chars().filter().collect() for digit extraction
  • d.as_bytes()[0] for byte access, d.starts_with() for prefix
  • Err("message") with static string slices
  • Comparison Table

    AspectOCamlRust
    BindResult.bind.and_then()
    Filter charsSeq.filter.chars().filter()
    String accesss.[i] (char)s.as_bytes()[i] (u8)
    Error typestring&'static str
    SubstringString.sub d 1 10d[1..].to_string()

    Learner Notes

  • • Rust's ? operator is syntactic sugar for and_then in many cases
  • and_then passes ownership of the Ok value to the next function
  • • OCaml's |> pipe and Rust's method chaining serve the same purpose
  • • Both approaches avoid nested if/else by linearizing validation steps
  • Exercises

  • Add a format_number(s: &str) -> Result<String, &'static str> function that returns the number formatted as (NXX) NXX-XXXX.
  • Extend validation to reject numbers with all the same digit (e.g. 111-111-1111).
  • Collect all errors (not just the first) by returning Result<String, Vec<&'static str>> and accumulating failures.
  • Write a version using ? in a helper function instead of and_then chaining, and compare readability.
  • In OCaml, use the let* syntax (monadic bind with ppx_let or Result.bind desugared) to write the validation in do-notation style.
  • Open Source Repos