Splitting and parsing text
Text arrives as one string and the program needs fields, words or numbers, and every step of that conversion can fail on input the program did not write. Rust splits with split and split_whitespace, converts with parse, and both return iterators or results that the caller has to handle. The same machinery parses a CSV line, a command line and a number typed at a prompt.
The lesson covers the split, the parse with its error, the whitespace forms and a custom FromStr implementation, which is what makes "3,4".parse() work for your own type. The last section is the compiler asking for the target type of a parse.
Splitting a line
split(',') returns an iterator of slices between the separators. Collecting it gives a vector of borrows from the original line, so nothing is copied and the fields cannot outlive the line.
The program splits a three-field line and prints the vector and its length. The fields are the parts between the two commas.
fn main() {
let line = "ada,36,maths";
let fields: Vec<&str> = line.split(',').collect();
println!("{fields:?}");
println!("{}", fields.len());
}
rustc --edition 2024 split.rs
./split
["ada", "36", "maths"]
3
The split takes a pattern, so a string, a character or a closure that matches several separators all work. The result is lazy, which the lesson on adaptors explains, and collecting is what allocates. A line with a trailing comma would produce an empty last field, and a line with two in a row an empty middle one. The parser must decide whether those are errors, which is the subject of CSV parsing.
Parsing a number
parse converts a string slice into any type that implements FromStr, and returns a Result. The error carries the reason, which is why a bad field can be reported instead of crashing.
The program parses a good number and asks whether a bad one failed. The first prints forty-three after the addition, the second prints true.
fn main() {
let text = "42";
let number: i32 = text.parse().unwrap();
println!("{}", number + 1);
let bad: Result<i32, _> = "x".parse();
println!("{:?}", bad.is_err());
}
rustc --edition 2024 parse.rs
./parse
43
true
The result is handled with unwrap in the good case and with is_err in the bad one. The lesson on Result shows the question mark for a function that propagates the failure.
Words and whitespace
split_whitespace treats any run of spaces, tabs and newlines as one separator, which is what a line of words needs. trim removes the whitespace at both ends before a length or a comparison.
The program collects the words of a padded line and prints the trimmed length. The leading and trailing spaces do not become empty words.
fn main() {
let line = " alpha beta gamma ";
let words: Vec<&str> = line.split_whitespace().collect();
println!("{words:?}");
println!("{}", line.trim().len());
}
rustc --edition 2024 words.rs
./words
["alpha", "beta", "gamma"]
19
The trim reports nineteen, which is the three words and the two spaces between them. The lesson on string slices explains why the number is bytes.
FromStr for your type
Implementing FromStr gives a type the parse method. The trait has one function and one error type, and the body does the splitting and the field conversions, turning each failure into the error.
The program parses a point from one comma-separated string and prints both coordinates. The error type here is a string, which is enough for a message.
use std::str::FromStr;
struct Point {
x: i32,
y: i32,
}
impl FromStr for Point {
type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let (x, y) = text.split_once(',').ok_or("missing comma")?;
let x = x.trim().parse().map_err(|_| "bad x")?;
let y = y.trim().parse().map_err(|_| "bad y")?;
Ok(Point { x, y })
}
}
fn main() {
let point: Point = "3, 4".parse().unwrap();
println!("{} {}", point.x, point.y);
}
rustc --edition 2024 fromstr.rs
./fromstr
3 4
The same trait is behind "42".parse::<i32>(), which is why the standard numbers already have it. The custom error type from the lesson on error types replaces the string.
A parse without a type
parse is generic over the result, so the compiler needs to know which type to build. A binding with no annotation and an immediate print leaves that open, and the error is E0284.
The program parses a number and prints it without a type. The help asks for an annotation or a turbofish, and either one fixes the call.
fn main() {
let number = "42".parse().unwrap();
println!("{number}");
}
rustc --edition 2024 annotation_err.rs
error[E0284]: type annotations needed
--> annotation_err.rs:2:9
|
2 | let number = "42".parse().unwrap();
| ^^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `number` an explicit type
|
2 | let number: /* Type */ = "42".parse().unwrap();
| ++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0284`.
A turbofish such as parse::<i32>() states the type at the call and is the shortest fix when an annotation would be far from the parse. The same ambiguity appears with collect and sum, which need to know the container or the number. The turbofish is the shortest way to say it.
Exercises
- Parse a line of three comma-separated numbers and print their sum.
- Count the words of a padded sentence with split_whitespace and with split on spaces.
- Implement FromStr for a colour written as three numbers separated by slashes.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
splitreturn?Answer
An iterator of slices
The parts are slices borrowed from the original, and collecting is a separate step.
-
What does
parsereturn?Answer
A Result with the value or the error
The conversion may fail, so the result is a Result whose Err says why.
-
What does
split_whitespacetreat as a separator?Answer
Any run of whitespace
Runs of spaces, tabs and newlines count as one separator.
-
What does implementing FromStr give a type?
Answer
The parse method
parse is generic over FromStr, so the implementation enables the call.
-
What does this program print?
fn main() { let line = "ada,36,maths"; let fields: Vec<&str> = line.split(',').collect(); println!("{:?}", fields); }Answer
["ada", "36", "maths"]
The line is split on the two commas into three slices.
-
What does this program print?
fn main() { let text = "42"; let number: i32 = text.parse().unwrap(); println!("{}", number + 1); }Answer
43
The annotation fixes the target type, and the parsed number plus one is forty-three.
-
What does this program print?
fn main() { let line = " alpha beta gamma "; let words: Vec<&str> = line.split_whitespace().collect(); println!("{words:?}"); println!("{}", line.trim().len()); }Answer
["alpha", "beta", "gamma"] / 19
The whitespace runs collapse, and the trimmed length counts the three words and two spaces.
-
What does this program print?
use std::str::FromStr; struct Point { x: i32, y: i32, } impl FromStr for Point { type Err = String; fn from_str(text: &str) -> Result<Self, Self::Err> { let (x, y) = text.split_once(',').ok_or("missing comma")?; let x = x.trim().parse().map_err(|_| "bad x")?; let y = y.trim().parse().map_err(|_| "bad y")?; Ok(Point { x, y }) } } fn main() { let point: Point = "3, 4".parse().unwrap(); println!("{} {}", point.x, point.y); }Answer
3 4
The implementation splits the two fields and parses each one.
-
What is the first line of the compiler output for this program?
fn main() { let number = "42".parse().unwrap(); println!("{number}"); }Answer
error[E0284]: type annotations needed
The target type of the parse is not known, and the help asks for an annotation.
-
What does this program print?
fn main() { let text = "not a number"; let bad: Result<i32, _> = text.parse(); println!("{}", bad.is_err()); }Answer
true
The text is not an integer, so the parse returns an Err and is_err is true.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev