10. Parse numbers from input
The task
Section titled “The task”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.
The versions
Section titled “The versions”WorseBad text becomes 0, and
as u16silently 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}OK
try_fromrejects values out of range, and zero is rejected. ButNonedoes 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)}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 does not check
Section titled “as does not check”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.
try_from and parse do check
Section titled “try_from and parse do check”u16::try_from(n)returnsErrwhenndoes not fit. It is the Rust version ofMath.toIntExact."70000".parse::<u16>()parses directly into the target type, and fails for anything outside its range, including negative numbers. There is no intermediatei64, so there is nothing to convert.- The
ParseIntErrorhas akind():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.
Verdict
Section titled “Verdict”| Criterion | OK | Better |
|---|---|---|
| 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. | ||
Trade-offs
Section titled “Trade-offs”asis correct where the value always fits:u8tou32,u16tousize,chartou32. 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)orx.into()says “this cannot fail” more clearly.asfrom float to integer saturates (stable since Rust 1.45):300.7_f64 as u8is 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_addand similar methods, which say so.