Advent of Code Tips

The most important thing about Advent of Code is: read the puzzle description and test input several times. There is often an edge case mentioned which isn’t obvious from a brief skim of the text.

Don’t get too disheartened if you get stuck, it’s unlikely that you are the only one. One year there was a puzzle with an ambiguous definition, and depending on how you interpreted it (there were two possibilities) the puzzle would either be trivial or impossible.

Remember that usually the most important part of determining how long your solution takes to run is the efficiency of your algorithms. Don’t worry about the most efficient way to read the input, whether to process data as it is read or afterwards etc. Don’t be afraid to break down each part into smaller bits, or create lots of small data structures - even if a struct type is simply a wrapper round a native data type to make it easier to read.

Reading input

Reading the input in one go is often the easiest solution. I also prefer to take the puzzle input from stdin, as this avoids having to implement command line arguments to get the filename. Unfortunately Go does not have a ‘read file contents into string’ function in the standard library (unlike PHP with file_get_contents) - instead you have to read in bytes and then convert them to a string. The way I do this is:

inputBytes, _ := os.ReadFile(os.Stdin.Name())
inputString := string(inputBytes)

This covers both scenarios:

Independent puzzle lines: This is the most frequent occurrence, especially in the early days. Having the input as a single string means you can call strings.Split(input, "\n") to get a slice of lines.

Dependent puzzle lines: This happens in later days, especially when you have a grid. Having the input as a single string is necessary because you often need to move back and forth, and a data structure may be made up of multiple lines.

A single string of input is also easy to implement as a test fixture. For example, most of my tests have something along the lines of:

var testInput string

func TestMain(m *testing.M) {
	testInputBytes, _ := os.ReadFile("testdata/test.txt")
	testInput = string(testInputBytes)

	os.Exit(m.Run())
}

Visualisation

Puzzles often have a visual element, usually in the form of a grid of characters. It is very useful when debugging if you can create the same visualisation from the current structure of your data structures, as it is often easier to spot an error (especially a character in the wrong place) this way than by examining the internals of your data structures with fmt.Println.

Designing data structures

Although some puzzles can be solved by reading the input and processing it immediately, it’s often easier to design and populate a data structure. There are a few reasons for this.

The most common generic data structures are slices, grids (slice of slices) and trees. You will also need to create specific struct types in most puzzles.

Testing

A test suite might seem overkill for these puzzles, but one thing that catches many people out is the edge cases, e.g. where a marker appears at the beginning or end of a line instead of in the middle. Testing every single function with expected outputs means you can find the source of a problem much quicker, even though there is an initial overhead in writing the tests. I’ve generally found that the time spent writing the tests is outweighed by the time saved on debugging, especially on later puzzles.

The great thing about using Go is that it has built-in support for tests, and this can be integrated with your IDE. I have found the testify library very helpful for extra functionality that I would otherwise have to write myself.

Break down steps into functions

Break each part of the problem into its smallest possible step, and write a function to perform that step and nothing else. For example, if you have a grid of characters, each of which represents a numeric value, write:

Having each step in a sepatate function makes it easier to test and debug. Once you know one function is working, you can build the next one, and if anything goes wrong you know the problem is likely to be with the latest function.

Don’t worry about the overhead of function calls, as they’re unlikely to be significant for the size of input involved in the puzzles.