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

Day 05

2023/day05.livemd

Day 05

Mix.install(kino: "~> 0.11")

Common

import Kino.Shorts

input = read_textarea("Puzzle input", monospace: true)
lines = String.split(input, "\n")
defmodule Almanac do
  defstruct [:seeds, :transformations]

  def parse_lines_p1(lines) do
    parse_lines(lines, &parse_seeds_p1/1)
  end

  def parse_lines_p2(lines) do
    parse_lines(lines, &parse_seeds_p2/1)
  end

  defp parse_lines(lines, seed_parser) do
    [raw_seeds | raw_transformations] = lines

    seeds = seed_parser.(raw_seeds)
    transformations = parse_transformations(raw_transformations)

    %__MODULE__{
      seeds: seeds,
      transformations: transformations
    }
  end

  defp parse_seeds_p1(raw_seeds) do
    [_, seeds_str] = String.split(raw_seeds, ":", parts: 2)
    seeds_str |> String.split() |> Enum.map(&String.to_integer/1)
  end

  defp parse_seeds_p2(raw_seeds) do
    raw_seeds
    |> parse_seeds_p1()
    |> Enum.chunk_every(2)
    |> Enum.map(fn [first, length] -> Range.new(first, first + length) end)
  end

  defp parse_transformations(raw_transformations) do
    raw_transformations
    |> Stream.chunk_by(&(&1 == ""))
    |> Stream.reject(&(&1 == [""]))
    |> Stream.map(fn list ->
      raw_ranges = Enum.drop(list, 1)

      Enum.map(raw_ranges, fn raw_range ->
        [dest_start, source_start, length] =
          raw_range
          |> String.split()
          |> Enum.map(&String.to_integer/1)

        {dest_start, Range.new(source_start, source_start + length)}
      end)
    end)
  end
end
defmodule Utils do
  def clamp(min, value, max) do
    max(min, min(value, max))
  end
end

Part 1

almanac = Almanac.parse_lines_p1(lines)

almanac.transformations
|> Enum.reduce(almanac.seeds, fn transformation, seeds ->
  Enum.map(seeds, fn seed ->
    range = Enum.find(transformation, fn {_, source_range} -> seed in source_range end)

    case range do
      {dest_start, source_range} ->
        delta = seed - source_range.first
        dest_start + delta

      nil ->
        seed
    end
  end)
end)
|> Enum.min()

Part 2

import Utils

almanac = Almanac.parse_lines_p2(lines)

almanac.transformations
|> Enum.reduce(almanac.seeds, fn transformation, seeds ->
  transformation
  |> Enum.reduce(seeds, fn {dest_start, source_range}, seeds ->
    seeds
    |> Enum.flat_map(fn
      {:shifted, _, _} = seed ->
        [seed]

      seed ->
        if Range.disjoint?(seed, source_range) do
          [seed]
        else
          start = clamp(seed.first, source_range.first, seed.last)
          stop = clamp(seed.first, source_range.last, seed.last)

          prefix = Range.new(seed.first, start - 1, 1)
          shifted = Range.new(start, stop, 1)
          suffix = Range.new(stop + 1, seed.last, 1)

          [prefix, {:shifted, shifted, dest_start - source_range.first}, suffix]
        end
    end)
    |> Enum.reject(fn
      {:shifted, range, _steps} -> Enum.empty?(range)
      range -> Enum.empty?(range)
    end)
  end)
  |> Enum.map(fn
    {:shifted, range, steps} -> Range.shift(range, steps)
    range -> range
  end)
end)
|> Enum.min_by(& &1.first)
|> Map.fetch!(:first)