Powered by AppSignal & Oban Pro

AOC 2023 - Day 5

2023/day5.livemd

AOC 2023 - Day 5

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Input

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "5", System.fetch_env!("LB_AOC_SESSION"))

Code

defmodule Day5Final do
  def parse_map_string(map_strings) do
    %{maps: maps} =
      Enum.reduce(map_strings, %{current_map: nil, maps: %{}}, fn string, acc ->
        cond do
          string == "" ->
            %{acc | current_map: nil}

          String.contains?(string, "map") ->
            [map_name, _map] = String.split(string, " ")
            %{acc | current_map: map_name}

          true ->
            [d, s, i] = String.split(string) |> Enum.map(&String.to_integer/1)
            %{acc | maps: Map.update(acc.maps, acc.current_map, [{d, s, i}], &[{d, s, i} | &1])}
        end
      end)

    maps
  end

  def seed_to_location(seed, ranges_map) do
    seed
    |> find_destination(ranges_map["seed-to-soil"])
    |> find_destination(ranges_map["soil-to-fertilizer"])
    |> find_destination(ranges_map["fertilizer-to-water"])
    |> find_destination(ranges_map["water-to-light"])
    |> find_destination(ranges_map["light-to-temperature"])
    |> find_destination(ranges_map["temperature-to-humidity"])
    |> find_destination(ranges_map["humidity-to-location"])
  end

  def find_destination(src, ranges) do
    destination =
      Enum.reduce_while(ranges, nil, fn {d, s, i}, acc ->
        if s <= src and src <= s + i - 1 do
          diff = src - s
          {:halt, d + diff}
        else
          {:cont, acc}
        end
      end)

    destination || src
  end

  def map_src_value_to_dest({src_start, src_end}, _range = {d, s, _i}) do
    offset = d - s
    {src_start + offset, src_end + offset}
  end

  def map_src_range_to_dest_range(src_range, range) do
    case find_overlap(src_range, range) do
      nil ->
        [src_range]

      {overlap_start, overlap_end} ->
        {src_start, src_end} = src_range

        start =
          if src_start < overlap_start, do: {src_start, overlap_start - 1}

        rest = if overlap_end < src_end, do: {overlap_end + 1, src_end}
        [start, {overlap_start, overlap_end}, rest] |> Enum.filter(&amp;(&amp;1 != nil))
    end
  end

  def find_overlap({src_start, src_end}, {_d, s, i}) do
    overlap_start = max(src_start, s)
    overlap_end = min(s + i - 1, src_end)
    if overlap_start < overlap_end, do: {overlap_start, overlap_end}, else: nil
  end
end

lines = String.split(puzzle_input, "\n")

{["seeds: " <> seed_string], ["" | map_strings]} = Enum.split(lines, 1)

ranges_map = Day5Final.parse_map_string(map_strings)

Part 1

seeds_to_be_planted =
  seed_string
  |> String.split()
  |> Enum.map(&amp;String.to_integer/1)

Enum.map(seeds_to_be_planted, fn seed ->
  Day5Final.seed_to_location(seed, ranges_map)
end)
|> Enum.min()

Part 2

all_mappings =
  [
    ranges_map["seed-to-soil"],
    ranges_map["soil-to-fertilizer"],
    ranges_map["fertilizer-to-water"],
    ranges_map["water-to-light"],
    ranges_map["light-to-temperature"],
    ranges_map["temperature-to-humidity"],
    ranges_map["humidity-to-location"]
  ]
  |> Enum.map(fn ranges ->
    ranges
    |> Enum.sort_by(fn {_, s, _} -> s end)
  end)

seed_ranges =
  seed_string
  |> String.split()
  |> Enum.map(&amp;String.to_integer/1)
  |> Enum.chunk_every(2)
  |> Enum.map(fn [start, interval] -> {start, start + interval - 1} end)

Enum.reduce(all_mappings, seed_ranges, fn dest_ranges, src_ranges ->
  split_src_ranges =
    Enum.map(src_ranges, fn src_range ->
      Enum.reduce(dest_ranges, [src_range], fn range, acc ->
        last_range = Enum.at(acc, -1)
        remaining = Enum.drop(acc, -1)
        remaining ++ Day5Final.map_src_range_to_dest_range(last_range, range)
      end)
    end)
    |> List.flatten()

  Enum.map(split_src_ranges, fn src_range ->
    dest_range = Enum.find(dest_ranges, &amp;Day5Final.find_overlap(src_range, &amp;1))

    if dest_range,
      do: Day5Final.map_src_value_to_dest(src_range, dest_range),
      else: src_range
  end)
end)
|> Enum.sort_by(&amp;elem(&amp;1, 0))
|> List.first()
|> elem(0)