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
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
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
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
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
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.
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)
Very good! ππ»ππ»ππ»
Nice one ! Thanks :D