From db91df6cfce2f230369929e9b3391f5d683d9186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ytterstr=C3=B6m?= Date: Sat, 26 Nov 2022 00:05:55 +0100 Subject: [PATCH] Solve 2015:7 "Some Assembly Required" With all new bootstrap script --- 2015-elixir-2/.formatter.exs | 4 + 2015-elixir-2/.gitignore | 26 ++++ 2015-elixir-2/README.md | 29 +++++ 2015-elixir-2/lib/aoc.ex | 58 +++++++++ 2015-elixir-2/lib/mix/tasks/bootstrap.ex | 101 +++++++++++++++ 2015-elixir-2/lib/mix/tasks/solve.ex | 11 ++ 2015-elixir-2/lib/mix/tasks/solve_all.ex | 9 ++ 2015-elixir-2/lib/solution.ex | 6 + 2015-elixir-2/lib/solutions/day_07.ex | 122 +++++++++++++++++++ 2015-elixir-2/lib/utils.ex | 65 ++++++++++ 2015-elixir-2/mix.exs | 28 +++++ 2015-elixir-2/test/aoc_test.exs | 5 + 2015-elixir-2/test/solutions/day_07_test.exs | 33 +++++ 2015-elixir-2/test/test_helper.exs | 1 + 14 files changed, 498 insertions(+) create mode 100644 2015-elixir-2/.formatter.exs create mode 100644 2015-elixir-2/.gitignore create mode 100644 2015-elixir-2/README.md create mode 100644 2015-elixir-2/lib/aoc.ex create mode 100644 2015-elixir-2/lib/mix/tasks/bootstrap.ex create mode 100644 2015-elixir-2/lib/mix/tasks/solve.ex create mode 100644 2015-elixir-2/lib/mix/tasks/solve_all.ex create mode 100644 2015-elixir-2/lib/solution.ex create mode 100644 2015-elixir-2/lib/solutions/day_07.ex create mode 100644 2015-elixir-2/lib/utils.ex create mode 100644 2015-elixir-2/mix.exs create mode 100644 2015-elixir-2/test/aoc_test.exs create mode 100644 2015-elixir-2/test/solutions/day_07_test.exs create mode 100644 2015-elixir-2/test/test_helper.exs diff --git a/2015-elixir-2/.formatter.exs b/2015-elixir-2/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/2015-elixir-2/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/2015-elixir-2/.gitignore b/2015-elixir-2/.gitignore new file mode 100644 index 0000000..e23ab19 --- /dev/null +++ b/2015-elixir-2/.gitignore @@ -0,0 +1,26 @@ +# 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"). +aoc-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/2015-elixir-2/README.md b/2015-elixir-2/README.md new file mode 100644 index 0000000..2b5607e --- /dev/null +++ b/2015-elixir-2/README.md @@ -0,0 +1,29 @@ +# Aoc + +Solutions for [Advent of Code 2021][1], this year in Elixir. + +[Elixir 1.12.0][2] preferred. + +## Help scripts + +Run tests using `mix test` as usual. + +Create input file, test file and solution file for a day, +using day 1 as example: + + mix Aoc.new 1 "Name of the puzzle" + +Solve a single puzzle, using puzzle from day 3 as example: + + mix Aoc.solve 3 + +Solve all puzzles, starting at the first: + + mix Aoc.solve_all + +## Log + +_Advent of Code has not started yet. See you in December!_ + +[1]: https://adventofcode.com/ +[2]: https://hexdocs.pm/elixir/Kernel.html \ No newline at end of file diff --git a/2015-elixir-2/lib/aoc.ex b/2015-elixir-2/lib/aoc.ex new file mode 100644 index 0000000..91c05fc --- /dev/null +++ b/2015-elixir-2/lib/aoc.ex @@ -0,0 +1,58 @@ +defmodule Aoc do + @year 2015 + def solve_all() do + """ + + ADVENT OF CODE #{@year} + =================== + """ + |> IO.puts() + + 1..25 |> Enum.map(&solve/1) + end + + def solve(n) do + id = n |> Integer.to_string() |> String.pad_leading(2, "0") + + case File.read("./inputs/" <> id <> ".in") do + {:error, _} -> + nil + + {:ok, input} -> + input + |> run(id) + |> present + |> IO.puts() + end + end + + def run(input, id) do + solution_module = Module.concat(["Aoc.Solution", "Day" <> id]) + + data = apply(solution_module, :parse!, [input]) + name = apply(solution_module, :get_name, []) + first_solution = apply(solution_module, :solve, [data]) + second_solution = apply(solution_module, :solve_again, [data]) + + [name, first_solution, second_solution] + end + + @doc """ + Generates a CLI-friendly presentation of the solutions. + + ## Examples + + iex> Aoc.present(["Day 1: tomten kommer", "10", "20"]) + "Day 1: tomten kommer\\n--------------------\\n\\n1. 10\\n2. 20\\n" + + """ + def present([name, first_solution, second_solution]) do + """ + #{name} + #{1..String.length(name) |> Enum.map_join(fn _x -> "-" end)} + + 1. #{first_solution} + 2. #{second_solution} + """ + end +end diff --git a/2015-elixir-2/lib/mix/tasks/bootstrap.ex b/2015-elixir-2/lib/mix/tasks/bootstrap.ex new file mode 100644 index 0000000..144f6bc --- /dev/null +++ b/2015-elixir-2/lib/mix/tasks/bootstrap.ex @@ -0,0 +1,101 @@ +defmodule Mix.Tasks.Aoc.New do + use Mix.Task + + @shortdoc "Bootstrap new solution" + @impl Mix.Task + + @year 2021 + + def run([n, name]) do + id = n |> String.pad_leading(2, "0") + + input_dir = "./inputs" + solutions_dir = "./lib/solutions" + tests_dir = "./test/solutions" + + input_file = input_dir <> "/" <> id <> ".in" + test_file = tests_dir <> "/day_" <> id <> "_test.exs" + solution_file = solutions_dir <> "/day_" <> id <> ".ex" + + for dir_path <- [input_dir, tests_dir, solutions_dir], + do: if(not File.exists?(dir_path), do: File.mkdir(dir_path)) + + IO.puts("Creating " <> input_file) + File.touch(input_file) + + IO.puts("Creating " <> test_file) + File.write!(test_file, test_file_content(id, name)) + + IO.puts("Creating " <> solution_file) + File.write!(solution_file, solution_file_content(name, id, n)) + + """ + \nDone! Start coding. + + Get your puzzle input here: + https://adventofcode.com/#{@year}/day/#{n}/input + + mix test -- run tests. + mix aoc.solve_all -- run all puzzles, starting with 1 + mix aoc.solve #{id} -- run single puzzle, 1-25 + """ + |> IO.puts() + end + + defp test_file_content(id, name) do + """ + defmodule Day#{id}Test do + use ExUnit.Case + doctest Aoc.Solution.Day#{id} + import Aoc.Solution.Day#{id} + + @input ~s( + REPLACE ME + ) + + test "#{id}: #{name}, part 1" do + expected = :something + + result = @input |> parse!() |> solve() + + assert result == expected + end + + test "#{id}: #{name}, part 2" do + expected = :something + + result = @input |> parse!() |> solve_again() + + assert result == expected + end + end + """ + end + + defp solution_file_content(name, id, n) do + ~s""" + defmodule Aoc.Solution.Day#{id} do + @name "Day #{n}: #{name}" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + raw + end + + @impl Solution + def solve(_input) do + "(TBW)" + end + + @impl Solution + def solve_again(_input) do + "(TBW)" + end + end + """ + end +end diff --git a/2015-elixir-2/lib/mix/tasks/solve.ex b/2015-elixir-2/lib/mix/tasks/solve.ex new file mode 100644 index 0000000..9cdd082 --- /dev/null +++ b/2015-elixir-2/lib/mix/tasks/solve.ex @@ -0,0 +1,11 @@ +defmodule Mix.Tasks.Aoc.Solve do + use Mix.Task + + @shortdoc "Solve single puzzle" + @impl Mix.Task + def run([id]) do + id + |> String.to_integer() + |> Aoc.solve() + end +end diff --git a/2015-elixir-2/lib/mix/tasks/solve_all.ex b/2015-elixir-2/lib/mix/tasks/solve_all.ex new file mode 100644 index 0000000..7fd5a6d --- /dev/null +++ b/2015-elixir-2/lib/mix/tasks/solve_all.ex @@ -0,0 +1,9 @@ +defmodule Mix.Tasks.Aoc.SolveAll do + use Mix.Task + + @shortdoc "Solve all puzzles" + @impl Mix.Task + def run(_) do + Aoc.solve_all() + end +end diff --git a/2015-elixir-2/lib/solution.ex b/2015-elixir-2/lib/solution.ex new file mode 100644 index 0000000..36e564c --- /dev/null +++ b/2015-elixir-2/lib/solution.ex @@ -0,0 +1,6 @@ +defmodule Solution do + @callback parse!(String.t()) :: Any + @callback get_name() :: String.t() + @callback solve(Any) :: String.t() + @callback solve_again(Any) :: String.t() +end diff --git a/2015-elixir-2/lib/solutions/day_07.ex b/2015-elixir-2/lib/solutions/day_07.ex new file mode 100644 index 0000000..e265c4c --- /dev/null +++ b/2015-elixir-2/lib/solutions/day_07.ex @@ -0,0 +1,122 @@ +defmodule Aoc.Solution.Day07 do + import Bitwise + import Aoc.Utils + + @name "Day 7: Some Assembly Required" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + split_lines(raw) + |> Enum.map(fn l -> parse_values(l, " -> ") end) + end + + @impl Solution + def solve(input, k \\ "a") do + wires = Map.new() + all = Enum.count(input) + wire(wires, all, input) |> Map.get(k) + end + + @impl Solution + def solve_again(input) do + value = solve(input) + + solve( + Enum.map( + input, + fn [o, i] -> + case i do + "b" -> [Integer.to_string(value), i] + _ -> [o, i] + end + end + ) + ) + end + + def wire(wires, all, queue) do + wires = walk(wires, queue) + + case Enum.count(wires) do + ^all -> wires + _ -> wire(wires, all, queue) + end + end + + def walk(wires, []), do: wires + + def walk(wires, [[output, input] | remain]) do + case apply_rule(wires, String.split(output)) do + nil -> + walk(wires, remain) + + v -> + walk( + Map.put_new( + wires, + input, + v + ), + remain + ) + end + end + + def apply_rule(wires, ["NOT", wire]) do + case Map.get(wires, wire) do + nil -> nil + v -> 65536 + bnot(v) + end + end + + def apply_rule(wires, [l, "AND", r]) do + l = _value(wires, l) + r = _value(wires, r) + case Enum.all?([l, r]) do + false -> nil + true -> l &&& r + end + end + + def apply_rule(wires, [l, "OR", r]) do + l = _value(wires, l) + r = _value(wires, r) + case Enum.all?([l, r]) do + false -> nil + true -> bor(l, r) + end + end + + def apply_rule(wires, [l, "RSHIFT", r]) do + case Map.get(wires, l) do + nil -> nil + l -> l >>> String.to_integer(r) + end + end + + def apply_rule(wires, [l, "LSHIFT", r]) do + case Map.get(wires, l) do + nil -> nil + l -> l <<< String.to_integer(r) + end + end + + def apply_rule(wires, [wire]) do + _value(wires, wire) + end + + def _value(seen, k) do + try do + case Map.get(seen, k) do + nil -> String.to_integer(k) + v -> v + end + rescue + _e -> nil + end + end +end diff --git a/2015-elixir-2/lib/utils.ex b/2015-elixir-2/lib/utils.ex new file mode 100644 index 0000000..4c06fe4 --- /dev/null +++ b/2015-elixir-2/lib/utils.ex @@ -0,0 +1,65 @@ +defmodule Aoc.Utils do + @doc """ + Extracts values as trimmed strings using a separator. + + ## Examples + + iex> Aoc.Utils.parse_values(~s( + ...> abc: + ...> 123 q1w2e3r4 + ...> ), ":") + ["abc", "123 q1w2e3r4"] + + iex> Aoc.Utils.parse_values(~s( + ...> abc AND 123 AND q1w2e3r4 + ...> ), "AND") + ["abc", "123", "q1w2e3r4"] + """ + def parse_values(input, separator) do + input + |> String.trim() + |> String.split(separator) + |> Enum.map(&String.trim/1) + end + + @doc """ + Extracts values from input by white space as trimmed strings. + + ## Examples + + iex> Aoc.Utils.parse_values(~s( + ...> abc + ...> 123 q1w2e3r4 + ...> )) + ["abc", "123", "q1w2e3r4"] + """ + def parse_values(input), do: String.split(input) + + @doc """ + Split input to trimmed lines. + Equal to parse_values(input, ["\n", "\r"]) + + ## Examples + + iex> Aoc.Utils.split_lines(~s( + ...> a bc + ...> 12 3 + ...> q1w2 e3r4 + ...> )) + ["a bc", "12 3", "q1w2 e3r4"] + """ + def split_lines(input), do: parse_values(input, ["\n", "\r"]) + + @doc """ + Split input to trimmed strings by comma as separator. + Equal to Aoc.Utils.parse_values(input, ",") + + ## Examples + + iex> Aoc.Utils.parse_csv(~s( + ...> abc,123,q1w2 e3r4 + ...> )) + ["abc", "123", "q1w2 e3r4"] + """ + def parse_csv(input), do: parse_values(input, ",") +end diff --git a/2015-elixir-2/mix.exs b/2015-elixir-2/mix.exs new file mode 100644 index 0000000..83ea864 --- /dev/null +++ b/2015-elixir-2/mix.exs @@ -0,0 +1,28 @@ +defmodule Aoc.MixProject do + use Mix.Project + + def project do + [ + app: :aoc, + version: "0.1.0", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/2015-elixir-2/test/aoc_test.exs b/2015-elixir-2/test/aoc_test.exs new file mode 100644 index 0000000..3a46923 --- /dev/null +++ b/2015-elixir-2/test/aoc_test.exs @@ -0,0 +1,5 @@ +defmodule AocTest do + use ExUnit.Case + doctest Aoc + doctest Aoc.Utils +end diff --git a/2015-elixir-2/test/solutions/day_07_test.exs b/2015-elixir-2/test/solutions/day_07_test.exs new file mode 100644 index 0000000..3977591 --- /dev/null +++ b/2015-elixir-2/test/solutions/day_07_test.exs @@ -0,0 +1,33 @@ +defmodule Day07Test do + use ExUnit.Case + doctest Aoc.Solution.Day07 + import Aoc.Solution.Day07 + + @input ~s( + 123 -> x +456 -> y +x AND y -> d +x OR y -> e +x LSHIFT 2 -> f +y RSHIFT 2 -> g +NOT x -> h +NOT y -> i + ) + + test "07: Some Assembly Required, part 1" do + expected = %{ + "d" => 72, + "e" => 507, + "f" => 492, + "g" => 114, + "h" => 65412, + "i" => 65079, + "x" => 123, + "y" => 456 + } + + result = @input |> parse!() |> solve("d") + + assert result == Map.get(expected, "d") + end +end diff --git a/2015-elixir-2/test/test_helper.exs b/2015-elixir-2/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2015-elixir-2/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()