AOC 2025 is right around the corner. Last year, my first year, I just started a raw Elixir application and manually created modules for each day’s challenge. This year I’m thinking of starting with the aoc package. Anyone have opinions about what the most helpful framework is for enjoying this year’s challenges?
Here’s a similar thread, which also contains my opinion ![]()
The aoc package is opinionated, it creates modules with the parse/2, part_one/1 and part_two/2 functions, and it creates tests if you fancy some TDD. If you do not like that, you can still use it just to fetch the inputs locally.
If you want to do your setup yourself I guess starting with a bash script that generates a module can be enough.
I’d suggest to try aoc right now, see the Are you trying this library before December 1st ? section so you know if it is useful to you before day 1.
This is my bash command to work with my package that I run every morning:
play:
mix aoc.create | rg '\.ex' | rg -v Compiling | xargs code
mix format
nohup firefox $(mix aoc.url | rg "https") >/dev/null 2>&1 &
echo 'Good Luck :)'
Frankly, with LLMs these days, you don’t necessarily need to rely on external packages for simple things - you can just ask the LLM of your choice to write your own little modules (or even write them yourself!).
My approach: 1) a module to download and cache the input files, called like this: AOC.input!(1, 2015) (first call downloads, consequtive calls read from the disk), 2) two simple templates called lib/y2015/day00.ex and test/y2015/day00_test.exs, 3) a mix tasks that would copy this tasks in appropriate places and change 00 to whatever the proper date is. So that I can modify the templates for my liking - they are just elixir files.
That’s it. That’s the whole setup.
# lib/aoc.ex
defmodule AOC do
@year 2025
@ua "Victor Kryukov (victor.kryukov@gmail.com; Elixir/Req)"
@input_dir "priv/aoc"
def session! do
".aoc_session"
|> File.read!()
|> String.trim()
end
def client(session \\ session!()) do
Req.new(
base_url: "https://adventofcode.com",
headers: [
{"cookie", "session=#{session}"},
{"user-agent", @ua}
],
redirect: true
)
end
def input!(day, year \\ @year) do
dir = @input_dir
path = input_path(day, year, dir)
cond do
File.exists?(path) -> read_input_file!(path)
true -> fetch_and_store_input!(day, year, path, dir)
end
end
def save_input!(day, dir \\ @input_dir, year \\ @year) do
path = input_path(day, year, dir)
File.mkdir_p!(dir)
File.write!(path, input!(day, year) <> "\n")
path
end
defp fetch_and_store_input!(day, year, path, dir) do
body = fetch_remote_input!(day, year)
File.mkdir_p!(dir)
File.write!(path, body <> "\n")
body
end
defp fetch_remote_input!(day, year) do
remote_path = "/#{year}/day/#{day}/input"
case Req.get(client(), url: remote_path) do
{:ok, %{status: 200, body: body}} -> String.trim_trailing(body)
{:ok, %{status: 400}} -> raise "Bad request (likely missing/invalid session)"
{:ok, %{status: 404}} -> raise "Not available yet (unlocks midnight ET Dec 1–25)"
{:ok, resp} -> raise "HTTP #{resp.status} for #{remote_path}"
{:error, err} -> raise err
end
end
defp input_path(day, year, dir) do
name = "#{year}_day#{day |> Integer.to_string() |> String.pad_leading(2, "0")}.txt"
Path.join(dir, name)
end
defp read_input_file!(path) do
path
|> File.read!()
|> String.trim_trailing()
end
end
# lib/y2015/day00.ex
defmodule Y2015.Day00 do
def part1(_s) do
end
def part2(_s) do
end
end
# test/y2015/day00_test.exs
defmodule Y2015.Day00Test do
use ExUnit.Case
import Y2015.Day00
@tag :skip
test "part1/1" do
assert part1(AOC.input!(0, 2015))
end
@tag :skip
test "part2/1" do
assert part2(AOC.input!(0, 2015))
end
end
# mix/tasks/aoc.new.ex
defmodule Mix.Tasks.Aoc.New do
use Mix.Task
@shortdoc "Generate scaffolding for a new Advent of Code day"
@moduledoc """
Creates solution and test files for the given `year` and `day` by copying the
`y2015/day00` templates and updating the placeholders.
## Examples
mix new 2018 7
"""
@impl Mix.Task
def run([year_str, day_str]) do
with {year, ""} <- Integer.parse(year_str),
{day, ""} <- Integer.parse(day_str) do
ensure_bounds!(year, day)
day_padded = day |> Integer.to_string() |> String.pad_leading(2, "0")
lib_target = Path.join(["lib", "y#{year}", "day#{day_padded}.ex"])
test_target = Path.join(["test", "y#{year}", "day#{day_padded}_test.exs"])
ensure_not_exists!(lib_target)
ensure_not_exists!(test_target)
copy_template!("lib/y2015/day00.ex", lib_target, year, day, day_padded)
copy_template!("test/y2015/day00_test.exs", test_target, year, day, day_padded)
Mix.shell().info([
"Generated ",
lib_target,
" and ",
test_target
])
else
_ ->
Mix.raise("YEAR and DAY must be integers")
end
end
def run(_args) do
Mix.raise("Usage: mix new YEAR DAY")
end
defp ensure_bounds!(year, day) when year in 1_900..3_000 and day in 0..25, do: :ok
defp ensure_bounds!(_year, _day) do
Mix.raise("YEAR must be reasonable and DAY must be between 0 and 25")
end
defp ensure_not_exists!(path) do
if File.exists?(path), do: Mix.raise("#{path} already exists")
end
defp copy_template!(source, target, year, day, day_padded) do
source_content = File.read!(source)
rendered = render(source_content, year, day, day_padded)
target |> Path.dirname() |> File.mkdir_p!()
File.write!(target, rendered)
end
defp render(content, year, day, day_padded) do
content
|> String.replace("Y2015", "Y#{year}")
|> String.replace("2015", Integer.to_string(year))
|> String.replace("Day00", "Day#{day_padded}")
|> String.replace("day00", "day#{day_padded}")
|> String.replace("input!(0, #{year})", "input!(#{day}, #{year})")
end
end
Note that AoC recommends that you don’t use LLMs. Here’s mine:
- copy
dayN.exsto relevant date - replace
REPLACE_MEwith relevant date - right click and save input file from website you’re reading already
- run with
elixir script.exs input
That’s it. That’s the whole setup. ![]()
Of course - I never use LLMs for solving the problems themselves, just to write the automation around fetching the problems etc.
I have one repo for alllllll of my puzzle solutions that I keep building in every year - GitHub - sevenseacat/advent_of_code: My Elixir solutions to Advent of Code (spoilers: includes solutions for 457 stars and counting)
My daily process is pretty simple -
- I run
mix day <year> <day>eg.mix day 2025 1to generate a skeleton module/test module for the day’s solution (which will be inlib/y2025/day01.exandtest/y2025/day01_test.exs) - I manually download the puzzle input for that day and put it in
lib/y2025/input/day01.txt - And away I go! Now I can run code like
Y2025.Day01.part1()iniexto test out my part 1 solution, etc.
I keep it super-minimal; each day is a subdirectory with part1.exs and part2.exs files (and inputs) and I run code with elixir part1.exs etc
Usually that file defines a module, then has top-level code afterwards that uses the module.
Some of the problems can be solved with literally just a chain of pipes, no module required.






















