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

Day 05

day05.livemd

Day 05

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

IEx.Helpers.c("/Users/johnb/dev/2023adventOfCode/advent_of_code.ex")
alias AdventOfCode, as: AOC
alias Kino.Input

# Note: when making the next template, something like this works well:
#   `cat day04.livemd | sed 's/03/04/' > day04.livemd`
#

Installation and Data

input_p1example = Kino.Input.textarea("Example Data")
input_p1puzzleInput = Kino.Input.textarea("Puzzle Input")
input_source_select =
  Kino.Input.select("Source", [{:example, "example"}, {:puzzle_input, "puzzle input"}])
p1data = fn ->
  (Kino.Input.read(input_source_select) == :example &&
     Kino.Input.read(input_p1example)) ||
    Kino.Input.read(input_p1puzzleInput)
end

Part 1

defmodule Day05 do
  def make_ranges(lines) do
    lines
    |> Enum.reduce([], fn line, acc ->
      [dest_start, src_start, len] =
        String.split(line, ~r/\W/, trim: true)
        |> Enum.map(&String.to_integer/1)

      [%{src: Range.new(src_start, src_start + len - 1), dest: dest_start, len: len} | acc]
    end)
    |> Enum.reverse()
  end

  def translate(seed, mappings, which_map) do
    map_list = mappings["#{which_map} map:"]
    # |> IO.inspect()
    case Enum.find(map_list, fn item ->
           seed in item.src
         end) do
      nil -> seed
      item -> seed - item.src.first + item.dest
    end
  end

  def parse_raw_seeds_and_mappings(text) do
    [seeds | mappings] = AOC.as_doublespaced_paragraphs(text)
    # IO.inspect(seeds)
    seeds =
      String.split(seeds, ~r/\W/, trim: true)
      |> Enum.slice(1..-1)
      |> Enum.map(&String.to_integer/1)

    mappings =
      mappings
      |> Enum.reduce(%{}, fn mapping, acc ->
        [name | maps] = String.split(mapping, "\n", trim: true)
        Map.put_new(acc, name, make_ranges(maps))
      end)

    [seeds, mappings]
  end

  def solve(text) do
    [seeds, mappings] = parse_raw_seeds_and_mappings(text)

    seeds
    |> Enum.map(fn seed ->
      seed
      |> translate(mappings, "seed-to-soil")
      |> translate(mappings, "soil-to-fertilizer")
      |> translate(mappings, "fertilizer-to-water")
      |> translate(mappings, "water-to-light")
      |> translate(mappings, "light-to-temperature")
      |> translate(mappings, "temperature-to-humidity")
      |> translate(mappings, "humidity-to-location")
    end)
    |> Enum.min()
  end

  def split_and_map(range, mapping_list) do
    # assume only one mapping will match
    mapping_list
    |> Enum.reduce_while(range, fn mapping, acc ->
      # pre-create what we need, whether we use it or not
      delta = mapping.dest - mapping.src.first
      mapit = fn r -> Range.new(r.first + delta, r.last + delta) end
      left = Range.new(range.first, mapping.src.first - 1)
      right = Range.new(mapping.src.last + 1, range.last)

      remapped =
        Range.new(
          max(mapping.src.first, range.first),
          min(mapping.src.last, range.last)
        )
        |> mapit.()

      case {range.first < mapping.src.first, range.last < mapping.src.first,
            range.first > mapping.src.last, range.last > mapping.src.last} do
        {true, true, _, _} ->
          {:cont, acc}

        {_, _, true, _} ->
          {:cont, acc}

        {true, false, false, true} ->
          {:halt,
           [split_and_map(left, mapping_list), remapped, split_and_map(right, mapping_list)]}

        {true, false, false, false} ->
          {:halt, [split_and_map(left, mapping_list), remapped]}

        {false, false, false, true} ->
          {:halt, [remapped, split_and_map(right, mapping_list)]}

        {false, false, false, false} ->
          {:halt, remapped}
      end
    end)
  end

  def translate2(seed_ranges, mappings, which_map) do
    # IO.puts("\ntranslate2(#{inspect(seed_ranges)},,#{inspect(which_map)})")
    mapping_list = mappings["#{which_map} map:"]

    seed_ranges
    |> List.flatten()
    |> Enum.map(fn seed_range ->
      seed_range
      |> split_and_map(mapping_list)
    end)
  end

  def solve2(text) do
    [seeds, mappings] = parse_raw_seeds_and_mappings(text)

    initial_seed_ranges =
      Enum.chunk_every(seeds, 2)
      |> Enum.map(fn [start, len] -> Range.new(start, start + len - 1) end)

    initial_seed_ranges
    |> translate2(mappings, "seed-to-soil")
    |> translate2(mappings, "soil-to-fertilizer")
    |> translate2(mappings, "fertilizer-to-water")
    |> translate2(mappings, "water-to-light")
    |> translate2(mappings, "light-to-temperature")
    |> translate2(mappings, "temperature-to-humidity")
    |> translate2(mappings, "humidity-to-location")
    |> List.flatten()
    |> Enum.map(fn range -> range.first end)
    |> Enum.min()
  end
end

p1data.()
|> Day05.solve()
|> IO.inspect(label: "\n*** Part 1 solution (example: 35)")

# 389056265

p1data.()
|> Day05.solve2()
|> IO.inspect(label: "\n*** Part 2 solution (example: 46)")

# 137516820