Reading formatted data from files
As you will certainly have guessed at this point, we'll use iterators once again. This is what the loading function will look like:
fn line_to_slice(line: &str) -> Vec<u32> { line.split(" ").filter_map(|nb| nb.parse::<u32>().ok()).collect() } fn load_highscores_and_lines() -> Option<(Vec<u32>, Vec<u32>)> { if let Ok(content) = read_from_file("scores.txt") { let mut lines = content.splitn(2, "\n").map(|line|
line_to_slice(line)).collect::<Vec<_>>(); if lines.len() == 2 { let (number_lines, highscores) = (lines.pop().unwrap(),
lines.pop().unwrap()); Some((highscores, number_lines)) } else { None } } else { None }
}
Once again, not easy to understand, at first sight. So let's explain all this!
fn line_to_slice(line: &str) -> Vec<u32> {
Our line_to_slice() function does the opposite of slice_to_string(); it transforms a &str into a slice of u32 (or &[u32]). Let's see the iterator now:
line.split(" ").filter_map(|nb| nb.parse::<u32>().ok()).collect()
Just like last time, let's split the calls:
line.split(" ") .filter_map(|nb| nb.parse::<u32>().ok()) .collect()
A bit better! Now let's explain:
line.split(" ")
We create an iterator that will contain all strings between spaces. So a b will contain a and b:
.filter_map(|nb| nb.parse::<u32>().ok())
This method is particularly interesting since it's the merge of two others: filter() and map(). We already know map() but what about filter()? If the condition isn't verified (so if the returned value of the closure is false), the iterator won't pass the value to the next method call. filter_map() works the same at this point: if the closure returns None, the value won't be passed to the next method call.
Now let's focus on this part:
nb.parse::<u32>().ok()
Here, we try to convert &str into u32. The parse() method returns a Result but the filter_map() expects an Option so we need to convert it. That's what the ok() method is for! If your Result is an Ok(value), then it'll convert it into a Some(value). However, if it's an Err(err), it'll convert it into a None (but you'll lose the error value).
To sum this up, this whole line tries to convert a &str into a number and ignores it if the conversion fails so it's not added to our final Vec. Amazing how much we can do with such small code!
And finally:
.collect()
We collect all the successful conversions into a Vec and return it.
That's it for this function, now let's look at the other one:
fn load_highscores_and_lines() -> Option<(Vec<u32>, Vec<u32>)> {
Here, if everything went fine (if the file exists and has two lines), we return an Option containing in the first position the highest scores and in the second position the highest number of lines:
if let Ok(content) = read_from_file("scores.txt") {
So if the file exists and we can get its content, we parse the data:
let mut lines = content.splitn(2, "\n").map(|line|
line_to_slice(line)).collect::<Vec<_>>();
Another iterator! As usual, let's rewrite it a bit:
let mut lines = content.splitn(2, "\n") .map(|line| line_to_slice(line)) .collect::<Vec<_>>();
I think you're starting to get how they work, but just in case you don't know, here's how:
content.splitn(2, "\n")
We make an iterator containing at most two entries (because of the 2 as the first argument) splitting lines:
.map(|line| line_to_slice(line))
We transform each line into a Vec<u32> by using the function described in the preceding code:
.collect::<Vec<_>>();
And finally, we collect those Vecs into a Vec<Vec<u32>>, which should only contain two entries.
Let's look at the next line now:
if lines.len() == 2 {
As said before, if we don't have two entries inside our Vec, it means something is wrong with the file:
let (number_lines, highscores) = (lines.pop().unwrap(),
lines.pop().unwrap());
In case our Vec has two entries, we can get the corresponding values. Since the pop method removes the last entry of the Vec, we get them in reverse (even though we return high scores first then the highest number of lines):
Some((highscores, number_lines))
Then everything else is just the error handling. As we said previously, if any error occurs, we return None. In this case, it's not really important to handle the error since it's just high scores. If we have errors with the sdl libraries, nothing will work as expected, so we need to handle them to avoid a panic.
It's now time to really start the game!