Powered by AppSignal & Oban Pro

Day 4: Printing Department

2025/day-04.livemd

Day 4: Printing Department

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

Day 4

sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
defmodule Paper do
  def part1(input) do
    map =
      input
      |> Kino.Input.read()
      |> String.split("\n")
      |> Enum.with_index()
      |> Map.new(fn {val, index} -> {index, val} end)

    for {row, row_info} <- map, col <- 0..(byte_size(row_info) - 1), reduce: 0 do
      prev ->
        if occupied?(map, {row, col}) and accessible?(map, {row, col}) do
          prev + 1
        else
          prev
        end
    end
  end

  def part2(input) do
    map =
      input
      |> Kino.Input.read()
      |> String.split("\n")
      |> Enum.with_index()
      |> Map.new(fn {val, index} -> {index, val} end)

    fn -> nil end
    |> Stream.repeatedly()
    |> Enum.reduce_while({map, 0}, fn _, {prev_map, prev_count} ->
      {new_map, removed_count} = remove_accessible(prev_map)

      if removed_count > 0 do
        {:cont, {new_map, prev_count + removed_count}}
      else
        {:halt, prev_count}
      end
    end)
  end

  def remove_accessible(map) do
    for {row, row_info} <- map, col <- 0..(byte_size(row_info) - 1) do
      {row, col}
    end
    |> Enum.filter(fn {row, col} ->
      occupied?(map, {row, col}) and accessible?(map, {row, col})
    end)
    |> Enum.reduce({map, 0}, fn cell, {map, count} ->
      {remove_paper_at_cell(map, cell), count + 1}
    end)
  end

  def remove_paper_at_cell(map, {row, col}) do
    updated_row =
      binary_part(map[row], 0, col) <>
        "." <> binary_part(map[row], col + 1, byte_size(map[row]) - col - 1)

    Map.put(map, row, updated_row)
  end

  def occupied?(_map, {row, col}) when row < 0 or col < 0, do: false

  def occupied?(map, {row, col}) do
    with contents when byte_size(contents) > col <- map[row] do
      <<:binary.at(contents, col)>> == "@"
    else
      _ -> false
    end
  end

  def accessible?(map, {row, col}) do
    occupied_neighbors =
      for dx <- -1..1, dy <- -1..1, dx != 0 or dy != 0, reduce: 0 do
        prev -> if occupied?(map, {row + dy, col + dx}), do: prev + 1, else: prev
      end

    occupied_neighbors < 4
  end
end
Paper.part1(sample_input)
Paper.part1(real_input)
Paper.part2(sample_input)
Paper.part2(real_input)