I’m three days behind, so this will be kept pretty short! This post details the set-up for the rest of the exercise. I make no guarantees that I’ll solve all of them, as I only made it to day 8 last year.
The Project
Advent of Code is a fun coding challenge that poses 2 new puzzles from Dec. 1
to Dec. 25. I enjoy [elixir] and don’t get to use it at work, so I will be
solving the puzzles with elixir. A quick mix new advent_of_code
will get us up
and running!
Some Niceties
Projectionist
I’ve decided that I should give projectionist.vim
a chance here, as
there’ll a lot of jumping around between the puzzle file and any potential test
file, so let’s set one up. In .projections.json
in the root of the project add
the following:
{
"lib/Y2019/*.ex": {
"alternate": "test/Y2019/{dirname}/{basename}_test.exs",
"type": "source",
"template": [
"defmodule AdventOfCode.Y2019.{camelcase|capitalize|dot} do",
"end"
]
},
"lib/*.ex": {
"alternate": "test/{dirname}/{basename}_test.exs",
"type": "puzzle",
"template": [
"import AdventOfCode",
"",
"aoc {dirname}, {basename} do",
"",
" def p1(), do: nil",
"",
" def p2(), do: nil",
"",
"end"
]
},
"test/*_test.exs": {
"alternate": "lib/{dirname}/{basename}.ex",
"type": "test",
"template": [
"defmodule {camelcase|capitalize|dot}Test do",
" use ExUnit.Case, async: true",
"",
" alias {camelcase|capitalize|dot}",
"end"
]
}
}
This creates 3 file types in our project: test
s, puzzle
s, and source
s.
Tests are for, well, tests. Puzzles are where my puzzles will go, and source
files contain any extra utilities that I might write during this journey. They
all contain templates for their respective filetypes (that I am ironing the
kinks out of), and allow me to jump to them with :Etest
(and friends), as well
as :A
.
Mix Task(s)
Fetching the input is annoying, but I can script it with a mix task
fairly
easily. First, add tzdata
and httpoison
to our dependencies:
defp deps do
[{:tzdata, "~> 1.0"}, {:httpoison, "~> 1.6"}]
end
Then, create mix/tasks/fetch_input.ex
, and start scripting!
defmodule Mix.Tasks.FetchInput do
use Mix.Task
@url "https://adventofcode.com"
# What gets run by `mix fetch_input`. We can accept no args...
def run(args) when args == [] do
date = get_date()
run([date.day, date.year])
end
# ... one arg for the day, or...
def run([d] = args) when length(args) == 1 do
run([d, get_date().year])
end
# ... an arg for the day, and one for the year.
def run([d, y | _]) do
HTTPoison.start()
# get the input file for a given day/year
case HTTPoison.get("#{@url}/#{y}/day/#{d}/input", [
# Add the Cookie header, as inputs are random so you need to be identified
{"Cookie", "session=#{session()}"},
# Ensure we accept plain text, otherwise it assumes we want HTML
{"Accept", "text/plain"}
]) do
{:ok, m} -> write_input(m, d, y)
error -> error
end
end
defp write_input(%HTTPoison.Response{body: body}, d, y) do
# write the output to something like "priv/2019/1.txt"
File.write!("priv/#{y}/#{d}.txt", body)
end
defp session do
# sessions are secret! you can get your session from the Devtools -> Storage
# and hide it in ~/.aocrc
"#{System.get_env("HOME")}/.aocrc"
|> File.read!()
|> String.trim()
end
defp get_date() do
# Manually start the time zone database, as mix tasks don't do any of that.
Application.load(:tzdata)
:ok = Application.ensure_started(:tzdata)
# New puzzles are released midnight Eastern Standard Time
case DateTime.now("America/Toronto", Tzdata.TimeZoneDatabase) do
{:ok, d} -> d
# If that flops somehow, just use utc today
_ -> Date.utc_today()
end
end
end
This allows me to run mix fetch_input [d [y]]
to fetch the puzzle input for a
given day and year. If none are provided, default to today.
To come: a mix task for running puzzles and printing results.
Macro(s)
I came across an amazing macro and entry point module for AdventOfCode
from
/u/Sentreen
that blows my mind with how powerful they can be. It allows me
to define puzzle solutions easily:
import AdventOfCode
aoc(2019, 1) do
#...
end
Display and break it down:
defmodule AdventOfCode do
@doc """
Cool macro from
https://www.reddit.com/r/elixir/comments/e4rdo0/my_try_at_advent_of_code_day_1_with_elixir_as_a/f9gwauh
with some added goodies.
"""
defmacro aoc(year, day, do: body) do
quote do
# Dynamically create a module of the format AdventOfCode.Yyyyy.Dd
defmodule unquote(module_name(year, day)) do
# store the path to the puzzle input
@path "priv/#{unquote(year)}/#{unquote(day)}.txt"
@behaviour AdventOfCode
def input_path, do: @path
# read the input as a stream, and trim the newlines
def input_stream do
@path
|> Path.expand()
|> File.stream!()
|> Stream.map(&String.trim/1)
end
# the solution
unquote(body)
end
end
end
defp module_name(year, day) do
mod_year = "Y#{year}" |> String.to_atom()
mod_day = "D#{day}" |> String.to_atom()
Module.concat(:AdventOfCode, Module.concat(mod_year, mod_day))
end
# Helpers to make the iex experience a bit smoother
def p1(), do: p1(get_date().day, get_date().year)
def p2(), do: p2(get_date().day, get_date().year)
def p1(day), do: p1(day, get_date().year)
def p2(day), do: p2(day, get_date().year)
def p1(day, year), do: module_name(year, day).p1()
def p2(day, year), do: module_name(year, day).p2()
# Remember, new puzzles are at midnight EST
defp get_date() do
case DateTime.now("America/Toronto") do
{:ok, d} -> d
_ -> Date.utc_today()
end
end
# define some callbacks so compilation warnings keep things structured.
@callback p1() :: any()
@callback p2() :: any()
end
This makes running solutions easier, too. After launching iex -S mix
, a simple
AdventOfCode.p1()
and AdventOfCode.p2()
will run my solutions!
Assistance
Do you have any suggestions on how to improve this? The repository is open source, so feel free to take a look and make suggestions!
Are you taking on the challenge? What language are you solving puzzles in? Look out for my solutions here!