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

Day 5: If You Give A Seed A Fertilizer

day-5.livemd

Day 5: If You Give A Seed A Fertilizer

Part one

sample_input =
  """
  seeds: 79 14 55 13

  seed-to-soil map:
  50 98 2
  52 50 48

  soil-to-fertilizer map:
  0 15 37
  37 52 2
  39 0 15

  fertilizer-to-water map:
  49 53 8
  0 11 42
  42 0 7
  57 7 4

  water-to-light map:
  88 18 7
  18 25 70

  light-to-temperature map:
  45 77 23
  81 45 19
  68 64 13

  temperature-to-humidity map:
  0 69 1
  1 0 69

  humidity-to-location map:
  60 56 37
  56 93 4
  """
defmodule SeedToLocation do
  def find_lowest_locations(input, mode) when mode in [:seeds_as_is, :seeds_as_ranges] do
    [seeds_block | mapping_blocks] = String.split(input, "\n\n", trim: true)

    finder_fn = create_location_finder(mapping_blocks)

    first.._ =
      seeds_block
      |> parse_seeds(mode)
      |> finder_fn.()
      |> Enum.min()

    first
  end

  defp create_location_finder(blocks) do
    mappers = Enum.map(blocks, &create_mapper/1)

    fn input_ranges ->
      for mapper <- mappers, reduce: input_ranges do
        acc -> mapper.(acc)
      end
    end
  end

  defp create_mapper(block) do
    [_preamble | rules] = String.split(block, "\n", trim: true)

    rules =
      Enum.map(rules, fn rule ->
        [dest_start, src_start, length] = parse_numbers(rule)
        # create a map with ranges as keys, and an offset as value
        # e.g. %{98..99 => -48, 50..97 => 2, ...}
        {src_start..(src_start + length - 1), dest_start - src_start}
      end)
      |> Enum.into(%{})

    fn input_ranges -> Enum.flat_map(input_ranges, &amp;map_range(&amp;1, rules)) end
  end

  defp map_range(input_range, rules) do
    # 1. process rules
    result =
      Enum.reduce(rules, %{}, fn {rule_range, rule_offset}, acc ->
        unless Range.disjoint?(rule_range, input_range) do
          Map.put(acc, intersection(input_range, rule_range), rule_offset)
        else
          acc
        end
      end)

    # 2. fill in ranges not covered by the rules (offset is 0)
    result =
      difference(input_range, Map.keys(result))
      |> Enum.reduce(result, &amp;Map.put(&amp;2, &amp;1, 0))

    # 3. apply all offsets
    Enum.map(result, fn {range, offset} -> Range.shift(range, offset) end)
  end

  defp intersection(range_1, range_2),
    do: max(range_1.first, range_2.first)..min(range_1.last, range_2.last)

  def difference(range, ranges_to_subtract) when is_list(ranges_to_subtract) do
    Enum.reduce(ranges_to_subtract, [range], fn to_subtract, acc ->
      Enum.flat_map(acc, &amp;difference(&amp;1, to_subtract))
      |> Enum.reject(&amp;(&amp;1 == ..))
    end)
  end

  def difference(range_1, range_2) do
    unless Range.disjoint?(range_1, range_2) do
      intersection = intersection(range_1, range_2)

      []
      |> then(fn result ->
        if intersection.first > range_1.first do
          [range_1.first..(intersection.first - 1) | result]
        else
          result
        end
      end)
      |> then(fn result ->
        if intersection.last < range_1.last do
          [(intersection.last + 1)..range_1.last | result]
        else
          result
        end
      end)
    else
      [range_1]
    end
  end

  defp parse_seeds("seeds: " <> fragment, mode) do
    numbers = parse_numbers(fragment)

    case mode do
      :seeds_as_ranges ->
        numbers
        |> Enum.chunk_every(2)
        |> Enum.map(fn [start, length] ->
          start..(start + length - 1)
        end)

      :seeds_as_is ->
        Enum.map(numbers, &amp;(&amp;1..&amp;1))
    end
  end

  defp parse_numbers(fragment), do: String.split(fragment) |> Enum.map(&amp;String.to_integer/1)
end
SeedToLocation.find_lowest_locations(sample_input, :seeds_as_is)
# expect 35
Path.join(__DIR__, "day-5.input")
|> File.read!()
|> SeedToLocation.find_lowest_locations(:seeds_as_is)

# 174137457

Part two

SeedToLocation.find_lowest_locations(sample_input, :seeds_as_ranges)
# expect 46
Path.join(__DIR__, "day-5.input")
|> File.read!()
|> SeedToLocation.find_lowest_locations(:seeds_as_ranges)

# 1493866