Setup 2021 (#1)
* chore: scaffold project * chore: stand up puzzle solver, puzzle case * feat: solution runner * feat: solution generator
This commit is contained in:
parent
da99ce538d
commit
a691461882
18 changed files with 357 additions and 0 deletions
5
2021/.formatter.exs
Normal file
5
2021/.formatter.exs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||||
|
locals_without_parens: [assert_solution: 2]
|
||||||
|
]
|
27
2021/.gitignore
vendored
Normal file
27
2021/.gitignore
vendored
Normal file
|
@ -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
|
2
2021/.tool-versions
Normal file
2
2021/.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
erlang 24.1.4
|
||||||
|
elixir 1.12.3-otp-24
|
22
2021/README.md
Normal file
22
2021/README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Advent of Code 2021
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Setup</summary>
|
||||||
|
|
||||||
|
Using [asdf]:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
asdf plugin add erlang
|
||||||
|
asdf plugin add elixir
|
||||||
|
asdf install
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
| 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/#/
|
12
2021/lib/advent_of_code.ex
Normal file
12
2021/lib/advent_of_code.ex
Normal file
|
@ -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
|
0
2021/lib/advent_of_code/day0.ex
Normal file
0
2021/lib/advent_of_code/day0.ex
Normal file
11
2021/lib/advent_of_code/day0/part0.ex
Normal file
11
2021/lib/advent_of_code/day0/part0.ex
Normal file
|
@ -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
|
18
2021/lib/advent_of_code/puzzle_solver.ex
Normal file
18
2021/lib/advent_of_code/puzzle_solver.ex
Normal file
|
@ -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
|
13
2021/lib/mix/advent_of_code.ex
Normal file
13
2021/lib/mix/advent_of_code.ex
Normal file
|
@ -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
|
94
2021/lib/mix/tasks/advent_of_code.gen.solution.ex
Normal file
94
2021/lib/mix/tasks/advent_of_code.gen.solution.ex
Normal file
|
@ -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
|
73
2021/lib/mix/tasks/advent_of_code.solve.ex
Normal file
73
2021/lib/mix/tasks/advent_of_code.solve.ex
Normal file
|
@ -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
|
31
2021/mix.exs
Normal file
31
2021/mix.exs
Normal file
|
@ -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
|
4
2021/mix.lock
Normal file
4
2021/mix.lock
Normal file
|
@ -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"},
|
||||||
|
}
|
3
2021/priv/inputs/0.0.input
Normal file
3
2021/priv/inputs/0.0.input
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
baz
|
7
2021/test/advent_of_code/day0/part_0_test.exs
Normal file
7
2021/test/advent_of_code/day0/part_0_test.exs
Normal file
|
@ -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
|
13
2021/test/advent_of_code_test.exs
Normal file
13
2021/test/advent_of_code_test.exs
Normal file
|
@ -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
|
21
2021/test/support/puzzle_case.ex
Normal file
21
2021/test/support/puzzle_case.ex
Normal file
|
@ -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
|
1
2021/test/test_helper.exs
Normal file
1
2021/test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
Loading…
Reference in a new issue