Advent of Rust 4: It’s Hard to Return an Iterator

Welcome again to this stream-of-consciousness log about learning the Rust programming language by doing the programming puzzles in Advent of Code 2020, or as I like to call it, On the Code by Jack Kerouac.1 Let’s get on with it!

Day 4, Part 1

I start with cargo new puzzle4 and copying over the code from yesterday, but this time I’d like to refactor it a bit. I would like to write a function is_part2() that tells you whether the solution for Part 2 of the puzzle was requested or not, and I would like to change read_lines() so that it already does the expect() calls that I am copy-pasting every day, rather than returning a Result of an iterator of Results of strings.

Changing read_lines() really does not work for me. I can’t figure out how to return an iterator from the function! Well, clearly the function is already returning an iterator, but I can’t figure out how to express the iterator that I want to return, as a return type.

What I want is this:

fn read_lines<P>(filename: P) -> ???
where
    P: AsRef<path::Path>,
{
    let file = fs::File::open(filename).expect("Bad file");
    io::BufReader::new(file)
        .lines()
        .map(|s| s.expect("Bad line in file"))
}

where ??? ought to be something like Iterator<String>. But I cannot figure out how to write an iterator type. The iterator types that are returned from standard library functions all seem to be type aliases of some sort. For example, io::Lines is the iterator that lines() returns, and I know it is an iterator of Results of strings, because the documentation says so, but I don’t know how to build my own iterator type.

I google “rust return iterator” and the first result is encouragingly titled “Returning Rust Iterators”. This article suggests asking the compiler by returning the empty type () and letting it suggest what to return instead, so I do that.

Unfortunately, I get a type that I don’t think I can put into my program! The compiler says “expected unit type (), found struct Map<std::io::Lines<BufReader<File>>, [closure@src/main.rs:31:14: 31:46]>” I am guessing that referring to a type by the file in which it’s found and the lines and columns that it spans, is not legal Rust syntax!

So I try poking around with no success. By trying things and following the compiler’s suggestions, I end up with the awkward return type of iter::Map<io::Lines<io::BufReader<fs::File>>, dyn FnMut(io::Result<&String>) -> &String>, but this is still producing errors about lifetimes that I don’t understand. So I give up on this idea.

However, maybe I can write read_lines() so that it at least calls expect() on the io::Lines iterator that it returns:

fn read_lines<P>(filename: P) -> io::Lines<io::BufReader<fs::File>>
where
    P: AsRef<path::Path>,
{
    let file = fs::File::open(filename).expect("Bad file");
    io::BufReader::new(file).lines()
}

To be clear, I don’t really understand the P: AsRef<path::Path> part either. I guess I will try to refactor this again in a few days when I have learned a bit more.

Writing is_part2(), on the other hand, is quite straightforward:

fn is_part2() -> bool {
    let args: Vec<String> = env::args().collect();
    let default = String::from("1");
    let arg = args.get(1).unwrap_or(&default);
    arg == "2"
}

This works, but I actually think that I might be able to make this even nicer by using a match statement. I haven’t encountered the match statement directly yet, but I’ve seen it in several google results over the past few days. I google “rust match syntax”, land here, and come up with this:

fn is_part2() -> bool {
    match env::args().nth(1) {
        Some("2") => true,
        _ => false,
    }
}

This almost works, but I have to replace "2" with String::from("2"), and then the compiler tells me that I have to take it out of the match statement, because function calls are not allowed in pattern matching. So in the end it looks like this:

fn is_part2() -> bool {
    let string2 = String::from("2");
    match env::args().nth(1) {
        Some(string2) => true,
        _ => false,
    }
}

The funny thing is, though, that the compiler warns that string2 is an unused variable in both of the places that it is used! It seems like I haven’t understood this well enough. I google “rust match string” and land on a Stack Overflow question titled “How to match a String against string literals in Rust?”, but that is actually only good for when you know that you already have a String! But I have an Option<String>, so I google “rust match option string”, and find another Stack Overflow post with the topic “How can I pattern match against an Option<String>?”. The helpful answer says this is a known limitation of pattern-matching, but suggests two things to do instead. The second solution looks good to me, so I implement it:

fn is_part2() -> bool {
    match env::args().nth(1) {
        Some(s) if s == "2" => true,
        _ => false,
    }
}

This works, and looks like what I had in mind. Time to start on the puzzle!

