Advent of Code 2023 - Day 4, in Kotlin - Scratchcards
Kotlin solutions to parts 1 and 2 of Advent of Code 2023, Day 4: 'Scratchcards'
Today’s puzzle is a good lesson in not getting too attached to the way data is show in the description. There is a strong temptation to store everything. And in the real world, that may actually be the right answer. But for a puzzle whose solution we really only need once, we can avoid a lot of complexity by discarding almost everything once we’ve calculated what we need.
If you’d rather just view the code, my GitHub Repository is here.
Puzzle Input
Because each scratchcard is on its own row, we can take our input
as a List<String>
, just like in past days. We won’t declare input
as a property because we’ll use it in the initializer to get what we need.
class Day04(input: List<String>) {
private val cardMatches = input.map { parseCard(it) }
private fun parseCard(input: String): Int {
val winningNumbers = input.substringAfter(":")
.substringBefore("|")
.split(" ")
.filter { it.isNotEmpty() }
.toSet()
val ourNumbers = input.substringAfter("|")
.split(" ")
.filter { it.isNotEmpty() }
.toSet()
return winningNumbers.intersect(ourNumbers).size
}
}
The fun part about this puzzle is that we don’t care about most of the data once we’ve got our score figured out. We don’t care what numbers are on the card or what numbers were picked once we know how many we match. And we certainly don’t care about the card’s id so long as we can keep them in order. Given that we will only store the score for each card.
We will use substringAfter
and substringBefore
to pick through the input
, and grab what we need. We don’t bother converting anything to integers because Strings will compare the same way, so why waste the effort?
We make two sets - one for the winningNumbrs
and one for ourNumbers
. The only part that we store, the score, is calculated by measuring how many elements these two sets have in common via the intersect
function.
⭐ Day 4, Part 1
The puzzle text can be found here.
The only thing left to do for part 1 is to count up the scores, using the formula provided. Because Kotlin doesn’t have a built in function to raise an Int
to an Int
power, we will have to call the existing pow
function off of a Double
(2.0).
// In Day04
fun solvePart1(): Int =
cardMatches.sumOf { 2.0.pow(it-1).toInt() }
Once again, we use the ever-helpful sumOf
function to sum up the scores of all our cardMatches
. We do this by raising 2 to the score-1’s power and casting the result back to an Int
.
Star earned! Onward!
⭐ Day 4, Part 2
The puzzle text can be found here.
For part 2 our data is in a usable format so our calculation is rather straight forward.
// In Day04
fun solvePart2(): Int {
val cards = IntArray(cardMatches.size) { 1 }
cardMatches.forEachIndexed { index, score ->
repeat(score) {
cards[index+it+1] += cards[index]
}
}
return cards.sum()
}
We will declare an IntArray
to represent how many cards
of each id we have. We seed this array with 1
in each spot because we have one of each card.
Next, we look through our cardMatches
list which contains the scores for each card. Using forEachIndexed
lets us know the place within the cardMatches
array as well as the value of the array (the score
). We use these facts to calculate how many cards we just won.
Finally, we repeat
a calculation score
number of times, which allows us to calculate how many cards we need to add to the next score
number piles of cards. I think there’s probably a way I could have used some version of fold
or reduce
here, but this seems clearer to me. In the real world, we would do some bounds checking here to make sure we don’t attempt to write to an element in cards
that doesn’t exist, but the puzzle explicitly states this cannot happen, so I’m not going to write code to handle that.
Star earned! See you tomorrow!
Further Reading
- Index of All Solutions - All posts and solutions for 2023, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 4
- Advent of Code - Come join in and do these challenges yourself!