DEV Community

Cover image for Solving Advent Of Code 2024 on a elixir project
Paulo Henrique da Silva
Paulo Henrique da Silva

Posted on

Solving Advent Of Code 2024 on a elixir project

TL;DR: If you want to take a look at the project itself, here's the GitHub repo

Every year, we have a cool event that happens in December. It's called Advent Of Code, and there are 25 challenges, one for each day until it reaches Christmas 🎅.
In previous editions, I never finished it, and I can list a lot of excuses here 😅, but the main one is that I have issues conciliating job with coding off-hours because I want to do something else, like play video games, watch a movie, or stay with the family.
For 2024, I'm trying to do it end to end. The programming language I've chosen to do so is Elixir because it's my main language and the one that I'm comfortable with nowadays. I know some people use the advent of code to learn new programming languages, which I think is pretty valuable, too.
So, as a way to have the minimum setup as possible to solve the day challenge, I've set an elixir project using the command:

$ mix new advent_of_code2024
Enter fullscreen mode Exit fullscreen mode

This will generate the following struct:

  • 📁 advent_of_code_2024
    • 📁 .elixir_ls
    • 📁 _build
    • 📁 lib
    • 📁 test
    • 📄 .formatter.exs
    • 📄 .gitignore
    • 📄 mix.exs
    • 📄 README.md

Then, I started to think in a generic way of implementing the entry point to execute a specific day challenge, and end up having a function, AdventOfCode2024.run/1:

defmodule AdventOfCode2024 do
  @moduledoc """
  Documentation for `AdventOfCode2024`.
  """

  @spec run(non_neg_integer()) :: {:ok, any()} | {:error, any()}
  def run(day) do
    solution_module = String.to_existing_atom("Elixir.AdventOfCode2024.Day#{day}")

    day
    |> input_file()
    |> File.read!()
    |> solution_module.run()
  rescue
    error ->
      {:error, error}
  end

  defp input_file(day), do: "#{File.cwd!()}/lib/advent_of_code2024/inputs/day_#{day}"
end
Enter fullscreen mode Exit fullscreen mode

That function receives an integer attribute, representing the day challenge you want to execute, and then, the solution_module variable concatenates it with the module name pattern I use.
Still using the day argument, I get the respective input that the advent of code provides and pass the input content using File.read!/1 to the solution module run/1 function.

If something fails, like the input file or the solution module was not found, or it does not implement the expected run function, I return {:error, Exception.t()}, otherwise the result of AdventOfCode2024 will be the result of the solution_module.run/0.

And, of course, I added tests 😄; this is the test for the entry point:

defmodule AdventOfCode2024Test do
  use ExUnit.Case
  doctest AdventOfCode2024

  alias AdventOfCode2024

  describe "run/1" do
    test "runs solution of the provided day" do
      assert {:ok, _} = AdventOfCode2024.run(1)
    end

    test "returns error when solution fail or is not found" do
      assert {:error, _} = AdventOfCode2024.run(100)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at the structure where the day solution module lives:

  • 📁 advent_of_code_2024
    • ...
    • 📁 lib
      • 📁 advent_of_code2024
        • 📁 inputs
          • 📄 day_1
        • 📄 day_1.ex
    • 📁 test
      • 📁 advent_of_code2024
        • 📄 day_1_test.exs

Every day challenge has an input, that follows the pattern day_#{n}, the solution module, day_#{n}.ex, and the test file day_#{n}_test.ex.
So, whenever I solve a new challenge, I just need to add these files to the project, and run AdventOfCode2024.run(1) for example.

(SPOILER) Here's an example of the day 1 solution, part 2:

defmodule AdventOfCode2024.Day1 do
  @moduledoc """
  Solution of day 1
  """

  def run(input) do
    result =
      input
      |> parse_input()
      |> find_distances()
      |> Enum.sum()

    {:ok, result}
  end

  defp parse_input(input) do
    input
    |> String.split("\n")
    |> Enum.reduce(%{left: [], right: []}, fn line, acc ->
      [left, right] = String.split(line, ~r/\s+/)

      %{
        acc
        | left: [String.to_integer(left) | acc.left],
          right: [String.to_integer(right) | acc.right]
      }
    end)
    |> Map.update!(:left, &Enum.sort/1)
    |> Map.update!(:right, &Enum.sort/1)
  end

  defp find_distances(parsed_input) do
    find_distances(parsed_input, [])
  end

  defp find_distances(
         %{left: [minor_left_value | left_rest]} = parsed_input,
         distances
       ) do
    count = Enum.count(parsed_input.right, &(&1 == minor_left_value))
    updated_distances = [minor_left_value * count | distances]

    find_distances(%{parsed_input | left: left_rest}, updated_distances)
  end

  defp find_distances(%{left: []}, distances), do: distances
end
Enter fullscreen mode Exit fullscreen mode

and the test:

defmodule AdventOfCode2024.Day1Test do
  use ExUnit.Case
  doctest AdventOfCode2024.Day1

  alias AdventOfCode2024.Day1

  describe "run/1" do
    test "calculates the distance" do
      input =
        Enum.join(
          [
            "3   4",
            "4   3",
            "2   5",
            "1   3",
            "3   9",
            "3   3"
          ],
          "\n"
        )

      assert Day1.run(input) == {:ok, 31}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

You may wonder how I am organizing part 1 and part 2 solutions in GitHub, and I'm using commits and tags like so:

commit <sha> (tag: day_2_2nd_solution)
    Day 2 - Second solution.

commit <sha> (tag: day_2_1st_solution)
    Day 2 - First solution.
Enter fullscreen mode Exit fullscreen mode

This way, I can use git checkout day_n_(1st|2nd)_solution and run AdventOfCore.run(n) to get the result of the specific day/part I want to.

That's pretty much it. Thank you for the reading, and if you're curious to see and try the project, here's the GitHub repo; depending on when you access it, I may (or may not 😅) have finished it, I hope this structure works for you as well.

Cheers!

Top comments (2)

Collapse
 
barbara_lopesmilani_3752 profile image
Barbara Lopes Milani

Very good! 👏🏻👏🏻👏🏻

Collapse
 
speedix profile image
Artur Uklejewski

Nice one ! Thanks :D