Skip to content

10. Parse numbers from input

Parse the quantity of an order line from a text field. A quantity is a whole number from 1 to 65,535, stored as a u16.

  1. WorseBad text becomes 0, and as u16 silently wraps: "-1" becomes 65535, "70000" becomes 4464.

    pub fn parse_quantity(field: &str) -> u16 {
    let n: i64 = field.trim().parse().unwrap_or(0);
    n as u16
    }
  2. OKtry_from rejects values out of range, and zero is rejected. But None does not say why.

    pub fn parse_quantity(field: &str) -> Option<u16> {
    let n: i64 = field.trim().parse().ok()?;
    let quantity = u16::try_from(n).ok()?; // None if n is out of range
    (quantity > 0).then_some(quantity)
    }
  3. BetterParse directly as u16, with a typed error that keeps the cause.

    #[derive(Debug, PartialEq, Eq, thiserror::Error)]
    pub enum QuantityError {
    #[error("quantity is not a whole number from 1 to 65535")]
    Invalid(#[from] ParseIntError),
    #[error("quantity must be at least 1")]
    Zero,
    }
    pub fn parse_quantity(field: &str) -> Result<u16, QuantityError> {
    let quantity: u16 = field.trim().parse()?; // rejects "-1", "70000" and "abc"
    if quantity == 0 {
    return Err(QuantityError::Zero);
    }
    Ok(quantity)
    }

as converts between number types without any check. When the value does not fit, it does not fail. It keeps the low bits:

Input “Worse” returns Why
"12" 12 Correct.
"-1" 65535 -1 as an i64 is all one bits. The low 16 bits are 65535.
"70000" 4464 70000 minus 65536.
"twelve" 0 unwrap_or(0) hides the parse error.

Every one of these values goes into an order. Nothing in the logs says that anything went wrong. The tests in examples/better-vs-worse/src/e10_parse_numbers.rs prove each row. See S23: as casts that can truncate or change sign.

  • u16::try_from(n) returns Err when n does not fit. It is the Rust version of Math.toIntExact.
  • "70000".parse::<u16>() parses directly into the target type, and fails for anything outside its range, including negative numbers. There is no intermediate i64, so there is nothing to convert.
  • The ParseIntError has a kind(): InvalidDigit, PosOverflow, Empty, and others. The “Better” version keeps it as the cause, so a caller or a log can say exactly what was wrong.
Each version compared with the Worse version
CriterionOKBetter
Correctness Better Better
Both reject every input that is not a valid quantity. "Worse" accepts all of them.
Failure behavior Better Better
"OK" says "no". "Better" says why: not a number, too large, or zero.
Performance Same Same
The same parsing work in all three.
Readability Better Better
The range check is visible in "OK", and in the type in "Better".
API ergonomics Same Better
A Result with a typed error works with ? and with error reports.
  • as is correct where the value always fits: u8 to u32, u16 to usize, char to u32. Clippy’s pedantic lints (cast_possible_truncation, cast_sign_loss, cast_possible_wrap) report the casts that can lose data, and leave these alone. For widening conversions, u32::from(x) or x.into() says “this cannot fail” more clearly.
  • as from float to integer saturates (stable since Rust 1.45): 300.7_f64 as u8 is 255, and NaN becomes 0. It never wraps, but it still loses data silently.
  • “OK” is fine when the caller only needs to know “valid or not”, for example to show a red border on a form field.
  • Wrapping can be intended, for example in hash functions. Then use wrapping_add and similar methods, which say so.