bjorng

bjorng

Erlang Core Team
defmodule Day04 do
  def part1(input) do
    grid = parse(input)
    Enum.count(removable(grid))
  end

  def part2(input) do
    grid = parse(input)
    remove(grid, 0)
  end

  defp remove(grid, num_removed) do
    case removable(grid) do
      [] ->
        num_removed
      [_|_]=ps ->
        grid = Enum.reduce(ps, grid, &(Map.delete(&2, &1)))
        remove(grid, num_removed + length(ps))
    end
  end

  defp removable(grid) do
    grid
    |> Enum.filter(fn {_position, what} -> what === ?@ end)
    |> Enum.filter(fn {position, _} ->
      adjacent_squares(position)
      |> Enum.count(fn position->
        get_grid(grid, position) === ?@
      end)
      |> then(&(&1 < 4))
    end)
    |> Enum.map(&elem(&1, 0))
  end

  defp get_grid(grid, position) do
    Map.get(grid, position, ?.)
  end

  defp adjacent_squares({row, col}) do
    [{row - 1, col - 1}, {row - 1, col}, {row - 1, col + 1},
     {row, col - 1}, {row, col + 1},
     {row + 1, col - 1}, {row + 1, col}, {row + 1, col + 1}]
  end

  defp parse(input) do
    parse_grid(input)
  end

  defp parse_grid(grid) do
    grid
    |> Enum.with_index
    |> Enum.flat_map(fn {line, row} ->
      String.to_charlist(line)
      |> Enum.with_index
      |> Enum.flat_map(fn {char, col} ->
        position = {row, col}
        [{position, char}]
      end)
    end)
    |> Map.new
  end
end

Most Liked

lud

lud

I love immutability because you can just compare a term against its previous version without managing the copy yourself:

case Map.drop(grid, Map.keys(accessibles(grid))) do
  ^grid -> grid
  new_grid -> loop_remove(new_grid)
end

In context:

defmodule AdventOfCode.Solutions.Y25.Day04 do
  alias AoC.Input

  def parse(input, _part) do
    lines = Input.stream!(input, trim: true)

    for {row, y} <- Enum.with_index(lines),
        {"@", x} <- Enum.with_index(String.graphemes(row)),
        reduce: %{},
        do: (grid -> Map.put(grid, {x, y}, true))
  end

  def part_one(grid) do
    map_size(accessibles(grid))
  end

  def part_two(grid) do
    clear_grid = loop_remove(grid)
    map_size(grid) - map_size(clear_grid)
  end

  defp loop_remove(grid) do
    case Map.drop(grid, Map.keys(accessibles(grid))) do
      ^grid -> grid
      new_grid -> loop_remove(new_grid)
    end
  end

  defp accessibles(grid) do
    Map.filter(grid, &less_than_four_neighbors?(&1, grid))
  end

  defp less_than_four_neighbors?({{x, y}, _}, grid) do
    for(nx <- (x - 1)..(x + 1), ny <- (y - 1)..(y + 1), {nx, ny} != {x, y}, do: {nx, ny})
    |> Enum.count(&Map.has_key?(grid, &1))
    |> Kernel.<(4)
  end
end
dompdv

dompdv

This one was pretty straightforward. Using MapSet (and MapSet operations) and the “for” construct, which is more concise than reduce. No optimization at all


