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

Day 2: Red-Nosed Reports

day2_red_nosed_reports.livemd

Day 2: Red-Nosed Reports

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

Reading

  • We’re analyzing data readings from a reactor
  • Input: One report per line
    • Each unmber in a report is called a level
  • Determine which are “save”
    • all levels are increasing or decreasing
    • adjacent levels differ by at least one and at most three

Input

File day2_input.bin in the attachments.

Loading the Data

reports =
  "day2_input.bin"
  |> Kino.FS.file_path()
  |> File.stream!()
  |> Stream.map(fn line ->
    line
    |> String.split()
    |> Enum.map(&String.to_integer/1)
  end)
  |> Enum.to_list()

NOTE: Elixir interprets some of the lists with integers as strings. This is why in the printed output they show up as weird strings. They’re all lists of numbers though!

all_same_elements? = fn [first | rest] ->
  Enum.all?(rest, fn element -> element == first end)
end

is_safe? = fn report ->
  chunks = Enum.chunk_every(report, 2, 1, :discard)
  
  one_direction? = chunks
    |> Enum.map(fn [a, b] -> a > b end)
    |> all_same_elements?.()
  
  step_size? = Enum.reduce_while(chunks, true, fn [a, b], _acc ->
      difference = abs(a - b)
      if difference > 3 or difference < 1, do: {:halt, false}, else: {:cont, true}
    end)
  
  one_direction? and step_size?
end

safe_count = Enum.reduce(reports, 0, fn report, count ->
  if is_safe?.(report), do: count + 1, else: count
end)

Part 2

  • We want to tolerate some level of unsafety
  • If a report only contains a single bad level, it is still considered safe

Brute force would be to remove one level left-to-right from an unsafe the report and run the safety check again. If either of them succeed, we count it safe. If not, it stays unsafe.

dampened_safe_count = Enum.reduce(reports, 0, fn report, count ->
  if is_safe?.(report) do
    count + 1
  else
    Enum.reduce_while(1..length(report), count, fn index, _ ->
      report
      |> List.delete_at(index - 1)
      |> is_safe?.()
      |> case do
        true -> {:halt, count + 1}
        false -> {:cont, count}
      end
    end)
  end
end)

Alternatively, we can make is_safe/1 recursive. It will receive the current status of our evaluation until now and the rest of the levels we still need to look at for this report.

# PSEUDO CODE!
recursive_is_safe? = fn {already_skipped?, direction, previous}, [level | rest] ->
  # TODO determine initial `direction` for first two items
  step_size? = abs(previous - level) # TODO check if valid jump
  one_direction? = same_direction(previous, level) # TODO implement using `:gte`, etz?
  case do
    step_size? and one_direction? ->
      recursive_is_safe?.({already_skipped?, direction, level}, rest)

    already_skipped? == false ->
      # Drop current level and try to continue
      recursive_is_safe?.({true, direction, previous}, rest)

    already_skipped? == true ->
      # We already dropped once before, no use
      false
  end
end