Skip to Content

Advent of Code 2022 - Day 20, in Kotlin - Grove Positioning System

Kotlin solutions to parts 1 and 2 of Advent of Code 2022, Day 20: 'Grove Positioning System'

Posted on

When I first read today’s puzzle I thought it would be really easy. It turns out that the sample data omits a condition that our real data has, and that threw me off for a good amount of time. It’s important to not make assumptions about your input!

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

Puzzle Input

We won’t do any special parsing with our input today. Since we’ll have to do this for each part of the puzzle, we’ll declare it as a List<String> property in the constructor and move on.

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

}

⭐ Day 20, Part 1

The puzzle text can be found here.

Before we get too far into the solution, let me tell you what messed me up for a non-trivial amount of time. My list had duplicates and that means I can’t always say “give me the index of the first 5” because there might be more than one 5, and they might have been shifted around so that the first 5 is really the second 5.

So we need not only keep track of the numbers to move in order, but their precise position within the original and target lists. One way to do this is to use a data class (or a Pair, if you want) to store the originalIndex of the number from the input and the value itself. When we look up a number, we find it by its originalIndex. That way, we know the 5 we get is the correct 5, not some shifty imposter 5.

We’ll call our tracking-class MappedNumber.

// In Day20

private data class MappedNumber(val originalIndex: Int, val value: Long)

Why is value a Long and not anInt? Because in Part Two we deal with large numbers. Let’s parse these with parseInput using mapIndexed, and return the result as a MutableList<MappedNumber>.

// In Day20

private fun parseInput(L): MutableList<MappedNumber> =
    input.mapIndexed { index, value -> 
      MappedNumber(index, value.toLong()) 
    }.toMutableList()

Before we get to the decryption function, let’s write the groveCoordinates function we’ll need to give us the answer.

// In Day20

private fun List<MappedNumber>.groveCoordinates(): Long {
    val zero = indexOfFirst { it.value == 0L }
    return listOf(1000, 2000, 3000).sumOf { this[(zero + it) % size].value }
}

As you can see, we can look up the location of zero directly - my input only had one of them. Maybe intentionally? We create a list of 1000, 2000, and 3000, and add the index of the zero to it, and modulus the size of the list to account for the fact that the list is circular. This gives us the MappedNumber objects we’re looking for, so we can take the sumOf their value.

On to decryption!

// In Day20

private fun MutableList<MappedNumber>.decrypt() {
    indices.forEach { originalIndex ->
        val index = indexOfFirst { it.originalIndex == originalIndex }
        val toBeMoved = removeAt(index)
        add((index + toBeMoved.value).mod(size), toBeMoved)
    }
}

Since we need to go through our input in order, and account for duplicates, we need to find the MappedNumber in our list via its originalIndex. The way I’ve chosen to do this is to get the indicies off the current list, which will be the same size (we could have also explicitly set up a range, which is what I did at first before I remembered indices). Once we find the index of the MappedNumber we’re after, we remove it from the list, and set it aside in toBeMoved. Then we add it back in, accounting for the fact that we need to use mod in order for the circular list thing to work out.

And now can put all this together in a single solvePart1 function to get our answer:

// In Day20

fun solvePart1(): Long {
    val theList = parseInput()
    theList.decrypt()
    return theList.groveCoordinates()
}

Star earned! Onward!

⭐ Day 20, Part 2

The puzzle text can be found here.

In order to account for the decryptionKey we need to do a bit of refactoring to our parseInput function. Since Kotlin has default values, we can set the default to 1 so we don’t have to alter solvePart1. In our case, that wouldn’t be a big deal. But if you maintain a larger system with more than one user of a service or something, adding a defaulted value is a nice way to not break your callers.

The only change to the implementation is to multiply the value by the decriptionKey before storing it in the MappedNumber.

// In Day20

private fun parseInput(decryptionKey: Long = 1L): MutableList<MappedNumber> =
    input.mapIndexed { index, value -> 
      MappedNumber(index, decryptionKey * value.toLong()) 
    }.toMutableList()

We’re now able to write our solvePart2 function. We call parseInput with the decryptionKey, and get a list similar to what we had in Part One. We’ll repeat the decrypt process 10 times, and then get the groveCoordinates to calculate our answer.

// In Day20

fun solvePart2(): Long {
    val theList = parseInput(811589153)
    repeat(10) {
        theList.decrypt()
    }
    return theList.groveCoordinates()
}

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 20
  4. Advent of Code - Come join in and do these challenges yourself!