Ticket Translation
defmodule Aoc2020.Day16 do
@moduledoc "Ticket Translation"
def run(), do: part_2()
def part_1() do
{rules, [_, nearby_tickets]} = parse_input()
all_rule_ranges = Enum.map(rules, &elem(&1, 1)) |> List.flatten()
invalid_values =
List.flatten(nearby_tickets)
|> Enum.filter(fn field ->
!Enum.any?(all_rule_ranges, fn {min, max} -> field >= min && field <= max end)
end)
Enum.sum(invalid_values)
end
def part_2() do
{rules, [[my_ticket], nearby_tickets]} = parse_input()
valid_nearby = filter_valid_tickets(nearby_tickets, rules)
field_order = map_field_order(rules, valid_nearby)
departure_fields =
Enum.filter(field_order, fn {_, v} -> String.starts_with?(v, "departure") end)
Enum.map(departure_fields, fn {index, _} -> Enum.at(my_ticket, index) end)
|> Enum.reduce(&*/2)
end
def parse_input() do
File.read!("priv/inputs/2020/day16.txt")
|> String.trim()
|> String.split("\n\n")
|> Enum.map(&String.split(&1, "\n"))
|> (fn [rules | ticket_groups] ->
{
parse_rules(rules),
Enum.map(ticket_groups, &parse_tickets/1)
}
end).()
end
def parse_rules(rules) do
Enum.map(rules, fn rule ->
[label, rule_set] = String.split(rule, ": ", parts: 2)
parsed_rules = String.split(rule_set, " or ") |> Enum.map(&parse_range/1)
{label, parsed_rules}
end)
end
def parse_range(str) do
String.split(str, "-") |> Enum.map(&String.to_integer/1) |> List.to_tuple()
end
def parse_tickets([_ | tickets]) do
Enum.map(tickets, fn line -> String.split(line, ",") |> Enum.map(&String.to_integer/1) end)
end
def filter_valid_tickets(tickets, rules) do
all_rule_ranges = Enum.map(rules, &elem(&1, 1)) |> List.flatten()
Enum.filter(tickets, fn ticket ->
Enum.all?(ticket, fn field ->
Enum.any?(all_rule_ranges, fn {min, max} -> field >= min && field <= max end)
end)
end)
end
def map_field_order(rules, tickets) do
find_possible_fields(rules, tickets)
|> Enum.sort_by(&length(elem(&1, 1)))
|> Enum.reduce({MapSet.new(), %{}}, &match_unused_field/2)
|> elem(1)
end
def find_possible_fields(rules, tickets) do
field_indexes = 0..(length(hd(tickets)) - 1)
Enum.reduce(field_indexes, [], fn index, acc ->
tickets_field = Enum.map(tickets, &Enum.at(&1, index))
matches =
Enum.filter(rules, fn {_, [{minA, maxA}, {minB, maxB}]} ->
Enum.all?(tickets_field, fn field ->
(field >= minA && field <= maxA) || (field >= minB && field <= maxB)
end)
end)
[{index, Enum.map(matches, &elem(&1, 0))} | acc]
end)
end
def match_unused_field({index, possible_fields}, {already_used, field_order}) do
selected_field =
MapSet.difference(MapSet.new(possible_fields), already_used) |> MapSet.to_list() |> hd()
{MapSet.put(already_used, selected_field), Map.put(field_order, index, selected_field)}
end
end