Advent of Code 2023 Day 2 Highlights

2023-12-02
aoc
aoc2023
elixir
livebook

Day 2 was more of a warmup kind of challenge, and funnily enough was much faster to solve for me than the day 1’s task. I used Elixir with Livebook this time too, primarily because I didn’t feel like there will be much difference in writing this in any of my preferred languages, except for Rust perhaps. Another thing that was pretty typical about this day is that first 7-8 days of AoC are dominated by parsing the input. After that the algorithmic part becomes more complex, but here it was a usual situation of more code needed to parse the task than to solve it :)

Anyhow, here’s my solution’s code.

Talking about parsing, there are usually 2 ways people approach it for those tasks:

  • String.split and similar tricks up to a full blown recursive-descent
  • Regular expressions

I don’t have a strong preference either way, usually. This particular input was easy to parse with a combination of String.split and pattern matching, so that’s what I went with.

I also find a bit of domain modelling very helpful when working on these tasks. I just modelled the input as two structs: Game (with the game id and a list of rounds) and Round with red, green, and blue cube counts.

The most interesting part wrt programming technique today was the parsing code still. Specifically, this code:

defmodule Round do
  defstruct red: 0, green: 0, blue: 0

  def parse(round) when is_binary(round) do
    String.split(round, ", ")
    |> Enum.map(fn entry ->
      [n, color] = String.split(entry)
      {String.to_existing_atom(color), String.to_integer(n)}
    end)
    |> then(fn fields -> struct!(Round, fields) end)
  end
end

There are a couple of interesting things happening here. We use default values for the struct fields (0), and this saves us some typing here: we can split each "3 red, 4 green", etc. on the ", ", which gives us a list of strings that we can split on space, giving us a list of two elements looking like ["3", "red"]. The number is easy enough to convert with String.to_integer, and the name of the color is the name of the field we use, so we can just use String.to_existing_atom to safely convert it to an atom.

Now, if we place those in a tuple of a form {:red, 3}, the resulting list will be a prop-list looking like [red: 3, green: 4]. Here, struct! from the Kernel module comes to our help: we can pass the name of the struct together with a prop-list of the fields with their values to it to construct the corresponding struct, in our case Round. And, since the construction logic will be pretty much the same as if we were to write %Round{red: 3, green: 4}, the default value we set on the blue field will also be used, giving us the result we want.

There’s only one complication: struct! expects the name of the struct as the first argument, but the pipeline operator |> will inject the first argument in the function, and in our case this is the prop-list. Thankfully, in the same Kernel module we have then macro! This macro accept a function (typically a lambda) and allows you to call another function from it while placing the argument you received at any argument position you wish. This is not necessary, of course: we could’ve just bound the prop-list and called struct!(Round, fields) outside of the pipeline. But it’s a neat trick to know.

The “main” functions for both part 1 and part 2 were placed in the Game module. Round struct turned out to be a good enough representation of the max number of cubes of each color needed for the part 1, as well as a good way to represent the minimal required number of cubes in part 2. Otherwise, it was just the usual Enum.map, Enum.reduce, and co.

If you enjoyed this content, you can sponsor me on Github to produce more videos / educational blog posts.

And if you're looking for consulting services, feel free to contact me .