Today’s puzzle is processing records that represent “passports”. These records consist of whitespace-separated key:value pairs, and records are separated by blank lines. There are eight possible keys, each consisting of three letters. The cid key is optional, and all the others are required. A record is valid if it contains all the required keys. The answer to the puzzle is the number of valid passports in the input file.

I start out by downloading the input file and put it in the project directory, as usual.

So first I think a bit about how I will process the data into records. My usual approach so far has been to split the file into lines and process each line using a chain of iterators, but maybe it’s better to let go of that approach for today. I could read the whole file into a string and split it on the blank lines in order to get an array where each element is a record, and then process each record. Or I could still process the file line-by-line, and use a for loop while keeping the notion of a “current” record, which I push into a vector when it is completed. I think I like that idea best so far.

The records can be hash sets, where I store the keys. That’s a bit wasteful since really all I need is one bit for each of the 8 possible keys, to tell whether the record has it or not! (I can ignore the values for now, since I only need to look at the keys.) But I decide to be wasteful nonetheless, for two reasons: first, I suspect Part 2 of the puzzle will require me to do something with the values, but if Rust is like other languages, a hash set will be easy enough to refactor into a hash map. Second, stuffing the string keys into a hash set or hash map is simple, and I won’t have to write code that translates key strings into record fields.

To make a first attempt, I google “rust hash set” and read the Rust by Example page. It’s interesting that you don’t have to specify the type of a HashSet, the compiler can figure it out by what you insert into it!

I also have to look in the API documentation for how to split a string on whitespace, but it seems there is conveniently a split_whitespace() method.

Here’s my attempt:

use std::collections::HashSet;
use std::env;
use std::fs;
use std::io::{self, BufRead};
use std::path;

fn main() {
    let mut passports = vec![];
    let mut current = HashSet::new();
    for line in read_lines("input").map(|s| s.expect("Bad line in file")) {
        if line == "" {
            passports.push(current);
            current = HashSet::new();
        }
        current = current.union(get_keys_from_line(&line));
    }

    if is_part2() {
        println!("part 2");
    } else {
        println!("the first passport is {:?}", passports[0]);
    }
}

fn get_keys_from_line(line: &str) -> HashSet<String> {
    let mut new_keys = HashSet::new();
    for pair in line.split_whitespace() {
        new_keys.insert(String::from(&pair[..3]));
    }
    new_keys
}

It looks like I got several & operators right this time, even though I still got one wrong:

error[E0308]: mismatched types
  --> src/main.rs:15:33
   |
15 |         current = current.union(get_keys_from_line(&line));
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^^
   |                                 |
   |                                 expected `&HashSet<_>`, found struct `HashSet`
   |                                 help: consider borrowing here: `&get_keys_from_line(&line)`
   |
   = note: expected reference `&HashSet<_>`
                 found struct `HashSet<String>`

error[E0308]: mismatched types
  --> src/main.rs:15:19
   |
15 |         current = current.union(get_keys_from_line(&line));
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `HashSet`, found struct `std::collections::hash_set::Union`
   |
   = note: expected struct `HashSet<_>`
              found struct `std::collections::hash_set::Union<'_, _, RandomState>`

I need a & on the return value of get_keys_from_line(), and it appears I didn’t read the documentation of union() well enough because it returns an iterator, not another hash set, so I need to add collect() after it. I’m momentarily confused since I thought collect() created a vector from an iterator, not a hash set! But I see that the examples in the documentation for union() are doing it like that, so I assume it must be OK. I’m also wondering if there isn’t a more idiomatic way to do what I’m trying to do (I’m thinking of Python’s dict.update()) but I decide to leave it for now.

Now I get this error:

error[E0277]: a value of type `HashSet<String>` cannot be built from an iterator over elements of type `&String`
  --> src/main.rs:15:61
   |
15 |         current = current.union(&get_keys_from_line(&line)).collect();
   |                                                             ^^^^^^^ value of type `HashSet<String>` cannot be built from `std::iter::Iterator<Item=&String>`
   |
   = help: the trait `FromIterator<&String>` is not implemented for `HashSet<String>`

At least I think I know enough to understand this error message now; the union() iterator is giving me the &String values that are owned by the original two hash sets (current and the temporary object returned from get_keys_from_line()). I don’t own them, so I can’t insert them into a hash set of values that I’m supposed to own.

