Advent of Code 2015: I Was Told There Would Be No Math

February 25, 2020

Now that the first challenge is behind us and we’ve built up a little experience, we’re going to jump right into the second Advent of Code 2015 problem. This one does require a bit of math, but it’s all laid out by the challenge problem and doesn’t require you to actually know any of the equations.

Part A: Wrapping Paper by the Foot2

The first problem statement is fairly dense, so I want to take some time to distil it into easily digestible points; I encourage you to try this for yourself before reading on because it’s an important skill to practice.

The elves are running low on wrapping paper, and so they need to submit an order for more. They have a list of the dimensions (length l, width w, and height h) of each present, and only want to order exactly as much as they need.

Fortunately, every present is a box (a perfect right rectangular prism), which makes calculating the required wrapping paper for each gift a little easier: find the surface area of the box, which is 2lw + 2wh + 2hl. The elves also need a little extra paper for each present: the area of the smallest side.

All numbers in the elves' list are in feet. How many total square feet of wrapping paper should they order?

Advent of Code, 2015, Day 2

When I look at this problem statement there are five things that stand out to me, yours may be slightly different and that’s okay. I noticed that:

This kind of terse requirements list is great to work from while solving a problem because it’s easy to scan for information.

I have to parse a list of gift dimensions, so to start I like to peek at the input format and decide how parsing responsibilities should be split between potential components. Looking at the input file, it’s made up of line-wise descriptions of gifts using a simple LxWxH format.

4x23x21
22x29x19
11x4x11
8x10x5
...

Thinking a little, I’ve decided that the parsing of an individual line should be done by whatever method is responsible for returning the area value. To me, this presents a nice interface where I can input a String like "2x3x4" and get back the area as an Integer like 58, and allows me to easily iterate over the lines in the file without a discrete parsing step. I’m going to begin with creating a few tests for the necessary_paper method I will create. Please be aware that in order to save space I am not showing the surrounding tests method, so click through to the source to see the full implementation.

assert necessary_paper('2x3x4'), 58
assert necessary_paper('1x1x10'), 43
assert necessary_paper('1x1x1'), 7

Now, I don’t think necessary_paper really needs to have the parsing for the LxWxH values in-line; I would prefer a separate class to have the responsibility of calculating geometry equations and the line parsing can be folded into that class. Taking these things into account, I’ve implemented the necessary_paper method as follows:

def necessary_paper(dimensions)
  b = Box.from(dimensions)
  b.areas.min + b.areas.sum * 2
end

If you recall from the list of requirements I made above, the total paper for a given gift is the sum of the smallest side’s area and the surface area of the gift. The Box#areas method returns the areas of the three distinct faces as an array, so we can easily use it to find the smallest area and the total surface area. One subtlety to this implementation is that I am returning the three distinct faces in areas, but each of these faces occurs twice since rectangular prisms have 6 sides, hence the multiplying by 2. If that doesn’t make sense to you, here is a short set of algebraic transformations that should be fairly easy to follow along with.

2*L*W + 2*W*H + 2*H*L = b.areas.sum * 2
                      = (L*W + W*H + H*L) * 2
                      = 2*L*W + 2*W*H + 2*H*L

With the necessary_paper method complete, I’m ready to start working on the supporting Box class, which I’ve decided to implement as a value-type using Ruby’s Struct class.

Box = Struct.new(:l, :w, :h) do
  def self.from(str)
    Box.new(*str.split('x').map(&:to_i))
  end

  def areas
    [l * w, l * h, w * h]
  end
end

The static method from is where I have chosen to push the line parsing responsibility, but it doubles as a very readable way of constructing a Box instance — I think Box.from(dimensions) reads beautifully. The interior call to Box.new might be difficult to understand, so I want to break it down for clarity.

The * syntax at the beginning allows the individual parameters of a method to come from an array; if I had a method call m(1, 2, 3) this could also be written as m(*[1, 2, 3]). When you’re building parameters dynamically this syntax is incredibly useful! Next there is the str.split('x') expression to turn the "LxWxH" entries into arrays like ["L", "W", "H"]. Finally map is called on that array and applies the to_i method to each String within it resulting in an array of Integer values for each dimension of the gift.

