diff --git a/2021/.formatter.exs b/2021/.formatter.exs new file mode 100644 index 0000000..fea6a6c --- /dev/null +++ b/2021/.formatter.exs @@ -0,0 +1,5 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: [assert_solution: 2] +] diff --git a/2021/.gitignore b/2021/.gitignore new file mode 100644 index 0000000..7eeb530 --- /dev/null +++ b/2021/.gitignore @@ -0,0 +1,27 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +advent_of_code-*.tar + + +# Temporary files for e.g. tests +/tmp diff --git a/2021/.tool-versions b/2021/.tool-versions new file mode 100644 index 0000000..eda65d9 --- /dev/null +++ b/2021/.tool-versions @@ -0,0 +1,2 @@ +erlang 24.1.4 +elixir 1.12.3-otp-24 diff --git a/2021/README.md b/2021/README.md new file mode 100644 index 0000000..f246dcc --- /dev/null +++ b/2021/README.md @@ -0,0 +1,22 @@ +# Advent of Code 2021 + +
+ Setup + + Using [asdf]: + + ```sh + asdf plugin add erlang + asdf plugin add elixir + asdf install + ``` +
+ +| S | M | T | W | T | F | S | +| :-: | :-: | :-: | :-: | :-: | :-: | :-: | +| | | | 1 | 2 | 3 | 4 | +| 5 | 6 | 7 | 8 | 9 | 10 | 11 | +| 12 | 13 | 14 | 15 | 16 | 17 | 18 | +| 19 | 20 | 21 | 22 | 23 | 24 | 25 | + +[asdf]: https://asdf-vm.com/#/ diff --git a/2021/lib/advent_of_code.ex b/2021/lib/advent_of_code.ex new file mode 100644 index 0000000..f8016fb --- /dev/null +++ b/2021/lib/advent_of_code.ex @@ -0,0 +1,12 @@ +defmodule AdventOfCode do + @moduledoc """ + Solutions to the 2021 Advent of Code puzzles + """ + + def solver(selector) do + [day, part] = String.split(selector, ".") + day_module = Macro.camelize("Day#{day}") + part_module = Macro.camelize("Part#{part}") + Module.concat([AdventOfCode, day_module, part_module]) + end +end diff --git a/2021/lib/advent_of_code/day0.ex b/2021/lib/advent_of_code/day0.ex new file mode 100644 index 0000000..e69de29 diff --git a/2021/lib/advent_of_code/day0/part0.ex b/2021/lib/advent_of_code/day0/part0.ex new file mode 100644 index 0000000..6c32387 --- /dev/null +++ b/2021/lib/advent_of_code/day0/part0.ex @@ -0,0 +1,11 @@ +defmodule AdventOfCode.Day0.Part0 do + alias AdventOfCode.PuzzleSolver + + use PuzzleSolver + + @impl PuzzleSolver + def solve(stream) do + Stream.run(stream) + "42" + end +end diff --git a/2021/lib/advent_of_code/puzzle_solver.ex b/2021/lib/advent_of_code/puzzle_solver.ex new file mode 100644 index 0000000..212df08 --- /dev/null +++ b/2021/lib/advent_of_code/puzzle_solver.ex @@ -0,0 +1,18 @@ +defmodule AdventOfCode.PuzzleSolver do + @moduledoc """ + Behaviour for a puzzle solution. + """ + + @doc """ + Given the input as a stream, return the solution as a string + """ + @callback solve(Enumerable.t()) :: String.t() + + def solve(mod, stream), do: apply(mod, :solve, [stream]) + + defmacro __using__(_) do + quote do + @behaviour AdventOfCode.PuzzleSolver + end + end +end diff --git a/2021/lib/mix/advent_of_code.ex b/2021/lib/mix/advent_of_code.ex new file mode 100644 index 0000000..a012629 --- /dev/null +++ b/2021/lib/mix/advent_of_code.ex @@ -0,0 +1,13 @@ +defmodule Mix.AdventOfCode do + @moduledoc """ + Helpers for `AdventOfCode` mix tasks. + """ + + def day_module(day) do + Module.concat(AdventOfCode, Macro.camelize("Day#{day}")) + end + + def part_module(day, part) do + Module.concat(day_module(day), Macro.camelize("Part#{part}")) + end +end diff --git a/2021/lib/mix/tasks/advent_of_code.gen.solution.ex b/2021/lib/mix/tasks/advent_of_code.gen.solution.ex new file mode 100644 index 0000000..f1d1165 --- /dev/null +++ b/2021/lib/mix/tasks/advent_of_code.gen.solution.ex @@ -0,0 +1,94 @@ +defmodule Mix.Tasks.AdventOfCode.Gen.Solution do + use Mix.Task + import Mix.Generator + import Mix.AdventOfCode + + @shortdoc "Generate a new solution module" + + @moduledoc """ + #{@shortdoc}. + + Includes new solution module, test, and empty problem input. + + ## Examples + + # Generate solution modules, tests, and empty input for Day 2 + $ mix advent_of_code.gen.solution 2 + """ + + @switches [] + + @impl Mix.Task + def run(args) do + case OptionParser.parse!(args, switches: @switches) do + {_, [day]} -> + day_module = day_module(day) + day_contents = day_template(mod: day_module) |> Code.format_string!() + day_path = Path.join("lib", Macro.underscore(day_module)) + day_tests_path = Path.join("test", Macro.underscore(day_module)) + day_file = "#{day_path}.ex" + + create_directory(day_path) + create_directory(day_tests_path) + create_file(day_file, day_contents) + + for part <- 1..2 do + part_module = part_module(day, part) + + part_contents = + part_template(mod: part_module, day_mod: day_module) |> Code.format_string!() + + part_file = Path.join("lib", "#{Macro.underscore(part_module)}.ex") + create_file(part_file, part_contents) + + part_test_module = Module.concat(day_module, Macro.camelize("Part#{part}Test")) + + part_test_contents = + part_test_template(mod: part_module, test_mod: part_test_module) + |> Code.format_string!() + + part_test_file = + Path.join(day_tests_path, "#{Macro.underscore("part_#{part}_test")}.exs") + + create_file(part_test_file, part_test_contents) + end + + _ -> + Mix.raise("Unknown arguments.") + end + end + + embed_template(:day, ~S""" + defmodule <%= inspect(@mod) %> do + + end + """) + + embed_template(:part, ~S""" + defmodule <%= inspect(@mod) %> do + alias AdventOfCode.PuzzleSolver + use PuzzleSolver + + import <%= inspect(@day_mod) %>, warn: false + + @impl PuzzleSolver + def solve(_input_stream) do + :ok |> to_string() + end + end + """) + + embed_template(:part_test, ~S[ + defmodule <%= inspect(@test_mod) %> do + use AdventOfCode.PuzzleCase, module: <%= inspect(@mod) %> + + test "returns :ok" do + input = ~S""" + input + """ + + assert_solution input, "ok" + end + end + ]) +end diff --git a/2021/lib/mix/tasks/advent_of_code.solve.ex b/2021/lib/mix/tasks/advent_of_code.solve.ex new file mode 100644 index 0000000..5c506c5 --- /dev/null +++ b/2021/lib/mix/tasks/advent_of_code.solve.ex @@ -0,0 +1,73 @@ +defmodule Mix.Tasks.AdventOfCode.Solve do + use Mix.Task + + @shortdoc "Runs solution code with problem input" + + @moduledoc """ + #{@shortdoc}. + + ## Options + + `--input` + - name of a `.input` file in `priv/inputs/` without the `.input` extension or `-` to read from stdin + + ## Examples + + # Run Day 2, Part 1 solution program against the `2.1.input` file + $ mix advent_of_code.solve 2.1 + + # Run Day 1, Part 2 with a special input + $ mix advent_of_code.solve 1.2 --input day-1-part-2-small # priv/inputs/day-1-part-2-small.input + + # Run Day 17, Part 1 with input from stdin + # $ mix advent_of_code.solve 17.1 --input - + """ + + @switches [input: :string] + + @impl Mix.Task + def run(args) do + case OptionParser.parse!(args, strict: @switches) do + {opts, [selector]} -> + case stream_for_input(selector, opts) do + {:indeterminate, stream} -> + ProgressBar.render_spinner( + [text: "Processing indeterminate data...", done: "Done."], + fn -> solve(selector, stream) end + ) + |> IO.puts() + + {size, stream} -> + stream = + stream + |> Stream.with_index(1) + |> Stream.each(fn {_, i} -> ProgressBar.render(i, size) end) + |> Stream.map(fn {l, _} -> l end) + + solve(selector, stream) + |> IO.puts() + end + + _ -> + nil + end + end + + defp solve(selector, stream) do + selector + |> AdventOfCode.solver() + |> AdventOfCode.PuzzleSolver.solve(stream) + end + + defp stream_for_input(selector, opts) do + input = Keyword.get(opts, :input, selector) + + if input == "-" do + {:indeterminate, IO.stream()} + else + file = Path.join("priv/inputs", "#{input}.input") + stream = File.stream!(file) + {Enum.count(stream), stream} + end + end +end diff --git a/2021/mix.exs b/2021/mix.exs new file mode 100644 index 0000000..a8ad075 --- /dev/null +++ b/2021/mix.exs @@ -0,0 +1,31 @@ +defmodule AdventOfCode.MixProject do + use Mix.Project + + def project do + [ + app: :advent_of_code, + version: "0.1.0", + elixir: "~> 1.11", + start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env()), + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger, :eex] + ] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:progress_bar, "~> 2.0"} + ] + end +end diff --git a/2021/mix.lock b/2021/mix.lock new file mode 100644 index 0000000..e43918e --- /dev/null +++ b/2021/mix.lock @@ -0,0 +1,4 @@ +%{ + "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"}, +} diff --git a/2021/priv/inputs/0.0.input b/2021/priv/inputs/0.0.input new file mode 100644 index 0000000..86e041d --- /dev/null +++ b/2021/priv/inputs/0.0.input @@ -0,0 +1,3 @@ +foo +bar +baz diff --git a/2021/test/advent_of_code/day0/part_0_test.exs b/2021/test/advent_of_code/day0/part_0_test.exs new file mode 100644 index 0000000..cd53141 --- /dev/null +++ b/2021/test/advent_of_code/day0/part_0_test.exs @@ -0,0 +1,7 @@ +defmodule AdventOfCode.Day0.Part0Test do + use AdventOfCode.PuzzleCase, module: AdventOfCode.Day0.Part0 + + test "returns the answer to live the universe and everything" do + assert_solution "life the universe and everything", "42" + end +end diff --git a/2021/test/advent_of_code_test.exs b/2021/test/advent_of_code_test.exs new file mode 100644 index 0000000..589fa60 --- /dev/null +++ b/2021/test/advent_of_code_test.exs @@ -0,0 +1,13 @@ +defmodule AdventOfCodeTest do + use ExUnit.Case + + import AdventOfCode + + describe "solver/1" do + test "returns a module for a solver" do + assert solver("1.1") == AdventOfCode.Day1.Part1 + assert solver("2.2") == AdventOfCode.Day2.Part2 + assert solver("3.1") == AdventOfCode.Day3.Part1 + end + end +end diff --git a/2021/test/support/puzzle_case.ex b/2021/test/support/puzzle_case.ex new file mode 100644 index 0000000..c0d7e09 --- /dev/null +++ b/2021/test/support/puzzle_case.ex @@ -0,0 +1,21 @@ +defmodule AdventOfCode.PuzzleCase do + @moduledoc """ + Defines tests for an `AdventOfCode.PuzzleSolver` module. + """ + + use ExUnit.CaseTemplate + + using module: module do + quote bind_quoted: [module: module] do + @module module + + defp assert_solution(input, desired_output) when is_binary(input) do + {:ok, stream_pid} = StringIO.open(input) + stream_input = IO.stream(stream_pid, :line) + actual_output = AdventOfCode.PuzzleSolver.solve(@module, stream_input) + @module.solve(stream_input) + assert actual_output == desired_output + end + end + end +end diff --git a/2021/test/test_helper.exs b/2021/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2021/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()