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

AoC 2023 - Day 03

2023/lang-elixir/03.livemd

AoC 2023 - Day 03

Mix.install([
  {:kino, "~> 0.11.3"}
])

Section

input_e1 =
  "day03-e1.txt"
  |> Kino.FS.file_path()
  |> File.read!()
  |> String.trim()

input =
  "day03.txt"
  |> Kino.FS.file_path()
  |> File.read!()
  |> String.trim()

[input_e1, input]
defmodule Data do
  defstruct grid: [], symbols: [], numbers: []
end

defmodule Solution do
  @neighbors [{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {1, 1}, {0, 1}, {-1, 1}]

  def parse(input) do
    input
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.reduce(%Data{}, fn {line, y},
                               %Data{
                                 grid: grid,
                                 symbols: symbols,
                                 numbers: numbers
                               } ->
      xy =
        line
        |> String.codepoints()
        |> Enum.with_index()

      symbol_coords =
        xy
        |> Enum.filter(fn {char, _x} -> is_symbol?(char) end)
        |> Enum.map(fn {_char, x} -> {x, y} end)

      number_coords =
        xy
        |> Enum.filter(fn {char, _x} -> is_digit?(char) end)
        |> Enum.map(fn {_char, x} -> {x, y} end)

      %Data{
        grid: Enum.concat(grid, [line]),
        symbols: Enum.concat(symbols, symbol_coords),
        numbers: Enum.concat(numbers, number_coords)
      }
    end)
  end

  def part_1(input) do
    data = parse(input)

    data.symbols
    |> Enum.reduce(MapSet.new(), fn coords, acc ->
      adjacent_coords = adjacent_coords(coords, data.numbers)

      Enum.reduce(adjacent_coords, acc, fn {x, y}, acc ->
        MapSet.put(acc, collect_numbers(Enum.at(data.grid, y), x, y))
      end)
    end)
    |> Enum.map(fn {n, _, _} -> n end)
    |> Enum.sum()
  end

  def part_2(input) do
    data = parse(input)

    data.symbols
    |> Enum.reduce(0, fn coords, acc ->
      adjacent_coords = adjacent_coords(coords, data.numbers)

      numbers =
        Enum.reduce(adjacent_coords, MapSet.new(), fn {x, y}, acc ->
          MapSet.put(acc, collect_numbers(Enum.at(data.grid, y), x, y))
        end)

      case MapSet.size(numbers) do
        2 -> acc + (numbers |> Enum.map(fn {n, _, _} -> n end) |> Enum.product())
        _ -> acc
      end
    end)
  end

  def collect_numbers(line, x, y) do
    {a, b} = {collect_numbers_directional(line, x, -1), collect_numbers_directional(line, x)}
    number = line |> String.slice(a, b - a + 1) |> String.to_integer()

    {number, {a, y}, {b, y}}
  end

  def collect_numbers_directional(line, x, direction \\ 1) do
    with char when char in ~w(1 2 3 4 5 6 7 8 9 0) <- String.at(line, x),
         pos <- x + direction,
         char when not is_nil(char) <- String.at(line, pos),
         {_digit, _discard} <- Integer.parse(char) do
      collect_numbers_directional(line, x + direction, direction)
    else
      _ -> x
    end
  end

  defp adjacent_coords({symbol_x, symbol_y}, number_coords) do
    Enum.reduce(@neighbors, [], fn {x, y}, acc ->
      new_x = symbol_x + x
      new_y = symbol_y + y

      if Enum.member?(number_coords, {new_x, new_y}) do
        [{new_x, new_y} | acc]
      else
        acc
      end
    end)
  end

  defp is_digit?(char) when char in ~w(1 2 3 4 5 6 7 8 9 0), do: true
  defp is_digit?(_char), do: false
  defp is_symbol?(char) when char not in ~w(1 2 3 4 5 6 7 8 9 0 .), do: true
  defp is_symbol?(_char), do: false
end
Solution.part_1(input)
Solution.part_2(input)