Advent of Code 2024 - Day 5, in Kotlin - Print Queue
Kotlin solutions to parts 1 and 2 of Advent of Code 2024, Day 5: 'Print Queue'
Today we’ll see the power of comparators. They let us compare any two objects and tell Kotlin (or Java) what their relationship is to one another. They’re very handy for sorting, as we’ll see below.
If you’d rather just view the code, my GitHub Repository is here.
Puzzle Input
My original solution had a lot more parsing and a different data structure, but I’m happy with where this ended up. It is tempting to see the sample input, read the puzzle, and think of numbers as integers. However, the only time we really, strictly need an integer is when we calculate the sum. At all other points in the process we could just as easily use a String
.
So that’s what we’ll do. We’ll parse the rules
into a Set<String>
, not doing any additional parsing. When we see “1|2”, we store that String in the set. The only thing to watch out for is that we have to parse the top stanza of the input, so we use takeWhile
and check if the row of text isNotEmpty()
. Once we hit an empty row, we stop. All of those results go into a set.
class Day05(input: List<String>) {
private val rules: Set<String> = input
.takeWhile { it.isNotEmpty() }
.toSet()
private val updates: List<List<String>> = input
.dropWhile { it.isNotEmpty() }
.drop(1)
.map { row -> row.split(",") }
}
The updates
are moderately more complex, but not very much so. Instead of taking while we don’t have an empty line, we dropWhile
we don’t have an empty line. This lets us skip the rules
and get right to the updates
. Because we’ve stopped skipping when we find that empty row, we need to drop
it. Then we can map
and split
the row on ,
. It is tempting here to map to Int
(and you can, go for it) but we don’t strictly need to, so I won’t.
⭐ Day 5, Part 1
The puzzle text can be found here.
Let’s get some housekeeping done first. We’re asked to get the midpoint of the update, so let’s define a midpoint()
function on List<T>
so we can reuse it. I took this from my 2021 solution and copied it into Extensions.kt
in the 2024 project. One could make an argument that midpoint
is really a property and not a function, but I’m content to leave this as-is.
// In Extensions.kt
fun <T> List<T>.midpoint(): T =
this[lastIndex / 2]
Now we can talk about our solution. Originally I had written some logic that dealt with sets and intersections, but then realized I was basically just implementing a sort with extra steps. What we really want to know is “Does this update have the same order as an update that we’ve sorted to be correct?”. To assist us in our sorting, we’ll write a Comparator
, which lets us tell Kotlin how to order any two String
objects (in this case).
// In Day05
private val comparator: Comparator<String> = Comparator { a, b ->
when {
"$a|$b" in rules -> -1
"$b|$a" in rules -> 1
else -> 0
}
}
The logic here is as follows: If we have a rule that states that a comes before b (“a|b”), then we return -1 to indicate a comes first. If we have a rule that states that b comes before a (“b|a”), we return 1 to indicate b comes first. Otherwise, we don’t have any kind of record of a and b, so we return 0 meaning they are equal as far as this comparison is concerned.
Now we can use our comparator
to order an update
and return both the original form and the ordered form:
// In Day05
private fun formatCorrectly(update: List<String>): Pair<List<String>, List<String>> =
update to update.sortedWith(comparator)
This returns a Pair such that the first
element is the original update
and the second
element is sorted.
Now we can solve Part 1:
// In Day05
fun solvePart1(): Int =
updates
.map { formatCorrectly(it) }
.filter { it.first == it.second }
.sumOf { it.second.midpoint().toInt() }
We filter the updates
so we only keep those that are formatted correctly. We get the sumOf
the midpoint()
, converted to an integer for our answer.
Star earned! Onward!
⭐ Day 5, Part 2
The puzzle text can be found here.
We have everything we need to solve Part 2! In fact, the only actual difference is using filterNot
instead of filter
!
// In Day05
fun solvePart2(): Int =
updates
.map { formatCorrectly(it) }
.filterNot { it.first == it.second }
.sumOf { it.second.midpoint().toInt() }
We’re basically looking for the opposite of Part 1. So instead of filter
, we use filterNot
- meaning filter out anything that was originally formatted correctly. Then we take the sumOf
the midpoints of the sorted (second
) updates, converted it to integers, and that’s our answer.
Star earned! See you tomorrow!
Further Reading
- Index of All Solutions - All posts and solutions for 2024, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 5
- Advent of Code - Come join in and do these challenges yourself!