I try to participate in Advent of Code every year. Most years I end up only going for a week or two before life gets in the way and I fall behind and give up. So, this year, my only goal is to do better than I did last year (which I did in F#, a language that was very new to me at the time).

This year I plan on doing most of the days in Rust, though I did start out today with C# just to get my feet wet and solve the problem quickly. I use C# for work, and I’m a big fan of the language and the new .NET ecosystem.

tl;dr

You can find my solutions here.

Day One, Go!

Today’s puzzle was pretty straightforward. Each line of input is a piece of food in an elf’s inventory, and each elf is separated by a double newline. That means that, for each line, we need to do one of two things:

  1. Add the current line to a specific elf’s inventory, or
  2. Move on to a new elf

I’m a big fan of abstraction over modeling. In the end, abstractions let us work with data much more efficiently than laboriously modeling every iota of available data.

In this case, elves are just a collection of numbers, each number representing a caloric value for a piece of food. A lot of newer developers would probably be tempted to make a couple classes when working in a highly-object-oriented language like C#: A Food class and an Elf class. The thing is, we don’t need any of that. Elves don’t need to have eyes and ears and clothes for this problem, and it’s not likely that the data from this puzzle will be relevant moving forward.

As such, I opted to use two lists of longs (or two Vectors of u64 in Rust): one to keep track of the current elf, and one to keep track of all elves. Whenever we hit a new line, we check if that line is made up of a bunch of digits with the regular expression \d+. If the line matches that expression, we add the value represented by that line (by parsing it to our numeric data type) to the currentElf list (current_elf vector in Rust. . .you get the idea). If the line does not match that regex, we add up the values in the current elf collection and plop that result into the collection containing all the elves.

Wait, why don’t you just add them as you go?

It turns out, I could have just added up each elf’s inventory as we went, obviating the need for two lists. However, when starting on part one, I figured I’d get screwed and have to make a list anyway if each discrete food item in each inventory became relevant. Maybe they have to swap supplies with one another or something. Turns out, no, I just needed the totals for each elf.

Well, now that that’s sorted. . .

From there, part one just has us find the elf with the most calories in his inventory. SInce our elves are being abstracted away into simple integers in a list, that is as simple as performing a Max call, which is what I did initially. This gave us the correct answer for part two.

However, this is where we hit our first “curve ball” ibn Advent of Code this year. Very often, the second part of a puzzle makes you re-think how you work out the first part. I mentioned previously that I made sure to have an array containing all of t eh calorie values from each elf, at least long enough to calculate that elf’s total calories. That wasn’t necessary, but it did become necessary for me to sort the array, rather than simply finding the maximum value.

Luckily for me, both C# and Rust have a sort functionality that I could just call on the collection of elves. Since part two just wanted the total calories held by the top three elves, I just had to grab those elves, add up their caloric load, and dump the result to the console

Conclusion

Today’s puzzle was an excellent opportunity to talk about the concept of abstraction. Very often in software development (especially in object-oriented languages), we are tempted or straight up told to model our problem space faithfully. We encapsulate the domain as closely as 1:1 in objects as we can. An elf gets an inventory, and that inventory can contain food items, and maybe even other items, for example.

But Advent of Code puzzles often benefit from abstracting away as much as possible. My favorite example of this was my solution for 2021 - Day Six. The state here gets so massive that tracking individual fish is a fool’s errand. Some of the naive implementations of this, which modeled each fish discretely, were taking people hours or even days to run. However, mine ran so fast that I arbitrarily added a Part Three, where I let it run for 8192 simulated days with no issue whatsoever. I had to stop there, because any larger was starting to overflow the uint64 data type!

So, if you take anything away from this article, let it be this: Lean heavily on abstractions. They make your life much, much easier!

Bonus Round: An Alternate Method

I was tempted to try just splitting the input file on double new lines (\n\n) and collapsing the resulting arrays into numbers in a “single pass” as it were. Currently, we’re kind of doubling the enumeration, since we go through every item in the list, and then convert it to a number, and then iterate through the resulting list again.

However, I felt that treating blocks of numbers as discrete objects rather than as individual pieces of data was removing a layer of logical abstraction. This is an interesting take, because it’s kind of the opposite of what most people mean by “layer of abstraction”.

Generally speaking, when people are talking about solving problems with computers, they talk about layers of abstraction from the bare metal. That is, abstracting away the specifics of how a computer works and working at higher and higher abstraction levels. However, the levels of abstraction that I’m talking about are abstracting real world concepts down to something that is easy to process on a computer.

Maybe this needs to be its own blog post some day, but I think the ideal level of abstraction is the one that strikes the best balance between “abstracting away from the inner workings of the computer” and “abstracting away the details of the real world so that we can use a computer to analyze it”.