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'
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 Char
s 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 String
s here after converting them to Set<Char>
? Because when we get to Part Two, we’ll need to intersect multiple String
s 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 String
s, we don’t have to write any new logic to handle three vs. two String
s. 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
- Index of All Solutions - All posts and solutions for 2022, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 3
- Advent of Code - Come join in and do these challenges yourself!