Advent of Code 2020 - Day 1, in Kotlin - Report Repair
Kotlin solutions to parts 1 and 2 of Advent of Code 2020, Day 1: 'Report Repair'
Update: If you read this before ~7:15 PM EST on 2020-12-01, I have rewritten it.
It’s December 1st, and you know what that means - Advent of Code is back! For the fourth year in a row, I will be attempting to solve each of the Advent of Code problems in Kotlin on the day they are released with a clear, idiomatic solution. This means that sometimes I will give up speed or memory in exchange for clarity. I’m more interested in being able to explain each solution than having the absolute quickest performing or shortest solution. While I will endeavor to have a timely solution each day, I will note that I do have a full time job and a family, and some of these puzzles can be hard to solve in a way that I can explain easily, so puzzle solutions might be late on some days.
If you’d rather just view code, the GitHub Repository is here .
Problem Input
We are given a file with one integer per line. Since we run into this pattern frequently, I have developed a couple of helper functions.
internal object Resources {
fun resourceAsString(fileName: String, delimiter: String = ""): String =
resourceAsList(fileName).reduce { a, b -> "$a$delimiter$b" }
fun resourceAsList(fileName: String): List<String> =
File(fileName.toURI()).readLines()
fun resourceAsListOfInt(fileName: String): List<Int> =
resourceAsList(fileName).map { it.toInt() }
private fun String.toURI(): URI =
Resources.javaClass.classLoader.getResource(this)?.toURI()
?: throw IllegalArgumentException("Cannot find Resource: $this")
}
Today we are going to use resourceAsListOfInt
, which takes a file and turns each row into an Int
, and returns them all as a List<Int>
. We will probably end up using the other functions for future days, but I won’t go over this helper code day-to-day. This is mostly a copy of code I’ve been using for Advent of Code for a few years now.
We will use our test to read and parse the input and pass it to today’s solution class. We’ll also sort the data
, because that will come in handy later:
class Day01(data: List<Int>) {
private val input = data.sorted()
// ...
}
⭐ Day 1, Part 1
The puzzle text can be found here.
Before we look at code, let’s talk about our approach. As with every Advent of Code problem, there are multiple ways to solve this one. I’m going to go with something I consider fairly straight forward: Two nested loops that will return a single value. The trick today is that we’re going to do this in a way that lets us review as few pairs as possible, all in a single expression.
Our algorithm is:
- Loop through every element in the
input
, call thisa
. Also get the index of that element and call itidx
. - For each
a
, loop through every element in theinput
and call itb
, and then:- Skip
idx + 1
number of entries we’ve already reviewed. - Drop elements from the sequence while the total is below 2020.
- Take one single entry. Because the elements are sorted, it will either be our answer or it will be out of range.
- Determine if this pair of
a
andb
are our answer. - If they are, multiply them together, otherwise return null.
- Skip
- Select the first element that step 2 produces.
// In Day01
fun solvePart1(): Int =
input.mapIndexedNotNull { idx, a ->
input
.drop(idx + 1)
.dropWhile { a + it < 2020 }
.take(1)
.firstOrNull { a + it == 2020 }
?.let { a * it }
}.first()
Some things to point out here - mapIndexedNotNull
is a great example of the power of the Kotlin standard library. It maps over an Iterable
and provides the index of each element, but only the mappings that are not null will be retained. This helps us because we can essentially make the inner loop say “We will either provide a non-null answer or a null”. Now we don’t have to filter out nulls, Kotlin does this for us!
Another thing to look at is drop
and dropWhile
. These do the same thing - skip elements in our input. The former will drop a specific number of elements, and the latter will drop elements so long as the predicate holds (returns true). Because anything less than 2020 is useless, we drop it. And knowing that, the next element is either our answer or is too big to be our answer. So we take
it (does the opposite of drop
!).
But what do we do with it? Enter another nice function - firstOrNull
which will take either the first element that matches the predicate or produce a null. This helps us determine if the single element we are looking at is our answer or not. If it is our answer (not null!), we use the let
scoping function to map our answer (a * it
).
This might look complicated, but I think it is a nice example of using some lesser known functions straight from the Kotlin standard library. We didn’t write any supporting code for this solution (except parsing the input), and I think that’s pretty fantastic.
Onward!
⭐ Day 1, Part 2
The puzzle text can be found here.
Part two is an extension of part one. Instead of having two nested loops (a
and b
), we have three (a
, b
, and c
). The part that does the real work is nearly identical except for the addition of c
.
// In Day01
fun solvePart2(): Int =
input.mapIndexedNotNull { aIdx, a ->
input
.drop(aIdx + 1)
.mapIndexedNotNull { bIdx, b ->
input
.drop(bIdx + 1)
.dropWhile { a + b + it < 2020 }
.take(1)
.firstOrNull { a + b + it == 2020 }
?.let { a * b * it }
}.firstOrNull()
}.first()
I’m pretty satisfied with today’s solutions because they show off the power of the Kotlin standard library, and because we lean heavily on Kotlin’s first-class treatment of nullability.
Stars earned! See you tomorrow!
Further Reading
- Index of All Solutions - All posts and solutions for 2020, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 1
- Advent of Code - Come join in and do these challenges yourself!