️🎄 Advent of Code 2021, Day 9

Smoke Basin

2022-08-28

#rust #advent-of-code

In which I decide to stop messing around and do things Properly™ when it comes to fallible parsing.

Day 9: Smoke Basin

The input this time around—as shown in the example—is a grid of integers:

2199943210
3987894921
9856789892
8767896789
9899965678

Parsing the input into a Vec<Vec<usize>> was easy enough:

fn get_lava_tubes(input: &str) -> Vec<Vec<usize>> {
    input
        .trim()
        .lines()
        .map(|line| {
            line.chars()
                .map(|c| c.to_digit(10).unwrap() as usize)
                .collect::<Vec<_>>()
        })
        .collect()
}

However, in retrospect there's one thing I really need to improve: my use of unwrap() everywhere.

The problem is that c.to_digit(10): I should, of course, return a Result in the event that it fails to parse a character.

Removing the unwrap() and and leaving the rest as-is leads to Vec<Option<u32>>. Rust, however, has a nice trick to invert that. If instead, I explicitly specify the type:

line.chars()
    .map(|c| c.to_digit(10))
    .collect::<Option<Vec<_>>>()

…then Vec<Option<T>> becomes Option<Vec<T>>. Using the same feature on the outer collect(), converts what would have been a Vec<Option<Vec<T>>> to a Option<Vec<Vec<T>>>.

That resulting Option can then be converted into the desired Result with ok_or():

fn get_lava_tubes(input: &str) -> Result<Vec<Vec<u32>>, ()> {
    input
        .trim()
        .lines()
        .map(|line| {
            line.chars()
                .map(|c| c.to_digit(10))
                .collect::<Option<Vec<_>>>()
        })
        .collect::<Option<Vec<_>>>()
        .ok_or(())
}

Skipping over my actual implementation (a breadth-first-search which is arguably less interesting than getting better at the above), the existing solution no longer works.

My standard layout—actually implemented as a Cookiecutter template—for each day of Advent of Code is something like this:

 tree .
.
├── Cargo.lock
├── Cargo.toml
├── input.txt
└── src
    ├── lib.rs
    └── main.rs

1 directory, 5 files

That is, most of the implementation is in lib.rs, with main.rs doing little more than provide an entry point. The main.rs for this day, for instance, looks something like:

use std::fs;

use ::day09::*;

fn main() {
    let input = fs::read_to_string("input.txt").expect("Error reading input.txt");

    println!(
        "What is the sum of the risk levels of all low points on your heightmap? {}",
        get_part_one(&input),
    );

    println!(
        "What do you get if you multiply together the sizes of the three largest basins? {}",
        get_part_two(&input),
    );
}

However, now that we're more correctly returning a Result due to our fallible parsing, this won't work: we'll try to print said Result instead of the answer.

So a correct implementation would be:

fn main() -> Result<(), ()> {
    let input = fs::read_to_string("input.txt").expect("Error reading input.txt");

    println!(
        "What is the sum of the risk levels of all low points on your heightmap? {}",
        get_part_one(&input)?,
    );

    println!(
        "What do you get if you multiply together the sizes of the three largest basins? {}",
        get_part_two(&input)?,
    );

    Ok(())
}

As documented in this from the Rust 1.61 release notes, it's now possible to specify the exact exit code, based on the return type.

That's one for another day.