Skip to Content

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'

Posted on

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

  1. Index of All Solutions - All posts and solutions for 2024, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 5
  4. Advent of Code - Come join in and do these challenges yourself!