Advent of Code 2021 in Kotlin - Day 25

Introduction
I was somehow afraid, that Day 25 will start with some really hard problem, as it was the last day and the previous ones were probably one of the hardest days in this time. Happily, we got a great present and the whole problem with proper representation in data was quite simple and didn’t even have a second part, so it didn’t take a lot of time. Let’s see how we can see this kind of problems to efficiently manage the transformed data in readable way.
Solution
The approach used in this day is quite similar to some previous days, where instead of
having some array of sea fields or the map from field to the type of the field, we store the sets of
the fields of each type, as there are only three of them east
, south
and empty
.
With the sets' representation, all moves transformations are really easy, as they operate on some current sets'
values and don’t require taking extra care about some intermediate state of the transformation. We could
even abstract some kind of partial step of transformation as separate function moveGroup
that for some current
set of empty places and of the places of to move from, was able to generate the pair of these transformed sets
with just a few lines of code.
Day25.kt
object Day25 : AdventDay() {
override fun solve() {
val data = reads<String>() ?: return
val sea = data.toSea()
generateSequence(sea) { it.step().takeIf { update -> update != it } }.count().printIt()
}
}
private fun List<String>.toSea(): Sea {
val east = HashSet<Region>()
val south = HashSet<Region>()
val empty = HashSet<Region>()
for ((y, line) in withIndex()) for ((x, c) in line.withIndex()) when (c) {
'.' -> empty
'>' -> east
'v' -> south
else -> error("Unknown input char: $c")
} += Region(x, y)
return Sea(east, south, empty, first().length, size)
}
private data class Region(val x: Int, val y: Int)
private data class Sea(
val east: Set<Region>, val south: Set<Region>, val empty: Set<Region>,
val xSize: Int, val ySize: Int,
) {
fun step(): Sea {
val (currEmpty, east) = moveGroup(empty, east) { east() }
val (finalEmpty, south) = moveGroup(currEmpty, south) { south() }
return copy(east = east, south = south, empty = finalEmpty)
}
private fun moveGroup(currEmpty: Set<Region>, moving: Set<Region>, move: Region.() -> Region) =
HashSet(currEmpty).let { empty ->
empty to moving.mapTo(HashSet()) { region ->
region.move().takeIf { it in currEmpty }
?.also { empty -= it }
?.also { empty += region }
?: region
}
}
private fun Region.east() = Region((x + 1) % xSize, y)
private fun Region.south() = Region(x, (y + 1) % ySize)
}
Extra notes
To implement the moveGroup
function, we used the lambda with receiver parameter move: Region.() -> Region
to simulate the movement of given field. In Kotlin, we can use this kind of definitions, to get a better
syntax look and better experience, when using these functions. That’s because they don’t need specifying
the lambda argument, as it is a this
object, for which we can call some method, e.g. in our code we just write
moveGroup(empty, east) { east() }
and the east
method is called on some default this
object in the specified context. We defined some extension
functions for these moves and located them in the Sea
class to take advantage of the Sea
context and
check for the size of the sea in the implementation of the method called on Region
.