lib/aoc.ex

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 do
      # Part 1 solution goes here
    end

    def p2 do
      # Part 1 solution goes here
    end
  end
  ```

  Defining a module with the `aoc/3` macro has a few advantages:

  - Helper functions to access the input and examples (if present) are inserted into the generated
  module.
  - The `AOC.IEx` functions can be used to call your solutions in the module, making your life a
  bit easier.

  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 in `AOC.IEx` will not work.

  ## Helper functions

  This module defines various functions such as `input_path/2`, `input_string/2`,
  `input_stream/2` and their `example_*/2` counter parts. Inside the generated module, helpers are
  inserted 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

    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/1` 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/1` 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/1` is mapped over the stream (using `Stream.map/2`), to remove trailing
  newlines and whitespace.
  """
  @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/1` is mapped over the stream (using `Stream.map/2`), to remove trailing
  newlines and whitespace.
  """
  @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()
  defp path_to_stream(path), do: path |> File.stream!() |> Stream.map(&String.trim/1)
end