Logo

dev-resources.site

for different kinds of informations.

Solving Advent Of Code 2024 on a elixir project

Published at
12/5/2024
Categories
elixir
adventofcode
github
Author
paulosilva
Categories
3 categories in total
elixir
open
adventofcode
open
github
open
Author
10 person written this
paulosilva
open
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!

elixir Article's
30 articles in total
Favicon
A RAG for Elixir in Elixir
Favicon
Enhancing Elixir Development in LazyVim: Quick Documentation Access by Telescope
Favicon
Learning Elixir: Control Flow with If and Unless
Favicon
Pseudolocalization in Phoenix with gettext_pseudolocalize
Favicon
The Journey of Optimization
Favicon
How to use queue data structure in programming
Favicon
Phoenix LiveView, hooks and push_event: json_view
Favicon
🥚 Crack Open These 20+ Elixir Goodies
Favicon
Learning Elixir: Understanding Atoms, Booleans and nil
Favicon
Unlocking the Power of Elixir Phoenix and Rust: A Match Made for High-Performance Web Applications
Favicon
Elixir: Concurrency & Fault-Tolerance
Favicon
Enhancements to dbg in elixir 1.18
Favicon
Learning Elixir: Working with Strings
Favicon
Leverage ETS for Shared State in Phoenix
Favicon
Elixir em Foco em 2024
Favicon
Building HTTP/JSON API In Gleam: Introduction
Favicon
Phoenix
Favicon
Sql commenter with postgrex
Favicon
Learning Elixir: Understanding Numbers
Favicon
For loops and comprehensions in Elixir - transforming imperative code
Favicon
Phoenix LiveView is slot empty?
Favicon
Customizing IEx: Personalizing Your Elixir Shell Environment
Favicon
Masters of Elixir: A Comprehensive Collection of Learning Resources
Favicon
Leveraging GenServer and Queueing Techniques: Handling API Rate Limits to AI Inference services
Favicon
Chekhov's gun principle for testing
Favicon
Solving Advent Of Code 2024 on a elixir project
Favicon
Bridging the Gap: Simplifying Live Component Invocation in Phoenix LiveView
Favicon
Harness PubSub for Real-Time Features in Phoenix Framework
Favicon
Debugging with dbg: Exploring Elixir's Built-in Debugger
Favicon
New to dev.to and Excited to Share ProxyConf: My Elixir-Powered API Control Plane

Featured ones: