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

Day 3

2023/day_3.livemd

Day 3

Mix.install([:kino])

Get input

input =
  Kino.Input.textarea("Paste input here",
    default: """
    467..114..
    ...*......
    ..35..633.
    ......#...
    617*......
    .....+.58.
    ..592.....
    ......755.
    ...$.*....
    .664.598..
    """
  )
defmodule Helper do
  def is_numeric?(string) when is_binary(string) do
    String.length(string) == 1 &&
      Enum.any?(?0..?9, fn x -> x == String.to_charlist(string) |> hd() end)
  end

  def get_neighbors(number, {x1, y1}) do
    end_x = x1 + String.length(number) - 1

    ([{x1 - 1, y1 - 1}, {x1 - 1, y1}, {x1 - 1, y1 + 1}] ++
       Enum.flat_map(x1..end_x, fn x -> [{x, y1 - 1}, {x, y1 + 1}] end) ++
       [{end_x + 1, y1 - 1}, {end_x + 1, y1}, {end_x + 1, y1 + 1}])
    |> Enum.filter(fn {x, y} -> x >= 0 and y >= 0 end)
  end

  def adjacent_to_symbol?(schematic_lookup, number, start_pos) do
    neighbors = get_neighbors(number, start_pos)

    Enum.any?(neighbors, fn {x, y} ->
      char = Map.get(schematic_lookup, {x, y}, ".")

      if is_numeric?(char) do
        raise "neighbor at (#{x},#{y}) unexpectedly was numeric: #{char}"
      end

      !is_numeric?(char) and char !== "."
    end)
  end
end
schematic =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Stream.map(&String.split(&1, "", trim: true))

schematic_lookup =
  schematic
  |> Stream.with_index()
  |> Stream.flat_map(fn {row, y} ->
    row |> Stream.with_index() |> Stream.map(fn {char, x} -> {{x, y}, char} end)
  end)
  |> Enum.into(%{})
part_nums =
  schematic
  |> Stream.map(fn arr -> arr ++ ["."] end)
  |> Stream.with_index()
  |> Stream.map(fn {row, y} ->
    # collect each full number along with its start position in the grid
    row
    |> Stream.with_index()
    |> Enum.reduce({"", []}, fn {char, x}, {current_n, part_numbers} ->
      cond do
        Helper.is_numeric?(char) ->
          {current_n <> char, part_numbers}

        String.length(current_n) === 0 ->
          {"", part_numbers}

        true ->
          {"", part_numbers ++ [{current_n, {x - String.length(current_n), y}}]}
      end
    end)
  end)
  |> Enum.flat_map(fn {_, n} -> n end)

Part 1

part_nums
|> Enum.filter(fn {n, start_pos} ->
  Helper.adjacent_to_symbol?(schematic_lookup, n, start_pos)
end)
|> Enum.map(fn {n, _c} -> String.to_integer(n) end)
|> Enum.sum()

Part 2

gears =
  schematic_lookup
  |> Enum.filter(fn
    {_, "*"} -> true
    _ -> false
  end)
  |> Enum.map(fn {coords, _} -> coords end)
  |> MapSet.new()

part_nums
|> Enum.reduce(%{}, fn {n, start_pos}, acc ->
  gear_pos = Helper.get_neighbors(n, start_pos)

  case Enum.find(gear_pos, &amp;MapSet.member?(gears, &amp;1)) do
    nil -> acc
    p -> Map.update(acc, p, [n], &amp;(&amp;1 ++ [n]))
  end
end)
|> Stream.filter(fn
  {_, [_, _]} -> true
  _ -> false
end)
|> Stream.map(fn {_, ns} -> Stream.map(ns, &amp;String.to_integer/1) |> Enum.product() end)
|> Enum.sum()