ExamplesBy LevelBy TopicLearning Paths
1023 Intermediate

1023-parse-int-safe — Safe Integer Parsing

Functional Programming

Tutorial

The Problem

Parsing integers from strings is the entry point for untrusted data in almost every application: reading configuration files, processing HTTP query parameters, deserializing CSV rows. C's atoi silently returns 0 on failure. C++'s std::stoi throws an exception. Python's int() raises a ValueError. Rust's str::parse::<i64>() returns Result<i64, ParseIntError>, forcing the caller to handle the failure case.

This example explores all the variants: raw parsing, custom error messages, range validation, and default fallbacks — covering the full spectrum of real-world needs.

🎯 Learning Outcomes

  • • Use str::parse::<i64>() and handle ParseIntError
  • • Add descriptive error context with map_err
  • • Chain parsing with domain validation (positive, in-range)
  • • Use unwrap_or and unwrap_or_else for safe defaults
  • • Know the difference between ParseIntError kinds (empty, invalid digit, overflow)
  • Code Example

    #![allow(clippy::all)]
    // 1023: Safe Integer Parsing
    // str::parse::<i64>() and handling ParseIntError
    
    use std::num::ParseIntError;
    
    // Approach 1: Basic parse with Result
    fn parse_int(s: &str) -> Result<i64, ParseIntError> {
        s.parse::<i64>()
    }
    
    // Approach 2: Parse with custom error message
    fn parse_int_msg(s: &str) -> Result<i64, String> {
        s.parse::<i64>()
            .map_err(|e| format!("cannot parse '{}' as integer: {}", s, e))
    }
    
    // Approach 3: Parse with validation
    fn parse_positive(s: &str) -> Result<i64, String> {
        let n: i64 = s.parse().map_err(|_| format!("not a number: {}", s))?;
        if n < 0 {
            Err(format!("negative: {}", n))
        } else {
            Ok(n)
        }
    }
    
    fn parse_in_range(s: &str, min: i64, max: i64) -> Result<i64, String> {
        let n: i64 = s.parse().map_err(|_| format!("not a number: {}", s))?;
        if n < min {
            Err(format!("{} < min({})", n, min))
        } else if n > max {
            Err(format!("{} > max({})", n, max))
        } else {
            Ok(n)
        }
    }
    
    // Parse with default (Option-based)
    fn parse_or_default(s: &str, default: i64) -> i64 {
        s.parse::<i64>().unwrap_or(default)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_basic_parse() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-17"), Ok(-17));
            assert_eq!(parse_int("0"), Ok(0));
        }
    
        #[test]
        fn test_parse_errors() {
            assert!(parse_int("abc").is_err());
            assert!(parse_int("").is_err());
            assert!(parse_int("12.5").is_err()); // no floats
            assert!(parse_int("99999999999999999999").is_err()); // overflow
        }
    
        #[test]
        fn test_parse_with_message() {
            let err = parse_int_msg("abc").unwrap_err();
            assert!(err.contains("cannot parse"));
            assert!(err.contains("abc"));
        }
    
        #[test]
        fn test_parse_positive() {
            assert_eq!(parse_positive("42"), Ok(42));
            assert_eq!(parse_positive("0"), Ok(0));
            assert!(parse_positive("-5").unwrap_err().contains("negative"));
            assert!(parse_positive("xyz").unwrap_err().contains("not a number"));
        }
    
        #[test]
        fn test_parse_in_range() {
            assert_eq!(parse_in_range("50", 1, 100), Ok(50));
            assert_eq!(parse_in_range("1", 1, 100), Ok(1));
            assert_eq!(parse_in_range("100", 1, 100), Ok(100));
            assert!(parse_in_range("0", 1, 100).is_err());
            assert!(parse_in_range("101", 1, 100).is_err());
            assert!(parse_in_range("abc", 1, 100).is_err());
        }
    
        #[test]
        fn test_parse_or_default() {
            assert_eq!(parse_or_default("42", 0), 42);
            assert_eq!(parse_or_default("abc", 0), 0);
            assert_eq!(parse_or_default("", -1), -1);
        }
    
        #[test]
        fn test_parse_int_error_kind() {
            // ParseIntError has useful information
            let err = "abc".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "invalid digit found in string");
    
            let err = "".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "cannot parse integer from empty string");
        }
    
        #[test]
        fn test_whitespace_handling() {
            // Rust's parse does NOT trim whitespace
            assert!(parse_int(" 42").is_err());
            assert!(parse_int("42 ").is_err());
            // Trim first if needed
            assert_eq!(" 42 ".trim().parse::<i64>(), Ok(42));
        }
    }

    Key Differences

  • Error type richness: Rust's ParseIntError has a kind() method for precise failure categorisation; OCaml returns None with no error detail.
  • Overflow detection: Rust detects overflow as a distinct ParseIntError::PosOverflow variant; OCaml's int_of_string raises on overflow.
  • **Composition with ?**: Rust composes parse + validation with ? in a linear style; OCaml uses let* or manual match.
  • Default handling: Rust's unwrap_or_else is a method on Result; OCaml uses Option.value ~default: for the option equivalent.
  • OCaml Approach

    OCaml's int_of_string_opt returns option int:

    let parse_int s =
      match int_of_string_opt s with
      | None -> Error (Printf.sprintf "cannot parse '%s' as integer" s)
      | Some n -> Ok n
    
    let parse_positive s =
      let* n = parse_int s in
      if n > 0 then Ok n
      else Error (Printf.sprintf "not positive: %d" n)
    

    The int_of_string function (without _opt) raises Failure on invalid input, which is the exception-based alternative.

    Full Source

    #![allow(clippy::all)]
    // 1023: Safe Integer Parsing
    // str::parse::<i64>() and handling ParseIntError
    
    use std::num::ParseIntError;
    
    // Approach 1: Basic parse with Result
    fn parse_int(s: &str) -> Result<i64, ParseIntError> {
        s.parse::<i64>()
    }
    
    // Approach 2: Parse with custom error message
    fn parse_int_msg(s: &str) -> Result<i64, String> {
        s.parse::<i64>()
            .map_err(|e| format!("cannot parse '{}' as integer: {}", s, e))
    }
    
    // Approach 3: Parse with validation
    fn parse_positive(s: &str) -> Result<i64, String> {
        let n: i64 = s.parse().map_err(|_| format!("not a number: {}", s))?;
        if n < 0 {
            Err(format!("negative: {}", n))
        } else {
            Ok(n)
        }
    }
    
    fn parse_in_range(s: &str, min: i64, max: i64) -> Result<i64, String> {
        let n: i64 = s.parse().map_err(|_| format!("not a number: {}", s))?;
        if n < min {
            Err(format!("{} < min({})", n, min))
        } else if n > max {
            Err(format!("{} > max({})", n, max))
        } else {
            Ok(n)
        }
    }
    
    // Parse with default (Option-based)
    fn parse_or_default(s: &str, default: i64) -> i64 {
        s.parse::<i64>().unwrap_or(default)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_basic_parse() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-17"), Ok(-17));
            assert_eq!(parse_int("0"), Ok(0));
        }
    
        #[test]
        fn test_parse_errors() {
            assert!(parse_int("abc").is_err());
            assert!(parse_int("").is_err());
            assert!(parse_int("12.5").is_err()); // no floats
            assert!(parse_int("99999999999999999999").is_err()); // overflow
        }
    
        #[test]
        fn test_parse_with_message() {
            let err = parse_int_msg("abc").unwrap_err();
            assert!(err.contains("cannot parse"));
            assert!(err.contains("abc"));
        }
    
        #[test]
        fn test_parse_positive() {
            assert_eq!(parse_positive("42"), Ok(42));
            assert_eq!(parse_positive("0"), Ok(0));
            assert!(parse_positive("-5").unwrap_err().contains("negative"));
            assert!(parse_positive("xyz").unwrap_err().contains("not a number"));
        }
    
        #[test]
        fn test_parse_in_range() {
            assert_eq!(parse_in_range("50", 1, 100), Ok(50));
            assert_eq!(parse_in_range("1", 1, 100), Ok(1));
            assert_eq!(parse_in_range("100", 1, 100), Ok(100));
            assert!(parse_in_range("0", 1, 100).is_err());
            assert!(parse_in_range("101", 1, 100).is_err());
            assert!(parse_in_range("abc", 1, 100).is_err());
        }
    
        #[test]
        fn test_parse_or_default() {
            assert_eq!(parse_or_default("42", 0), 42);
            assert_eq!(parse_or_default("abc", 0), 0);
            assert_eq!(parse_or_default("", -1), -1);
        }
    
        #[test]
        fn test_parse_int_error_kind() {
            // ParseIntError has useful information
            let err = "abc".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "invalid digit found in string");
    
            let err = "".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "cannot parse integer from empty string");
        }
    
        #[test]
        fn test_whitespace_handling() {
            // Rust's parse does NOT trim whitespace
            assert!(parse_int(" 42").is_err());
            assert!(parse_int("42 ").is_err());
            // Trim first if needed
            assert_eq!(" 42 ".trim().parse::<i64>(), Ok(42));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_basic_parse() {
            assert_eq!(parse_int("42"), Ok(42));
            assert_eq!(parse_int("-17"), Ok(-17));
            assert_eq!(parse_int("0"), Ok(0));
        }
    
        #[test]
        fn test_parse_errors() {
            assert!(parse_int("abc").is_err());
            assert!(parse_int("").is_err());
            assert!(parse_int("12.5").is_err()); // no floats
            assert!(parse_int("99999999999999999999").is_err()); // overflow
        }
    
        #[test]
        fn test_parse_with_message() {
            let err = parse_int_msg("abc").unwrap_err();
            assert!(err.contains("cannot parse"));
            assert!(err.contains("abc"));
        }
    
        #[test]
        fn test_parse_positive() {
            assert_eq!(parse_positive("42"), Ok(42));
            assert_eq!(parse_positive("0"), Ok(0));
            assert!(parse_positive("-5").unwrap_err().contains("negative"));
            assert!(parse_positive("xyz").unwrap_err().contains("not a number"));
        }
    
        #[test]
        fn test_parse_in_range() {
            assert_eq!(parse_in_range("50", 1, 100), Ok(50));
            assert_eq!(parse_in_range("1", 1, 100), Ok(1));
            assert_eq!(parse_in_range("100", 1, 100), Ok(100));
            assert!(parse_in_range("0", 1, 100).is_err());
            assert!(parse_in_range("101", 1, 100).is_err());
            assert!(parse_in_range("abc", 1, 100).is_err());
        }
    
        #[test]
        fn test_parse_or_default() {
            assert_eq!(parse_or_default("42", 0), 42);
            assert_eq!(parse_or_default("abc", 0), 0);
            assert_eq!(parse_or_default("", -1), -1);
        }
    
        #[test]
        fn test_parse_int_error_kind() {
            // ParseIntError has useful information
            let err = "abc".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "invalid digit found in string");
    
            let err = "".parse::<i64>().unwrap_err();
            assert_eq!(err.to_string(), "cannot parse integer from empty string");
        }
    
        #[test]
        fn test_whitespace_handling() {
            // Rust's parse does NOT trim whitespace
            assert!(parse_int(" 42").is_err());
            assert!(parse_int("42 ").is_err());
            // Trim first if needed
            assert_eq!(" 42 ".trim().parse::<i64>(), Ok(42));
        }
    }

    Deep Comparison

    Safe Integer Parsing — Comparison

    Core Insight

    Both languages evolved from exception-based parsing to Result/Option-based. The safe versions are now idiomatic in both.

    OCaml Approach

  • int_of_string raises Failure — old style, avoid
  • int_of_string_opt returns option — safe, preferred
  • • No built-in range validation — wrap manually
  • Rust Approach

  • str::parse::<i64>() returns Result<i64, ParseIntError>
  • ParseIntError has descriptive messages
  • • Chain with .map_err() for custom errors
  • unwrap_or(default) for quick defaults
  • Comparison Table

    AspectOCamlRust
    Safe parseint_of_string_optstr::parse::<i64>()
    Unsafe parseint_of_string (exception)No equivalent (always safe)
    Error typeNone (option)ParseIntError (descriptive)
    Default valueOption.value ~default.unwrap_or(default)
    WhitespaceTrimmed automaticallyNOT trimmed — explicit .trim()
    OverflowPlatform-dependentReturns Err

    Exercises

  • Write a parse_hex(s: &str) -> Result<i64, String> function that parses a hexadecimal string like "0xFF" or "ff".
  • Implement parse_list(s: &str) -> Result<Vec<i64>, String> that parses a comma-separated string of integers and collects errors.
  • Write a function that attempts to parse a string as i64, then f64, then bool, returning the first successful parse as a boxed value.
  • Open Source Repos