Skip to Content

Advent of Code 2025 - Day 6, in Kotlin - Trash Compactor

Kotlin solutions to parts 1 and 2 of Advent of Code 2025, Day 6: 'Trash Compactor'

Posted on

This has been my favorite puzzle of Advent of Code 2025 so far! My original code worked fine but was a MESS. After taking a good long walk with my Golden Retriever Charlie (he’s very good at listening to me describe all sorts of programming problems), I came up with something I’m mostly happy with, which you’ll find below.

If you’d rather just view the code, my GitHub Repository is here.

Puzzle Input

I usually leave out any code that loads the puzzle input from a file to a List<String> or whatever we are using that day because I have a set of functions that I’ve been using for the past several years and frankly, they aren’t that interesting. This might mean you’ve written your own analogues to these. One work of caution for today: whitespace is important in part 2, so don’t trim your inputs.

The only part of our puzzle input that we use consistently across both parts will be the final row which comprises the symbols to use in the mathematical operations. To get these, we’ll get the last() row of the input, trim() it (yes, I said not to above, but here we need to), and split it on any whitespace (the regular expression here captures this concept). We could convert the String to a Char, but I left it alone.


class Day06(private val input: List<String>) {

    private val symbols: List<Char> = 
        input
          .last()
          .trim()
          .split("""\s+""".toRegex())

}

Also note that our input is a private val property as we’ll parse it two different ways in parts 1 and 2.

⭐ Day 6, Part 1

The puzzle text can be found here.

To solve part 1, we’ll need to parse the input into a Map<Int, List<Long>> where the key is the grouping number from left to right (starting at zero) and the value is the numbers in that grouping (as Long). To do this, we discard the last row of input data (the symbols) via dropLast(1) and then flatMap over the rows of input.

Within our flatMap, we mimic the logic we have for parsing the symbols - split on whitespace, and then pair them up with their index via withIndex. For example, if our input row is 123 456 789, we end up with [IndexedVaue(0, 123), IndexedVaue(1, 456), IndexedVaue(2, 789)]. Once we have all of the rows parsed, we use groupBy to form these into a Map<Int, List<Long>>. Note the groupBy function takes two functions as arguments, one to find the key (it.index, which is really IndexedValue.index) and one for the value (which we convert to a Long here).

// In Day06

private fun parseNumbersPart1(input: List<String>): Map<Int, List<Long>> =
    input.dropLast(1).flatMap { row ->
        row.trim().split("""\s+""".toRegex()).withIndex()
    }.groupBy({ it.index }, { it.value.toLong() })

The parseNumbersPart1 function returns a Map where the key is an index (yes, we could have done this in a list with its implicit indexes but groupBy conveniently does a lot of work for us so I left it in a Map) and the value is all of the numbers in that group.

Let’s write a solve function which will turn our Map<Int, List<Long>> into a single Long value. We will use this for both parts 1 and 2.

// In Day06

private fun solve(numbers: Map<Int, List<Long>>): Long =
    symbols.withIndex().sumOf { (index, symbol) ->
        numbers.getValue(index).reduce { a, b ->
            if (symbol == "*") a * b
            else a + b
        }
    }

We start by iterating over all the symbols and calling withIndex on them. We then get the sumOf a calculation we perform over the IndexedValue objects. First, we destructure the IndexedValue into index and symbol to make things easier. Then we get the numbers at the given index and reduce them to a Long. Inside the reduction we examine the symbol and perform either multiplication or addition on the two values being reduced.

We have enough to solve part 1 now!

// In Day06

fun solvePart1(): Long =
    solve(parseNumbersPart1(input))

Star earned! Onward!

⭐ Day 6, Part 2

The puzzle text can be found here.

Before we solve part 2, let’s talk about strategy. I ignored the example about moving from right to left and parsed everything left to right. Why? First off, it’s easier for me to think about, and since Kotlin indexes everything starting at zero, it just made sense. Also, since we’re multiplying and adding, operations that do not depend on order, we can go in either direction.

Our strategy for part 2 will be to group the numbers again into a Map<Int, List<Long>>. However, we’ll need to do this column by column. Let’s look at the sample input:

123 328  51 64 
 45 64  387 23 
  6 98  215 314
*   +   *   +  

If we ignore the last row of symbols and look column by column, we see that there are spaces dividing up the groups. This is our clue that a group is over. So let’s write a function to turn a column of the input into a Long?, where null represents a break in the groups.

// In Day06

private fun List<String>.columnAsLongOrNull(column: Int): Long? =
    dropLast(1)
        .map { row -> row[column] }
        .joinToString("")
        .trim()
        .toLongOrNull()

We’ll write columnAsLongOrNull as an extension function on List<String>. Again, since we don’t want to count the symbols, we’ll drop the last line via dropLast(1). We then map the Char at the given column for each row and form them up into a single String via joinToString("") (specifying “”, the empty String as the separator). Because we may have encountered a column of whitespace, we want to trim() that down to the empty string so the next step of converting whatever String we have to either a Long or null will work properly. I love that Kotlin has things like toLongOrNull.

Now that we can parse a single column of input to a Long?, we can use it to parse our input into a Map<Int, List<Long>>, which we can pass to solve.

// In Day06

private fun parseNumbersPart2(input: List<String>): Map<Int, List<Long>> =
    input.first().indices
        .map { input.columnAsLongOrNull(it) }
        .fold(mutableListOf(mutableListOf<Long>())) { carry, maybeNumbers ->
            when(maybeNumbers) {
                null -> carry.apply { add(mutableListOf()) }
                else -> carry.apply { last().add(maybeNumbers) }
            }
        }.mapIndexed { index, list -> index to list }.toMap()

One thing to note about the input is that all of the rows have the same length (because we didn’t trim them). This means we can get the indices for any row and use that to map our columns. So we grab the first() row of input and get the indices from it and map each of those columns to a Long? via the columnAsLongOrNull function we just wrote.

Next, we set up a fold, seeding it with a MutableList<MutableList<Long>>. I know that’s a bit much but I really wanted to do this in a functional style rather than imperative with side-effects. The values passed to the lambda on the fold are carry (the list of lists) and maybeNumbers, which is either a Long or null.

When we find a null, we know the group we were just filling is over and a new one will begin on the next column. In that case, we add a new mutableList() to the end of the carry list for next time. Otherwise, we’ve found a Long and add it to the last() list in the carry list.

At this point we have a List<List<Long>> (really they’re mutable lists, but go with it, it doesn’t much matter here) and we want them in a map by their index. To do this, we call mapIndexed again, giving us each of the inner lists by their index value. Conveniently, we can call toMap on this directly and get a Map<Int, List<Long>>.

And we finally have enough to solve part 2!

// In Day06

fun solvePart2(): Long =
    solve(parseNumbersPart2(input))

Star earned! See you tomorrow!

Further Reading

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