Advent of Code 2021 - Day 13, in Kotlin - Transparent Origami
Kotlin solutions to parts 1 and 2 of Advent of Code 2021, Day 13: 'Transparent Origami'
When I saw the title of today’s puzzle (“Transparent Origami”) I thought we would be folding up some crabs , octopuses , or whales . Instead, we’re pretending we’ve bought a cool game from the 1980s and need to look up a code in the decoder ring to play it. Except this time it’s a sub-mounted thermal camera.
I like to think there’s a nonzero chance that Eric Wastl decided to have us fold paper to confuse the functional programmers out there who use the fold function a lot.
If you’d rather just view code, the GitHub Repository is here .
Problem Input
Since our input is a single file with two different parts, we’ll write two functions to do the parsing. We could write a single function that returns a Pair
, but I think this just looks cleaner.
class Day13(input: List<String>) {
private val paper: Set<Point2d> = parsePoints(input)
private val instructions: List<Point2d> = parseInstructions(input)
private fun parsePoints(input: List<String>): Set<Point2d> =
input.takeWhile { it.isNotEmpty() }
.map { it.split(",") }
.map { Point2d(it.first().toInt(), it.last().toInt()) }
.toSet()
private fun parseInstructions(input: List<String>): List<Point2d> =
input.takeLastWhile { it.isNotEmpty() }
.map { it.split("=") }
.map {
if (it.first().endsWith("y")) {
Point2d(0, it.last().toInt())
} else {
Point2d(it.last().toInt(), 0)
}
}
}
The first function, parsePoints
reads input until it finds an empty line and then stops using takeWhile
. For every line it finds, we split
the coordinates on a comma and then map
the results into a Point2d
, which we wrote a few days ago and have used a few times now.
The second parseInstructions
will skip lines in the input until it finds an empty line, and then lets us have every one after that. I haven’t used the takeLastWhile
function too many times, but it’s very handy for situations like this. Every instruction row gets split
on the equals sign, so we end up with a list like this: ["fold along x", "655"]
. We could parse the first part a bit further to isolate the x
or y
from the instruction, but since it is always at the end of the String
, we can just use endsWith
to figure it out. Depending on whether we are parsing an x instruction or a y instruction, we’ll once again use Point2d
to represent this. There is a catch - we can’t represent an instruction that deals with 0. But since I don’t have any in my input, I assume that’s generally safe.
⭐ Day 13, Part 1
The puzzle text can be found here.
Before we start in on our solution, I want to talk about terminology. As we’ve seen from days past, Kotlin has a function called fold
that allows us to operate on a sequence of values and carry some state along. And the puzzle for today involves folding. Because we’ll legitimately use the fold
function in part two, and I don’t want there to be any confusion (this is as much for my sanity as it is for yours, I assure you), anything involving the puzzle’s concept of folding paper will be called crease
. So if you see fold
, think function. If you see crease
, think bending paper.
Let’s start with the math we’ll have to do when calculating where a Point2d
ends up when we crease the paper. We’ll implement it as an extension function in Int
:
// In Day13
private fun Int.creaseAt(crease: Int): Int =
if (this < crease) this else (crease * 2) - this
Another way to think of that calculation would be crease - (this - crease)
. Calculate the different between the point being moved and its crease point, and then subtract that amount from the crease point to mirror the point on the other side of the crease.
Now that we have this, we can actually perform the crease of an entire Set<Point2d>
. We’ll do this with another extension function called crease
.
// In Day13
private fun Set<Point2d>.crease(instruction: Point2d): Set<Point2d> =
if (instruction.x != 0) this.map { it.copy(x = it.x.creaseAt(instruction.x)) }.toSet()
else this.map { it.copy(y = it.y.creaseAt(instruction.y)) }.toSet()
This function takes an instruction
and figures out if we’re talking about the x or y axis to crease on. Depending on which one it is, we make a copy
of each Point2d
, specifying the new x or y value as needed. This calls the creaseAt
extension function we wrote above.
Now we can solve part one:
// In Day13
fun solvePart1(): Int =
paper.crease(instructions.first()).size
Because part one only calls for the number of points (the size of our paper!) after applying the first crease instruction, we can use instructions.first()
to grab that and pass it to our crease
function.
Star earned! Onward!
⭐ Day 13, Part 2
The puzzle text can be found here.
There have been a couple of puzzles in the past where the instructions ask for us to interpret some printed data. If we really wanted to, we could write some code to interpret our data and output the text instead of printing it and interpreting ourselves, but I’m not going to do that. Not every problem we solve as programmers involves programming.
In order to print our Set<Point2d>
, we’ll need to know both the maximum x and y values. Similar to how we’ve parsed data in the past, we’ll set up two ranges along y and then x, and print
an “on” character if we find the point in the set, and an “off” character otherwise. I picked hash and space respectively, but you can pick whatever you think looks best.
// In Day13
private fun Set<Point2d>.printout() {
(0..this.maxOf { it.y }).forEach { y ->
(0..this.maxOf { it.x }).forEach { x ->
print(if (Point2d(x, y) in this) "#" else " ")
}
println()
}
}
I found that backing up from my computer and reading the results from about a meter away made them more clear.
Now we can actually use the fold
function (read the note above, we’re using a “functional fold” here, not creasing a piece of paper. I mean, we are, but still…). Anyway.
// In Day13
fun solvePart2(): Unit =
instructions.fold(paper) { paper, instruction -> paper.crease(instruction) }.printout()
We fold
over our instructions
and seed our calculation with the start state of the paper
. For each new instruction
, we crease
the paper
. At the end, we have a fully creased Set<Point2d>
, so we call printout
on it.
Star (very confusingly) earned!
Congratulations for making it this far, there are now fewer 2021 Advent of Code puzzles ahead of us than there are behind us.
Further Reading
- Index of All Solutions - All posts and solutions for 2021, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 13
- Advent of Code - Come join in and do these challenges yourself!