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, &map_range(&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, &Map.put(&2, &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, &difference(&1, to_subtract))
|> Enum.reject(&(&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, &(&1..&1))
end
end
defp parse_numbers(fragment), do: String.split(fragment) |> Enum.map(&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