Advent of Code 2023 Day 1 Rust

2024-02-07
8 min read

I want to start off by saying, “I don’t know rust.” Not a shocker of a statement, I know. I know very little programming/scripting, what I do know is very limited (I’m most familiar with bash, I know a little python). Diving into rust, then, is a big endeavor.

I’m starting this by going through 2023’s Advent of Code. The big problem is, I don’t even really know what questions to ask to work my way through the challenges. As a result, I’m going to walk through a blog I found from Bryson Meiling for the first day just to get my feet wet.

Part 1

Here’s the code I ended up with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::fs;

#[allow(unused)]

fn main() {
    let mut input = fs::read_to_string("/path/to/input/file")
        .expect("Can't read from file");
    day1(&input);
}

fn day1(input: &str) {
    let calibration: u32 = input
        .lines()
        .map(|x| {
            let mut chars = x.chars().filter(|c| c.is_digit(10));

            let first_chars = chars.next().expect("Gimme a number");

            let last_chars = match chars.last() {
                Some(last) => format!("{}{}", first_chars, last),
                None => format!("{}{}", first_chars, first_chars),
            };
            last_chars.parse::<u32>().unwrap()
        })
        .sum();
    println!("Answer: {}", calibration)
}

As you’ll see, it’s damn near identical to what Bryson has. So what good is doing this if I’m just copying and pasting and then renaming a variable or two? Well, I’m going to walk through the code and explain it for myself, that way I’ll actually understand what it’s doing!

On the first line you’ll see the use std::fs;, or the standard library’s file system module. This is used later on line 5, so I’ll explain that then.

I used an attribute to disable a warning I was getting for unused variables (input) on line 3. This was just to stop errors during compiling.

fn main

At first, I originally was trying to do all of this within the main function. For the test, I tried to create an array with the example variables given, and wanted to iterate through each array index and then index each word again to get each character separated. This didn’t work well. I might have been able to overcome this by doing the “read to string” method I ended up with, but at the time I didn’t know just how many strings there were going to be to iterate through…

After seeing the number of values I needed to sort through, I knew I needed to do this as a text file that gets read from. I had seen the fs::read_to_string part from a Rust 101 course I took, so I knew what to look for here.

fn day1

This takes as an input the input variable, which I defined in fn main as being the file containing the variables.

To get rid of all the extra arguments, this really boils down to let calibration: u32 = input.lines().map().sum() and that then gets printed, and in theory that’s the result.

The problem I encountered was, I hadn’t seen any of those methods before!

  • Line 12: lines() was pretty easy. It takes a string with multiple lines, and iterates over them. It splits on a newline \n character (or a line feed \r\n) and returns the string excluding the line terminator. This meant that I would take that gigantic file of strings and work with each line at a time. Simple enough! The documentation can be found here.
  • Line 13: map() threw me for a loop. This was the first time I’d seen that kind of syntax in an argument, specifically the |x| part. Based on the docs, it reads to me like the |x| is the equivalent of saying for x in input.lines(), meaning that the x part is totally arbitrary..
    • map() is lazy. First and foremost, same. But personal character attributes aside, I had no idea what this meant. I found the documentation on the crate lazy_st which said that “lazy evaluation allows you to define computations whose evaluation is deferred to when they are actually needed… Lazy evaluation is useful if you have an expensive computation of which you might need the result more than once during runtime, but you do not know in advance whether you will need it at all.” Good to know, but I don’t know how relevant this aspect is in this case.
  • Line 24: sum() is taking the transformed characters from map() and adding it all together, which is where we get the total number from (the result).

With the skeleton of the function built, it’s time to look at the meat of it, which is inside the map() method. There are three variables created, and then at the end there’s some parsing going on to get an integer.

  • Line 14: Here again contains some methods that I wasn’t familiar with.
    • It starts by using x, which it gets from earlier in the map() arguments.
    • chars(). I knew what a “char” was, but I didn’t know what the function itself did. This takes a string slice (each line from our input file), and working on each line individually it iterates over the string slice to get each UTF-8 character. Remember up above where I said I tried to set each value to an array so I could take each index in the array and break it into characters? Yeah, that’s what just happened here. Moving on…
    • filter(). Another format that was similar to map() which meant I spent some time scratching my head. While intuitively I know what it means to “filter” for something, the format threw me off. Assuming that anything in the | |-type format means it’s treated like a for-loop, this reads to me like “for c in x.chars() check if c meets some criteria, and if it does, return true.” Simple enough.
      • is_digit. Again, I intuitively know what the question “is this a digit” implies, I was incorrect in believe it meant “only numbers” since it also includes a-z and A-Z. Good to know. When I was reading the docs on it, I saw the format as is_digit(10) and is_digit(16). It specified that it “panics if given radix larger than 36”. Me not knowing what a “radix” was first tried to run the program with it written as is_digit(0-9) because that’s what it said a digit was defined as. Nope. Turns out that “radix” is fancy terminology for “base”, so is_digit(10) means “base 10” and is_digit(16) means “base 16” (hexadecimal). Even after fixing this, though, my linter still gives me a warning saying “use of char::is_digit with literal radix of 10… try: is_ascii_digit().” No, I don’t think I will.
  • Line 16: This was simple enough. Take that variable I just set, go to the first one (which would be a number), and set that to the variable. Otherwise say “Gimme a number” as an error.
  • Line 18: I was familiar with matching from my Rust 101 course, so this wasn’t too difficult. This uses last() to get the last character from chars (so the last digit), and uses that to match. Now, I hadn’t seen the Some(_)/None format before, but it made enough sense to know what it was trying to do. This was also my first encounter with format!(), as up until now I’ve really only used prinln!(). I didn’t really know the difference, but I did find that using println!() here meant I couldn’t parse and unwrap the variable (i.e. convert it into an integer), which defeated the purpose. I did find a discussion post here that described the difference as “format!() allocates a new String and writes to it, while println!() will write each bit of the formatted message to std::io::stdout() directly.” To me this means, format! will return a string, println! just prints. Makes enough sense to me.
  • Line 22: I actually had a hard time here, not because of a comprehension issue but because I actually screwed up line 19. I originally had it as format!("{} {}", first_chars, last), where if you’re looking you’ll see there’s a space between the two curly brackets. This meant that it couldn’t be parsed, because spaces aren’t numbers! Once I found that, it was a simple fix.

So now that map() is done, we can see that it takes each line, pulls out every base 10 digit, grabs the first one, and either the last digit or duplicates the first digit, converts it to a u32, and adds it all together!

Finally, on line 25, it takes that variable and prints it. There you have it, the correct number!


This is only part one of day one. Because it took me all day to sort through what I was looking at, it’ll probably be some time before I tackle part two. Seeing that I have to now read arbitrary text and convert that to digits where applicable kind of makes my head spin right now… But it’ll happen eventually!

Next I Lied