The tests I wrote pass now that the Box class implementation is complete, and as the next step we can start implementing the solve_a method. I will start by writing a test for the method, keeping in mind that I previously decided this function will take multiple lines of input.

assert solve_a("2x3x4\n1x1x1"), 65

From this test, the implementation of solve_a seems obvious to me; I know the input must be split line-wise, that each line needs its necessary paper area calculated, and a sum of those areas is the result.

def solve_a(input)
  input.split.map { |d| necessary_paper(d) }.sum
end

Very quickly my solution is complete and highlights Ruby’s potential for writing succinct, left-to-right expressions which solve problems elegantly. This is also the reason I have enjoyed learning Ruby over the last few weeks — it’s uncanny ability to stay out of my way.

If I run the solution for part A, I get the correct answer.

$ run -y 2015 -q 2 -a
1598415

Part B: Ending with a Bow

The second part of this challenge is a similar task involving finding the length of ribbon required for each gift.

The elves are also running low on ribbon. Ribbon is all the same width, so they only have to worry about the length they need to order, which they would again like to be exact.

The ribbon required to wrap a present is the shortest distance around its sides, or the smallest perimeter of any one face. Each present also requires a bow made out of ribbon as well; the feet of ribbon required for the perfect bow is equal to the cubic feet of volume of the present. Don’t ask how they tie the bow, though; they’ll never tell.

How many total feet of ribbon should they order?

Advent of Code, 2015, Day 2

Again, I am going to condense this description into a set of bullet points to ensure I understand the requirements. The important bits I see are:

The implementation I created for pat A will help immensely now, I can extend the Box class to include methods for these new requirements. First, I want to create some tests for a new method called necessary_ribbon which will follow the same interface as necessary_paper: it will take a String and return the total length as an Integer.

assert necessary_ribbon('2x3x4'), 34
assert necessary_ribbon('1x1x10'), 14
assert necessary_ribbon('1x1x1'), 5

These are nearly identical to the prior tests we’ve written — it’s very easy to hammer out new tests to ensure we’re implementing things correctly when we follow a similar interface for each method. The body of necessary_ribbon will also look familiar because of that interface similarity.

def necessary_ribbon(dimensions)
  b = Box.from(dimensions)
  b.perimeters.min + b.volume
end

I had to make an interesting decision regarding whether Box should know about the ribbon requirements and return the minimum perimeter directly, or if it should simply return the face perimeters. I’ve opted for the latter because I consider it cleaner for Box to avoid knowledge of the challenge requirements, it mirrors the areas implementation nicely, and I feel that it’s not too inefficient to call min on a 3 element array.

def volume
  l * w * h
end

I began by implementing volume since it’s trivial to complete, but do allow yourself to enjoy the simplicity and low ceremony of Ruby here, as I did, before progressing to the perimeters method.

def perimeters
  ll = l + l
  ww = w + w
  hh = h + h
  [ll + ww, ll + hh, ww + hh]
end

One thing of minor interest with perimeters is I’ve chosen to cache the reusable, partial perimeter sums to improve the legibility of the final array expression. With these two methods complete, I can move on to writing tests and code for the solve_b method.

assert solve_b("2x3x4\n1x1x1"), 39

I used identical test input again for quickly implementing the new assertions; having a similar interface is really great. I’m sorry to say that solve_b is really not interesting — it’s almost identical to solve_a.

def solve_b(input)
  input.split.map { |d| necessary_ribbon(d) }.sum
end

Of course, now I run the solution to get the correct answer for part B.

$ run -y 2015 -q 2 -b
3812909

All Wrapped Up

With that done challenge 2015-2 is complete and it was really pretty simple, but it still gave me an opportunity to learn about the Struct class which Ruby offers. In my opinion that’s a win, and proof that a challenge doesn’t have to be very difficult to provide a valuable learning experience.