1
0
Fork 0

Setup 2021 (#1)

* chore: scaffold project

* chore: stand up puzzle solver, puzzle case

* feat: solution runner

* feat: solution generator
This commit is contained in:
Sloane Perrault 2022-09-21 09:19:53 -04:00
parent da99ce538d
commit a691461882
18 changed files with 357 additions and 0 deletions

5
2021/.formatter.exs Normal file
View 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
View 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
View file

@ -0,0 +1,2 @@
erlang 24.1.4
elixir 1.12.3-otp-24

22
2021/README.md Normal file
View 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/#/

View 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

View file

View 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

View 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

View 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

View 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

View 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
View 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
View 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"},
}

View file

@ -0,0 +1,3 @@
foo
bar
baz

View 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

View 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

View 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

View file

@ -0,0 +1 @@
ExUnit.start()