Day 6: Probably a Fire Hazard
Mix.install([
{:kino, "~> 0.12.3"}
])
Modules
defmodule Lights do
@moduledoc """
Functions for creating and manipulating an (n x m) grid
of lights.
"""
@typedoc "A grid of lights"
@type t :: %{row_index() => light_row()}
@typedoc "A row of lights."
@type light_row :: %{col_index() => brightness()}
@typedoc "An index corresponding to the row"
@type row_index :: non_neg_integer()
@typedoc "An index corresponding to the column"
@type col_index :: non_neg_integer()
@typedoc "The coordinates of a light"
@type position :: {col_index(), row_index()}
@typedoc "The instruction type"
@type action :: :turn_on | :turn_off | :toggle
@typedoc "An instruction"
@type instruction :: {action(), position(), position()}
@typedoc "The brightness of a light"
@type brightness :: non_neg_integer()
@typedoc "The rules used when instructing the light grid"
@type rules :: :part1 | :part2
@doc """
Creates a nested Map structure to model the light grid, where each row is a
Map. Arranging the structure this way is significantly more performant than
using a single Map with every tuple of coordinates as keys.
## Example
```elixir
iex> Lights.new(2, 3)
%{
0 => %{
0 => 0,
1 => 0,
2 => 0
},
1 => %{
0 => 0,
1 => 0,
2 => 0
}
}
```
"""
@spec new(pos_integer(), pos_integer()) :: t()
def new(n \\ 1000, m \\ 1000) do
Map.new(for i <- 0..(n - 1), do: {i, Map.new(for j <- 0..(m - 1), do: {j, 0})})
end
@doc """
Runs a list of instructions, given a ruleset to follow.
## Example
```elixir
iex> instructions = [{:turn_on, {0, 0}, {0, 1}}, {:toggle, {0, 0}, {1, 1}}]
iex> Lights.new(2, 2)
iex> |> Lights.run(instructions, :part1)
%{
0 => %{
0 => 0,
1 => 1
},
1 => %{
0 => 0,
1 => 1
}
}
```
"""
@spec run(t(), list(instruction()), rules()) :: t()
def run(lights \\ Lights.new(), instructions, rules) do
instructions
|> Enum.reduce(lights, &instruct(&2, &1, rules))
end
@doc """
Counts the number of lights that are on.
## Example
```elixir
iex> Lights.new(3, 3)
iex> |> Lights.run([{:turn_on, {0, 0}, {2, 2}}], :part1)
iex> |> Lights.total_lit()
9
```
"""
@spec total_lit(t()) :: non_neg_integer()
def total_lit(lights) do
lights
|> lit()
|> Enum.count()
end
@doc """
Returns the total brightness of all the lights
## Example
```elixir
iex> Lights.new(2, 2)
iex> |> Lights.run([{:toggle, {0, 0}, {0, 1}}], :part2)
iex> |> Lights.total_brightness()
4
```
"""
@spec total_brightness(t()) :: non_neg_integer()
def total_brightness(grid) do
grid
|> lit()
|> Enum.reduce(0, fn {_col, val}, acc -> acc + val end)
end
# processes a single instruction
defp instruct(lights, instruction, rules) do
{action, start, finish} = instruction
{{j0, i0}, {j1, i1}} = {start, finish}
updater = updater(action, rules)
i0..i1
|> Enum.reduce(lights, fn
i, lights -> update_row(lights, i, j0, j1, updater)
end)
end
# updates the map at lights[row]
defp update_row(row, i, j0, j1, updater) do
Map.update!(row, i, fn row -> update_row_columns(row, j0, j1, updater) end)
end
# updates the neccessary column values of the row
defp update_row_columns(i, j0, j1, updater) do
j0..j1
|> Enum.reduce(i, fn j, row -> Map.update!(row, j, updater) end)
end
# returns a list of all the lit lights brightness values
defp lit(lights) do
lights
|> Map.values()
|> Enum.flat_map(fn row -> filter_lit(row) end)
end
# returns a list of the lit lights in the row
defp filter_lit(row) do
Map.filter(row, fn {_col, val} -> val > 0 end)
end
# the lambda to use when updating
defp updater(action, rules)
# rules for part1
defp updater(action, :part1) do
case action do
:turn_on -> fn _val -> 1 end
:turn_off -> fn _val -> 0 end
:toggle -> fn val -> if(val == 0, do: 1, else: 0) end
end
end
# rules for part2
defp updater(action, :part2) do
case action do
:turn_on -> fn val -> val + 1 end
:turn_off -> fn val -> if(val > 0, do: val - 1, else: 0) end
:toggle -> fn val -> val + 2 end
end
end
end
defmodule Parser do
@moduledoc """
For parsing text into instructions to use with a grid of lights.
"""
@doc ~S"""
Parses a string into a list of instructions.
## Example
```elixir
iex> Parser.parse("toggle 107,322 through 378,688\nturn off 235,899 through 818,932")
[{:toggle, {107, 322}, {378, 688}}, {:turn_off, {235, 899}, {818, 932}}]
```
"""
@spec parse(String.t()) :: list(Lights.instruction())
def parse(str) do
str
|> String.split("\n")
|> Enum.map(&parse_line/1)
end
# parse a single instruction
defp parse_line(str) do
pattern = ~r/(turn on|turn off|toggle) (\d+,\d+) through (\d+,\d+)/
[inst_type, start, finish] = Regex.run(pattern, str, capture: :all_but_first)
{
parse_action(inst_type),
parse_tuple(start),
parse_tuple(finish)
}
end
# parse a string eg. "20,550" into a tuple {20, 550}
defp parse_tuple(str) do
String.split(str, ",")
|> Enum.map(&String.to_integer/1)
|> List.to_tuple()
end
# parse a type eg. "turn on" into an atom :turn_on
defp parse_action(str)
defp parse_action("turn on"), do: :turn_on
defp parse_action("turn off"), do: :turn_off
defp parse_action("toggle"), do: :toggle
end
Input
input = Kino.Input.textarea("Please paste your puzzle input:")
Part 1
input
|> Kino.Input.read()
|> Parser.parse()
|> Lights.run(:part1)
|> Lights.total_lit()
Part 2
input
|> Kino.Input.read()
|> Parser.parse()
|> Lights.run(:part2)
|> Lights.total_brightness()