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

Day 03

day03.livemd

Day 03

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

IEx.Helpers.c("/Users/johnb/dev/2023adventOfCode/advent_of_code.ex")
alias AdventOfCode, as: AOC
alias Kino.Input

# Note: when making the next template, something like this works well:
#   `cat day03.livemd | sed 's/03/04/' > day04.livemd`
#

Installation and Data

input_p1example = Kino.Input.textarea("Example Data")
input_p1puzzleInput = Kino.Input.textarea("Puzzle Input")
input_source_select =
  Kino.Input.select("Source", [{:example, "example"}, {:puzzle_input, "puzzle input"}])
p1data = fn ->
  (Kino.Input.read(input_source_select) == :example &&
     Kino.Input.read(input_p1example)) ||
    Kino.Input.read(input_p1puzzleInput)
end

Part 1

defmodule Day03 do
  @island %{cells: [], part_number: 0, perimeter: []}
  @non_symbols [".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", nil]
  @gear "*"

  def no_adjacent_symbols?(island, grid) do
    island.perimeter
    |> Enum.all?(fn cell -> grid[cell] in @non_symbols end)
  end

  def find_numeric_islands(grid) do
    width = grid.grid_width

    all =
      0..grid.last_cell
      |> Enum.reduce(
        %{islands: [], in_progress: @island},
        fn cell,
           %{
             islands: islands,
             in_progress:
               %{cells: cells, part_number: part_number, perimeter: perimeter} = in_progress
           } = acc ->
          case {grid[cell] =~ ~r/\d/, part_number} do
            # assume no parts start with a 0
            {true, 0} ->
              %{
                acc
                | in_progress: %{
                    cells: [cell | cells],
                    part_number: String.to_integer(grid[cell]),
                    perimeter: [
                      cell - 1 - width,
                      cell - width,
                      cell - 1,
                      cell - 1 + width,
                      cell + width
                    ]
                  }
              }

            {true, n} ->
              %{
                acc
                | in_progress: %{
                    cells: [cell | cells],
                    part_number: 10 * n + String.to_integer(grid[cell]),
                    perimeter: [cell - width, cell + width | perimeter]
                  }
              }

            {false, 0} ->
              acc

            {false, _n} ->
              %{
                islands: [
                  %{in_progress | perimeter: [cell - width, cell, cell + width | perimeter]}
                  | islands
                ],
                in_progress: @island
              }
          end
        end
      )

    all[:islands]
  end

  def solve(text) do
    grid = AOC.as_grid(text)

    find_numeric_islands(grid)
    |> Enum.reject(fn island -> no_adjacent_symbols?(island, grid) end)
    |> Enum.map(fn %{part_number: part_number} -> part_number end)
    |> Enum.sum()
  end

  def find_gear_ratios(islands, grid) do
    0..grid.last_cell
    |> Enum.reduce([], fn cell, acc ->
      if grid[cell] == @gear do
        neighbors = AOC.neighbors8(grid, cell) |> MapSet.new()

        overlaps =
          Enum.filter(islands, fn island ->
            MapSet.intersection(neighbors, MapSet.new(island.cells))
            |> Enum.any?()
          end)

        if Enum.count(overlaps) == 2 do
          [List.first(overlaps).part_number * List.last(overlaps).part_number | acc]
        else
          acc
        end
      else
        acc
      end
    end)
    |> Enum.uniq()
  end

  def solve2(text) do
    grid = AOC.as_grid(text)
    islands = find_numeric_islands(grid)

    find_gear_ratios(islands, grid)
    |> Enum.sum()
  end
end

p1data.()
|> Day03.solve()
|> IO.inspect(label: "\n*** Part 1 solution (example: 4361)")

# 533784

p1data.()
|> Day03.solve2()
|> IO.inspect(label: "\n*** Part 2 solution (example: 467835)")

# 78826761