defmodule AOC do
@moduledoc """
Advent of Code solution module macro and helpers.
This module contains the `aoc/3` macro, which should be used to write a solution module for a
given advent of code challenge. The intended use is to write your solution for day `<day>`, year
`<year>` as follows:
```
import AOC
aoc <year>, <day> do
def p1(input) do
# Part 1 solution goes here
end
def p2(input) do
# Part 1 solution goes here
end
end
```
Writing a solution module with the `aoc/3` macro enables you to use the functions defined in the
`AOC.IEx` module to test your solutions with ease. For instance, you can use `AOC.IEx.p1e/1` to
call `p1` with the example input of the current day and `AOC.IEx.p1i/1` to call `p1` with the
puzzle input of the current day. Similar functions are available for `p2`.
Note that the code skeleton shown above can be generated by running `mix aoc.gen` or `mix aoc`.
"""
alias AOC.Helpers
@doc """
Part 1 solution.
Must accept a string which represents the puzzle or example input.
"""
@callback p1(String.t()) :: any()
@doc """
Part 2 solution.
Must accept a string which represents the puzzle or example input. This callback is marked as
optional as day 25 does not have a second part; this also prevents warnings while working on
part 1.
"""
@callback p2(String.t()) :: any()
@optional_callbacks p2: 1
@doc """
Generate an advent of code solution module for a given year and day.
The generated module will be named `Y<year>.D<day>`. The helpers in `AOC.IEx` rely on this
convention to find your solution module.
## Examples
```
import AOC
aoc 2020, 1 do
def some_function do
:foo
end
end
```
is equivalent to:
```
defmodule Y2020.D1 do
@behaviour AOC
def some_function do
:foo
end
end
```
"""
defmacro aoc(year, day, do: body) do
quote do
defmodule unquote(Helpers.module_name(year, day)) do
@behaviour AOC
unquote(body)
end
end
end
@doc """
Generate an advent of code test module for a given year and day.
The generated module will be named `Y<year>.D<day>.AOCTest`. It will be tagged with the year,
day and date of the puzzle, and will contain helper functions, `input_path/0`, `example_path/0`,
`input_string/0` and `example_string/0` which can be used to access the example and puzzle
input, as described in `AOC.Case`.
The generated module will import the solution module of the same date (unless `import?: false`
is provided as an option) and automatically calls `ExUnit.DocTest.doctest/1` on the solution
module, unless `doctest?: false` is provided as an option. Any other options (such as `async:
true`) are passed to ExUnit.
## Examples
```
import AOC
aoc_test 2020,1, async: true do
test "does my helper work?" do
assert some_helper(:foo) == 42
end
end
```
Is equivalent to:
```
defmodule Y2020.D1.AOCTest do
use AOC.Case, year: 2020, day: 1, async: true
import Y2020.D1
test "does my helper work?" do
assert some_helper(:foo) == 42
end
doctest Y2020.D1
end
```
"""
defmacro aoc_test(year, day, opts \\ [], do: body) do
target = Helpers.module_name(year, day)
opts = opts ++ [year: year, day: day]
maybe_import =
if Keyword.get(opts, :import?, true) do
quote(do: import(unquote(target)))
end
maybe_doctest =
if Keyword.get(opts, :doctest?, true) do
quote(do: doctest(unquote(target)))
end
quote do
defmodule unquote(Helpers.test_module_name(year, day)) do
use AOC.Case, unquote(opts)
unquote(maybe_import)
unquote(body)
unquote(maybe_doctest)
end
end
end
end