Hmm, this is not what I wanted at all. What I actually wanted was an update() method that would move the elements from the second set into the first set. I start to think this might have been easier after all if I’d used 8 bits to store whether the keys were present… 😝

I google “rust hashset update” and land on this encouragingly titled Stack Overflow post: “How can I insert all values of one HashSet into another HashSet?” It comes as a surprise to me that there is an extend() method for this! I guess as the commenter says under that post,

Plus it teaches me that I should not only look at the ‘methods’ section of the doc to find methods, but also into the traits a struct implements.

I guess I’ve learned that too now! Wow, that really is confusing, that a note down at the bottom of the API documentation page saying only impl<T, S> Extend<T> for HashSet<T, S> is actually telling you that there is an extend() method.

Regardless, I change the line to read current.extend(&get_keys_from_line(&line)); and I don’t quite get what I want either:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:15:25
   |
12 |             passports.push(current);
   |                            ------- borrow later used here
...
15 |         current.extend(&get_keys_from_line(&line));
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
   |                         |
   |                         creates a temporary which is freed while still in use
   |
   = note: consider using a `let` binding to create a longer lived value

This is another head-scratcher. I don’t see why it should matter that the temporary value is freed, when I thought I was moving all the elements out of it and into current! From reading about ownership yesterday, I thought I understood that ownership of values is always moved into function calls, unless they are borrowed with the & operator.

But while I was browsing the HashSet documentation I do remember coming across the drain() method and wondering if that would be useful. Maybe the problem is that the ownership of the values isn’t getting transferred, and I need to drain() them out of the temporary object so that current owns them. I change the line to current.extend(get_keys_from_line(&line).drain()); and it works!

So, I file away the knowledge that the operation I think of as a.update(b) is written as a.extend(b.drain()) in Rust. I wonder if that is actually the idiomatic way to do it?

Now that I have the data in the form that I wanted, I can write the code to get the answer:

let count = passports.iter().filter(passport_is_valid).count();
println!("{}", count);

// [...]

fn passport_is_valid(passport: &HashSet<String>) -> bool {
    let n_keys = passport.len();
    n_keys == 8 || (n_keys == 7 && !passport.contains("cid"))
}

But this doesn’t work:

error[E0631]: type mismatch in function arguments
  --> src/main.rs:21:45
   |
21 |         let count = passports.iter().filter(passport_is_valid).count();
   |                                             ^^^^^^^^^^^^^^^^^ expected signature of `for<'r> fn(&'r &HashSet<String>) -> _`