defmodule AdventOfCode.Solution.Year2025.Day04 do
  import Enum, only: [flat_map: 2, with_index: 1, sum: 1, filter: 2, count: 1]

  def parse(input) do
    # Create a MapSet of the {row,col} of the rolls
    with_index(String.split(input, "\n", trim: true))
    |> flat_map(fn {line, row} ->
      for {c, col} <- with_index(to_charlist(line)), c == ?@, do: {row, col}
    end)
    |> MapSet.new()
  end

  def vec_sum({a, b}, {c, d}), do: {a + c, b + d}

  @around for r <- -1..1, c <- -1..1, {r, c} != {0, 0}, do: {r, c}
  def liftable(roll, rolls) do
    sum(for delta <- @around, vec_sum(roll, delta) in rolls, do: 1) < 4
  end

  def part1(input) do
    rolls = parse(input)
    rolls |> filter(&liftable(&1, rolls)) |> count()
  end

  def remove_all(rolls, counter \\ 0) do
    # Identify the removable rolls
    removable = for roll <- rolls, liftable(roll, rolls), into: MapSet.new(), do: roll
    # If nothing to remove, we're done, else remove the removable rolls and increment the counter
    if MapSet.size(removable) == 0,
      do: counter,
      else: remove_all(MapSet.difference(rolls, removable), counter + MapSet.size(removable))
  end

  def part2(input), do: input |> parse() |> remove_all()
end
mudasobwa

mudasobwa

Creator of Cure

I did my best to never construct a matrix, binaries rock. Also, Stream.iterate(&calc/1) is a lovely approach to code reuse.

  defmodule Day4 do
    @input "day4_1.input" |> File.read!() |> String.trim()

    defp read_input(input) do
      input
      |> String.split(["\s", "\n"], trim: true)
      |> then(fn [h | t] ->
        stub = "." |> List.duplicate(byte_size(h)) |> Enum.join()
        [stub, h | t] ++ [stub]
      end)
      |> Enum.map(&("." <> &1 <> "."))
    end

    def calc({input, acc} \\ {@input, 0}) do
      input
      |> read_input()
      |> Enum.chunk_every(3, 1, :discard)
      |> Enum.reduce({0, []}, fn [h, l, t], {count, res} ->
        res = ["" | res]

        Enum.reduce(1..(byte_size(l) - 2), {count, res}, fn idx, {count, [hres | tres]} ->
          with <<_::binary-size(idx - 1), r1::binary-size(3), _::binary>> <- h,
               <<_::binary-size(idx - 1), r21::binary-size(1), r22::binary-size(1),
                 r23::binary-size(1), _::binary>> <- l,
               <<_::binary-size(idx - 1), r3::binary-size(3), _::binary>> <- t,
               false <- r22 == "@" and String.count(r1 <> r21 <> r23 <> r3, "@") < 4,
               do: {count, [r22 <> hres | tres]},
               else: (_ -> {count + 1, ["x" <> hres | tres]})
        end)
      end)
      |> then(fn {count, data} -> {Enum.join(data, "\n"), acc + count} end)
    end

    def calc_2(input \\ @input) do
      {input, 0}
      |> Stream.iterate(&calc/1)
      |> Enum.reduce_while({0, -1}, fn
        {_, prev}, {acc, prev} -> {:halt, acc}
        {_, count}, {acc, _} -> {:cont, {count, acc}}
      end)
    end
  end

Last Post!

billylanchantin

billylanchantin

defmodule Day04 do
  def part1(file) when is_binary(file), do: file |> to_grid() |> part1()
  def part1(%{} = grid), do: Enum.count(grid, &accessible?(grid, &1))

  def to_grid(file), do: file |> Util.file_to_char_map() |> Map.filter(fn {_, v} -> v == ?@ end)
  def accessible?(grid, {pos, _}), do: Enum.count(adj(pos), &(grid[&1] == ?@)) < 4
  def adj({x, y}), do: for(dx <- -1..1, dy <- -1..1, {dx, dy} != {0, 0}, do: {x + dx, y + dy})

  def part2(file) do
    to_grid(file)
    |> Stream.unfold(fn grid ->
      {count, new_grid} = remove_accessible(grid)
      if count > 0, do: {count, new_grid}, else: nil
    end)
    |> Enum.sum()
  end

  def remove_accessible(grid) do
    removable = grid |> Enum.filter(&accessible?(grid, &1)) |> Enum.map(&elem(&1, 0))
    {length(removable), Enum.reduce(removable, grid, &Map.delete(&2, &1))}
  end
end

Where Next? Top

Trending in Challenges Top

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews