#!/usr/bin/env elixir defmodule Day8 do def part1({x, y, antennas}) do Enum.reduce(antennas, MapSet.new(), fn coords, antinodes -> MapSet.union(antinodes, find_antinodes(coords, x, y)) end) |> MapSet.size() end def find_antinodes(coords, x, y) do coords |> find_pairs() |> Enum.flat_map(fn pair -> calc_antinodes(pair, x, y) end) |> MapSet.new() end def find_pairs([_]), do: [] def find_pairs([a | rest]), do: for(b <- rest, do: {a, b}) ++ find_pairs(rest) def calc_antinodes({{x_a, y_a}, {x_b, y_b}}, max_x, max_y) do dx = x_a - x_b dy = y_a - y_b antinode_1 = {x_a + dx, y_a + dy} antinode_2 = {x_b - dx, y_b - dy} Enum.filter([antinode_1, antinode_2], fn {x, y} -> 0 <= x and x < max_x and 0 <= y and y < max_y end) end def part2({x, y, antennas}) do Enum.reduce(antennas, MapSet.new(), fn coords, antinodes -> MapSet.union(antinodes, find_resonant_harmonics(coords, x, y)) end) |> MapSet.size() end def find_resonant_harmonics(coords, x, y) do coords |> find_pairs |> Enum.flat_map(fn pair -> calc_resonant_harmonics(pair, x, y) end) |> MapSet.new() end def calc_resonant_harmonics({{x_a, y_a}, {x_b, y_b}}, max_x, max_y) do dx = x_a - x_b dy = y_a - y_b [{x_a, y_a}, {x_b, y_b}] ++ resonate(x_a, y_a, dx, dy, max_x, max_y) ++ resonate(x_b, y_b, dx * -1, dy * -1, max_x, max_y) end def resonate(x, y, dx, dy, max_x, max_y, multiplier \\ 1) do next_x = x + dx * multiplier next_y = y + dy * multiplier if next_x < 0 or next_x >= max_x or next_y < 0 or next_y >= max_y do [] else [{next_x, next_y} | resonate(x, y, dx, dy, max_x, max_y, multiplier + 1)] end end def input do with [input_filename] <- System.argv(), {:ok, input} <- File.read(input_filename) do {_, x, y, antennas} = for <>, reduce: {0, 0, 0, []} do {x, max_x, y, antennas} -> case char do "." -> {x + 1, max_x, y, antennas} "\n" -> {0, x, y + 1, antennas} freq -> {x + 1, max_x, y, [{freq, {x, y}} | antennas]} end end {x, y, antennas |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) |> Map.values()} else _ -> :error end end ####################### # HERE BE BOILERPLATE # ####################### def run do case input() do :error -> print_usage() input -> run_parts_with_timer(input) end end defp run_parts_with_timer(input) do run_with_timer(1, fn -> part1(input) end) run_with_timer(2, fn -> part2(input) end) end defp run_with_timer(part, fun) do {time, result} = :timer.tc(fun) IO.puts("Part #{part} (completed in #{format_time(time)}):\n") IO.puts("#{inspect(result)}\n") end defp format_time(μsec) when μsec < 1_000, do: "#{μsec}μs" defp format_time(μsec) when μsec < 1_000_000, do: "#{μsec / 1000}ms" defp format_time(μsec), do: "#{μsec / 1_000_000}s" defp print_usage do IO.puts("Usage: elixir day8.exs input_filename") end end Day8.run()