Day 5: Print Queue
Mix.install([
  {:kino, "~> 0.14.0"}
])
require IntegerReading
- We’re printing a new safety manual
- The pages for the manual must be printed in a specific order
- 
Notation x|ymeans if both page X and Y must be printed, page X must be printed before page Y- Note: “some time after” means there can be other pages between these two!
 
- Input The order rules in the above notation
- Input A list of pages to print
- 
Goal    - From the given rules, find which print jobs are valid
- For each valid job, find the middle page number
- Add all middle page numbers together
 
Step 3 becons the question: Do all jobs have an odd number of pages - and if not, whats the middle?
Input
[raw_rules, raw_jobs] =
  Kino.FS.file_path("day5_input.bin")
  |> File.read!()
  |> String.split("\n\n", trim: true)
rules = raw_rules
  |> String.split("\n", trim: true)
  |> Enum.map(fn rule ->
    [l, r] = String.split(rule, "|")
    {String.to_integer(l), String.to_integer(r)}
  end)
  |> IO.inspect(label: "rules")
jobs = raw_jobs
  |> String.split("\n", trim: true)
  |> Enum.map(fn job ->
    job
    |> String.split(",", trim: true)
    |> Enum.map(&String.to_integer/1)
  end)
  |> IO.inspect(label: "jobs")
:okImplementation
rule_lookup = rules
  |> Enum.reduce(%{}, fn {before, later} = rule, lookup ->
    lookup
    |> Map.update(before, [rule], fn list -> [rule | list] end)
    |> Map.update(later, [rule], fn list -> [rule | list] end)
  end)
find_middle_page! = fn job ->
  length = length(job)
  if Integer.is_even(length), do: raise "Middle of even list?!?"
  Enum.at(job, Integer.floor_div(length, 2))
end
check_valid = fn lookup, job ->
  job
  |> Enum.with_index()
  |> Enum.reduce_while(:valid, fn {page, index}, _acc ->
    lookup
    |> Map.get(page, [])
    # NOTE: Double :cont or :halt tag because we have `reduce_while` _inside_
    # another `reduce_while`
    |> Enum.reduce_while({:cont, :valid}, fn
      {^page, later}, _acc ->
        case Enum.find_index(job, &Kernel.==(&1, later)) do
          nil -> {:cont, {:cont, :valid}}
          found_at when found_at > index -> {:cont, {:cont, :valid}}
          _ -> {:halt, {:halt, :invalid}}
        end
        
      {before, ^page}, _acc ->
        case Enum.find_index(job, &Kernel.==(&1, before)) do
          nil -> {:cont, {:cont, :valid}}
          found_at when found_at < index -> {:cont, {:cont, :valid}}
          _ -> {:halt, {:halt, :invalid}}
        end
    end)
  end)
  |> case do
    :valid -> {:valid, find_middle_page!.(job)}
    :invalid -> :invalid
  end
end
middle_page_sum = jobs
  |> Enum.reduce(0, fn job, count ->
    case check_valid.(rule_lookup, job) do
      {:valid, middle} -> count + middle
      :invalid -> count
    end
  end)Part 2
- Now we’re correcting the incorrect print jobs using the same rules
- Output Sum of all previously incorrect jobs middle pages
Idea
This sounds just like sorting an array but the comparison isn’t done with the value but rather the rule…
correct_job = fn lookup, job ->
  Enum.sort(job, fn a, b ->
    lookup
    |> Map.get(a, [])
    |> Enum.filter(fn {l, r} -> l == b or r == b end)
    |> case do
      [] -> true # same
      [{^a, ^b}] -> true # a before b
      [{^b, ^a}] -> false # a after b
      rule -> raise "invalid rule found: #{inspect(rule)}"
    end
  end)
end
corrected_jobs_middle_page_sum = jobs
  |> Stream.filter(fn job -> check_valid.(rule_lookup, job) == :invalid end)
  |> Stream.map(fn invalid_job -> correct_job.(rule_lookup, invalid_job) end)
  |> Enum.reduce(0, fn job, count ->
    case check_valid.(rule_lookup, job) do
      {:valid, middle} -> count + middle
      :invalid -> raise "job should be valid! #{inspect(job)}"
    end
  end)
NOTE: The raise statements are just here as assertions to validate if the code does the right thing.
Also, we don’t need to validate the jobs after we have corrected them, but if the correction has a bug, we can find it early.
In production, I’d split this between runtime code and test. But for the puzzle, it’s one and the same.