5.6Parsing strings into numbers
Since lesson 1.12, every interactive program in this course has leaned on one magic line:
let num: i32 = input.trim().parse().expect("that wasn't a whole number");
You were told to copy it faithfully and promised a full teardown "in lesson 5.6." This is that lesson, and the satisfying part is how little is left to teach: the chapter has quietly dismantled the incantation piece by piece, and all that remains is to lay the parts on the table.
The teardown
input is a String (lesson 5.3): owned, growable, and holding whatever the user typed, Enter included, because read_line keeps the newline (lesson 1.6).
.trim() returns a &str (lesson 5.4): a view into the same text with the whitespace and that newline skipped, no copying involved.
.parse() is the one new part, and it's the border crossing lesson 5.3 flagged: it reads text and produces the value the text depicts. "42" goes in, 42 comes out.
.expect(...) handles the possibility that the text depicts no number at all. More on it below, because the user is owed a demonstration of their power to type potato.
let num: i32 you've understood since lesson 4.8: parse can target many types, so something must pick one, and the annotation is the evidence inference needs. Delete it and the compiler asks for "type annotations needed," exactly as 4.8 showed.
The turbofish
Lesson 4.8 also promised a second way to answer the which-type question, "with a syntax the community has nicknamed the turbofish for its silhouette." Behold:
fn main() {
let typed = "42";
let n = typed.parse::<i32>().expect("not a number");
println!("{}", n + 1);
}43
parse::<i32>() tells parse its target type directly, right at the call, instead of routing the information through the variable's annotation. Tilt your head: ::<> is a fish, allegedly turbocharged. The two spellings do the same thing, and the choice is style: the annotation reads better when the variable was getting a type anyway; the turbofish shines when the parsed value is used immediately, with no variable to annotate. You'll see both in the wild, and now neither can surprise you.
Floats parse the same way, which the chapter 4 calculator already exploited (changing one annotation made the recipe read an f64):
fn main() {
let measurement = "98.6".parse::<f64>().expect("not a number");
println!("{measurement:.1}");
}98.6When the user types potato
parse is a fallible operation: the text might not depict a number, and Rust won't pretend otherwise. What parse actually returns is a value that says "here's the number" or "here's what went wrong," and expect is the blunt instrument that unwraps it: hand over the number, or stop the whole program and print my message. The or-value is chapter 12's grown-up error handling story (where stopping the program stops being acceptable); until then, expect remains the standard move, and now you get to see it earn its keep:
use std::io;
fn main() {
println!("How many cookies?");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("failed to read input");
let cookies: i32 = input.trim().parse().expect("that wasn't a whole number");
println!("{cookies} cookies coming up.");
}
Run it and type potato:
How many cookies?
potato
thread 'main' (2124427) panicked at src/main.rs:11:45:
that wasn't a whole number: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
A panic, anatomy per lesson 3.1: your expect message at the crash site, plus the underlying cause in debug view ({:?} formatting, recognizable by yesterday's lesson). InvalidDigit means exactly what it says: parse met a character that can't be part of an i32.
It's worth knowing how strict the border is. For an i32, all of these panic: potato, 7.5 (a dot is not a digit an integer can contain), 4 2 (inner spaces don't trim away, lesson 1.6 warned you), and even an empty line (ParseIntError { kind: Empty }). A minus sign is fine for i32 but InvalidDigit for a u32, the never-negative types having opinions about -7. And parse accepts only a clean number with optional sign: no $, no commas, no trailing cookies please.
Warning
The most common parse panic in beginner code isn't potato; it's forgetting trim. read_line keeps the Enter, "42\n" contains a character that isn't a digit, and InvalidDigit falls out of an input that looks perfect on screen. If a parse fails on input you'd swear is a number, print it with {:?} first (lesson 5.5): the debug view shows the \n that the normal view politely hides.
The idiom, fully assembled
One more upgrade and the recipe reaches its final form. The raw input and the parsed number are one concept in two forms, which is lesson 5.2's cue exactly, shadow straight through the type change:
use std::io;
fn main() {
println!("How old are you?");
let mut age = String::new();
io::stdin()
.read_line(&mut age)
.expect("failed to read input");
let age: u32 = age.trim().parse().expect("that wasn't a whole number");
println!("Next year you'll be {}.", age + 1);
}
age the String becomes age the u32, the stringly version drops safely out of reach, and every line reads as English. This exact shape (read, then let name: type = name.trim().parse().expect(...)) is how the rest of this course reads numbers, and as of this lesson, every word of it is yours. The packaged version from lesson 2.2, a read_number() function with the recipe inside, stays the right call the moment a program needs two or more numbers (the chapter 4 quiz's calculator owes its tidiness to it).
Quiz time
Question #1
For each input typed at the prompt, does let n: i32 = input.trim().parse().expect("no"); produce a value or a panic? If a value, which?
a) 42 (spaces around it)
b) -7
c) 3.0
d) 12cookies
Show solution
a) The value 42: trim eats the outer spaces and the newline, leaving a clean number.
b) The value -7: a leading sign is part of a valid i32. (The same input panics if the target were u32.)
c) A panic, InvalidDigit: the dot isn't valid in an integer, and parse targeting i32 won't round, truncate, or negotiate. Parsing 3.0 needs an f64 target.
d) A panic, InvalidDigit: parse takes the whole trimmed text or nothing; trailing words aren't ignored.
Question #2
Rewrite this fragment to use the turbofish instead of the annotation, leaving the behavior identical:
let lives: u32 = "3".parse().expect("not a number");Show solution
let lives = "3".parse::<u32>().expect("not a number");
The type information moves from the variable to the call. Inference then types lives as u32 from the evidence on the right, per lesson 4.8.
Question #3
Write a temperature converter: ask for a temperature in Celsius (decimals allowed), and print the Fahrenheit equivalent to one decimal place. The formula is F = C × 9/5 + 32. A sample run:
Temperature in Celsius?
36.6
36.6 C is 97.9 F
(Careful with the formula's 9/5: lesson 4.2 taught you what integer division would do to it.)
Show solution
use std::io;
fn main() {
println!("Temperature in Celsius?");
let mut celsius = String::new();
io::stdin()
.read_line(&mut celsius)
.expect("failed to read input");
let celsius: f64 = celsius.trim().parse().expect("that wasn't a number");
let fahrenheit = celsius * 9.0 / 5.0 + 32.0;
println!("{celsius} C is {fahrenheit:.1} F");
}
The recipe with an f64 target, the shadowing idiom from this lesson, and {:.1} from lesson 5.5 keeping the output civilized. Writing 9.0 / 5.0 keeps the arithmetic in float country; 9 / 5 would be integer division, quietly computing 1, and producing conversions that are wrong by exactly enough to be embarrassing.
That closes the chapter's syllabus: constants, shadowing, both string types, formatting out, parsing in. The summary and quiz will make you use all of it at once, which is, as always, the point.