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.
- By splitting the reading of the input, population of the data structure, and solving the puzzle, you can test each step separately. This is usually much easier to debug than if you try and move straight to solving the puzzle.
- Part 2 of the puzzle involves using the same input to solve a slightly different problem. If you already have a data structure, in most cases you can move straight to solving part 2, instead of having to rethink your input processing (this isn’t always possible, as sometimes part 2 means tweaking your data structure).
- If you can’t see why something isn’t working, printing the data structure will often give you a clue.
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:
- A function to convert a character to its numeric value.
- A function to convert one line into a slice.
- A function to convert a series of lines into a slice of slices (calling the previous function).
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.