Advent of Code in Kotlin 2021 - Day 10
This is part 10 of a series, so if you haven't read the previous parts, start here.
For today's puzzle we are given a list of lines of various brackets, e.g. [({(<(())[]>[[{[]{<()<>>. For each line we have to check whether it is corrupted, meaning a closing bracket occurs, which does not match the last unmatched opening bracket.
I did a fair bit of refactoring for this one after I had a working solution, but in the end, I defined a couple of extension properties on the Char class. These can be used just like native properties and are one of Kotlin's killer features in my opinion. To clarify that these are not meant to be used on all Chars, I defined a typealias.
typealias Bracket = Char
private val Bracket.isClosing: Boolean
get() = setOf(')', ']', '}', '>').contains(this)``
private val Bracket.match: Char
get() = when (this) {
')' -> '('
'(' -> ')'
']' -> '['
'[' -> ']'
'}' -> '{'
'{' -> '}'
'>' -> '<'
'<' -> '>'
else -> throw IllegalArgumentException("Unexpected character")
}
With these extension functions, we can now build up a list of unmatched brackets for a line by keeping track of the unmatched brackets. If a closing bracket matches the last unmatched open bracket, we remove the opening bracket from the list (consider it matched). Opening brackets and closing brackets that don't match up are added to the list. If the list of unmatched brackets includes closing brackets, the line is considered corrupted, otherwise it is merely incomplete.
private data class ProgramLine(val brackets: String) {
val unmatchedBrackets: List<Bracket>
get() = brackets
.fold(emptyList()) { unmatchedBrackets, bracket ->
when {
bracket.isClosing && bracket.match == unmatchedBrackets.last() -> unmatchedBrackets.dropLast(1)
else -> unmatchedBrackets.plus(bracket)
}
}
val isCorrupted: Boolean
get() = unmatchedBrackets.any { it.isClosing }
}
Finally, we have to score the lines. For part 1, we have to score each corrupted line based on the first unmatched closing bracket and sum over all lines. We add another virtual property to ProgramLine and another extension property to our Bracket type.
private data class ProgramLine(val brackets: String) {
val unmatchedBrackets: List<Bracket>
get() = brackets
.fold(emptyList()) { unmatchedBrackets, bracket ->
when {
bracket.isClosing && bracket.match == unmatchedBrackets.last() -> unmatchedBrackets.dropLast(1)
else -> unmatchedBrackets.plus(bracket)
}
}
val isCorrupted: Boolean
get() = unmatchedBrackets.any { it.isClosing }
// New!
val syntaxErrorScore: Int
get() = unmatchedBrackets
.firstOrNull { it.isClosing }?.illegalCharacterScore ?: 0
}
private val Bracket.illegalCharacterScore: Int
get() = when (this) {
')' -> 3
']' -> 57
'}' -> 1197
'>' -> 25137
else -> throw IllegalArgumentException("Not a closing bracket")
}
For part 2, we have to do a bit more work, iterating over all unmatched open brackets of the non-corrupted lines and building up a score. The approach is more or less the same though - another virtual property for ProgramLine and another extension property for Bracket:
private data class ProgramLine(val brackets: String) {
val unmatchedBrackets: List<Bracket>
get() = brackets
.fold(emptyList()) { unmatchedBrackets, bracket ->
when {
bracket.isClosing && bracket.match == unmatchedBrackets.last() -> unmatchedBrackets.dropLast(1)
else -> unmatchedBrackets.plus(bracket)
}
}
val isCorrupted: Boolean
get() = unmatchedBrackets.any { it.isClosing }
// New!
val completionStringScore: Long
get() = unmatchedBrackets
.reversed()
.fold(0L) { score, bracket -> score * 5 + bracket.closingCharacterScores }
}
private val Bracket.closingCharacterScores: Int
get() = when (this) {
'(' -> 1
'[' -> 2
'{' -> 3
'<' -> 4
else -> throw IllegalArgumentException("Not an opening bracket")
}
Finally, we can calculate the scores for both parts and solve the puzzle.
fun main() {
val lines = inputLines(2021, 10).map(::ProgramLine)
println(part1(lines))
println(part2(lines))
}
private fun part1(lines: List<ProgramLine>) = lines
.sumOf { it.syntaxErrorScore }
private fun part2(lines: List<ProgramLine>) = lines
.filterNot { it.isCorrupted }.let { incompleteLines ->
incompleteLines.map { it.completionStringScore }.sorted()[incompleteLines.size / 2]
}
Two more stars in the bag.