Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 6: Probably a Fire Hazard

2015/6.livemd

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, &amp;instruct(&amp;2, &amp;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(&amp;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(&amp;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()