diff --git a/2021/.gitignore b/2021/.gitignore index b8bfe89..49beaff 100644 --- a/2021/.gitignore +++ b/2021/.gitignore @@ -28,4 +28,4 @@ advent_of_code-*.tar .envrc -priv/inputs/ +input/ diff --git a/2021/.iex.exs b/2021/.iex.exs new file mode 100644 index 0000000..795d6b6 --- /dev/null +++ b/2021/.iex.exs @@ -0,0 +1 @@ +import AOC.IEx diff --git a/2021/README.md b/2021/README.md index 28c371f..c61a9c9 100644 --- a/2021/README.md +++ b/2021/README.md @@ -19,26 +19,7 @@ | 12 | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ---- - -## Mix Tasks - -- `advent_of_code.gen.solution` -- `advent_of_code.solve` -- `advent_of_code.fetch_input` - -Run `mix help ` for details. - -## [`AdventOfCode.PuzzleSolver`](./lib/advent_of_code/puzzle_solver.ex) - -A behaviour for a solution to a puzzle. Must define a `solve[2] callback. - -## [`AdventOfCode.PuzzleCase`](./test/support/puzzle_case.ex) - -Case template defining an `assert_solution[2] helper. - -[1]: ./lib/advent_of_code/day01.ex - -[2]: ./lib/advent_of_code/day02.ex +[1]: ./lib/2021/1.ex +[2]: ./lib/2021/2.ex diff --git a/2021/config/config.exs b/2021/config/config.exs index b319e95..99094ca 100644 --- a/2021/config/config.exs +++ b/2021/config/config.exs @@ -1,4 +1,5 @@ import Config -config :advent_of_code, - event: "2021" +config :advent_of_code_utils, + session: System.get_env("ADVENT_OF_CODE_SESSION"), + auto_compile?: true diff --git a/2021/config/runtime.exs b/2021/config/runtime.exs deleted file mode 100644 index 3a98dbe..0000000 --- a/2021/config/runtime.exs +++ /dev/null @@ -1,3 +0,0 @@ -import Config - -config :advent_of_code, session: System.get_env("ADVENT_OF_CODE_SESSION") diff --git a/2021/lib/2021/1.ex b/2021/lib/2021/1.ex new file mode 100644 index 0000000..885cc14 --- /dev/null +++ b/2021/lib/2021/1.ex @@ -0,0 +1,35 @@ +import AOC + +aoc 2021, 1 do + def input_list, + do: + input_stream() + |> Stream.map(&String.to_integer/1) + |> Enum.to_list() + + def p1 do + input_list = input_list() + input_list_ = Enum.drop(input_list, 1) + + Enum.zip_reduce( + input_list, + input_list_, + 0, + &if(&1 < &2, do: &3 + 1, else: &3) + ) + end + + def p2 do + input_list() + |> window_increases() + end + + defp window_increases(list, acc \\ 0) + + defp window_increases([a, b, c, d | _] = list, acc) do + acc = if a + b + c < b + c + d, do: acc + 1, else: acc + window_increases(Enum.drop(list, 1), acc) + end + + defp window_increases(_, acc), do: acc +end diff --git a/2021/lib/2021/2.ex b/2021/lib/2021/2.ex new file mode 100644 index 0000000..54c63bb --- /dev/null +++ b/2021/lib/2021/2.ex @@ -0,0 +1,32 @@ +import AOC + +aoc 2021, 2 do + def input_stream(), + do: + super() + |> Stream.map(fn + "forward " <> v -> {:forward, String.to_integer(v)} + "down " <> v -> {:down, String.to_integer(v)} + "up " <> v -> {:up, String.to_integer(v)} + end) + + def p1 do + input_stream() + |> Enum.reduce({0, 0}, fn + {:forward, v}, {h, d} -> {h + v, d} + {:down, v}, {h, d} -> {h, d + v} + {:up, v}, {h, d} -> {h, d - v} + end) + |> then(fn {h, d} -> h * d end) + end + + def p2 do + input_stream() + |> Enum.reduce({0, 0, 0}, fn + {:forward, v}, {h, d, a} -> {h + v, d + a * v, a} + {:down, v}, {h, d, a} -> {h, d, a + v} + {:up, v}, {h, d, a} -> {h, d, a - v} + end) + |> then(fn {h, d, _v} -> h * d end) + end +end diff --git a/2021/lib/advent_of_code.ex b/2021/lib/advent_of_code.ex deleted file mode 100644 index 26304d6..0000000 --- a/2021/lib/advent_of_code.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule AdventOfCode do - @moduledoc """ - Solutions to the 2021 Advent of Code puzzles - """ - - def solver(selector) do - [day, part] = String.split(selector, ".") - part_module(day, part) - end - - def part_module(day, part) do - Module.concat(day_module(day), Macro.camelize("Part#{part_number(part)}")) - end - - def day_module(day) do - Module.concat(AdventOfCode, Macro.camelize("Day#{day_number(day)}")) - end - - def day_input_path(day), do: "priv/inputs/#{day_number(day)}.input" - - defp day_number(day), do: String.pad_leading(day, 2, "0") - defp part_number(part), do: String.trim_leading(part, "0") -end diff --git a/2021/lib/advent_of_code/day01.ex b/2021/lib/advent_of_code/day01.ex deleted file mode 100644 index 9f69f13..0000000 --- a/2021/lib/advent_of_code/day01.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule AdventOfCode.Day01 do - @moduledoc """ - Day 1 - """ - - def parse_input_stream(input_stream) do - input_stream - |> Stream.map(&String.trim/1) - |> Stream.map(&String.to_integer/1) - |> Enum.to_list() - end -end - -defmodule AdventOfCode.Day01.Part1 do - @moduledoc """ - Day 1, Part 1 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import AdventOfCode.Day01, warn: false - - @impl PuzzleSolver - def solve(input_stream) do - depths = parse_input_stream(input_stream) - - Enum.zip_reduce( - depths, - Enum.drop(depths, 1), - 0, - &if(&1 < &2, do: &3 + 1, else: &3) - ) - end -end - -defmodule AdventOfCode.Day01.Part2 do - @moduledoc """ - Day 1, Part 2 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import AdventOfCode.Day01, warn: false - - @impl PuzzleSolver - def solve(input_stream) do - parse_input_stream(input_stream) - |> window_increases() - end - - defp window_increases(list, acc \\ 0) - - defp window_increases([a, b, c, d | _] = list, acc) do - acc = if a + b + c < b + c + d, do: acc + 1, else: acc - window_increases(Enum.drop(list, 1), acc) - end - - defp window_increases(_, acc), do: acc -end diff --git a/2021/lib/advent_of_code/day02.ex b/2021/lib/advent_of_code/day02.ex deleted file mode 100644 index 42e2c19..0000000 --- a/2021/lib/advent_of_code/day02.ex +++ /dev/null @@ -1,62 +0,0 @@ -defmodule AdventOfCode.Day02 do - @moduledoc """ - Day 2 - """ - - def parse_action("forward " <> v), do: {:forward, String.to_integer(v)} - def parse_action("down " <> v), do: {:down, String.to_integer(v)} - def parse_action("up " <> v), do: {:up, String.to_integer(v)} -end - -defmodule AdventOfCode.Day02.Part1 do - @moduledoc """ - Day 2, Part 1 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import AdventOfCode.Day02, warn: false - - @impl PuzzleSolver - def solve(input_stream) do - input_stream - |> Stream.map(&String.trim/1) - |> Stream.map(&parse_action/1) - |> Enum.reduce({0, 0}, &apply_action/2) - |> product() - end - - defp apply_action({:forward, v}, {horiz, depth}), do: {horiz + v, depth} - defp apply_action({:down, v}, {horiz, depth}), do: {horiz, depth + v} - defp apply_action({:up, v}, {horiz, depth}), do: {horiz, depth - v} - - defp product({x, y}), do: x * y -end - -defmodule AdventOfCode.Day02.Part2 do - @moduledoc """ - Day 2, Part 2 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import AdventOfCode.Day02, warn: false - - @impl PuzzleSolver - def solve(input_stream) do - input_stream - |> Stream.map(&String.trim/1) - |> Stream.map(&parse_action/1) - |> Enum.reduce({0, 0, 0}, &apply_action/2) - |> product() - end - - defp apply_action({:forward, v}, {horiz, depth, aim}), do: {horiz + v, depth + aim * v, aim} - defp apply_action({:down, v}, {horiz, depth, aim}), do: {horiz, depth, aim + v} - defp apply_action({:up, v}, {horiz, depth, aim}), do: {horiz, depth, aim - v} - - defp product({x, y, _z}), do: x * y -end - diff --git a/2021/lib/advent_of_code/puzzle_solver.ex b/2021/lib/advent_of_code/puzzle_solver.ex deleted file mode 100644 index 766525c..0000000 --- a/2021/lib/advent_of_code/puzzle_solver.ex +++ /dev/null @@ -1,18 +0,0 @@ -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()) :: any() - - 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 deleted file mode 100644 index 3776ee2..0000000 --- a/2021/lib/mix/advent_of_code.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Mix.AdventOfCode do - @moduledoc """ - Helpers for `AdventOfCode` mix tasks. - """ - - defdelegate day_module(day), to: AdventOfCode - defdelegate part_module(day, part), to: AdventOfCode - defdelegate day_input_path(day), to: AdventOfCode -end diff --git a/2021/lib/mix/tasks/advent_of_code.fetch_input.ex b/2021/lib/mix/tasks/advent_of_code.fetch_input.ex deleted file mode 100644 index d80e2d7..0000000 --- a/2021/lib/mix/tasks/advent_of_code.fetch_input.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule Mix.Tasks.AdventOfCode.FetchInput do - use Mix.Task - - import Mix.Generator - import Mix.AdventOfCode - - @shortdoc "Fetches the inputs for day and saves them in `priv/inputs`" - - @moduledoc """ - #{@shortdoc}. - - Expects `:event` and `:session` keys to be configured for the `:advent_of_code` application. - These values can be overridden. - - ## Examples - - # Fetch day 1's inputs - $ mix advent_of_code.fetch_input 1 - - # Fetch Day 1 of 2020 - $ mix advent_of_code.fetch_input --event 2020 1 - - # Pass your session cookie value - $ mix advent_of_code.fetch_input 13 --session foobar - """ - - @finch Mix.AdventOfCode.FetchInput.Finch - - @switches [event: :string, session: :string] - - @impl Mix.Task - def run(args) do - Mix.Task.run("app.config") - :ok = Application.ensure_started(:telemetry) - {:ok, _pid} = Finch.start_link(name: @finch) - - default_event = Application.get_env(:advent_of_code, :event) - default_session = Application.get_env(:advent_of_code, :session) - - case OptionParser.parse!(args, strict: @switches) do - {opts, [day]} -> - input_file_path = day_input_path(day) - - if overwrite?(input_file_path) do - event = Keyword.get(opts, :event, default_event) - session = Keyword.get(opts, :session, default_session) - - {:ok, %{body: data}} = - Finch.build( - :get, - "https://adventofcode.com/#{event}/day/#{day}/input", - [{"Cookie", "session=#{session}"}] - ) - |> Finch.request(@finch) - - create_file(input_file_path, data, force: true) - end - - _ -> - Mix.raise("Unexpected arguments.") - end - 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 deleted file mode 100644 index 8d32c3b..0000000 --- a/2021/lib/mix/tasks/advent_of_code.gen.solution.ex +++ /dev/null @@ -1,119 +0,0 @@ -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, day: day) |> 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_file(day_file, day_contents) - - create_directory(day_tests_path) - - for part <- 1..2 do - part = to_string(part) - part_module = part_module(day, part) - - 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 - - day_regex = ~r/\W(#{day})\W/ - readme_file = "README.md" - readme_contents = File.read!("README.md") - - readme_contents = - "#{String.replace(readme_contents, day_regex, "[\\1]")}\n[#{day}]: ./#{day_file}\n" - - File.write!(readme_file, readme_contents) - Mix.shell().info([:green, "* updating ", :reset, "README.md"]) - - _ -> - Mix.raise("Unknown arguments.") - end - end - - embed_template(:day, ~S[ - defmodule <%= inspect(@mod) %> do - @moduledoc """ - Day <%= @day %> - """ - end - - defmodule <%= inspect(@mod) %>.Part1 do - @moduledoc """ - Day <%= @day %>, Part 1 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import <%= inspect(@mod) %>, warn: false - - @impl PuzzleSolver - def solve(_input_stream) do - :ok |> to_string() - end - end - - defmodule <%= inspect(@mod) %>.Part2 do - @moduledoc """ - Day <%= @day %>, Part 2 - """ - - alias AdventOfCode.PuzzleSolver - use PuzzleSolver - - import <%= inspect(@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 deleted file mode 100644 index 01026a5..0000000 --- a/2021/lib/mix/tasks/advent_of_code.solve.ex +++ /dev/null @@ -1,91 +0,0 @@ -defmodule Mix.Tasks.AdventOfCode.Solve do - use Mix.Task - import Mix.AdventOfCode - - @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 - case Keyword.fetch(opts, :input) do - {:ok, "-"} -> - {:indeterminate, IO.stream()} - - {:ok, name} -> - Path.join("priv/inputs", "#{name}.input") - |> get_size_and_stream() - - :error -> - [day | _] = String.split(selector, ".") - - file = day_input_path(day) - - unless File.exists?(file) or - not Mix.shell().yes?("Input missing. Fetch input for day #{day}?") do - Mix.Task.run("advent_of_code.fetch_input", [day]) - end - - day_input_path(day) - |> get_size_and_stream() - end - end - - def get_size_and_stream(file) do - stream = File.stream!(file) - {Enum.count(stream), stream} - end -end diff --git a/2021/mix.exs b/2021/mix.exs index 4e1f743..cad5b4b 100644 --- a/2021/mix.exs +++ b/2021/mix.exs @@ -25,9 +25,7 @@ defmodule AdventOfCode.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:finch, "~> 0.9.0"}, - {:mix_test_interactive, "~> 1.0", only: :dev, runtime: false}, - {:progress_bar, "~> 2.0"} + {:advent_of_code_utils, "~> 1.0"} ] end end diff --git a/2021/mix.lock b/2021/mix.lock index d136f80..780f685 100644 --- a/2021/mix.lock +++ b/2021/mix.lock @@ -1,4 +1,5 @@ %{ + "advent_of_code_utils": {:hex, :advent_of_code_utils, "1.0.0", "e9ce8be22988e095991168e9d2dde802afc213aaae64491e9fde84dd877db41c", [:mix], [], "hexpm", "1fa5217ccb95a38ce2312dd7ea6057b0d0002a3eba7490feab704df4b8a94038"}, "castore": {:hex, :castore, "0.1.13", "ccf3ab251ffaebc4319f41d788ce59a6ab3f42b6c27e598ad838ffecee0b04f9", [:mix], [], "hexpm", "a14a7eecfec7e20385493dbb92b0d12c5d77ecfd6307de10102d58c94e8c49c0"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, diff --git a/2021/test/advent_of_code/day01/part_1_test.exs b/2021/test/advent_of_code/day01/part_1_test.exs deleted file mode 100644 index b1ca9a5..0000000 --- a/2021/test/advent_of_code/day01/part_1_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -defmodule AdventOfCode.Day01.Part1Test do - use AdventOfCode.PuzzleCase, module: AdventOfCode.Day01.Part1 - - test "solves for a small input" do - input = ~S""" - 199 - 200 - 208 - 210 - 200 - 207 - 240 - 269 - 260 - 263 - """ - - assert_solution(input, 7) - end -end diff --git a/2021/test/advent_of_code/day01/part_2_test.exs b/2021/test/advent_of_code/day01/part_2_test.exs deleted file mode 100644 index 7a09811..0000000 --- a/2021/test/advent_of_code/day01/part_2_test.exs +++ /dev/null @@ -1,20 +0,0 @@ -defmodule AdventOfCode.Day01.Part2Test do - use AdventOfCode.PuzzleCase, module: AdventOfCode.Day01.Part2 - - test "solves for a small input" do - input = ~S""" - 199 - 200 - 208 - 210 - 200 - 207 - 240 - 269 - 260 - 263 - """ - - assert_solution(input, 5) - end -end diff --git a/2021/test/advent_of_code/day02/part_1_test.exs b/2021/test/advent_of_code/day02/part_1_test.exs deleted file mode 100644 index 5af6f6e..0000000 --- a/2021/test/advent_of_code/day02/part_1_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule AdventOfCode.Day02.Part1Test do - use AdventOfCode.PuzzleCase, module: AdventOfCode.Day02.Part1 - - test "returns the product of horizontal position and depth" do - input = ~S""" - forward 5 - down 5 - forward 8 - up 3 - down 8 - forward 2 - """ - - assert_solution(input, 150) - end -end diff --git a/2021/test/advent_of_code/day02/part_2_test.exs b/2021/test/advent_of_code/day02/part_2_test.exs deleted file mode 100644 index e6f91d6..0000000 --- a/2021/test/advent_of_code/day02/part_2_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule AdventOfCode.Day02.Part2Test do - use AdventOfCode.PuzzleCase, module: AdventOfCode.Day02.Part2 - - test "returns the product of horizontal position and depth" do - input = ~S""" - forward 5 - down 5 - forward 8 - up 3 - down 8 - forward 2 - """ - - assert_solution(input, 900) - end -end diff --git a/2021/test/advent_of_code_test.exs b/2021/test/advent_of_code_test.exs deleted file mode 100644 index e9a64bb..0000000 --- a/2021/test/advent_of_code_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -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.Day01.Part1 - assert solver("2.2") == AdventOfCode.Day02.Part2 - assert solver("3.1") == AdventOfCode.Day03.Part1 - end - - test "returns a solver module for zero padded numbers" do - assert solver("01.01") == AdventOfCode.Day01.Part1 - assert solver("02.2") == AdventOfCode.Day02.Part2 - assert solver("3.01") == AdventOfCode.Day03.Part1 - assert solver("12.1") == AdventOfCode.Day12.Part1 - assert solver("24.02") == AdventOfCode.Day24.Part2 - end - end -end diff --git a/2021/test/support/puzzle_case.ex b/2021/test/support/puzzle_case.ex deleted file mode 100644 index 77b12be..0000000 --- a/2021/test/support/puzzle_case.ex +++ /dev/null @@ -1,20 +0,0 @@ -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) - assert actual_output == desired_output - end - end - end -end