Skip to Content

Advent of Code 2022 - Day 3, in Kotlin - Rucksack Reorganization

Kotlin solutions to parts 1 and 2 of Advent of Code 2022, Day 3: 'Rucksack Reorganization'

Posted on

Today’s puzzle gives us an opportunity to use reduce and chunked, two functions I use all the time when solving Advent of Code problems. The puzzle today as a nice one for illustrating various aspects of how Strings can be manipulated, and I had a lot of fun with it.

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

Puzzle Input

⭐ Day 3, Part 1

The puzzle text can be found here.

Let’s start and the end first by defining an extension function on Char called priority. This will implement the priority logic on a given character.

// In Day03

private fun Char.priority(): Int =
    when (this) {
        in 'a'..'z' -> (this - 'a') + 1
        in 'A'..'Z' -> (this - 'A') + 27
        else -> throw IllegalArgumentException("Letter not in range: $this")
    }

The priority function makes nice use of Kotlin’s when expression. It also takes advantage of the fact that we can subtract characters from one another and get their ordinal values. In our case, we take the value of the character and subtract either a or A (depending on which case we’re using) and then add back an offset (either 1 or 27, again depending on the case). When called on a Char, this function will tell us the priority according to the instructions in the puzzle.

The instructions ask us to find a letter that is common to both the fist half and second half of each line of input. We can get the first and second half of each String by calling substring on it, but then what? Since we don’t care how many matches we have, and duplicate characters on any one side of the dividing line don’t really matter, if we could put all the Chars into a Set<Char> and then intersect them, we would have a single Char that is common to both sets.

Let’s implement this logic in a way that might seem a little odd now but will make sense when we get to part two.

// In Day03

private fun List<String>.sharedItem(): Char =
    map { it.toSet() }
        .reduce { left, right -> left intersect right}
        .first()

I know there is a lot going on in sharedItem, so let’s go over it. Our sharedItem function is an extension function on List<String>. This allows us to pass any number of strings to our function and we will get the single Char that is common to all of them. Note, in real life we would probably have some kind of error checking (what if there was more than one match? Or no matches?) but since this is an Advent of Code puzzle, we can take the requirements as-is and assume the input will conform to it.

The first thing we do in this function is convert each String to a Set<String> via map. Once we have a List<Set<String>>, we can call reduce on them. The reduce function lets us take our List<Set<Char>> and reduce it down to a single Set<Char> according to whatever logic we want. When we write a reduce, we look at each pair of items in succession and perform some action. In our case, we’re going to intersect each of the Set<Char>s together. Eventually this will result in a Set<Char> that has a single Char in it, which we will retrieve with first().

To call this function, let’s write another function to split our String and find the common Char:

// In Day03

private fun String.sharedItem(): Char =
    listOf(
        substring(0..length / 2),
        substring(length / 2)
    ).sharedItem()

This sharedItem is an extension function on String. I wanted to name these the same to show you that we can do this - Kotlin is smart enough to know what type we’re calling sharedItem on and disambiguates this call for us. In this case, our function sets up a List<String> with two elements - the first half of the String and the second half of the String. We get these by calling substring and knowing the length of the String.

I know that seems overkill - why not just intersect these Strings here after converting them to Set<Char>? Because when we get to Part Two, we’ll need to intersect multiple Strings and the hard work will already be done.

All that’s left is to use our good friend sumOf again to go over every line of input, get the sharedItem and then find its priority. Adding these together gives us our answer!

// In Day03

fun solvePart1(): Int =
    input.sumOf { it.sharedItem().priority() }

Star earned! Onward!

⭐ Day 3, Part 2

The puzzle text can be found here.

See? I told you the hard work was done. In fact, the solution to Part Two is not all that different than the solution to Part One. The only real difference is our call to chunked. The chunked function is one of my favorites in the Kotlin Standard Library because I find uses for it all the the time (especially in Advent of Code). When we call chunked(3) on our List<String>, we will get back a List<List<String>> where each inner List<String> is 3 elements long. For example, if we have ["A", "B", "C", "D", "E", "F"] as a List<String> and call chunked(3) on it, we get [["A", "B", "C"], ["D", "E", "F"]]. Two inner lists, each containing 3 strings. This is exactly what we need to break our input into chunks of three strings.

Because our List<String>.sharedItem() function takes any number of Strings, we don’t have to write any new logic to handle three vs. two Strings. We did all the hard work in Part One!

Now that we know all about chunked, we can write our solution.

// In Day03

fun solvePart2(): Int =
    input.chunked(3).sumOf { it.sharedItem().priority() }

Star earned! See you tomorrow.

Further Reading

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