AdventOfCode/2021/day4.exs

134 lines
3.5 KiB
Elixir

defmodule Day4 do
@rows for y <- 0..4, do: for(x <- 0..4, do: {x, y})
@cols for x <- 0..4, do: for(y <- 0..4, do: {x, y})
def part1(input) do
{current_number, winning_board} = play(input)
sum_unmarked(winning_board) * current_number
end
def part2(input) do
{current_number, last_board} = play_until_last(input)
sum_unmarked(last_board) * current_number
end
def play({[current_number | next_numbers], boards}) do
boards = for board <- boards, do: mark(board, current_number)
case check_for_winner(boards) do
nil -> play({next_numbers, boards})
board -> {current_number, board}
end
end
def play_until_last({_numbers, [_last_board]} = input), do: play(input)
def play_until_last({numbers, boards} = input) do
{_current_number, current_winner} = play(input)
current_winner_no_markers = Map.new(current_winner, fn {k, {v, _marker}} -> {k, v} end)
remaining_boards =
Enum.reject(boards, fn board ->
current_winner_no_markers == Map.new(board, fn {k, {v, _marker}} -> {k, v} end)
end)
play_until_last({numbers, remaining_boards})
end
def sum_unmarked(board) do
board
|> Enum.filter(&match?({_key, {_number, false}}, &1))
|> Enum.map(fn {_key, {number, _marked}} -> number end)
|> Enum.sum()
end
def mark(board, current_number) do
case Enum.find(board, &match?({_key, {^current_number, false}}, &1)) do
nil -> board
{key, _value} -> Map.put(board, key, {current_number, true})
end
end
def check_for_winner([]), do: nil
def check_for_winner([board | boards]) do
row_complete? =
Enum.any?(@rows, fn row ->
values = for key <- row, do: board[key]
Enum.all?(values, &match?({_number, true}, &1))
end)
col_complete? =
Enum.any?(@cols, fn col ->
values = for key <- col, do: board[key]
Enum.all?(values, &match?({_number, true}, &1))
end)
if row_complete? or col_complete? do
board
else
check_for_winner(boards)
end
end
def input do
with [input_filename] <- System.argv(),
{:ok, input} <- File.read(input_filename) do
[numbers | boards] = String.split(input, "\n", trim: true)
numbers = numbers |> String.split(",") |> Enum.map(&String.to_integer/1)
boards = parse_boards(boards)
{numbers, boards}
else
_ -> :error
end
end
defp parse_boards([]), do: []
defp parse_boards(boards) do
{board, boards} = Enum.split(boards, 5)
board = Enum.map(board, &String.split/1)
board =
for {row, y} <- Enum.with_index(board), {value, x} <- Enum.with_index(row), into: %{} do
{{x, y}, {String.to_integer(value), false}}
end
[board | parse_boards(boards)]
end
#######################
# HERE BE BOILERPLATE #
#######################
def run do
case input() do
:error -> print_usage()
input -> run_parts_with_timer(input)
end
end
defp run_parts_with_timer(input) do
run_with_timer(1, fn -> part1(input) end)
run_with_timer(2, fn -> part2(input) end)
end
defp run_with_timer(part, fun) do
{time, result} = :timer.tc(fun)
IO.puts("Part #{part} (completed in #{format_time(time)}):\n")
IO.puts("#{result}\n")
end
defp format_time(μsec) when μsec < 1_000, do: "#{μsec}μs"
defp format_time(μsec) when μsec < 1_000_000, do: "#{μsec / 1000}ms"
defp format_time(μsec), do: "#{μsec / 1_000_000}s"
defp print_usage do
IO.puts("Usage: elixir day4.exs input_filename")
end
end
Day4.run()