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

Day 2: Red-Nosed Reports

2024/day-02.livemd

Day 2: Red-Nosed Reports

Mix.install([{:kino, "~> 0.11.3"}])

Day 2

sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
Enum.chunk_every([1,2,3,4,5], 2, 1, :discard)
differences = fn levels ->
  levels |> Enum.chunk_every(2, 1, :discard) |> Enum.map(fn [a, b] -> b - a end)
end

is_safe = fn raw_report ->
  diffs =
    raw_report
    |> String.split(" ", trim: true)
    |> Enum.map(&String.to_integer/1)
    |> differences.()

  Enum.all?(diffs, &amp; abs(&amp;1) >  0 &amp;&amp; abs(&amp;1) < 4)
  &amp;&amp; (Enum.all?(diffs, &amp; &amp;1 > 0) || Enum.all?(diffs, &amp; &amp;1 < 0))
end

safe_report_count = fn input ->
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.reduce(0, fn raw_report, safe_count ->
    if is_safe.(raw_report), do: safe_count + 1, else: safe_count
  end)
end
safe_report_count.(sample_input)
safe_report_count.(real_input)
defmodule Part2 do
  def safe_count(input) do
    input
    |> Kino.Input.read()
    |> String.split("\n", trim: true)
    |> Enum.reduce(0, fn raw_report, safe_count ->
      if safe?(raw_report), do: safe_count + 1, else: safe_count
    end)
  end

  defp safe?(raw_report) when is_binary(raw_report) do
    [first, second | rest] =
      raw_report
      |> String.split(" ", trim: true)
      |> Enum.map(&amp;String.to_integer/1)

    # seed the prev value and check the case where we skip it as well
    safe?(%{can_skip: true, direction: nil, prev: first, levels: [second | rest]}) or
      safe?(%{can_skip: false, direction: nil, prev: second, levels: rest})
  end

  # base case, we got to the end so we're good
  defp safe?(%{levels: []}), do: true

  # two possible paths to safety, and we check them both:
  # - the first level is good and we use it and recurse down the list
  # - we have a skip card and we play it and recurse down the list
  defp safe?(%{levels: [level | rest]} = state) do
    (safe_difference?(state) and
       %{state | levels: rest, prev: level} |> ensure_direction(level, state.prev) |> safe?()) or
      (state.can_skip and safe?(%{state | levels: rest, can_skip: false}))
  end

  # best guard ever
  def safe_difference?(%{levels: [next | _], prev: prev, direction: dir}) when
    abs(next - prev) > 0 and abs(next - prev) < 4 and
      ((prev < next and dir == :increasing) or
         (prev > next and dir == :decreasing) or
         dir == nil), do: true

  def safe_difference?(_), do: false

  defp ensure_direction(%{direction: nil} = state, next, prev) do
    %{state | direction: if(next < prev, do: :decreasing, else: :increasing)}
  end

  defp ensure_direction(state_with_direction, _a, _b), do: state_with_direction
end
Part2.safe_count(sample_input)
Part2.safe_count(real_input)