AOC 2023 - Day 04
Mix.install([
{:kino_aoc, "~> 0.1"}
])
AOC Helper
{:ok, puzzle_input} =
KinoAOC.download_puzzle("2023", "4", System.fetch_env!("LB_AOC_SESSION"))
Part 1
Code
defmodule PartOne do
def check_card(card) do
[_id, numbers] = String.split(card, ": ")
[winning_numbers, existing_numbers] = String.split(numbers, " | ")
winning_numbers =
winning_numbers |> String.split(" ") |> Enum.reject(&(&1 == "")) |> MapSet.new()
existing_numbers =
existing_numbers |> String.split(" ") |> Enum.reject(&(&1 == "")) |> MapSet.new()
winning_numbers
|> MapSet.intersection(existing_numbers)
|> MapSet.to_list()
|> score()
end
def score([]), do: 0
def score([_]), do: 1
def score([_ | rest]), do: Enum.reduce(rest, 1, fn _, score -> score * 2 end)
def play(cards, index) do
count = Enum.at(cards, index)
copies = Enum.slice(cards, (index + 1)..(index + 1 + count))
Enum.reduce(copies, 1, fn copy, total ->
total + elem(copy, 1)
end)
end
def solve(input) do
IO.puts("--- Part One ---")
IO.puts("Result: #{run(input)}")
end
def run(input) do
input
|> String.split("\n")
|> Enum.map(&check_card/1)
|> Enum.sum()
end
end
Test
ExUnit.start(autorun: false)
defmodule PartOneTest do
use ExUnit.Case, async: true
import PartOne
@input "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11"
@expected 13
test "part one" do
actual = run(@input)
assert actual == @expected
end
end
ExUnit.run()
Solution
PartOne.solve(puzzle_input)
Part 2
Code
defmodule PartTwo do
def check_card(card) do
[id, numbers] = String.split(card, ": ")
[winning_numbers, existing_numbers] = String.split(numbers, " | ")
winning_numbers =
winning_numbers |> String.split(" ") |> Enum.reject(&(&1 == "")) |> MapSet.new()
existing_numbers =
existing_numbers |> String.split(" ") |> Enum.reject(&(&1 == "")) |> MapSet.new()
matches =
winning_numbers
|> MapSet.intersection(existing_numbers)
|> MapSet.to_list()
|> Enum.count()
%{id: id, matches: matches, instances: 1}
end
def count_copies(cards) do
cards
|> Enum.with_index()
|> Enum.reduce(cards, fn {_card, index}, cards ->
copies(cards, index)
end)
|> Enum.reduce(0, fn card, total ->
total + card.instances
end)
end
def copies(cards, index) do
case Enum.at(cards, index) do
%{matches: 0} ->
cards
%{matches: matches, instances: instances} ->
(index + 1)..(index + min(matches, Enum.count(cards)))
|> Enum.to_list()
|> Enum.reduce(cards, fn copy_index, cards ->
copy = Enum.at(cards, copy_index)
List.replace_at(
cards,
copy_index,
Map.put(copy, :instances, copy.instances + instances)
)
end)
end
end
def solve(input) do
IO.puts("--- Part Two ---")
IO.puts("Result: #{run(input)}")
end
def run(input) do
input
|> String.split("\n")
|> Enum.map(&check_card/1)
|> count_copies()
end
end
Test
ExUnit.start(autorun: false)
defmodule PartTwoTest do
use ExUnit.Case, async: true
import PartTwo
@input "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11"
@expected 30
test "part two" do
actual = run(@input)
assert actual == @expected
end
end
ExUnit.run()
Solution
PartTwo.solve(puzzle_input)