...
26 | fn passport_is_valid(passport: &HashSet<String>) -> bool {
   | -------------------------------------------------------- found signature of `for<'r> fn(&'r HashSet<String>) -> _`

error[E0599]: no method named `count` found for struct `Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>` in the current scope
    --> src/main.rs:21:64
     |
21   |           let count = passports.iter().filter(passport_is_valid).count();
     |                                                                  ^^^^^ method not found in `Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>`
     |
     = note: the method `count` exists but the following trait bounds were not satisfied:
             `<for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid} as FnOnce<(&&HashSet<String>,)>>::Output = bool`
             which is required by `Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>: Iterator`
             `for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}: FnMut<(&&HashSet<String>,)>`
             which is required by `Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>: Iterator`
             `Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>: Iterator`
             which is required by `&mut Filter<std::slice::Iter<'_, HashSet<String>>, for<'r> fn(&'r HashSet<String>) -> bool {passport_is_valid}>: Iterator`

Well, I saw an error just like this yesterday, and it was a missing & operator on the type of the function parameter. But I already have a & there! I try putting a second one (since the error message also has two of them) and sure enough, it works. I get an answer, but it’s the wrong answer.

According to the Advent of Code website, my answer is too low. I look over my code again and I see that I forgot to add the last passport to my vector of passports! Maybe that’s the problem? I add passports.push(current); below the loop, run the program again, and yes, I get an answer that’s one more than the previous answer. This time, it’s correct according to the website.

Here’s the full code (I’ll start leaving out the definitions of is_part2() and read_lines() each time unless they change, though):

use std::collections::HashSet;

fn main() {
    let mut passports = vec![];
    let mut current = HashSet::new();
    for line in read_lines("input").map(|s| s.expect("Bad line in file")) {
        if line == "" {
            passports.push(current);
            current = HashSet::new();
        }
        current.extend(get_keys_from_line(&line).drain());
    }
    passports.push(current);

    if is_part2() {
        println!("part 2");
    } else {
        let count = passports.iter().filter(passport_is_valid).count();
        println!("{}", count);
    }
}

fn passport_is_valid(passport: &&HashSet<String>) -> bool {
    let n_keys = passport.len();
    n_keys == 8 || (n_keys == 7 && !passport.contains("cid"))
}

fn get_keys_from_line(line: &str) -> HashSet<String> {
    let mut new_keys = HashSet::new();
    for pair in line.split_whitespace() {
        new_keys.insert(String::from(&pair[..3]));
    }
    new_keys
}

Day 4, Part 2

Well, as I suspected, the second part of the puzzle does require looking at the values. So I will start by refactoring the hash set into a hash map, that saves the values as well as the keys.

This refactor is pretty straightforward, I look up the documentation for HashMap to check if the methods are different (contains() has to change to contains_key()) but other than that, I just have to change HashSet to HashMap throughout. (I also originally overlooked that the correct type is HashMap<String, String>, not HashMap<String>, but the compiler helpfully reminded me.)

use std::collections::HashMap;

fn main() {
    let mut passports = vec![];
    let mut current = HashMap::new();
    for line in read_lines("input").map(|s| s.expect("Bad line in file")) {
        if line == "" {
            passports.push(current);
            current = HashMap::new();
        }
        current.extend(get_pairs_from_line(&line).drain());
    }
    passports.push(current);

    if is_part2() {
        println!("part 2");
    } else {
        let count = passports.iter().filter(passport_is_valid).count();
        println!("{}", count);
    }
}

fn passport_is_valid(passport: &&HashMap<String, String>) -> bool {
    let n_keys = passport.len();
    n_keys == 8 || (n_keys == 7 && !passport.contains_key("cid"))
}

fn get_pairs_from_line(line: &str) -> HashMap<String, String> {
    let mut new_pairs = HashMap::new();
    for pair in line.split_whitespace() {
        new_pairs.insert(String::from(&pair[..3]), String::from(&pair[4..]));
    }
    new_pairs
}

Now I can start changing passport_is_valid() to implement the rules for each field. I’ll make it look something like this:

    (n_keys == 8 || (n_keys == 7 && !passport.contains_key("cid")))
        && valid_birth_year(&passport["byr"])
        && valid_issue_year(&passport["iyr"])
        && valid_expiry_year(&passport["eyr"])
        && valid_height(&passport["hgt"])
        && valid_hair_color(&passport["hcl"])
        && valid_eye_color(&passport["ecl"])
        && valid_passport_id(&passport["pid"])

Then I can write a function for each validation criterion. To start with I write all these functions but make them only contain true, so I can write them one by one but still run the program. (This went pretty smoothly although the compiler did have to remind me to add the & operators before passing each passport data field into each function.)

I wonder for a bit whether I should use scan_fmt! like I did on Day 2, or regular expressions, or parse(). I decide that maybe it’s time for me to use some regular expressions in Rust. It’s new for me, would be useful to learn, and these regular expressions will be simple. But for the fields where I need to check that the numbers are between certain ranges, I’ll also need to use parse().

Let’s start at the top. Birth year must be a 4-digit number between 1920 and 2002 inclusive. (What happens to the 17-year-old or 101-year-old travelers?) I google “rust regex” and find out that you need to install a package for regular expressions, so I add regex = "1" to Cargo.toml and use regex::Regex; to my program. I start reading the documentation for the regex package.

I already know regular expression syntax, which will be very helpful for completing this task. The one thing that I should take note of is that patterns match anywhere in the string, so I need to use ^ and $ to anchor them if I only want them to match the whole string. (That is, matching is like re.search() and not re.match() in Python.)

I come up with this as a first attempt:

fn valid_birth_year(byr: &str) -> bool {
    let re = Regex::new(r"^[12]\d{3}$").unwrap();
    if !re.is_match(byr) {
        return false;
    }
    let year = byr.parse::<u16>();
    year >= 1920 && year <= 2002
}

It seems the only thing I’ve forgotten is to unwrap() the result of parse(), which I had learned on Day 1 but forgot about. So I add that. Then, since issue year and expiry year are validated in a very similar way, just with different maximum and minimum years, I extract that into a valid_year() function:

fn valid_birth_year(byr: &str) -> bool {
    valid_year(byr, 1920, 2002)
}

fn valid_issue_year(iyr: &str) -> bool {
    valid_year(iyr, 2010, 2020)
}

fn valid_expiry_year(eyr: &str) -> bool {
    valid_year(eyr, 2020, 2030)
}

fn valid_year(y: &str, min: u16, max: u16) -> bool {
    let re = Regex::new(r"^[12]\d{3}$").unwrap();
    if !re.is_match(y) {
        return false;
    }
    let year = y.parse::<u16>().unwrap();
    year >= min && year <= max
}

(I initially forgot the -> bool on the new function. I’m so used to either not having a return type on functions (Python, JavaScript) or having the return type come before the name (C, C++) that I forget this a lot!)

Next I write a first attempt at valid_height(), using the match statement that I learned earlier today, and browsing the regex documentation to find that I have to use captures() to get the values of the regular expression’s capture groups:

fn valid_height(hgt: &str) -> bool {
    let re = Regex::new(r"^(\d{2,3})(cm|in)$").unwrap();
    let groups = re.captures(hgt);
    let height = groups[1].parse::<u8>().unwrap();
    match &groups[2] {
        "cm" => height >= 150 && height <= 193,
        "in" => height >= 59 && height <= 76,
    }
}

The compiler tells me that I forgot to unwrap() the result from captures(), and after I do that, it tells me that my match pattern is “non-exhaustive” — I guess this means that I don’t provide any instructions for what to do if the unit is neither cm nor in. I remember from earlier that a default case is written as _ =>, so I add _ => false to the match statement.

Now it runs! But it panics at unwrapping the return value of captures(). I look at the captures() documentation again, and it says that it will return None if the regex doesn’t match. So I decide to do some quick’n’dirty println!("{}", hgt) debugging to see what the string is that doesn’t match. It’s 97, without a unit after it, so indeed it doesn’t match. A string that doesn’t match should make the function return false, not panic.

I’m sure there’s a nice idiom for “return if None, unwrap otherwise” that I’m not yet aware of. I google “rust option return if none” and I learn a bit more about the ? operator, but since I’m not returning a Result from this function that doesn’t help me. However, another thing I learn is that the body of a match statement doesn’t actually have to be the same type as the expression! You can put return false in there and it will return from the function. How nice! So, the captures() call becomes:

let groups = match re.captures(hgt) {
    Some(groups) => groups,
    None => return false,
};

Next comes hair color. This is very similar to the year validation that I’ve done already, only without having to parse the string:

fn valid_hair_color(hcl: &str) -> bool {
    let re = Regex::new(r"^#[0-9a-f]{6}$").unwrap();
    re.is_match(hcl)
}

Eye color doesn’t actually need to use a regular expression:

fn valid_eye_color(ecl: &str) -> bool {
    ecl == "amb"
        || ecl == "blu"
        || ecl == "brn"
        || ecl == "gry"
        || ecl == "grn"
        || ecl == "hzl"
        || ecl == "oth"
}

This works, but I wonder if I could take this opportunity to apply something that I think I remember, from reading about match earlier:

fn valid_eye_color(ecl: &str) -> bool {
    match ecl {
        "amb" | "blu" | "brn" | "gry" | "grn" | "hzl" | "oth" => true,
        _ => false,
    }
}

This also works, and looks much nicer!

Finally, there is the passport ID, which is a nine-digit number:

fn valid_passport_id(pid: &str) -> bool {
    let re = Regex::new(r"^\d{9}$").unwrap();
    re.is_match(pid)
}

I should now be able to get my answer! Before I check whether it’s correct on the Advent of Code website, I would like to make one improvement that was mentioned in the regex documentation. Running the program takes an almost noticeably long time, and it’s probably because I’m rebuilding the same Regex objects once for each passport in the file, several hundred times. The documentation mentions you can use the lazy_static package to build them only once, and so I add that to Cargo.toml and follow the example in the regex documentation.

According to the example, I need to add this to the top of the file:

#[macro_use]
extern crate lazy_static;

And change this:

let re = Regex::new(r"^[12]\d{3}$").unwrap();

to this:

lazy_static! {
    static ref YEAR_REGEX: Regex = Regex::new(r"^[12]\d{3}$").unwrap();
}

I initially forget to add the type : Regex and the compiler’s error message isn’t so helpful:

error: no rules expected the token `=`
  --> src/main.rs:57:23
   |
57 |         static ref re = Regex::new(r"^[12]\d{3}$").unwrap();
   |                       ^ no rules expected this token in macro call

I still don’t quite know how macros work in Rust, or why lazy_static! is a macro and not a function, but my guess is that it’s harder to generate good error messages for macros.

I also try to keep the name re for the regular expressions, but the compiler helpfully tells me that it’s bad style for static variables to have lower case names! So I rename them.

My program seems to run faster now, and still gives me the same answer. So I put that answer into the Advent of Code website, and it’s correct! Hooray!

Here’s the program, quite long this time because of all the validation rules:

use regex::Regex;
use std::collections::HashMap;

#[macro_use]
extern crate lazy_static;

fn main() {
    let mut passports = vec![];
    let mut current = HashMap::new();
    for line in read_lines("input").map(|s| s.expect("Bad line in file")) {
        if line == "" {
            passports.push(current);
            current = HashMap::new();
        }
        current.extend(get_pairs_from_line(&line).drain());
    }
    passports.push(current);

    let count = passports.iter().filter(passport_is_valid).count();
    println!("{}", count);
}

fn passport_is_valid(passport: &&HashMap<String, String>) -> bool {
    let n_keys = passport.len();
    (n_keys == 8 || (n_keys == 7 && !passport.contains_key("cid")))
        && (!is_part2()
            || valid_birth_year(&passport["byr"])
                && valid_issue_year(&passport["iyr"])
                && valid_expiry_year(&passport["eyr"])
                && valid_height(&passport["hgt"])
                && valid_hair_color(&passport["hcl"])
                && valid_eye_color(&passport["ecl"])
                && valid_passport_id(&passport["pid"]))
}

fn valid_birth_year(byr: &str) -> bool {
    valid_year(byr, 1920, 2002)
}

fn valid_issue_year(iyr: &str) -> bool {
    valid_year(iyr, 2010, 2020)
}

fn valid_expiry_year(eyr: &str) -> bool {
    valid_year(eyr, 2020, 2030)
}

fn valid_year(y: &str, min: u16, max: u16) -> bool {
    lazy_static! {
        static ref YEAR_REGEX: Regex = Regex::new(r"^[12]\d{3}$").unwrap();
    }
    if !YEAR_REGEX.is_match(y) {
        return false;
    }
    let year = y.parse::<u16>().unwrap();
    year >= min && year <= max
}

fn valid_height(hgt: &str) -> bool {
    lazy_static! {
        static ref HEIGHT_REGEX: Regex = Regex::new(r"^(\d{2,3})(cm|in)$").unwrap();
    }
    let groups = match HEIGHT_REGEX.captures(hgt) {
        Some(groups) => groups,
        None => return false,
    };
    let height = groups[1].parse::<u8>().unwrap();
    match &groups[2] {
        "cm" => height >= 150 && height <= 193,
        "in" => height >= 59 && height <= 76,
        _ => false,
    }
}

fn valid_hair_color(hcl: &str) -> bool {
    lazy_static! {
        static ref HAIR_REGEX: Regex = Regex::new(r"^#[0-9a-f]{6}$").unwrap();
    }
    HAIR_REGEX.is_match(hcl)
}

fn valid_eye_color(ecl: &str) -> bool {
    match ecl {
        "amb" | "blu" | "brn" | "gry" | "grn" | "hzl" | "oth" => true,
        _ => false,
    }
}

fn valid_passport_id(pid: &str) -> bool {
    lazy_static! {
        static ref ID_REGEX: Regex = Regex::new(r"^\d{9}$").unwrap();
    }
    ID_REGEX.is_match(pid)
}

fn get_pairs_from_line(line: &str) -> HashMap<String, String> {
    let mut new_pairs = HashMap::new();
    for pair in line.split_whitespace() {
        new_pairs.insert(String::from(&pair[..3]), String::from(&pair[4..]));
    }
    new_pairs
}

Afterword

I don’t have that much to reflect on, this time. I did still forget the & operators all over the place, but less often than I did on the previous days, and I had a better understanding of the errors when they occurred.

I’m still a bit mystified about why it’s so difficult to express the type of an iterator in Rust. It seems like it shouldn’t be that hard, so maybe I’m missing something or approaching it the wrong way?

Finally, it seems like I’m reading the API documentation more often, and googling less. I think this is a sign that I have a better idea of where to look and what I’m looking for in the API documentation, but googling is still useful when I need to figure out how to do something that’s not covered by the examples in the API documentation, like a.extend(b.drain()).


[1] I don’t, in fact, like to call it that