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(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