Advent of Code 2022
I'm doing Advent of Code 2022 in Rust as a chance to get more familiar with the language. In this ongoing post, I'm documenting each day's progress, my thought process, and so on. I'm better at writing than recording screencasts or what have you. This is essentially a small journal. Ah, and I'm trying AI tools for doing this as well, so this is bound to be fun.
I don't really know Rust, but it's a fascinating language for me. I mostly write Elixir. Even when it comes to other languages, I'm used to and fond of functional programming, immutable data everywhere, and things like those. Rust is quite different. I'm not used to working with static types, mutability, or low-level memory access. This is a good excuse to learn a bit more about the language, given its recent rise to fame.
The thing is this: it's hard to search specific stuff on the web when learning a new programming language, especially at the beginning. I'm clueless, so I go about it by searching for stuff like "how to read a file and split it into lines in Rust?". It takes a lot of Stack Overflow questions, small snippets of code lying around in blog posts, and documentation. However, 2022 is the year of AI, isn't it? I have not been too deep into the AI scene. I've used DALL·E 2 a bit, but not much else. Never tried writing software with the help of GitHub Copilot, for example. And at the time of writing this, a lot of what I read on the Internet is about the recent release of ChatGPT. There's no time like the present, right? I figured these tools might be a nice help when learning a new language.
Anyway, enough prefacing. I'll post each day, and I'll probably break that promise. Most recent day on top. All complete solutions are on GitHub.
Also, a disclaimer: this is not a polished post. I went with the approach that publishing something is better than publishing nothing, so I'm going for it. I'd absolutely love to know if this was interesting for you, so reach out on Twitter or via email (links in footer) if you have feedback.
Day 25
I could only do part one here since I don't have all other puzzles completed yet. The most fun thing about today's puzzle was implementing a bunch of Rust traits for these "SNAFU" numbers to play more with Rust.
I went with:
std::fmt::Display
, which prints a SNAFU as a string (as seen in the puzzle input).std::str::FromStr
, which lets me parse a string representation of a SNAFU number.TryFrom<i128>
, which lets me converti128
numbers toSNAFU
numbers.
Other than that, this is a base conversion thing. Early in my career, I got obsessed for a little bit with base conversions and even created a library for Elixir and a library for Ruby to do base conversion. Go figure.
Day 24
Haven't attempted yet. Time.
Day 23
Some kind of Conway's Game of Life thing for today's puzzle.
Absolutely nothing interesting to write about. I didn't really learn anything new, didn't play with any Rust features, or anything like that. Just plain old working-through-the-problem. Same for part two.
Day 22
Uh, sort-of-2D work. Not a fan here.
Part one was a matter of getting the wrapping and directions and grid right. I don't even know what to write about here, as the code is pretty boring to look at. The only thing I enjoyed doing was using types a lot to "make illegal states unrepresentable". For example, instead of having a board of char
s (' '
, '.'
, or '#'
), I went with an enum:
Other than this… Not my jam. I didn't even attempt part two yet because it goes from sort-of-2D to sort-of-3D. I don't want anything to do with that!
Day 21
Nice puzzle today. For part one, I was able to get away with a quick-enough solution: iterate through the monkeys and "solve" the ones that point to monkeys that already have a number. Without all the parsing code, the core looks something like this:
let monkeys = input
.lines
.map
.;
let mut computed_monkeys = new;
loop
println!
Not very pretty, but fast enough to spit out the correct result.
Day 21: Part Two
Part two was more fun! The same brute-force approach as above crumbled down. I naively tried this: if I know how to calculate the root
monkey's values, and I can compare them for equality… Let's try all numbers (0..
) for the humn
value and see if we find one that works. Failed miserably.
So, time to roll up my sleeves and build an AST of operations. I went with a straightforward structure:
Parsing the AST was easy enough. The fun part was to "solve" the full AST for the humn
variable. To do that, I went with the simplest one-variable equation solving approach I could think of. The idea was to do something to both sides of the =
that would simplify the side where the humn
variable is. This is really early grades math, I know. So, I went with a simplify_equation(left_ast, right_ast)
function:
To make this work, I also added a simplify(ast)
function that simplifies an AST made entirely of numbers (which can be reduced to a single number). My initial solution did not work, so I turned out to Reddit and someone mentioned the QuickMath website. I pretty-printed my original equation, pasted it in there, and it spit out the right number. After a bit of debugging, I figured that the issue in my code was that I was not handling the -
operation correctly (which I did in the code above). All is well now.
Day 20
Today's puzzle was surprisingly easy considering that it's day 20 at this point. It's also the sort of puzzle that a language like Rust, with constant-access vectors and mutable data structures.
For part one, I mostly used the puzzle as an excuse to learn more about some traits.
>);
I stored each number (i64
) along its "ID" (the usize
), which is just its original index in the provided input. There are duplicates in the input numbers, so trying to find each number by its value wouldn't have worked. Now for the traits:
// Allows me to do some_iter.collect::<CircularList>().
// Nice for reproducing the input example with println!("{}", circular_list).
After this, it was a matter of looping through each original number and moving each one to the new position. The only thing that took a second to realize was that the way to calculate the right destination index was to do modulo length - 1
. This makes sense as we remove the element before finding its new position.
After this, I just looped through the original elements, moved each one, and then calculated the 1000th, 2000th, and 3000th element in the list with:
Day 20: Part Two
Part two was such a small iteration over part one today. I only had to multiply each number in the input with the "decryption key" and run through the full iteration 10 times instead of only once.
Day 19
Loved today's puzzle. I got re-acquainted with a bunch of new stuff that I had studied in university many ages ago. Namely:
-
What linear programming is. I did not actually do anything with this info, but it was great to brush up.
-
Depth-first search and breadth-first search for trees.
For part one, I did not have to look anything up. The basic idea was to build a tree of possible successive states, branching at each state by the possible next states. Then, I walked down the tree and calculate the number of geodes at each final state, choosing the maximum for that given tree. I called trees "simulations".
I liked the approach I went with: I took advantage of the Option<State>
type to figure out the possible next states. The heart of the puzzle is the following method in the State
implementation:
Then, you have your sort of usual tree walk function that recursively goes down the tree. Initially, this was so painfully slow for the input 24 minutes that I had to figure out some optimizations. After lightly browsing /r/adventofcode, I understood that the main idea here is to try to prune as many tree branches as possible. This can be done in several ways. For example, an easy way was:
-
When building the state, calculate the maximum resource needed for each robot for each type of resource.
-
Now, when building the next possible states, you can ignore the "idle" state (
Some(self.clone())
above) if you already have the max of each needed resource, since you can only build a single robot at each minute anyway.
Since the tree growth is exponential on the number of possible next states, reducing that number has quite a big effect in general. With these optimizations, part one of the puzzle took a handful of seconds to run.
Day 19: Part Two
Part two's deal was bumping the minutes from 24 to 32, which is a significant exponential growth in the search space. So, I had to scout for a few more optimizations:
-
Say the number you mine for resource R reaches the maximum number of R needed for any robot. Also, say you won't be able to mine enough for new robots with the time left. Then, you can stop building robots that produce R. This is harder to write than to implement, but the main idea is to prune useless states where you build robots that you're not going to need.
-
The biggest optimization, by far, was to keep track of the maximum number of geodes produced in any final state of a simulation. When exploring that simulation, you can then prune whole subtrees where the optimal number of geodes produced is lower than that tree. The optimal number is calculated by pretending that you start producing a geode robot each minute from the current state, and you crack as many geodes as possible. This optimization ended up reducing the search space significantly.
With these, it took about ~3 minutes to solve part two. I saw on Reddit that many folks managed to optimize this enough to run in a few hundred milliseconds. That's pretty interesting, and as far as I could tell it was all done through other optimizations, playing with DFS/BFS, and stuff like that. Great (re-)learning experience today.
Day 18
Today I managed to squeeze in the puzzle. Parsing the input and defining types was straightforward:
;
Then, I went for an inefficient-but-effective approach. I added a method to Cube
that counts the exposed sides of the given cube when compared to a given set of other cubes. The trick of the function is to build a set of all cubes "adjacent" to the target one, and then check how many of those are in the given set of other cubes. The total of exposed sides is six minus that number.
Calculating all the exposed sides was then a matter of this:
let total_exposed_sides = cubes
.iter
.map
.;
This approach works because two cubes that are adjacent on one side are both adjacent to each other on that side, which means that we'll only count the uniquely-exposed sides.
Day 17
Same as yesterday… Had to skip for now!
Day 16
Had to skip for now due to time constraints, I'll try to come back to this later.
I went back to this. I used the help of /r/adventofcode again. The idea I found to work was:
-
Build an undirected graph of valves. I used the
petgraph
crate (in particular, thepetgraph::graphmap::UnGraphMap
type). -
Use the Floyd-Warshall algorithm to build a matrix of distance between each node in the graph, assigning the value of
1
to each edge. I typed the distance matrix as:, u32>);
and has one method,
DistanceMatrix::get(u, v)
, that returns the distance betweenu
andv
(orv
andu
, since the graph is undirected). -
Instead of exploring all solutions by moving one step at a time, I explored solutions by moving directly to the next valve to open, using the distance matrix to calculate how long it would take.
Building the graph was fairly easy. It looks something like this:
// Valve is just a struct with a few fields that you can easily guess
// from this function.
Then, the idea is to run simulations on successive states:
With this strategy, I found a solution after exploring less than 100k states, in about 300ms.
Day 16: Part Two
I didn't really know where to start on part two, and honestly out of laziness I went to Reddit straight away. This post suggested that I do something like this:
-
Run the simulation like in part one.
-
Now, take away all the valves opened by the human, and run the simulation again as the elephant, seeing which valves get opened.
With the data structures I used, the easiest way to do this was to set the flow rate of the vales opened in part one to 0
before running the "elephant simulation". Still under 100k states explored, and still under 300ms to get a solution.
Day 15
Today's puzzle was interesting: in the second part, I had to actually shrink my little brain and figure out an efficient way to solve it, instead of brute forcing. But let's start from part one. Data structures first.
// Love using tuple-like structs.
;
I won't show how I parsed the input into a Grid
, as parsing the input is becoming quite repetitive. The only interesting function in Grid
is Grid::manhattan_distance
. The best thing? GitHub Copilot basically wrote this.
For part one of the puzzle, I was able to find a solution through sheer brute force.
let target_row = 10;
let mut forbidden_positions = 0;
// Grid::points_in_row returns an iterator of points on the given y.
for point in grid.points_in_row
println!;
One thing to note: the more days go by, the more I find it useful to have a way to represent data structures on the terminal. In this case, I also implemented a Grid::draw
method, which helped me match my representation and calculations against the provided examples.
Day 15: Part Two
Part two was a fun challenge. The problem instructs you to search through the space (0, 0) -> (4000000, 4000000)
. That's 16 trillion points. Rust is fast, but apparently not that fast. So, I had to get clever. The thing is: I'm not clever! I rarely have to think through optimizations like these, because I tend to work in higher-level domains (like the Web™).
The intuition I followed was to build a list of ranges on each row from the list of beacons. Ranges are fairly cheap to build. I'm sure I could've done something more clever, but I went with something like this:
let mut ranges_by_row = new;
// Prefill each row with an empty vector. There's probably a better way to do
// this in Rust, but laziness got the best of me.
for y in self.top_left_corner.1..=self.bottom_right_corner.1
for in &self.sensors_and_closest_beacons
This was reasonably fast, in the ballpark of a few seconds tops. Then, for each row, I went through all the ranges and merged them. This was fun to write:
let mut merged_ranges = new;
for in ranges_by_row
Now the trick: the point we're searching for is on the only row (in the search space) that has two instead of one "detected ranges".
for y in 0..=4000000
This was done in a few seconds, too. There's a good chance that there would be a smarter and more efficient solution, but hey, not my day job. Another day another puzzle!
Day 14
It seems to me like picking the right data structures, just like in the real world, is a game changer when it comes to Advent of Code puzzles. Today, I was lucky enough to start out with a data structure that made solving the problem straightforward.
Usually, when the puzzle input is a sort of grid, I think many of us tend to reach for arrays of arrays (vectors of vectors in Rust) to represent the grid as a bi-dimensional matrix. Point (x, y)
can be accessed as grid[x][y]
. However, I find that sometimes a map (or dict, depending on the language) of (x, y)
to value is a good alternative. That's what I used for today's puzzle. The HashMap
came in handy especially for the second part, as we'll see later.
For part one, I started out with a Point
type alias and a function for initializing the "world" (that is, the grid map):
use HashMap;
type Point = ;
type World = ;
Then, it was time to parse the input. This was the deal: go through each input line, go through each x1,y1 -> x2,y2
pair, and "draw" lines between each pair. Drawing a line with the World
data structure meant adding a (x, y) -> '#'
entry for each point on the line.
The heart of the puzzle is pouring sand. So, I wrote a pour_sand
function that does exactly that:
// Returns the point where the sand comes to rest.
Finding the solution was a matter of loop
ing until we could not put sand to rest anymore.
Day 14: Part Two
The second part was fairly straightforward with a couple of small changes. The first one was changing the World
type a bit: now, I wanted a struct with the same map as before, but also the y
coordinate of the "floor".
Then, the trick was to replace the HashMap::get
method in pour_sand
with a custom at_point
method in impl World
. The custom method accounts for the presence of the floor, and looks like this:
Now, the solution was about loop
ing until a sand's resting point was (500, 0)
(the starting point).
I loved today's puzzle: simple enough to be done in a reasonable amount of time, but still fun to work through.
Day 13
Today's problem could be solved with different data structures. The important property was the ability to arbitrarily nest lists of integers. I used a well-known data structure for us functional programmers: linked lists. Thanks to day 7, I now know a bit more about Rust's smart pointers, so I was able to use Box
es here. I started with the main data structure. A linked list is made of a cons cell that holds a value, plus a link to the next cons cell. In this case, since we can have nested lists, a cons cell can be:
- an integer (I went with
u16
) - a linked list (think of the first nested list in
[[1], 2, 3]
) - the empty cell, which signals the end of the linked list
There are several ways to model this. You can use Option
to model the empty cell as None
, for example. I went with an approach I liked.
// The value contained in a cons cell.
// This is essentially a cons cell itself.
After this, the problem was really about two things:
- Parsing a line of input into a linked list.
- Making linked lists orderable in order to compare them.
To parse lines into LinkedList
structs, I went with a simple function in the LinkedList
implementation:
Then, I took the chance to learn a bit more about Rust's trait, and in particular std::cmp::Ord
. Pretty straightforward stuff: you implement a cmp
function that returns a Ordering
enum (equal, less than, greater than).
// This required me to implement PartialOrd as well, which I did
// by just proxying to Ord. That's exactly what the docs for Ord
// show, after all.
The implementation of cmp
is tedious to look at, so I'm including it in the collapsed code block below. It essentially implements the rules described by the puzzle description.
fn cmp(&self, other: &Self) -> Ordering
Now it was just a matter of counting the pairs where left < right
, which we can do thanks to PartialOrd
.
Day 13: Part Two
Today's second part was quite easy to build on top of part one. This is especially true in Rust, where PartialOrd
allowed me to build a Vec<LinkedList>
and then just call sort
on it. All the new code I needed to solve part two is what's below.
let mut packets = input
.lines
.filter
.map
.;
// Push the divider packets.
let divider_packet1 = from_string;
let divider_packet2 = from_string;
packets.push;
packets.push;
// Comes for free with PartialOrd.
packets.sort;
let position1 = packets.iter.position.unwrap + 1;
let position2 = packets.iter.position.unwrap + 1;
println!;
Day 12
No time for screencasting today, so I'm gonna type it out real quick.
Today's puzzle was about applying Dijkstra's shortest-path algorithm. The algorithm is somewhat straightforward, and the thing that took me some time was figuring out little things here and there to do with distance between vertices and so on.
It took me a while to get to this implementation, and I won't really go through the steps today. I'll just explain what I got to. First off, a couple of data structures, one for nodes in the graph and one fo the graph itself:
;
Then, a few methods on Graph
. They're all standard graph methods, and I could've probably used any graph library for Rust, but I wanted to try this out myself. The body of the methods below is not included here, but you can find it out on GitHub.
Then, just a couple more helper functions:
After that, I really just went through the Wikipedia pseudocode and turned it into Rust.
fn dijkstra(graph: &mut Graph, start_node: Node, end_node: Node) -> Option<u32>
The function is more parametrized than needed for part one, but it turned out to be useful for part two (hindsight!). With this code in place, it's a matter of running the dijkstra
function.
Day 12: Part Two
For the second part, it's about "brute forcing", really. We want to calculate the dijkstra
function above for all starting point a
s. It was a bit slow to run, but it could've been parallelized easily. Oh well. Not enough time today!
Day 11
Part one went smoothly. Part two took quite a lot, and I ended up needing some help from r/adventofcode. Bums me out a bit, but hey, never stop learning and being humbled!
Day 10
You know the drill: it's a screencast.
Day 9
I didn't have time for a screencast today, so we'll walk through the solution here.
I started out with a few data structures. The first thing was trying out type
aliases in Rust:
// Grid looks like this:
// (2, 0) (2, 1) (2, 2) (2, 3)
// (1, 0) (1, 1) (1, 2) (1, 3)
// (0, 0) (0, 1) (0, 2) (0, 3)
type Position = ;
A position can be negative, too, that's why the indexes are i32
s. Next, we have "moves":
Pretty straightforward to model this with types.
Alright, the central data structure of this puzzle is a rope. For the first part of the puzzle, I started with a simple struct with head
and tail
fields.
To count the visited positions, instead, I opted for a HashSet
. The initialization looks like this:
let input = include_str!;
let mut visited_positions: = new;
let mut rope = Rope ;
Then, the gist of the problem is going through all moves in the input, update the rope, and then print out the number of elements in visited_positions
at the end.
for move_ in input.lines.map
println!;
I won't bother you with the details of .move_head()
and .update_tail()
here, but it's a bunch of index math (that you can find in GitHub).
Day 9: Part Two
The second part was really about generalizing the rope to have a bigger number of knots. Instead of having just head
and tail
, I went for a fixed-length slice:
Then, it was a matter of renaming update_tail
to update_other_knots
. The math is the same, but generalized to go through all knots and make each one "follow" the next, with the exact same logic as before.
One note I had fun with: I got stuck initially with some small bugs here and there, and the best thing I did was writing some code to print the grid. Visually debugging the puzzle against the example one on the website helped a lot. To do that, I implemented the std::fmt::Display
trait for my Rope
struct.
Day 8
Today's puzzle was pretty fun, and not that hard. I opted for a screencast again.
Day 7
Holy. Shit. I do not know Rust. I started recording a screencast for today's puzzle, but I stopped in anger one hour and twenty minutes in. It took me around four hours to get this right. As it turns out, recursive data structures are easy in functional programming languages like Elixir, but a whole other level of nightmares in something like Rust. That's due to the ownership and all that jazz. I struggled a lot with the tree data structure that is kind of needed to solve the puzzle: how to reference parent and children nodes was just not working for me.
In the end, I had to do just a bit of searching the web, and I stumbled across this post on how to implement these recursive data structures in Rust. I thought of using Box
initially, without having any idea what that really is, but I would have never arrived to the conclusion that the solution was Rc
and RefCell
.
In any case, let's go through my solution, without the four hours of head smashing thought process. Let's start out with some data structures: the file system is a tree. Each node can be a directory or a file. Directories can have children, which are other tree nodes. Directories and files also have a parent node, but I only bothered with adding it to directories (and not to files) for simplicity.
One interesting thing was that I had to implement the Debug
trait for Node
, because otherwise it would recursively try to print a node's parent, which prints another node that has children, that have parents, and so on.
Then, just a couple of methods on the Node
struct to construct it, append nodes, and calculate the size:
After this, I went with a couple of enums to give structure to the lines in the input:
// A command is either "cd <>" or "ls".
I also wrote a small function to parse each line into one a Line
:
fn parse_line(line: &str) -> Line
Alright. Now for the heart of this puzzle: a function that walks through all the lines in the input, and builds out the tree data structure.
let input = include_str!;
let root = new;
let mut current_dir = clone;
let lines = input.lines.skip.map;
for line in lines
A lot of .borrow()
, .borrow_mut()
, Rc::clone()
, and what have you. I'm sure this is not peak Rust, but I'm pretty proud that this worked!
To solve part one of the puzzle, I then ended up writing a small function to calculate the size of all directories whose total size is at most 100_000
.
fn calc_size(node: &Node) -> u64
Day 7: Part Two
The second part had virtually no complexity once I had the first part down. I just:
- Walked the tree again.
- Calculated the size of each node.
- If the size of a node was enough to "free up enough space", I stored it as the minimum size.
I got this one right on the first try, and it took about 2% of the time it took to do the first part! Crossing my fingers that tomorrow's puzzle will have more mercy on me.
Day 6
A screencast you said? Let's try that! I'm tired of typing!
Day 5
Ah, this starts to be more convoluted. The most annoying thing with this puzzle was to parse out the "stacks", since each stack is on a variable number of input lines.
For this puzzle, I took a bottom-up approach. I started out with creating a new type for a "move", and a function to parse a move N from X to Y
line into said struct.
// Deriving PartialEq because I want to be able to check if move1 == move2.
I'm starting to get the hang of references. I understand that the input to this function needs to be a &str
because I want just a little window to that string, and I'm not modifying it in any way. We're good.
Next is a little function to find the line that goes 1 2 3 ...
to get to the count of stacks in the input. In hindsight, this is probably unnecessary as I could've hardcoded that to 9
, but here you go if you're curious.
fn count_stacks(input: &str) -> u16
Last but not least, a way to parse stacks. I created a new Stack
struct first:
Then, I went with the most imperative approach I could think of. In a functional language, I'd have probably tried to matrices-and-transpositions my way out of this, but here I went with just slicing strings. I created a parse_stack()
function that takes the whole input: &str
as well as a "column index". Here you go:
Now, the final push: getting it all together. First, I built the "world", which is just a vector of Stack
s:
let input = include_str!;
let mut world: = Vec new;
for column_index in 0..count_stacks
// A couple of tests can't hurt.
assert_eq!;
assert_eq!;
Now, I parsed all the Move
structs:
let moves: = input
.lines
.filter
.map
.collect;
// Spot check the first move.
assert_eq!;
Finally, I iterated through moves
and applied each move:
for move_ in moves
// This is to get the puzzle output in the desired format.
let top_chars_iter = world.iter.map;
println!;
The move_crates
function was fun to write because it was the first time
I used &mut
. I had to pass &mut world
to the function in order to be able
to modify the stacks in place in the function.
The most interesting thing about the code above? I tried to let
both start_stack
and end_stack
after each other, but the compiler yelled at me after I successively tried to modify start_stack
. This was pretty cool, and it makes sense: I can have one mutable reference to a piece of world
at a time. If I get one by doing &mut world[...]
, then I need to use it first, and only then I can get more references. Neat, and I'm quite happy that I have some idea of what's going on.
Day 5: Part Two
The second part didn't take too long. It's essentially about swapping the pop()
+ push()
calls with a pop_many
-sort-of-thing that keeps the order of the popped crates. I let Copilot guide me in writing this:
Wrote my first function with a parametrized type! How cool. Now, I rewrote move_crates()
from earlier into move_craates_9001()
(after the model number of the new crane):
Another successful puzzle solution!
Day 4
- Split each line into two ranges,
left
andright
, by splitting at the comma,
. - Convert each text range (
<start>-<end>
) into a range data structure (Rust should have one). - Count the occurrences of range pairs where
left
is included inright
orright
inleft
.
The first nice thing I found out today is the include_str!
macro. It's exactly what I want: it reads a file at compile time and includes its contents into the compiled binary as a static string.
let input = include_str!;
Ready to go. First, I split up each line into two (text) ranges. I found the .split_once()
method for strings, which does exactly what I want. I like that it returns a Option
type.
for line in input.lines
Next, I wanted to write a small helper function that parses a textual range into a std::ops::Range
struct. Hello, Copilot! Wow. I wrote the signature (fn parse_into_range(input: &str) -> Range<u32> {}
), and Copilot did its magic:
I think I can do better at naming variable, but other than that, Copilot beats me easily. I understand Rust ranges are [...)
, so we should do right_range + 1
there to behave like the puzzle description wants us to. But I'm really only using Rust ranges here to play around, because we'd probably be better off with a (u32, u32)
tuple anyway.
Now I want a function that checks whether a Range<u32>
is contained in another range. Once again, Copilot sketches it out great:
Ah, AI, you. I can't read that because of the order in which the boolean expression is written, but it does check if right
is contained in left
. Amazing.
All that's left now is to count occurrences. Once again, let's go imperative mutable style.
let mut count: u32 = 0;
for line in input.lines
Here, I changed is_contained()
to take &Range<u32>
values instead of Range<u32>
. I'm not sure if it was the right thing to do, but the checker was complaining about moving values. It makes sense that here I only want to pass references to the same piece of data around, and I'm not modifying anything, so let's go for this.
Day 4: Part Two
Okay, easy peasy. The change is that the puzzle is now about finding overlapping ranges, not just included ranges.
I started with a is_overlapping()
function, and it turns out the boolean check I wrote was buggy. I submitted the puzzle solution and, for the first time this year, I got yelled at! Guess it's time for some testing, isn't it. I don't know much about Rust testing, but I saw assert_eq!()
used everywhere, so I figured it'd be easy to test is_overlapping()
before doing anything in the main function.
What I got is something like this:
// Returns true if left and right overlap.
The assertions look something like this:
Works now.
Day 3
The "translated-to-computerese" puzzle is this:
- Split each input line in half to get the items in the two compartments.
- For each line, find the char that appears in both halves.
- Associate a score with that char.
- Sum the score for all lines.
When I hear "appears in both halves", the first thing I think is sets. Rust seems to have a pretty straightforward set implementation in the standard library, std::collections::hash_set::HashSet
.
I'm taking the same approach as day 2 with the total
accumulator and the for
loop. For each line, I split it in half, turn each half into a set, find the intersection of the two sets, and calculate the score associated with that character.
priority
is a small helper function that uses ASCII trickery to assign scores to a..z
and A..Z
. There's probably a better Rust way to do that, but I'm going for what I know.
Day 3: Part Two
Okay, the second part boils down to:
- Get chunks of three lines each from the input
- Split each line into characters like before
- Find the character in common among the three lines
- Calculate its score and sum all the scores
The main struggle I found here was, wait for it, doing the intersection of three sets. I'm not good with references and pointers and whatnot. What I ended up doing is looking like the worst Rust code I've ever seen!
The first step I did was to get the lines in the file and chunk them up into 3-line chunks.
let contents = read_to_string.expect;
let lines = contents.lines.;
let chunks = lines.chunks.;
I kind of let rust-analyzer figure out the types here. It's not the clearest thing to me. Then, for the main for
loop:
let mut total: i32 = 0;
for chunk in chunks
Rust people: how do I make this right?
Day 2
Okay, I'm already over doing stuff in src/main.rs
. I create src/day2.rs
.
The gist of the puzzle:
- Parse each input line into a
(opponent_choice, my_choice)
tuple. - Calculate the score of
my_choice
based on the rules. - Calculate the score of the game (win, loss, draw).
I start with making an enum for the possible choices. Overkill? Maybe, but I'm not used to static types, so I like doing stuff like this.
Awesome. Now, I kind of translate the algorithm above. In the spirit of trying new things, I go for a for
loop instead of the functional map + sum
approach that I'd naturally go for.
Now, I just need to implement the two helper functions. I love pattern matching.
Day 2: Part Two
The second part here is about turning the second letter in each line into a "round end", that is, a win, loss, or draw. That sounds like an enum to me.
Now we can parse the round end into a RoundEnd
enum with a match
expression as before, and then calculate which choice we should make in order to finish the round as instructed. That function looks like this:
Calculating the total is a matter of parsing the round end, feeding it into this function, and adding up to total
.
Day 1
This is set-up day. I'm not completely clueless about Rust, so I have Cargo installed and I'm capable of creating a project. I created a new aoc22
crate. I shove puzzle inputs in there with something like:
I did the whole first day in src/main.rs
. Too early for refactoring!
The gist of the puzzle is to:
- Split a file into "chunks of lines" separated by an empty line.
- Parse each line into an integer and calculate the sum of the lines in each cluster.
- Find the cluster with the highest sum.
The solution, with comments:
I have not used Copilot or ChatGPT yet at this point. This was fairly straightforward. Some notes:
-
I like the
.expect()
and.unwrap()
calls. For example, I expect that reading a file would return a result type, and I like having a simple method to panic on the non-OK result. Practical for use cases like this. -
My solution is pretty functional. I would have written something very similar in Elixir.
Day 1: Part Two
The second part of the puzzle boils down to finding the sum of the three highest chunks. It takes little changes to get there: I only need to sort chunk_sums
, get the first three elements, and sum those.
let mut chunk_sums =
// Same as before
.;
chunk_sums.sort;
chunk_sums.reverse;
chunk_sums.truncate;
println!;
It feels weird to modify the data in place. Sure, it's efficient! I know this could be done more efficiently. I could go through chunk_sums
and keep just three i32
s for the top three integers, but it's a small data set plus a fast language plus toy code.
The let mut
makes sense here. I love that I have to declare that a variable is mutable.
Surely I'm missing something, but I'm also weirded out by having to turn chunk_sums
into an iterator in order to call .sum()
on it. Why wouldn't Vec
implement something like sum()
directly?