Kotlin
A concise multiplatform language developed by JetBrains
Tips and Tricks for Solving Advent of Code
Advent Of Code is currently ongoing and many people all over the world are taking part. We’d like to share some tips and tricks for solving the puzzles in Kotlin – we hope you find some of them helpful! Feel free to add your favorite tips or thoughts in the comments!.
If you’re solving the puzzles in Kotlin, don’t forget to add the aoc-2021-in-kotlin
topic to your repo to take part in our giveaway! If you’re starting from scratch, consider using our prepared Github template.
How do you usually go about solving the puzzles? First of all, you read and parse the input data. Let’s start with some tips for this process, and then discuss tips for writing the actual solutions in Kotlin.
1. Jumping between sample and real input
All of the Advent of Code puzzles provide your own “personal” input and the sample input to check your solution. You often need to switch back and forth between the sample and real input. If the solution for the first part works for the sample input, you then need to run it on the real input to get your result. If the solution then works on the real input, you likely switch back to the sample input to start solving the second part of the puzzle.
One of the ways to switch between two inputs quickly is by using comments with the IntelliJ IDEA action “Comment with Line Comment”. This changes the line state and removes the comment from the commented line, rather than simply adding a comment.
You can put the sample and real inputs in different files and toggle between comments for the file names by pressing the Ctrl+/
shortcut twice:
These small things make a difference and make working with IntelliJ IDEA a pleasure!
2. Parsing input
The next thing to do after reading the input is to parse it. You can either use the Kotlin library functions for working with strings or regular expressions. Library functions like substringBefore()
and substringAfter()
cover lots of cases and are often enough. Use String.toInt()
and Char.digitToInt()
to convert the string content or characters into integers.
For more complicated scenarios, there are regular expressions. Use the destructured
function to assign the output to different variables right away:
The action “Check RegExp” in IntelliJ IDEA allows you to quickly check whether the sample input satisfies your regular expression.
3. Storing input
It’s often helpful to introduce domain types, even for smaller tasks like these puzzles. Solving the task for Cells and Boards, or for Segments and SevenSegmentDigits can be much easier than working directly with Ints, List of Lists of Ints, or Chars and Sets of Chars. Types help to direct your thinking to the heart of the problem.
Kotlin makes this really easy – define a one-line data class and that’s it. You don’t need a separate file, like in Java, since it’s in your main file with the rest of your solution code:
In Kotlin, you can use an extension to convert an input line directly to a required type:
Then your typical starter code will look like this:
You can either use the function reference String::toStep
or the lambda expression map { it.toStep() }
.
4. Enumerations instead of strings
It might be tempting to manipulate the string literals directly, but making them enum
constants makes it easier to write the code and reason about how it works. You never know what comes in the second part of the puzzle!
It is easy to convert an input string to an enum
and use the EnumClassName.valueOf("")
function to get the constant by name:
With when
expressions you can check all of the options and you don’t need to include the else
branch:
IntelliJ IDEA can generate all the branches automatically.
Note that in the destructuring declaration syntax in the for loop, you automatically assign two properties, the contents of each Step
, into two loop variables.
5. From typealias
to a class
If working with primitives is easier at first, and defining a separate class looks too cumbersome, consider defining a typealias
. You can convert it into a class later, if needed. When you replace a typealias
with a class, most of the code continues to compile, and you immediately see which functions are missing.
For example, the characters from 'a'
to 'g'
used to encode the seven-segment display can be referred to as Segment
s in the code and be regular Char
s underneath. You can define the SevenSegmentDigit
class first as a typealias
for Set
and later convert it to a class, for example, if you want to replace a default toString
with a custom one.
6. Building lists and maps
In addition to the standard listOf()
, mutableListOf(),
and similar functions, you can use other methods to build collections.
You can call the List
function that looks like a constructor (but is not!) to provide a way to calculate each element:
Use buildList
and buildMap
functions to build data structures imperatively:
In this example, we call add
on a MutableList
inside the lambda, and the resulting type is the read-only List
.
The similar sequence {..}
function builds a Sequence
lazily yielding values one by one.
7. Associate and group
The task of building a map from a list occurs quite often, and associate
and groupBy
functions make this operation straightforward. You can group elements by the provided property with groupBy
, use elements as map keys to provide a way to build values (associateWith
), use elements as values (associateBy
), or provide a way to build a key-value pair from each element (associate
).
The groupBy
function groups the elements with the specified value used as a map key:
The result of calling countSegmentInAllDigits
in this example becomes the key in the map.
If you don’t need the groups directly, but you need to find the size of each group, use a lazy counterpart to groupBy
: the groupingBy
function. It doesn’t return a map straightaway, but it allows you to analyze the groups in a lazy manner:
If the property you’re using to group the elements is unique, use associateBy
. For instance, if you need to access elements by their indexes, associateBy
will build a map from indexes to elements for you:
Let’s imagine you need to build a map representing an initial state by associating each of the input numbers with the corresponding Node
. Use associateWith
:
If you need more complicated keys and values, use associate
:
If you don’t remember which associate function you need, choose the general associate
one, and IntelliJ IDEA will suggest a better one automatically:
8. Sliding by windows
Sliding a list to get chunks of a given size is often useful, like in this year’s first puzzle.
An alternative to building a list of chunks of size 2 is to use zipWithNext().
9. Sum of, min of
You don’t need to map the elements first to later find the resulting sum – the sumOf
function combines these two operations. IntelliJ IDEA even suggests these replacements automatically:
There are similar functions maxOf
and minOf
for finding the maximum and minimum among the transformed values.
10. Adding index
Need to perform computations with an element index? In addition to the withIndex()
extension that returns a list of pairs to iterate through, you can use many “indexed
” counterparts for standard library functions, such as forEachIndexed
, filterIndexed
, mapIndexed
, foldIndexed,
and so on.
In the following example, the final calculateScore
function uses the indexed version of fold
to include an index into a computation of the result:
Note how we marked 0
as a Long constant here (0L
) to perform the computation on Long
values.
11. Also logging
If the puzzle answer isn’t correct and you want to track the intermediate results step by step, you can print or log the intermediate values. The also
function allows you to include println
or log
directives in the middle of the call chain or display the function result if you use an expression-body syntax.
In this example, we return the result of the function and also
do some logging:
Here, we insert also
calls in the middle of the call chain to observe the intermediate results of the computation:
If you need to print each list element on a separate line, you can include .onEach(::println)
to the middle of the call chain. onEach
performs an operation on each element and returns the unmodified list.
To avoid commenting on the lines with println
, make a habit of using your own small log
function instead. This way, you only need to change it in one place to stop printing all of the intermediate values for your solution.
12. Queue and stack together
Need a queue or a stack to implement an algorithm when solving the puzzle? Use ArrayDeque
, a double-ended queue that provides quick access to both ends. It can be used either as a queue or a stack when needed.
For instance, in a classic implementation of a depth-first search, create a queue as an ArrayDeque
and call its add
and removeFirst
methods inside:
In this example, we use the short syntax +=
to call the plusAssign(element: T)
and plusAssign(elements: Iterable
operators, which simply redirect to the corresponding add
functions.
Of course, the ArrayDeque
structure is useful any time you need quick access to both the start and end of the list of elements.
13. Operators
Operator overloading, which looks like mostly library or DSL-magic functionality, might also be useful when solving such small puzzles.
Consider overloading get
and set
operators to simplify the code for working with your class. For instance, by providing the get operator that takes Cell
as an argument for the following class you can access its content more easily:
Instead of writing board.content[cell.i][cell.j]
for all invocations, you write
board[cell].
You can provide the set
operator for mutable content accordingly.
Using the contains
operator might make the code cleaner:
Then you can call it via the in
keyword:
If your elements are comparable, you can make them implement the Comparable
interface, and then compare the elements using the standard <
, <=
, >
, and >=
operations.
———
Last but not least, consider creating a collection of your utilities specifically for solving Advent of Code puzzles. For example, you’ll definitely find a task that requires a point with two integer coordinates and uses its neighboring points!
That’s all for now! We hope that you enjoy solving the AdventOfCode puzzles as much as we do, and find these small tips useful!