Advent of Code 2024 - Day 8

I feel like there were probably some really clever ways to approach this problem but my solution is just kind of basic. Still nice for it to feel easy after all my false starts with day 7.

defmodule Day8 do
  @test """
  ............
  ........0...
  .....0......
  .......0....
  ....0.......
  ......A.....
  ............
  ............
  ........A...
  .........A..
  ............
  ............
  """
  @real File.read!(__DIR__ <> "/input.txt")

  def run(mode) do
    {antennae, max_row, max_col} =
      mode
      |> input()
      |> parse()

    part_1(antennae, max_row, max_col) |> IO.inspect(label: :part1)
    part_2(antennae, max_row, max_col) |> IO.inspect(label: :part2)
  end

  defp input(:test), do: @test
  defp input(:real), do: @real
  defp input(_), do: raise("Please use :test or :real as the mode to run.")

  defp parse(data) do
    data
    |> grid()
  end

  defp grid(data) do
    data
    |> lines()
    |> Enum.with_index()
    |> Enum.reduce({%{}, 0, 0}, fn {row, r}, {map, max_r, max_c} ->
      row
      |> String.graphemes()
      |> Enum.with_index()
      |> Enum.reduce({map, max_r, max_c}, fn
        {".", c}, {m, m_r, m_c} ->
          {m, max(m_r, r), max(m_c, c)}

        {ant, c}, {m, m_r, m_c} ->
          {Map.update(m, ant, [{r, c}], fn curr -> [{r, c} | curr] end), max(m_r, r), max(m_c, c)}
      end)
    end)
  end

  defp lines(data) do
    data
    |> String.split("\n", trim: true)
  end

  defp part_1(antennae, max_row, max_col) do
    antennae
    |> Task.async_stream(fn {_freq, ants} ->
      antinodes(ants, [], max_row, max_col)
    end)
    |> Enum.map(&elem(&1, 1))
    |> Enum.reduce(MapSet.new(), fn nodes, ms ->
      nodes |> Enum.reduce(ms, fn n, m -> MapSet.put(m, n) end)
    end)
    |> MapSet.size()
  end

  defp antinodes([_], antinodes, _, _), do: antinodes

  defp antinodes([a, b | rest], antinodes, max_row, max_col) do
    antinodes([a | rest], antinodes, max_row, max_col) ++
      antinodes([b | rest], antinodes, max_row, max_col) ++ find_antinodes(a, b, max_row, max_col)
  end

  defp find_antinodes({a, b}, {c, d}, max_r, max_c) do
    d_r = a - c
    d_c = b - d

    {i, j} = {a + d_r, b + d_c}
    {k, l} = {c - d_r, d - d_c}

    [{i, j}, {k, l}]
    |> Enum.filter(fn {x, y} -> x <= max_r and y <= max_c and x >= 0 and y >= 0 end)
  end

  defp part_2(antennae, max_row, max_col) do
    antennae
    |> Task.async_stream(fn {_freq, ants} -> harmonic_antinodes(ants, max_row, max_col) end)
    |> Enum.map(&elem(&1, 1))
    |> Enum.reduce(MapSet.new(), fn nodes, ms ->
      nodes |> Enum.reduce(ms, fn n, m -> MapSet.put(m, n) end)
    end)
    |> MapSet.size()
  end

  defp harmonic_antinodes(ants, max_r, max_c) do
    for ant1 <- ants,
        ant2 <- ants -- [ant1],
        reduce: [] do
      acc ->
        acc ++ find_harmonics(ant1, ant2, max_r, max_c)
    end
  end

  defp find_harmonics({a, b}, {c, d}, max_r, max_c) do
    for i <- 0..max_r,
        j <- 0..max_c,
        slope({a, b}, {c, d}) == slope({a, b}, {i, j}),
        reduce: [{a, b}, {c, d}] do
      acc ->
        [{i, j} | acc]
    end
  end

  defp slope({_a, b}, {_c, b}), do: :infinity

  defp slope({a, b}, {c, d}) do
    (a - c) / (b - d)
  end
end