Day 11: Hex Ed

Notice: If you have coding experience, I highly recommend to try and solve the puzzle in a language you already know. It's a lot of fun and you'll be able to focus on Go more later on.

Do you like it a little more difficult? Yeah, me too. Go read the problem description, it’s a fun one. Go on, I’ll wait here until you’re ready.

Good, you’re back! Let’s get started. On the agenda for today: anonymous structs and test fixtures.

There are a kind of evil puzzles in the advent of code. It’s the kind that sound simple enough, but trick you into a long and complicated solution. And a very simple solution may be waiting just around the corner. This is one of those puzzles for me. I went down the complex path of trying to “compress” the path along the hexagons, replacing two moves nw and s with a single move sw as example. I won’t post that solution here - it’s too long and ugly - but you can still find it in my github repository.

After solving the puzzle, a friend pointed me to a site with a lot of theory about hexagonal grids: https://www.redblobgames.com/grids/hexagons/ It’s a real treasure for todays puzzle. It allowed me to trim down my ugly, slow solution to a super fast solution with a single loop.

func abs(i int) int {
	if i < 0 {
		return -i
	}
	return i
}

func distance(x, y int) int {
	return (abs(-x) + abs(-x-y) + abs(-y)) / 2
}

func walk(in []string) (int, int) {
	max := 0

	var d, x, y int
	for _, i := range in {
		switch i {
		case "nw":
			x -= 1
		case "se":
			x += 1

		case "n":
			y -= 1
		case "s":
			y += 1

		case "ne":
			x += 1
			y -= 1
		case "sw":
			x -= 1
			y += 1
		}
		d = distance(x, y)
		if d > max {
			max = d
		}
	}
	return d, max
}

But how was I sure that this solution really did the same thing as my first solution. With tests, ofcourse! On day 5, I showed you the test framework that comes with the Go language, but I kept the usage very basic: one function, one check. In many cases you want to run the same check for many input/output combinations. And you definitely don’t want a seperate function for each test case. That’s where test fixtures come in.

Test fixtures originally come from hardware manufacturing. They’re a set of tooling necessairy to execute the tests. In our case, the tooling is a set of inputs with their known output values. Those can located on disk, but it’s often easier to keep them in the code. If we want to keep it in our code, we could create a struct to hold the input and its corresponding expected output value:

type PartAFixture struct {
	input  string
	output int
}

func TestPartA(t *testing.T) {
	fixtures := []PartAFixture{
		PartAFixture{"ne,ne,ne", 3},
		PartAFixture{"ne,ne,sw,sw", 0},
	}

	...
}

There’s a couple of things we can improve on this piece of code:

  • Leave out the struct name of the new objects in the array: PartAFixture{"ne,ne,ne", 3}, becomes {"ne,ne,ne", 3},.
  • Structs don’t necessairily need to have a type name. The struct definition can be anonymous and inlined: []struct{...}{{...},{...}}
func TestPartA(t *testing.T) {
	fixtures := []struct{
		input  string
		output int
	}{
		{"ne,ne,ne", 3},
		{"ne,ne,sw,sw", 0},
	}

	for i, f := range fixtures {
		out, _ := walk(strings.Split(f.input, ","))
		if out != f.output {
			t.Errorf("Case %d failed: got %d, wanted %d", i, out, f.output)
		}
	}
}

That’s it! Now you can go back and create proper tests for all the previous puzzles ;) As usual, my full solution is available in my github repository. See you all tomorrow!