Powered by AppSignal & Oban Pro

Advent of code day 03

2023/livebooks/day-03.livemd

Advent of code day 03

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

Setup input

example = Kino.Input.textarea("Please paste your input example:")
input = Kino.Input.textarea("Please paste your real input:")
parsed = example
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.map(&(String.split(&1, "", trim: true) |> List.to_tuple()))
  |> List.to_tuple()

rows = tuple_size(parsed) - 1
cols = tuple_size(elem(parsed, 0)) - 1

grid =
  for l <- 0..rows, c <- 0..cols, into: %{} do
    {{l, c}, elem(elem(parsed, l), c)}
  end
|> Enum.sort
|> Enum.into(%{})

Part 01

digit? = fn
  nil -> false
  v -> String.match?(v, ~r/^\d$/)
end

number_starts =
  Enum.reduce(grid, MapSet.new(), fn {{r, c}, v}, acc ->
    if digit?.(v) or v == "." do
      acc
    else
      neighbors = [
        {r, c - 1},
        {r, c + 1},
        {r - 1, c},
        {r + 1, c},
        {r - 1, c - 1},
        {r - 1, c + 1},
        {r + 1, c - 1},
        {r + 1, c + 1}
      ]

      Enum.reduce(neighbors, acc, fn {nr, nc}, acc ->
        d = Map.get(grid, {nr, nc})

        if digit?.(d) do
          # walk left to find start of number
          start_col =
            Stream.iterate(nc - 1, &amp;(&amp;1 - 1))
            |> Enum.reduce_while(nc, fn cc, _acc ->
              if digit?.(Map.get(grid, {nr, cc})) do
                {:cont, cc}
              else
                {:halt, cc + 1}
              end
            end)

          MapSet.put(acc, {nr, start_col})
        else
          acc
        end
      end)
    end
  end)

# Extract numbers from start positions
numbers =
  Enum.map(number_starts, fn {row, start_col} ->
    # Walk right and concatenate digits
    number_string =
      Stream.iterate(start_col, &amp;(&amp;1 + 1))
      |> Enum.reduce_while("", fn col, acc ->
        d = Map.get(grid, {row, col})

        if digit?.(d) do
          {:cont, acc <> d}
        else
          {:halt, acc}
        end
      end)

    String.to_integer(number_string)
  end)
  |> Enum.sum()

Part 02

symbol_to_numbers =
  Enum.reduce(grid, %{}, fn {{r, c}, v}, acc ->
    if  v != "*" do
      acc
    else
      neighbors = [
        {r, c - 1},
        {r, c + 1},
        {r - 1, c},
        {r + 1, c},
        {r - 1, c - 1},
        {r - 1, c + 1},
        {r + 1, c - 1},
        {r + 1, c + 1}
      ]
      
      number_starts =
        Enum.reduce(neighbors, MapSet.new(), fn {nr, nc}, set ->
          d = Map.get(grid, {nr, nc})
          
          if digit?.(d) do
            # walk left to find start of number
            start_col =
              Stream.iterate(nc - 1, &amp;(&amp;1 - 1))
              |> Enum.reduce_while(nc, fn cc, _acc ->
                if digit?.(Map.get(grid, {nr, cc})) do
                  {:cont, cc}
                else
                  {:halt, cc + 1}
                end
              end)
            
            MapSet.put(set, {nr, start_col})
          else
            set
          end
        end)
      
      Map.put(acc, {r, c}, number_starts)
    end
  end)

# Filter symbols with exactly 2 adjacent numbers and multiply them
result =
  symbol_to_numbers
  |> Enum.filter(fn {_pos, number_starts} -> MapSet.size(number_starts) == 2 end)
  |> Enum.map(fn {_pos, number_starts} ->
    number_starts
    |> Enum.map(fn {row, start_col} ->
      # Walk right and concatenate digits
      number_string =
        Stream.iterate(start_col, &amp;(&amp;1 + 1))
        |> Enum.reduce_while("", fn col, acc ->
          d = Map.get(grid, {row, col})
          if digit?.(d) do
            {:cont, acc <> d}
          else
            {:halt, acc}
          end
        end)
      
      String.to_integer(number_string)
    end)
    |> Enum.product()
  end)
  |> Enum.sum()