1023-parse-int-safe — Safe Integer Parsing
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
str::parse::<i64>() and handle ParseIntErrormap_errunwrap_or and unwrap_or_else for safe defaultsParseIntError 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
ParseIntError has a kind() method for precise failure categorisation; OCaml returns None with no error detail.ParseIntError::PosOverflow variant; OCaml's int_of_string raises on overflow.?**: Rust composes parse + validation with ? in a linear style; OCaml uses let* or manual match.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));
}
}#[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, avoidint_of_string_opt returns option — safe, preferredRust Approach
str::parse::<i64>() returns Result<i64, ParseIntError>ParseIntError has descriptive messages.map_err() for custom errorsunwrap_or(default) for quick defaultsComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| 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) |
| Whitespace | Trimmed automatically | NOT trimmed — explicit .trim() |
| Overflow | Platform-dependent | Returns Err |
Exercises
parse_hex(s: &str) -> Result<i64, String> function that parses a hexadecimal string like "0xFF" or "ff".parse_list(s: &str) -> Result<Vec<i64>, String> that parses a comma-separated string of integers and collects errors.i64, then f64, then bool, returning the first successful parse as a boxed value.