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
```
Defining a module with the `aoc/3` macro has a few advantages:
- The `AOC.IEx` functions can be used to call your solutions in the module, making your life
easier.
- Helper functions to access the input and examples (if present) are inserted into the generated
module.
Each of these advantages corresponds with a way to write your solution module, which we discuss
below. Overall, the `aoc/3` macro is intended to allow you to forego writing boilerplate code
which is shared between all the solutions.
Note that the code skeleton shown above can be generated by running `mix aoc.gen` or `mix aoc`.
## `aoc/3` and `use AOC`
Internally, `aoc/3` generates a module with a predefined name (`Y<year>.D<day>`) which contains
a `use AOC, day: <day>, year: <year>` statement. In turn, the `__using__/1` macro defined in
this module is responsible for generating helper functions. Thus, if you prefer to use a
different naming scheme than the one imposed by `aoc/3`, you can write your module as follows:
```
defmodule MySolution do
use AOC, day: <day>, year: <year>
...
end
```
When the `AOC` module is used like this, the helper functions defined below are still usable,
but the helpers defined in `AOC.IEx` will not work.
## IEx
The `AOC.IEx` module defines various helpers which facilitate testing the code in this module
within iex, the elixir shell. For instance, if you write your solution for part 1 as follows:
```
aoc <year>, <day> do
def p1(input) do
# solutions go here
end
end
```
You can use `AOC.IEx.p1e/1` to call `p1` with the example input of that day and `AOC.IEx.p1i/1`
to call `p1` with the puzzle input of that day. Similar functions are available for `p2`.
## Helper functions
You can also directly access the puzzle input or examples for a day inside an `aoc/3` module.
This module defines various functions such as `input_path/2`, `input_string/2`,
`input_stream/2` and their `example_*/2` counter parts. Helpers are inserted inside the
generated module which call these functions with the module's day / year. Thus, if you call
`input_path()` inside your solution module, it will call `input_path/2` for you with the
module's day and year, obtaining the path to the appropriate input file.
The following table provides an overview of the inserted functions and their counterparts in
this module:
| Generated | Calls |
|--------------------|--------------------|
| `input_path/0` | `input_path/2` |
| `input_string/0` | `input_string/2` |
| `input_stream/0` | `input_stream/2` |
| `example_path/0` | `example_path/2` |
| `example_string/0` | `example_string/2` |
| `example_stream/0` | `example_stream/2` |
The generated functions are overridable, i.e. you can define your own version of these functions
which will overwrite the generated function. This is handy to do something like the following:
```
def input_stream, do: super() |> Stream.map(&String.to_integer/1)
```
"""
alias AOC.Helpers
@doc """
Generate an advent of code solution module for a given year and day.
The generated module will be named `Y<year>.D<day>`. `use AOC` will be injected in the body of
the module, so that the input helpers described in the module documentation are available.
## Examples
```
import AOC
aoc 2020, 1 do
def some_function do
:foo
end
end
```
is equivalent to:
```
defmodule Y2020.D1 do
use AOC, year: 2020, day: 1
def some_function do
:foo
end
end
```
"""
defmacro aoc(year, day, do: body) do
quote do
defmodule unquote(Helpers.module_name(year, day)) do
use unquote(__MODULE__), year: unquote(year), day: unquote(day)
unquote(body)
end
end
end
defmacro __using__(opts) do
day = Keyword.fetch!(opts, :day)
year = Keyword.fetch!(opts, :year)
quote do
@spec input_path() :: Path.t()
def input_path, do: unquote(__MODULE__).input_path(unquote(year), unquote(day))
@spec input_string() :: String.t()
def input_string, do: unquote(__MODULE__).input_string(unquote(year), unquote(day))
@spec input_stream() :: Enumerable.t()
def input_stream, do: unquote(__MODULE__).input_stream(unquote(year), unquote(day))
@spec example_path() :: Path.t()
def example_path, do: unquote(__MODULE__).example_path(unquote(year), unquote(day))
@spec example_string() :: String.t()
def example_string, do: unquote(__MODULE__).example_string(unquote(year), unquote(day))
@spec example_stream() :: Enumerable.t()
def example_stream, do: unquote(__MODULE__).example_stream(unquote(year), unquote(day))
defoverridable input_path: 0
defoverridable input_stream: 0
defoverridable input_string: 0
defoverridable example_path: 0
defoverridable example_stream: 0
defoverridable example_string: 0
end
end
@doc """
Get the input path for `year`, `day`.
Obtains the path where `mix aoc.get` stores the input for `year`, `day`. This path defaults to
`input/<year>_<day>.txt`, but can be customized. Please refer to the `mix aoc.get` documentation
for more information.
"""
@spec input_path(pos_integer(), pos_integer()) :: Path.t()
def input_path(year, day), do: Helpers.input_path(year, day)
@doc """
Get the example path for `year`, `day`.
Obtains the path where `mix aoc.get` stores the input for `year`, `day`. This path defaults to
`input/<year>_<day>_example.txt`, but can be customized. Please refer to the `mix aoc.get`
documentation for more information.
"""
@spec example_path(pos_integer(), pos_integer()) :: Path.t()
def example_path(year, day), do: Helpers.example_path(year, day)
@doc """
Get the input contents of `year`, `day`.
Obtained by calling `File.read!/1` on the path returned by `input_path/2`.
`String.trim_trailing(string, "\n")` is called on the resulting string to remove trailing
whitespace.
"""
@spec input_string(pos_integer(), pos_integer()) :: String.t()
def input_string(year, day), do: input_path(year, day) |> path_to_string()
@doc """
Get the example contents of `year`, `day`.
Obtained by calling `File.read!/1` on the path returned by `example_path/2`.
`String.trim_trailing(string, "\n")` is called on the resulting string to remove trailing
whitespace.
"""
@spec example_string(pos_integer(), pos_integer()) :: String.t()
def example_string(year, day), do: example_path(year, day) |> path_to_string()
@doc """
Stream the contents of the input for `year`, `day`.
The stream is created by calling `File.stream!/1` on the path returned by `input_path/2`.
Afterwards, `String.trim_trailing(&1, "\n")` is mapped over the stream (using `Stream.map/2`),
to remove trailing newlines.
"""
@spec input_stream(pos_integer(), pos_integer()) :: Enumerable.t()
def input_stream(year, day), do: input_path(year, day) |> path_to_stream()
@doc """
Stream the contents of the example for `year`, `day`.
The stream is created by calling `File.stream!/1` on the path returned by `example_path/2`.
Afterwards, `String.trim_trailing(&1, "\n")` is mapped over the stream (using `Stream.map/2`),
to remove trailing newlines.
"""
@spec example_stream(pos_integer(), pos_integer()) :: Enumerable.t()
def example_stream(year, day), do: example_path(year, day) |> path_to_stream()
defp path_to_string(path), do: path |> File.read!() |> String.trim_trailing("\n")
defp path_to_stream(path) do
path |> File.stream!() |> Stream.map(&String.trim_trailing(&1, "\n"))
end
end