๐Ÿฆ€ Functional Rust

1023: Safe Integer Parsing

Difficulty: Beginner Category: Error Handling Concept: Safe string-to-integer parsing with `str::parse::<i64>()` and `ParseIntError` Key Insight: `parse()` returns `Result<T, ParseIntError>` โ€” no exceptions, no sentinel values. Chain with `map_err` for custom messages or `?` for propagation.
// 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)
}

fn main() {
    println!("parse '42': {:?}", parse_int("42"));
    println!("parse 'abc': {:?}", parse_int("abc"));
    println!("parse_positive '-5': {:?}", parse_positive("-5"));
    println!("parse_in_range '50' [1,100]: {:?}", parse_in_range("50", 1, 100));
    println!("Run `cargo test` to verify all examples.");
}

#[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));
    }
}
(* 1023: Safe Integer Parsing *)
(* OCaml int_of_string_opt vs exception-based parsing *)

(* Approach 1: Exception-based (old style) *)
let parse_exn s =
  try int_of_string s
  with Failure _ -> 0  (* silent default โ€” BAD *)

(* Approach 2: Option-based (safe) *)
let parse_opt s = int_of_string_opt s

let parse_or_default default s =
  match int_of_string_opt s with
  | Some n -> n
  | None -> default

(* Approach 3: Result-based with error message *)
let parse_result s =
  match int_of_string_opt s with
  | Some n -> Ok n
  | None -> Error (Printf.sprintf "cannot parse '%s' as integer" s)

let parse_positive s =
  match int_of_string_opt s with
  | None -> Error (Printf.sprintf "not a number: %s" s)
  | Some n when n < 0 -> Error (Printf.sprintf "negative: %d" n)
  | Some n -> Ok n

(* Parse with range validation *)
let parse_in_range ~min ~max s =
  match int_of_string_opt s with
  | None -> Error (Printf.sprintf "not a number: %s" s)
  | Some n when n < min -> Error (Printf.sprintf "%d < min(%d)" n min)
  | Some n when n > max -> Error (Printf.sprintf "%d > max(%d)" n max)
  | Some n -> Ok n

let test_exception () =
  assert (parse_exn "42" = 42);
  assert (parse_exn "abc" = 0);  (* silent failure! *)
  Printf.printf "  Approach 1 (exception, unsafe): passed\n"

let test_option () =
  assert (parse_opt "42" = Some 42);
  assert (parse_opt "abc" = None);
  assert (parse_opt "" = None);
  assert (parse_or_default 0 "abc" = 0);
  assert (parse_or_default 0 "42" = 42);
  Printf.printf "  Approach 2 (option): passed\n"

let test_result () =
  assert (parse_result "42" = Ok 42);
  (match parse_result "abc" with Error _ -> () | Ok _ -> assert false);
  assert (parse_positive "42" = Ok 42);
  assert (parse_positive "-5" = Error "negative: -5");
  assert (parse_in_range ~min:1 ~max:100 "50" = Ok 50);
  assert (parse_in_range ~min:1 ~max:100 "0" |> Result.is_error);
  assert (parse_in_range ~min:1 ~max:100 "101" |> Result.is_error);
  Printf.printf "  Approach 3 (result): passed\n"

let () =
  Printf.printf "Testing safe integer parsing:\n";
  test_exception ();
  test_option ();
  test_result ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed 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 parse`int_of_string_opt``str::parse::<i64>()`
Unsafe parse`int_of_string` (exception)No equivalent (always safe)
Error type`None` (option)`ParseIntError` (descriptive)
Default value`Option.value ~default``.unwrap_or(default)`
WhitespaceTrimmed automaticallyNOT trimmed โ€” explicit `.trim()`
OverflowPlatform-dependentReturns `Err`