Parsing a file line by line
A line of comma-separated values is the smallest data format that still has real failure modes: a field that is not a number, a row with too few columns, a file that does not exist. This lesson parses one line, then a file of lines, and reports the failures by line number instead of stopping at the first one.
The pieces come from the previous lessons: split from parsing text, lines from text files and parse with its Result. The example writes its own file first, so the program runs anywhere, and the last section is the type error for using a parse result as the number it might produce.
A CSV line
split(',') gives the three fields of the sample line as slices of the line itself. The program prints them in order, which is the whole first step of any parser.
The fields are borrowed, so the line stays alive while they are used. A row with more separators would give more fields, which is why the parser counts them before indexing.
fn main() {
let line = "ada,36,maths";
let fields: Vec<&str> = line.split(',').collect();
println!("{} {} {}", fields[0], fields[1], fields[2]);
}
rustc --edition 2024 line.rs
./line
ada 36 maths
The output is the three values with no commas, because the print adds spaces. The next section converts the numeric fields.
Fields into numbers
A slice of the fields can be mapped and summed. The closure parses each field and unwraps it, which is acceptable for a line the program wrote itself and not for input from outside.
The program sums the two numeric fields of a row and prints the name and the total. The sum is one hundred and twenty-one.
fn main() {
let line = "ada,36,85";
let fields: Vec<&str> = line.split(',').collect();
let scores: i32 = fields[1..].iter().map(|field| field.parse::<i32>().unwrap()).sum();
println!("{} {}", fields[0], scores);
}
rustc --edition 2024 parse_row.rs
./parse_row
ada 121
The unwrap panics on a bad field, which the bad-row section replaces with a reported error. The lesson on parsing text shows the Result that unwrap discards.
A file of rows
The file is written by the program, read back with read_to_string and split into lines. Each line takes its second field, parses it and adds it to the total.
The program reports three rows and a total of one hundred and sixty-two. The count and the sum are the two numbers a report needs before it formats anything.
use std::fs;
fn main() {
fs::write("scores.csv", "ada,36\ngrace,85\nalan,41\n").unwrap();
let text = fs::read_to_string("scores.csv").unwrap();
let mut total = 0;
let mut count = 0;
for line in text.lines() {
let score: i32 = line.split(',').nth(1).unwrap().parse().unwrap();
total += score;
count += 1;
}
println!("{count} rows, total {total}");
}
rustc --edition 2024 file.rs
./file
3 rows, total 162
The file is small enough for one read; a log would use the line-by-line form from text files. The parsing code would not change.
Reporting a bad row
A row that is not a name and a number must not stop the whole file. The match treats the name as optional and the number as a Result, and the wildcard arm reports the line number.
The program writes a file with a bad line in the middle and reports it while printing the good rows. The output names line two as the bad one.
use std::fs;
fn main() {
fs::write("scores.csv", "ada,36\nbad-line\ngrace,85\n").unwrap();
let text = fs::read_to_string("scores.csv").unwrap();
for (index, line) in text.lines().enumerate() {
let mut parts = line.split(',');
match (parts.next(), parts.next().map(|value| value.parse::<i32>())) {
(Some(name), Some(Ok(score))) => println!("{name}: {score}"),
_ => println!("line {}: bad row", index + 1),
}
}
}
rustc --edition 2024 report.rs
./report
ada: 36
line 2: bad row
grace: 85
The line number starts at one because enumerate counts from zero and the print adds one. The next lesson turns the failure into a typed error instead of a message.
A Result where a number belongs
parse returns a Result, and assigning it to an i32 is E0308. The error names the two types and points at the annotation, which is the place to add the handling or the unwrap.
The program parses a field into a total without unwrapping. The fix is a match, a question mark or an explicit unwrap with a message.
fn main() {
let line = "ada,36";
let fields: Vec<&str> = line.split(',').collect();
let total: i32 = fields[1].parse();
println!("{total}");
}
rustc --edition 2024 parse_err.rs
error[E0308]: mismatched types
--> parse_err.rs:4:22
|
4 | let total: i32 = fields[1].parse();
| --- ^^^^^^^^^^^^^^^^^ expected `i32`, found `Result<_, _>`
| |
| expected due to this
|
= note: expected type `i32`
found enum `Result<_, _>`
help: consider using `Result::expect` to unwrap the `Result<_, _>` value, panicking if the value is a `Result::Err`
|
4 | let total: i32 = fields[1].parse().expect("REASON");
| +++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The compiler cannot know whether the field is a number, so it refuses to pretend. The lesson on error types gives that refusal a name and a message.
Exercises
- Parse a line of three fields and print them as a table row.
- Sum a column of a file and report the rows that could not be parsed.
- Skip the header line of a CSV file and parse only the rows below it.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
splitreturn for a CSV line?Answer
An iterator of field slices
The fields are slices borrowed from the line, and collecting is a separate step.
-
Why does the bad-row parser report the line number?
Answer
So a reader can find the row in the file
A message without a position is hard to act on, and the number is cheap to keep.
-
What does
parsereturn on a field that is not a number?Answer
An Err with the reason
The conversion reports the failure in the Result, and the caller decides what to do.
-
Which method reads a file into one String?
Answer
fs::read_to_string
read_to_string reads the whole file and checks that it is UTF-8.
-
What does this program print? ``` fn main() { let line = "ada,36,maths"; let fields: Vec<&str> = line.split(
Answer
ada 36 maths
The three fields print in order with spaces between them.
-
What does this program print? ``` fn main() { let line = "ada,36,85"; let fields: Vec<&str> = line.split(
Answer
ada 121
The two numeric fields are parsed and summed to one hundred and twenty-one.
-
What does this program print? ``` use std::fs; fn main() { fs::write("rows.csv", "ada,36\ngrace,85\n").unwrap(); let text = fs::read_to_string("rows.csv").unwrap(); let mut total = 0; for line in text.lines() { let score: i32 = line.split(
Answer
121
The two scores are parsed and added.
-
What does this program print? ``` use std::fs; fn main() { fs::write("rows.csv", "ada,36\nbad-row\ngrace,85\n").unwrap(); let text = fs::read_to_string("rows.csv").unwrap(); for (index, line) in text.lines().enumerate() { let mut parts = line.split(
Answer
ada: 36 / line 2: bad row / grace: 85
The middle row has no numeric field, and the report names its one-based line number.
-
What is the first line of the compiler output for this program? ``` fn main() { let line = "ada,36"; let fields: Vec<&str> = line.split(
Answer
error[E0308]: mismatched types
The annotation asks for a number and parse returns a Result.
-
What does this program print? ``` fn main() { let line = "a,1,b,2"; let fields: Vec<&str> = line.split(
Answer
3
Only the two numeric fields survive the filter, and one plus two is three.
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