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