Andrew Fontaine

Mostly code

Advent of Code 2019: Day 0

03 Dec 2019
Code Snippet for Advent of Code 2019: Day 0

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: tests, puzzles, and sources. 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!

I ❤ feedback. Let me know what you think of this article on Twitter @afontaine_ca