Advent of Code #4 (in Crystal)
Caleb Weeks
Posted on December 4, 2023
If you've followed any of my blog posts, you know that I am a fan of functional programming. And although I'm still a fan of the functional paradigm, I've been trying to improve my knowledge of object oriented programming and finding use cases to apply that knowledge.
Don't get me wrong, I still think that FP has advantages over OOP in many if not most situations. Even when an OOP solution might be more ergonomic or simple or elegant, it often introduces foot-guns that might not be immediately apparent. Specifically, the practice of mutable state can cause subtle issues, even if it is nicely encapsulated in an object.
Anyway, I solved part 1 with a tidy functional solution. You can see most of that process in this video. When it came to part 2, it seemed like a good use case to apply some OOP. So I did a large refactor and made sure that part 1 still gave me the right answer.
The Card
type alias worked well for part 1, but I needed to store the number of cards as well for part 2. This meant expanding the type of Card
from Hash(String, Array(Int32))
to Hash(String, Array(Int32) | Int32)
. But then, any place in the code where I am setting the value, I have to explicitly define the type. As far as I am aware, Crystal doesn't have literal hash types, meaning I could not define a hash with specific keys and corresponding value types. At this point, an object (class) seemed like the better option.
I actually really like how the refactor turned out. The parsing of the card was moved to the from_str
constructor, and other derived values were split up into their respective sections. And the code for part 1 became a simple one liner.
Here's where the foot-gun comes: part 2 involves mutating the cards in the Cards
hash. Fortunately, part 1 leaves the cards list alone, but if you were to reuse the cards
variable after part 2, it would contain cards with counts that have been modified. Maybe not the biggest deal, but it's something to keep in mind.
Alright, enough about that. Here's the code:
input = File.read("input").strip
alias Cards = Hash(Int32, Card)
class Card
@winning : Array(Int32)
@have : Array(Int32)
property count : Int32
def initialize(@winning, @have, @count = 1)
end
def self.from_str(str)
winning, have = str.split('|')
winning = winning.scan(/\d+/).map(&.[0].to_i)
have = have.scan(/\d+/).map(&.[0].to_i)
self.new(winning, have)
end
def wins
(@winning&(@have)).size
end
def worth
self.wins > 0 ? 2 ** (self.wins - 1) : 0
end
def duplicate(times)
@count += times
end
end
cards = input.split("\n").reduce(Cards.new) do |cards, line|
head, body = line.split(':')
card_number = head.lchop("Card ").to_i
cards.merge({card_number => Card.from_str(body)})
end
part1 = cards.values.map(&.worth).sum
puts part1
part2 = cards.reduce(0) do |sum, (card_number, card)|
if card.wins > 0
((card_number + 1)..(card_number + card.wins)).each do |number|
cards[number].duplicate(card.count)
end
end
sum + card.count
end
puts part2
Posted on December 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.