From 7daf307f21868f69f3a2eb6079eb3d0d3b113602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ytterstr=C3=B6m?= Date: Mon, 1 Nov 2021 16:40:46 +0100 Subject: [PATCH] Add solutions from AOC 2016-2020 --- 2015-elixir/.formatter.exs | 4 + 2015-elixir/.tool-versions | 1 + 2015-elixir/README.md | 29 + 2015-elixir/lib/aoc.ex | 61 ++ 2015-elixir/lib/mix/tasks/bootstrap.ex | 89 ++ 2015-elixir/lib/mix/tasks/solve.ex | 11 + 2015-elixir/lib/mix/tasks/solve_all.ex | 9 + 2015-elixir/lib/solution.ex | 6 + 2015-elixir/lib/solutions/day_01.ex | 57 + 2015-elixir/lib/solutions/day_02.ex | 42 + 2015-elixir/lib/solutions/day_03.ex | 53 + 2015-elixir/lib/solutions/day_04.ex | 35 + 2015-elixir/lib/solutions/day_05.ex | 73 ++ 2015-elixir/mix.exs | 28 + 2015-elixir/test/aoc_test.exs | 4 + 2015-elixir/test/day_01_test.exs | 51 + 2015-elixir/test/day_02_test.exs | 45 + 2015-elixir/test/day_03_test.exs | 31 + 2015-elixir/test/day_04_test.exs | 20 + 2015-elixir/test/day_05_test.exs | 43 + 2015-elixir/test/test_helper.exs | 1 + 2016-python/README.md | 22 + 2016-python/aoc2016.py | 17 + 2016-python/day_01.py | 108 ++ 2016-python/day_02.py | 70 ++ 2016-python/day_03.py | 35 + 2016-python/day_04.py | 56 + 2016-python/day_05.py | 47 + 2016-python/day_06.py | 29 + 2016-python/day_07.py | 44 + 2016-python/day_08.py | 75 ++ 2016-python/day_09.py | 52 + 2016-python/day_10.py | 65 ++ 2017-python/.gitignore | 3 + 2017-python/README.md | 54 + 2017-python/aoc.py | 65 ++ 2017-python/solutions/__init__.py | 26 + 2017-python/solutions/day_01.py | 22 + 2017-python/solutions/day_02.py | 34 + 2017-python/solutions/day_03.py | 37 + 2017-python/solutions/day_04.py | 25 + 2017-python/solutions/day_05.py | 28 + 2017-python/solutions/day_06.py | 48 + 2017-python/solutions/day_07.py | 94 ++ 2017-python/solutions/day_08.py | 40 + 2017-python/solutions/day_09.py | 33 + 2017-python/solutions/day_10.py | 47 + 2017-python/solutions/day_11.py | 41 + 2017-python/solutions/day_12.py | 39 + 2017-python/solutions/day_13.py | 67 ++ 2017-python/solutions/day_14.py | 56 + 2017-python/solutions/day_15.py | 42 + 2017-python/solutions/day_16.py | 55 + 2017-python/solutions/day_17.py | 32 + 2017-python/solutions/day_18.py | 94 ++ 2017-python/solutions/day_19.py | 63 ++ 2017-python/solutions/day_20.py | 56 + 2017-python/solutions/day_21.py | 89 ++ 2017-python/solutions/day_22.py | 98 ++ 2017-python/tests/__init__.py | 0 2017-python/tests/day_01_tests.py | 25 + 2017-python/tests/day_02_tests.py | 31 + 2017-python/tests/day_03_tests.py | 18 + 2017-python/tests/day_04_tests.py | 37 + 2017-python/tests/day_05_tests.py | 20 + 2017-python/tests/day_06_tests.py | 26 + 2017-python/tests/day_07_tests.py | 42 + 2017-python/tests/day_08_tests.py | 30 + 2017-python/tests/day_09_tests.py | 31 + 2017-python/tests/day_10_tests.py | 38 + 2017-python/tests/day_11_tests.py | 22 + 2017-python/tests/day_12_tests.py | 36 + 2017-python/tests/day_13_tests.py | 30 + 2017-python/tests/day_14_tests.py | 18 + 2017-python/tests/day_15_tests.py | 26 + 2017-python/tests/day_16_tests.py | 16 + 2017-python/tests/day_17_tests.py | 15 + 2017-python/tests/day_18_tests.py | 39 + 2017-python/tests/day_19_tests.py | 34 + 2017-python/tests/day_20_tests.py | 28 + 2017-python/tests/day_21_tests.py | 19 + 2017-python/tests/day_22_tests.py | 30 + 2018-elixir/.formatter.exs | 4 + 2018-elixir/.gitignore | 25 + 2018-elixir/README.md | 21 + 2018-elixir/config/config.exs | 30 + 2018-elixir/data/01.in | 993 ++++++++++++++++++ 2018-elixir/data/02.in | 250 +++++ 2018-elixir/lib/aoc2018e.ex | 7 + 2018-elixir/lib/day01.ex | 44 + 2018-elixir/lib/day02.ex | 40 + 2018-elixir/mix.exs | 28 + 2018-elixir/test/day01_test.exs | 17 + 2018-elixir/test/day02_test.exs | 38 + 2018-elixir/test/test_helper.exs | 1 + 2018-python/.gitignore | 3 + 2018-python/README.md | 52 + 2018-python/aoc.py | 66 ++ 2018-python/solutions/__init__.py | 36 + 2018-python/solutions/day_01.py | 28 + 2018-python/solutions/day_02.py | 33 + 2018-python/solutions/day_03.py | 49 + 2018-python/solutions/day_04.py | 67 ++ 2018-python/solutions/day_05.py | 35 + 2018-python/solutions/day_06.py | 51 + 2018-python/solutions/day_07.py | 84 ++ 2018-python/solutions/day_08.py | 51 + 2018-python/solutions/day_10.py | 72 ++ 2018-python/solutions/day_11.py | 54 + 2018-python/solutions/day_12.py | 66 ++ 2018-python/solutions/day_13.py | 186 ++++ 2018-python/solutions/day_14.py | 19 + 2018-python/tests/day_01_tests.py | 30 + 2018-python/tests/day_02_tests.py | 38 + 2018-python/tests/day_03_tests.py | 29 + 2018-python/tests/day_04_tests.py | 37 + 2018-python/tests/day_05_tests.py | 18 + 2018-python/tests/day_06_tests.py | 34 + 2018-python/tests/day_07_tests.py | 27 + 2018-python/tests/day_08_tests.py | 18 + 2018-python/tests/day_10_tests.py | 59 ++ 2018-python/tests/day_11_tests.py | 25 + 2018-python/tests/day_12_tests.py | 31 + 2018-python/tests/day_13_tests.py | 174 +++ 2018-python/tests/day_14_tests.py | 16 + 2019-elixir/.formatter.exs | 4 + 2019-elixir/.gitignore | 24 + 2019-elixir/README.md | 29 + 2019-elixir/lib/aoc19.ex | 61 ++ 2019-elixir/lib/mix/tasks/bootstrap.ex | 79 ++ 2019-elixir/lib/mix/tasks/solve.ex | 11 + 2019-elixir/lib/mix/tasks/solve_all.ex | 9 + 2019-elixir/lib/solution.ex | 6 + 2019-elixir/lib/solutions/day_01.ex | 113 ++ 2019-elixir/lib/solutions/day_02.ex | 102 ++ 2019-elixir/lib/solutions/day_03.ex | 159 +++ 2019-elixir/lib/solutions/day_04.ex | 126 +++ 2019-elixir/lib/solutions/day_05.ex | 320 ++++++ 2019-elixir/lib/solutions/day_06.ex | 210 ++++ 2019-elixir/lib/solutions/day_08.ex | 217 ++++ 2019-elixir/mix.exs | 28 + 2019-elixir/test/aoc19_test.exs | 4 + 2019-elixir/test/day_01_test.exs | 4 + 2019-elixir/test/day_02_test.exs | 4 + 2019-elixir/test/day_03_test.exs | 4 + 2019-elixir/test/day_04_test.exs | 4 + 2019-elixir/test/day_05_test.exs | 4 + 2019-elixir/test/day_06_test.exs | 4 + 2019-elixir/test/day_08_test.exs | 4 + 2019-elixir/test/test_helper.exs | 1 + 2020-elixir/.formatter.exs | 4 + 2020-elixir/.gitignore | 26 + 2020-elixir/.tool-versions | 1 + 2020-elixir/README.md | 29 + 2020-elixir/lib/aoc.ex | 58 + 2020-elixir/lib/mix/tasks/bootstrap.ex | 92 ++ 2020-elixir/lib/mix/tasks/solve.ex | 11 + 2020-elixir/lib/mix/tasks/solve_all.ex | 9 + 2020-elixir/lib/solution.ex | 6 + 2020-elixir/lib/solutions/day_01.ex | 52 + 2020-elixir/mix.exs | 28 + 2020-elixir/test/aoc_test.exs | 4 + 2020-elixir/test/day_01_test.exs | 47 + 2020-elixir/test/test_helper.exs | 1 + .../.github/workflows/test-and-linters.yml | 38 + 2020-python/.gitignore | 4 + 2020-python/.python-version | 1 + 2020-python/README.md | 29 + 2020-python/aoc.py | 113 ++ 2020-python/solutions/__init__.py | 32 + 2020-python/solutions/day_01.py | 28 + 2020-python/solutions/day_02.py | 46 + 2020-python/solutions/day_03.py | 36 + 2020-python/solutions/day_04.py | 97 ++ 2020-python/solutions/day_05.py | 68 ++ 2020-python/solutions/day_06.py | 32 + 2020-python/solutions/day_07.py | 49 + 2020-python/solutions/day_08.py | 85 ++ 2020-python/solutions/day_09.py | 35 + 2020-python/solutions/day_10.py | 49 + 2020-python/solutions/day_11.py | 111 ++ 2020-python/solutions/day_12.py | 79 ++ 2020-python/solutions/day_13.py | 54 + 2020-python/solutions/day_14.py | 61 ++ 2020-python/solutions/day_15.py | 40 + 2020-python/solutions/day_16.py | 84 ++ 2020-python/solutions/day_18.py | 65 ++ 2020-python/tests/__init__.py | 0 2020-python/tests/test_day_01.py | 28 + 2020-python/tests/test_day_02.py | 37 + 2020-python/tests/test_day_03.py | 43 + 2020-python/tests/test_day_04.py | 121 +++ 2020-python/tests/test_day_05.py | 47 + 2020-python/tests/test_day_06.py | 58 + 2020-python/tests/test_day_07.py | 82 ++ 2020-python/tests/test_day_08.py | 39 + 2020-python/tests/test_day_09.py | 51 + 2020-python/tests/test_day_10.py | 77 ++ 2020-python/tests/test_day_11.py | 143 +++ 2020-python/tests/test_day_12.py | 67 ++ 2020-python/tests/test_day_13.py | 43 + 2020-python/tests/test_day_14.py | 55 + 2020-python/tests/test_day_15.py | 31 + 2020-python/tests/test_day_16.py | 103 ++ 2020-python/tests/test_day_18.py | 65 ++ 205 files changed, 10423 insertions(+) create mode 100644 2015-elixir/.formatter.exs create mode 100644 2015-elixir/.tool-versions create mode 100644 2015-elixir/README.md create mode 100644 2015-elixir/lib/aoc.ex create mode 100644 2015-elixir/lib/mix/tasks/bootstrap.ex create mode 100644 2015-elixir/lib/mix/tasks/solve.ex create mode 100644 2015-elixir/lib/mix/tasks/solve_all.ex create mode 100644 2015-elixir/lib/solution.ex create mode 100644 2015-elixir/lib/solutions/day_01.ex create mode 100644 2015-elixir/lib/solutions/day_02.ex create mode 100644 2015-elixir/lib/solutions/day_03.ex create mode 100644 2015-elixir/lib/solutions/day_04.ex create mode 100644 2015-elixir/lib/solutions/day_05.ex create mode 100644 2015-elixir/mix.exs create mode 100644 2015-elixir/test/aoc_test.exs create mode 100644 2015-elixir/test/day_01_test.exs create mode 100644 2015-elixir/test/day_02_test.exs create mode 100644 2015-elixir/test/day_03_test.exs create mode 100644 2015-elixir/test/day_04_test.exs create mode 100644 2015-elixir/test/day_05_test.exs create mode 100644 2015-elixir/test/test_helper.exs create mode 100644 2016-python/README.md create mode 100755 2016-python/aoc2016.py create mode 100644 2016-python/day_01.py create mode 100644 2016-python/day_02.py create mode 100644 2016-python/day_03.py create mode 100644 2016-python/day_04.py create mode 100644 2016-python/day_05.py create mode 100644 2016-python/day_06.py create mode 100644 2016-python/day_07.py create mode 100644 2016-python/day_08.py create mode 100644 2016-python/day_09.py create mode 100644 2016-python/day_10.py create mode 100644 2017-python/.gitignore create mode 100644 2017-python/README.md create mode 100644 2017-python/aoc.py create mode 100644 2017-python/solutions/__init__.py create mode 100644 2017-python/solutions/day_01.py create mode 100644 2017-python/solutions/day_02.py create mode 100644 2017-python/solutions/day_03.py create mode 100644 2017-python/solutions/day_04.py create mode 100644 2017-python/solutions/day_05.py create mode 100644 2017-python/solutions/day_06.py create mode 100644 2017-python/solutions/day_07.py create mode 100644 2017-python/solutions/day_08.py create mode 100644 2017-python/solutions/day_09.py create mode 100644 2017-python/solutions/day_10.py create mode 100644 2017-python/solutions/day_11.py create mode 100644 2017-python/solutions/day_12.py create mode 100644 2017-python/solutions/day_13.py create mode 100644 2017-python/solutions/day_14.py create mode 100644 2017-python/solutions/day_15.py create mode 100644 2017-python/solutions/day_16.py create mode 100644 2017-python/solutions/day_17.py create mode 100644 2017-python/solutions/day_18.py create mode 100644 2017-python/solutions/day_19.py create mode 100644 2017-python/solutions/day_20.py create mode 100644 2017-python/solutions/day_21.py create mode 100644 2017-python/solutions/day_22.py create mode 100644 2017-python/tests/__init__.py create mode 100644 2017-python/tests/day_01_tests.py create mode 100644 2017-python/tests/day_02_tests.py create mode 100644 2017-python/tests/day_03_tests.py create mode 100644 2017-python/tests/day_04_tests.py create mode 100644 2017-python/tests/day_05_tests.py create mode 100644 2017-python/tests/day_06_tests.py create mode 100644 2017-python/tests/day_07_tests.py create mode 100644 2017-python/tests/day_08_tests.py create mode 100644 2017-python/tests/day_09_tests.py create mode 100644 2017-python/tests/day_10_tests.py create mode 100644 2017-python/tests/day_11_tests.py create mode 100644 2017-python/tests/day_12_tests.py create mode 100644 2017-python/tests/day_13_tests.py create mode 100644 2017-python/tests/day_14_tests.py create mode 100644 2017-python/tests/day_15_tests.py create mode 100644 2017-python/tests/day_16_tests.py create mode 100644 2017-python/tests/day_17_tests.py create mode 100644 2017-python/tests/day_18_tests.py create mode 100644 2017-python/tests/day_19_tests.py create mode 100644 2017-python/tests/day_20_tests.py create mode 100644 2017-python/tests/day_21_tests.py create mode 100644 2017-python/tests/day_22_tests.py create mode 100644 2018-elixir/.formatter.exs create mode 100644 2018-elixir/.gitignore create mode 100644 2018-elixir/README.md create mode 100644 2018-elixir/config/config.exs create mode 100644 2018-elixir/data/01.in create mode 100644 2018-elixir/data/02.in create mode 100644 2018-elixir/lib/aoc2018e.ex create mode 100644 2018-elixir/lib/day01.ex create mode 100644 2018-elixir/lib/day02.ex create mode 100644 2018-elixir/mix.exs create mode 100644 2018-elixir/test/day01_test.exs create mode 100644 2018-elixir/test/day02_test.exs create mode 100644 2018-elixir/test/test_helper.exs create mode 100644 2018-python/.gitignore create mode 100644 2018-python/README.md create mode 100644 2018-python/aoc.py create mode 100644 2018-python/solutions/__init__.py create mode 100644 2018-python/solutions/day_01.py create mode 100644 2018-python/solutions/day_02.py create mode 100644 2018-python/solutions/day_03.py create mode 100644 2018-python/solutions/day_04.py create mode 100644 2018-python/solutions/day_05.py create mode 100644 2018-python/solutions/day_06.py create mode 100644 2018-python/solutions/day_07.py create mode 100644 2018-python/solutions/day_08.py create mode 100644 2018-python/solutions/day_10.py create mode 100644 2018-python/solutions/day_11.py create mode 100644 2018-python/solutions/day_12.py create mode 100644 2018-python/solutions/day_13.py create mode 100644 2018-python/solutions/day_14.py create mode 100644 2018-python/tests/day_01_tests.py create mode 100644 2018-python/tests/day_02_tests.py create mode 100644 2018-python/tests/day_03_tests.py create mode 100644 2018-python/tests/day_04_tests.py create mode 100644 2018-python/tests/day_05_tests.py create mode 100644 2018-python/tests/day_06_tests.py create mode 100644 2018-python/tests/day_07_tests.py create mode 100644 2018-python/tests/day_08_tests.py create mode 100644 2018-python/tests/day_10_tests.py create mode 100644 2018-python/tests/day_11_tests.py create mode 100644 2018-python/tests/day_12_tests.py create mode 100644 2018-python/tests/day_13_tests.py create mode 100644 2018-python/tests/day_14_tests.py create mode 100644 2019-elixir/.formatter.exs create mode 100644 2019-elixir/.gitignore create mode 100644 2019-elixir/README.md create mode 100644 2019-elixir/lib/aoc19.ex create mode 100644 2019-elixir/lib/mix/tasks/bootstrap.ex create mode 100644 2019-elixir/lib/mix/tasks/solve.ex create mode 100644 2019-elixir/lib/mix/tasks/solve_all.ex create mode 100644 2019-elixir/lib/solution.ex create mode 100644 2019-elixir/lib/solutions/day_01.ex create mode 100644 2019-elixir/lib/solutions/day_02.ex create mode 100644 2019-elixir/lib/solutions/day_03.ex create mode 100644 2019-elixir/lib/solutions/day_04.ex create mode 100644 2019-elixir/lib/solutions/day_05.ex create mode 100644 2019-elixir/lib/solutions/day_06.ex create mode 100644 2019-elixir/lib/solutions/day_08.ex create mode 100644 2019-elixir/mix.exs create mode 100644 2019-elixir/test/aoc19_test.exs create mode 100644 2019-elixir/test/day_01_test.exs create mode 100644 2019-elixir/test/day_02_test.exs create mode 100644 2019-elixir/test/day_03_test.exs create mode 100644 2019-elixir/test/day_04_test.exs create mode 100644 2019-elixir/test/day_05_test.exs create mode 100644 2019-elixir/test/day_06_test.exs create mode 100644 2019-elixir/test/day_08_test.exs create mode 100644 2019-elixir/test/test_helper.exs create mode 100644 2020-elixir/.formatter.exs create mode 100644 2020-elixir/.gitignore create mode 100644 2020-elixir/.tool-versions create mode 100644 2020-elixir/README.md create mode 100644 2020-elixir/lib/aoc.ex create mode 100644 2020-elixir/lib/mix/tasks/bootstrap.ex create mode 100644 2020-elixir/lib/mix/tasks/solve.ex create mode 100644 2020-elixir/lib/mix/tasks/solve_all.ex create mode 100644 2020-elixir/lib/solution.ex create mode 100644 2020-elixir/lib/solutions/day_01.ex create mode 100644 2020-elixir/mix.exs create mode 100644 2020-elixir/test/aoc_test.exs create mode 100644 2020-elixir/test/day_01_test.exs create mode 100644 2020-elixir/test/test_helper.exs create mode 100644 2020-python/.github/workflows/test-and-linters.yml create mode 100644 2020-python/.gitignore create mode 100644 2020-python/.python-version create mode 100644 2020-python/README.md create mode 100644 2020-python/aoc.py create mode 100644 2020-python/solutions/__init__.py create mode 100644 2020-python/solutions/day_01.py create mode 100644 2020-python/solutions/day_02.py create mode 100644 2020-python/solutions/day_03.py create mode 100644 2020-python/solutions/day_04.py create mode 100644 2020-python/solutions/day_05.py create mode 100644 2020-python/solutions/day_06.py create mode 100644 2020-python/solutions/day_07.py create mode 100644 2020-python/solutions/day_08.py create mode 100644 2020-python/solutions/day_09.py create mode 100644 2020-python/solutions/day_10.py create mode 100644 2020-python/solutions/day_11.py create mode 100644 2020-python/solutions/day_12.py create mode 100644 2020-python/solutions/day_13.py create mode 100644 2020-python/solutions/day_14.py create mode 100644 2020-python/solutions/day_15.py create mode 100644 2020-python/solutions/day_16.py create mode 100644 2020-python/solutions/day_18.py create mode 100644 2020-python/tests/__init__.py create mode 100644 2020-python/tests/test_day_01.py create mode 100644 2020-python/tests/test_day_02.py create mode 100644 2020-python/tests/test_day_03.py create mode 100644 2020-python/tests/test_day_04.py create mode 100644 2020-python/tests/test_day_05.py create mode 100644 2020-python/tests/test_day_06.py create mode 100644 2020-python/tests/test_day_07.py create mode 100644 2020-python/tests/test_day_08.py create mode 100644 2020-python/tests/test_day_09.py create mode 100644 2020-python/tests/test_day_10.py create mode 100644 2020-python/tests/test_day_11.py create mode 100644 2020-python/tests/test_day_12.py create mode 100644 2020-python/tests/test_day_13.py create mode 100644 2020-python/tests/test_day_14.py create mode 100644 2020-python/tests/test_day_15.py create mode 100644 2020-python/tests/test_day_16.py create mode 100644 2020-python/tests/test_day_18.py diff --git a/2015-elixir/.formatter.exs b/2015-elixir/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/2015-elixir/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/2015-elixir/.tool-versions b/2015-elixir/.tool-versions new file mode 100644 index 0000000..8c00e8a --- /dev/null +++ b/2015-elixir/.tool-versions @@ -0,0 +1 @@ +elixir 1.11.1 \ No newline at end of file diff --git a/2015-elixir/README.md b/2015-elixir/README.md new file mode 100644 index 0000000..e0b58fa --- /dev/null +++ b/2015-elixir/README.md @@ -0,0 +1,29 @@ +# Aoc + +Solutions for [Advent of Code 2020][1], this year in Elixir. + +[Elixir 1.11.1][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/lib/aoc.ex b/2015-elixir/lib/aoc.ex new file mode 100644 index 0000000..f20e7a6 --- /dev/null +++ b/2015-elixir/lib/aoc.ex @@ -0,0 +1,61 @@ +defmodule Aoc do + @moduledoc """ + Solutions for Advent of Code 2020, written in Elixir 1.11.1. + """ + + def solve_all() do + """ + + ADVENT OF CODE 2020 + =================== + """ + |> 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_first_part, [data]) + second_solution = apply(solution_module, :solve_second_part, [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/lib/mix/tasks/bootstrap.ex b/2015-elixir/lib/mix/tasks/bootstrap.ex new file mode 100644 index 0000000..45a061b --- /dev/null +++ b/2015-elixir/lib/mix/tasks/bootstrap.ex @@ -0,0 +1,89 @@ +defmodule Mix.Tasks.Aoc.New do + use Mix.Task + + @shortdoc "Bootstrap new solution" + @impl Mix.Task + def run([n, name]) do + id = n |> String.pad_leading(2, "0") + + input_file = "./inputs/" <> id <> ".in" + test_file = "./test/day_" <> id <> "_test.exs" + solution_file = "./lib/solutions/day_" <> id <> ".ex" + + IO.puts("Creating " <> input_file) + File.touch(input_file) + + IO.puts("Creating " <> test_file) + File.write!(test_file, test_file_content(id)) + + IO.puts("Creating " <> solution_file) + File.write!(solution_file, solution_file_content(name, id)) + + """ + \nDone! Start coding. + + Get your puzzle input here: + https://adventofcode.com/2015/day/#{id}/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) do + """ + defmodule Day#{id}Test do + use ExUnit.Case + doctest Aoc.Solution.Day#{id} + import Aoc.Solution.Day#{id} + + test "parses the input" do + expected = 10 + + assert parse!("10") == expected + end + + test "solves first part" do + a = "something" |> parse!() |> solve_first_part() + + expect a == :something + end + + test "solves second part" do + a = "something" |> parse!() |> solve_second_part() + + expect a == :something + end + end + """ + end + + defp solution_file_content(name, id) do + ~s""" + defmodule Aoc.Solution.Day#{id} do + @name "#{name}" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(_raw) do + "10" + end + + @impl Solution + def solve_first_part(_input) do + "(TBW)" + end + + @impl Solution + def solve_second_part(_input) do + "(TBW)" + end + end + """ + end +end diff --git a/2015-elixir/lib/mix/tasks/solve.ex b/2015-elixir/lib/mix/tasks/solve.ex new file mode 100644 index 0000000..9cdd082 --- /dev/null +++ b/2015-elixir/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/lib/mix/tasks/solve_all.ex b/2015-elixir/lib/mix/tasks/solve_all.ex new file mode 100644 index 0000000..7fd5a6d --- /dev/null +++ b/2015-elixir/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/lib/solution.ex b/2015-elixir/lib/solution.ex new file mode 100644 index 0000000..d1f8d8f --- /dev/null +++ b/2015-elixir/lib/solution.ex @@ -0,0 +1,6 @@ +defmodule Solution do + @callback parse!(String.t()) :: Any + @callback get_name() :: String.t() + @callback solve_first_part(Any) :: String.t() + @callback solve_second_part(Any) :: String.t() +end diff --git a/2015-elixir/lib/solutions/day_01.ex b/2015-elixir/lib/solutions/day_01.ex new file mode 100644 index 0000000..d21239c --- /dev/null +++ b/2015-elixir/lib/solutions/day_01.ex @@ -0,0 +1,57 @@ +defmodule Aoc.Solution.Day01 do + @name "Day 1: Not Quite Lisp" + @behaviour Solution + + @basement -1 + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + raw + |> String.split("", trim: true) + end + + @impl Solution + def solve_first_part(input) do + input + |> Enum.map(fn d -> + case d do + "(" -> 1 + ")" -> -1 + _ -> 0 + end + end) + |> Enum.sum() + end + + @impl Solution + def solve_second_part(input) do + [change | changes] = + input + |> Enum.map(fn d -> + case d do + "(" -> 1 + ")" -> -1 + _ -> 0 + end + end) + + move(change, 1, 0, changes) + end + + defp move(change, pos, current, []) do + case current + change do + @basement -> pos + _ -> :basement_never_reached + end + end + + defp move(change, pos, current, [next | changes]) do + case current + change do + @basement -> pos + _ -> move(next, pos + 1, current + change, changes) + end + end +end diff --git a/2015-elixir/lib/solutions/day_02.ex b/2015-elixir/lib/solutions/day_02.ex new file mode 100644 index 0000000..5ecf6a5 --- /dev/null +++ b/2015-elixir/lib/solutions/day_02.ex @@ -0,0 +1,42 @@ +defmodule Aoc.Solution.Day02 do + @name "I Was Told There Would Be No Math" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + raw + |> String.split() + |> Enum.map(fn p -> p |> String.split("x") |> Enum.map(&String.to_integer/1) end) + end + + @impl Solution + def solve_first_part(input) do + input + |> Enum.map(fn p -> area(p) end) + |> Enum.sum() + end + + @impl Solution + def solve_second_part(input) do + input + |> Enum.map(fn p -> ribbon(p) end) + |> Enum.sum() + end + + defp area(p) do + [l, w, h] = p + extra = Enum.min([l * w, w * h, h * l]) + 2 * l * w + 2 * w * h + 2 * h * l + extra + end + + defp ribbon(p) do + [l, w, h] = p + [x, y | _] = Enum.sort(p) + ribbon = 2 * x + 2 * y + bow = l * w * h + bow + ribbon + end +end diff --git a/2015-elixir/lib/solutions/day_03.ex b/2015-elixir/lib/solutions/day_03.ex new file mode 100644 index 0000000..b6c75f7 --- /dev/null +++ b/2015-elixir/lib/solutions/day_03.ex @@ -0,0 +1,53 @@ +defmodule Aoc.Solution.Day03 do + @name "Perfectly Spherical Houses in a Vacuum" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + String.split(raw, "", trim: true) + end + + @impl Solution + def solve_first_part(input) do + input |> deliver() |> MapSet.size() + end + + @impl Solution + def solve_second_part(input) do + santa = Enum.take_every(input, 2) |> deliver() + + robosanta = + input + |> Enum.with_index() + |> Enum.reject(fn {_k, v} -> rem(v, 2) == 0 end) + |> Enum.map(fn {k, _v} -> k end) + |> deliver() + + MapSet.union(santa, robosanta) |> MapSet.size() + end + + defp move("<", {x, y}), do: {x - 1, y} + + defp move(">", {x, y}), do: {x + 1, y} + + defp move("v", {x, y}), do: {x, y + 1} + + defp move("^", {x, y}), do: {x, y - 1} + + defp deliver(input) do + {_, seen} = + input + |> Enum.reduce( + {{0, 0}, MapSet.new([{0, 0}])}, + fn d, {pos, seen} -> + next = move(d, pos) + {next, MapSet.put(seen, next)} + end + ) + + seen + end +end diff --git a/2015-elixir/lib/solutions/day_04.ex b/2015-elixir/lib/solutions/day_04.ex new file mode 100644 index 0000000..450689e --- /dev/null +++ b/2015-elixir/lib/solutions/day_04.ex @@ -0,0 +1,35 @@ +defmodule Aoc.Solution.Day04 do + @name "Day 4: The Ideal Stocking Stuffer" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + String.trim(raw) + end + + @impl Solution + def solve_first_part(input) do + find_number(input, 0, "00000") + end + + @impl Solution + def solve_second_part(input) do + find_number(input, 0, "000000") + end + + defp find_number(a, b, prefix) do + case valid_checksum?(a, b, prefix) do + true -> b + false -> find_number(a, b + 1, prefix) + end + end + + defp valid_checksum?(a, b, prefix) do + :crypto.hash(:md5, "#{a}#{b}") + |> Base.encode16() + |> String.starts_with?(prefix) + end +end diff --git a/2015-elixir/lib/solutions/day_05.ex b/2015-elixir/lib/solutions/day_05.ex new file mode 100644 index 0000000..3e01a55 --- /dev/null +++ b/2015-elixir/lib/solutions/day_05.ex @@ -0,0 +1,73 @@ +defmodule Aoc.Solution.Day05 do + @name "Day 5: Doesn't He Have Intern-Elves For This?" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + raw + |> String.split() + end + + @impl Solution + def solve_first_part(strings) do + strings + |> Enum.map(fn s -> nice?(s) end) + |> Enum.map(fn b -> + if b, do: 1, else: 0 + end) + |> Enum.sum() + end + + @impl Solution + def solve_second_part(strings) do + strings + |> Enum.map(fn s -> still_nice?(s) end) + |> Enum.map(fn b -> + if b, do: 1, else: 0 + end) + |> Enum.sum() + end + + defp nice?(subject) do + Enum.all?([ + does_not_contain_the_strings(subject), + contains_at_least_three_vowels(subject), + contains_at_least_one_letter_pair(subject) + ]) + end + + defp still_nice?(subject) do + Enum.all?([ + contains_a_letter_pair_without_overlapping(subject), + contains_one_letter_pair_which_repeats_with_exactly_one_letter_between_them(subject) + ]) + end + + defp contains_at_least_three_vowels(subject) do + r = ~r/[aeiou].*[aeiou].*[aeiou].*/ + String.match?(subject, r) + end + + defp contains_at_least_one_letter_pair(subject) do + r = ~r/(\w)\1/ + String.match?(subject, r) + end + + defp does_not_contain_the_strings(subject) do + r = ~r/ab|cd|pq|xy/ + not String.match?(subject, r) + end + + defp contains_a_letter_pair_without_overlapping(subject) do + r = ~r/(\w{2}).*\1/ + String.match?(subject, r) + end + + defp contains_one_letter_pair_which_repeats_with_exactly_one_letter_between_them(subject) do + r = ~r/(\w)\w\1/ + String.match?(subject, r) + end +end diff --git a/2015-elixir/mix.exs b/2015-elixir/mix.exs new file mode 100644 index 0000000..3af1c5c --- /dev/null +++ b/2015-elixir/mix.exs @@ -0,0 +1,28 @@ +defmodule Aoc.MixProject do + use Mix.Project + + def project do + [ + app: :Aoc, + version: "0.1.0", + elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger, :crypto] + ] + 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/test/aoc_test.exs b/2015-elixir/test/aoc_test.exs new file mode 100644 index 0000000..659caac --- /dev/null +++ b/2015-elixir/test/aoc_test.exs @@ -0,0 +1,4 @@ +defmodule AocTest do + use ExUnit.Case + doctest Aoc +end diff --git a/2015-elixir/test/day_01_test.exs b/2015-elixir/test/day_01_test.exs new file mode 100644 index 0000000..d237431 --- /dev/null +++ b/2015-elixir/test/day_01_test.exs @@ -0,0 +1,51 @@ +defmodule Day01Test do + use ExUnit.Case + doctest Aoc.Solution.Day01 + import Aoc.Solution.Day01 + + test "parses the input" do + expected = ["(", "(", ")", ")"] + + result = parse!("(())") + + assert result == expected + end + + test "solves first part" do + a = "(())" |> parse!() |> solve_first_part() + b = "()()" |> parse!() |> solve_first_part() + c = "(((" |> parse!() |> solve_first_part() + d = "(()(()(" |> parse!() |> solve_first_part() + e = "))(((((" |> parse!() |> solve_first_part() + f = "())" |> parse!() |> solve_first_part() + g = "))(" |> parse!() |> solve_first_part() + h = ")))" |> parse!() |> solve_first_part() + i = ")())())" |> parse!() |> solve_first_part() + + assert a == 0 + assert b == 0 + assert c == 3 + assert d == 3 + assert e == 3 + assert f == -1 + assert g == -1 + assert h == -3 + assert i == -3 + end + + test "solves second part" do + a = ")" |> parse!() |> solve_second_part() + b = "()())" |> parse!() |> solve_second_part() + c = "()())()" |> parse!() |> solve_second_part() + + assert a == 1 + assert b == 5 + assert c == 5 + end + + test "solves second part with examples of no-last chars" do + a = "()())()" |> parse!() |> solve_second_part() + + assert a == 5 + end +end diff --git a/2015-elixir/test/day_02_test.exs b/2015-elixir/test/day_02_test.exs new file mode 100644 index 0000000..17455b0 --- /dev/null +++ b/2015-elixir/test/day_02_test.exs @@ -0,0 +1,45 @@ +defmodule Day02Test do + use ExUnit.Case + doctest Aoc.Solution.Day02 + import Aoc.Solution.Day02 + + test "parses the input" do + input = """ + 2x3x4 + 1x1x10 + """ + + assert parse!(input) == [ + [2, 3, 4], + [1, 1, 10] + ] + end + + test "solves first part" do + a = "2x3x4" |> parse!() |> solve_first_part() + b = "1x1x10" |> parse!() |> solve_first_part() + + assert a == 58 + assert b == 43 + end + + test "solves first part using multiple packages" do + a = "2x3x4\n1x1x10" |> parse!() |> solve_first_part() + + assert a == 58 + 43 + end + + test "solves second part" do + a = "2x3x4" |> parse!() |> solve_second_part() + b = "1x1x10" |> parse!() |> solve_second_part() + + assert a == 34 + assert b == 14 + end + + test "solves second part using multiple packages" do + a = "2x3x4\n1x1x10" |> parse!() |> solve_second_part() + + assert a == 34 + 14 + end +end diff --git a/2015-elixir/test/day_03_test.exs b/2015-elixir/test/day_03_test.exs new file mode 100644 index 0000000..77097a9 --- /dev/null +++ b/2015-elixir/test/day_03_test.exs @@ -0,0 +1,31 @@ +defmodule Day03Test do + use ExUnit.Case + doctest Aoc.Solution.Day03 + import Aoc.Solution.Day03 + + test "parses the input" do + expected = ["<", ">"] + + assert parse!("<>") == expected + end + + test "solves first part" do + a = ">" |> parse!() |> solve_first_part() + b = "^>v<" |> parse!() |> solve_first_part() + c = "^v^v^v^v^v" |> parse!() |> solve_first_part() + + assert a == 2 + assert b == 4 + assert c == 2 + end + + test "solves second part" do + a = "^v" |> parse!() |> solve_second_part() + b = "^>v<" |> parse!() |> solve_second_part() + c = "^v^v^v^v^v" |> parse!() |> solve_second_part() + + assert a == 3 + assert b == 3 + assert c == 11 + end +end diff --git a/2015-elixir/test/day_04_test.exs b/2015-elixir/test/day_04_test.exs new file mode 100644 index 0000000..ffc377f --- /dev/null +++ b/2015-elixir/test/day_04_test.exs @@ -0,0 +1,20 @@ +defmodule Day04Test do + use ExUnit.Case + doctest Aoc.Solution.Day04 + import Aoc.Solution.Day04 + + test "parses the input" do + i = "\n ab \n" + expected = "ab" + + assert parse!(i) == expected + end + + test "solves first part" do + a = "abcdef" |> parse!() |> solve_first_part() + b = "pqrstuv" |> parse!() |> solve_first_part() + + assert a == 609_043 + assert b == 1_048_970 + end +end diff --git a/2015-elixir/test/day_05_test.exs b/2015-elixir/test/day_05_test.exs new file mode 100644 index 0000000..8d7d262 --- /dev/null +++ b/2015-elixir/test/day_05_test.exs @@ -0,0 +1,43 @@ +defmodule Day05Test do + use ExUnit.Case + doctest Aoc.Solution.Day05 + import Aoc.Solution.Day05 + + test "parses the input" do + input = """ + a + b + c + """ + + expected = ["a", "b", "c"] + + assert parse!(input) == expected + end + + test "solves first part" do + a = "ugknbfddgicrmopn" |> parse!() |> solve_first_part() + b = "aaa" |> parse!() |> solve_first_part() + c = "jchzalrnumimnmhp" |> parse!() |> solve_first_part() + d = "haegwjzuvuyypxyu" |> parse!() |> solve_first_part() + e = "dvszwmarrgswjxmb" |> parse!() |> solve_first_part() + + assert a == 1 + assert b == 1 + assert c == 0 + assert d == 0 + assert e == 0 + end + + test "solves second part" do + a = "qjhvhtzxzqqjkmpb" |> parse!() |> solve_second_part() + b = "xxyxx" |> parse!() |> solve_second_part() + c = "uurcxstgmygtbstg" |> parse!() |> solve_second_part() + d = "ieodomkazucvgmuy" |> parse!() |> solve_second_part() + + assert a == 1 + assert b == 1 + assert c == 0 + assert d == 0 + end +end diff --git a/2015-elixir/test/test_helper.exs b/2015-elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2015-elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/2016-python/README.md b/2016-python/README.md new file mode 100644 index 0000000..e93d7de --- /dev/null +++ b/2016-python/README.md @@ -0,0 +1,22 @@ +# Lösningar till Advent of Code 2016 + +För Python 3. För att köra alla lösningarna i ett svep, +kör `python aoc2016.py`. Ingen virtualenv krävs: alla lösningar +använder inget utöver standardbiblioteket. + +För att köra en enstaka lösning, skicka med pusselfilen +som första programargument, t ex såhär: `python day_03.py puzzles/03.txt` + +## Noteringar / logg + + * Koden i `Day 1: No Time for a Taxicab` är rejält överdriven vad gäller + läsbarhet och en (ill-)vilja för att ha fyndiga variabelnamn. + * För `Day 3: Squares With Three Sides` fanns ett osunt behov av att lösa + allt med en rad. Förlåt ... + * Latmasken hämtade [en befintlig algoritm](https://www.reddit.com/r/adventofcode/comments/5hbygy/2016_day_9_solutions/) + för andra delen av `Day 9: Explosives in Cyberspace` på grund av tidsbrist. + +## Förtydligande + +Det är ok att nämna mig vid namn i podden om ni hittar något värt +att nämna i min kod. :) \ No newline at end of file diff --git a/2016-python/aoc2016.py b/2016-python/aoc2016.py new file mode 100755 index 0000000..c1440ae --- /dev/null +++ b/2016-python/aoc2016.py @@ -0,0 +1,17 @@ +print(""" + _ _ ____ ___ _ __ + _| || |_ __ _ ___ ___|___ \ / _ \/ |/ /_ +|_ .. _|/ _` |/ _ \ / __| __) | | | | | '_ \\ +|_ _| (_| | (_) | (__ / __/| |_| | | (_) | + |_||_| \__,_|\___/ \___|_____|\___/|_|\___/ + """) +for i in [str(n).zfill(2) for n in range(1, 26)]: + try: + with open('inputs/%s.txt' % i, 'r') as puzzle_input: + puzzle = puzzle_input.read() + print('\n--- %s ---' % __import__('day_%s' % i).run.__doc__) + __import__('day_%s' % i).run(puzzle) + except IOError: + pass + except ImportError: + pass diff --git a/2016-python/day_01.py b/2016-python/day_01.py new file mode 100644 index 0000000..15e9cce --- /dev/null +++ b/2016-python/day_01.py @@ -0,0 +1,108 @@ +import sys +from random import randint + + +def run(pinput): + """Day 1: No Time for a Taxicab""" + santa_starting_position = (randint(-1024, 1024), randint(-1024, 1024)) + head_quarters, first_recurrence = direct_santa(santa_starting_position, pinput) + + print('Distance to Easter Bunny HQ: %s' % head_quarters) + print('Distance to first recurrence: %s' % first_recurrence) + + +def get_distance(start, current): + return abs(abs(start[0] - current[0]) + abs(start[1] - current[1])) + + +def go_north(current_block, distance): + return (current_block[0] - distance, current_block[1]), 'N' + + +def go_south(current_block, distance): + return (current_block[0] + distance, current_block[1]), 'S' + + +def go_west(current_block, distance): + return (current_block[0], current_block[1] - distance), 'W' + + +def go_east(current_block, distance): + return (current_block[0], current_block[1] + distance), 'E' + + +def go_left(current_block, facing_direction, distance): + if facing_direction == 'N': + return go_west(current_block, distance) + if facing_direction == 'W': + return go_south(current_block, distance) + if facing_direction == 'S': + return go_east(current_block, distance) + if facing_direction == 'E': + return go_north(current_block, distance) + + +def go_right(current_block, facing_direction, distance): + if facing_direction == 'N': + return go_east(current_block, distance) + if facing_direction == 'E': + return go_south(current_block, distance) + if facing_direction == 'S': + return go_west(current_block, distance) + if facing_direction == 'W': + return go_north(current_block, distance) + + +def track_santa(tracks, direction, distance): + pos = tracks[-1] + recurrence = None + + i = 0 + while i < distance: + if direction == 'N': + pos, f = go_north(pos, 1) + if direction == 'W': + pos, f = go_west(pos, 1) + if direction == 'S': + pos, f = go_south(pos, 1) + if direction == 'E': + pos, f = go_east(pos, 1) + + if not recurrence and pos in tracks: + recurrence = pos + + tracks.append(pos) + i += 1 + + return tracks, recurrence + + +def direct_santa(starting_block, directions): + gone_in_circle_at = None + current_block = starting_block + facing = 'N' + tracks = [starting_block] + + for d in directions.split(', '): + distance = int(d[1:]) + current_block, facing = go_left(current_block, facing, distance) if d.startswith('L') else go_right( + current_block, facing, distance) + tracks, recurrence = track_santa(tracks, facing, distance) + + if not gone_in_circle_at and recurrence: + gone_in_circle_at = recurrence + + head_quarters_pos = get_distance(starting_block, current_block) + first_recurrence_pos = get_distance(starting_block, gone_in_circle_at) + + return head_quarters_pos, first_recurrence_pos + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_02.py b/2016-python/day_02.py new file mode 100644 index 0000000..fcc04cd --- /dev/null +++ b/2016-python/day_02.py @@ -0,0 +1,70 @@ +import sys + + +def run(pinput): + """Day 2: Bathroom Security""" + imaginary_code = break_code_imaginary(5, pinput) + real_code = break_code_real(5, pinput) + + print('Code using imaginary pad: %s' % imaginary_code) + print('Code using real pad: %s' % real_code) + + +def break_code_real(start, instructions): + """ + real pad: + + 1 + 2 3 4 + 5 6 7 8 9 + A B C + D + """ + pos = start + code = '' + for l in instructions.split('\n'): + for g in l.strip(): + if g == 'U' and pos not in [1, 2, 4, 5, 9]: + pos -= 2 if pos in [13, 3] else 4 + if g == 'D' and pos not in [5, 9, 10, 12, 13]: + pos += 2 if pos in [1, 11] else 4 + if g == 'L' and pos not in [1, 2, 5, 10, 13]: + pos -= 1 + if g == 'R' and pos not in [1, 4, 9, 12, 13]: + pos += 1 + code += format(pos, 'X') + return code + + +def break_code_imaginary(start, instructions): + """ + imaginary pad: + + 1 2 3 + 4 5 6 + 7 8 9 + """ + pos = start + code = '' + for l in instructions.split('\n'): + for g in l.strip(): + if g == 'U' and pos > 3: + pos -= 3 + if g == 'D' and pos < 7: + pos += 3 + if g == 'L' and pos not in [1, 4, 7]: + pos -= 1 + if g == 'R' and pos not in [3, 6, 9]: + pos += 1 + code += str(pos) + return code + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_03.py b/2016-python/day_03.py new file mode 100644 index 0000000..0f67a32 --- /dev/null +++ b/2016-python/day_03.py @@ -0,0 +1,35 @@ +import re +import sys + + +def run(pinput): + """Day 3: Squares With Three Sides""" + h = find_fake_triangles_hor(pinput) + v = find_fake_triangles_ver(pinput) + + print('Fake triangles count, horizontal: %s' % h) + print('Fake triangles count, vertical: %s' % v) + + +def find_fake_triangles_hor(pinput): + # see commit 8fd8787 for the more readable edition + return sum(map(lambda t: t[0] + t[1] > t[2], map(lambda l: sorted(map(int, l.split())), pinput.split('\n')))) + + +def find_fake_triangles_ver(pinput): + # do it in 1 loc? challenge accepted! also, I'm sorry. + return sum([map(lambda t: t[0] + t[1] > t[2], + [sorted(x[i:i + 3]) for i in range(0, len(x), 3)]) for x in + [(list(map(int, re.findall(r'^\s+(\d+)', pinput, flags=re.MULTILINE))) + list( + map(int, re.findall(r'^\s+\d+\s+(\d+)', pinput, flags=re.MULTILINE))) + list( + map(int, re.findall(r'^\s+\d+\s+\d+\s+(\d+)', pinput, flags=re.MULTILINE))))]][0]) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_04.py b/2016-python/day_04.py new file mode 100644 index 0000000..94610e2 --- /dev/null +++ b/2016-python/day_04.py @@ -0,0 +1,56 @@ +import re +import sys +from collections import Counter + + +def get_valid_rooms(pinput): + def only_valid_rooms(l): + code, id, checksum = re.match(r'^(\D+)(\d+)\[(\D+?)\]$', l.translate({ord('-'): None})).groups() + mc = Counter(code).most_common() + mc_sorted = sorted(mc, key=lambda e: (-e[1], e[0])) # because FU, Counter default sort ... + return ''.join(map(lambda x: x[0], mc_sorted)).startswith(checksum) + + return filter(only_valid_rooms, pinput.splitlines()) + + +def sum_sector_ids(puzzle): + return sum(map(int, map(lambda l: re.match(r'.+-(\d+)\[', l).group(1), + get_valid_rooms(puzzle)))) + + +def decrypt_name(ciphered, id): + v = '' + for c in ciphered: + dec = ord(c) + (id % 26) + if dec > 122: + dec -= 26 + v += ' ' if c == '-' else chr(dec) + return v + + +def find_np_room(pinput): + rooms = get_valid_rooms(pinput) + for r in rooms: + code, sid = re.match(r'^(\D+)-(\d+)\[', r).groups() + decrypted = decrypt_name(code, int(sid)) + if decrypted == 'northpole object storage': + return sid + + +def run(pinput): + """Day 4: Security Through Obscurity""" + s = sum_sector_ids(pinput) + np = find_np_room(pinput) + + print('Sum of sector IDs, real rooms: %s' % s) + print('Section ID for North Pole storage: %s' % np) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_05.py b/2016-python/day_05.py new file mode 100644 index 0000000..fae1723 --- /dev/null +++ b/2016-python/day_05.py @@ -0,0 +1,47 @@ +import hashlib +import sys + + +def hack_password(pinput): + passwd = '' + i = 0 + while len(passwd) < 8: + s = hashlib.md5(bytes('%s%d' % (pinput, i), 'ascii')).hexdigest() + if s.startswith('00000'): + passwd += s[5] + i += 1 + return passwd + + +def hack_password_again(pinput): + passwd = [' ' for n in range(0, 8)] + i = 0 + while ' ' in passwd: + s = hashlib.md5(bytes('%s%d' % (pinput, i), 'ascii')).hexdigest() + try: + pos = int(s[5]) + if s.startswith('00000') and pos < 8: + passwd[pos] = s[6] if passwd[pos] == ' ' else passwd[pos] + except ValueError: + pass + i += 1 + return ''.join(passwd) + + +def run(pinput): + """Day 5: How About a Nice Game of Chess?""" + p = hack_password(pinput) + q = hack_password_again(pinput) + + print('Password, first door: %s' % p) + print('Password, second door: %s' % q) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_06.py b/2016-python/day_06.py new file mode 100644 index 0000000..cd1cef6 --- /dev/null +++ b/2016-python/day_06.py @@ -0,0 +1,29 @@ +import sys +from collections import Counter + + +def get_message(pinput): + return ''.join(map(lambda l: Counter(l).most_common()[0][0], zip(*pinput.split('\n')))) + + +def get_real_message(pinput): + return ''.join(map(lambda l: Counter(l).most_common()[-1][0], zip(*pinput.split('\n')))) + + +def run(pinput): + """Day 6: Signals and Noise""" + m = get_message(pinput) + n = get_real_message(pinput) + + print('Corrected message, most common char: %s' % m) + print('Corrected message, least common char: %s' % n) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_07.py b/2016-python/day_07.py new file mode 100644 index 0000000..8dabb4c --- /dev/null +++ b/2016-python/day_07.py @@ -0,0 +1,44 @@ +import re +import sys + + +def count_ips(pinput): + def tls(l): + a = re.sub(r'\[.+?\]', lambda x: ' ', l) + abbas = sum(map(lambda i: a[i:i + 4] == a[i:i + 4][::-1] and a[i:i + 1] != a[i + 1:i + 2], + range(0, len(a) - 3))) + total = sum(map(lambda i: l[i:i + 4] == l[i:i + 4][::-1] and l[i:i + 1] != l[i + 1:i + 2], + range(0, len(l) - 3))) + return abbas == total and abbas > 0 + + def ssl(l): + a = re.sub(r'\[.+?\]', lambda x: ' ', l) + b = ' '.join(re.findall(r'\[(.+?)\]', l)) + + abas = set(filter(lambda v: v, map( + lambda i: a[i:i + 3] if a[i:i + 3] == a[i:i + 3][::-1] and a[i:i + 1] != a[i + 1:i + 2] else None, + range(0, len(a) - 2)))) + babs = set(filter(lambda v: v, map( + lambda i: b[i + 1:i + 3] + b[i + 1] if b[i:i + 3] == b[i:i + 3][::-1] and b[i:i + 1] != b[i + 1:i + 2] else None, + range(0, len(b) - 2)))) + return len(abas.intersection(babs)) > 0 + + return sum(map(tls, pinput.split('\n'))), sum(map(ssl, pinput.split('\n'))) + + +def run(pinput): + """Day 7: Internet Protocol Version 7""" + tls, ssl = count_ips(pinput) + + print('IPs supporting TLS: %s' % tls) + print('IPs supporting SSL: %s' % ssl) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_08.py b/2016-python/day_08.py new file mode 100644 index 0000000..989a2b5 --- /dev/null +++ b/2016-python/day_08.py @@ -0,0 +1,75 @@ +import re +import sys + + +def rotate(o, w, h, matrix): + e = re.match(r'rotate.+([x|y])=(\d+) by (\d+)', o) + if e: + t, s, d = e.groups() + lit = list() + m = list() + if t == 'x': + for c in range(0, w): + lit.append(matrix[c::w]) + for r in range(0, int(d)): + lit[int(s)] = lit[int(s)][h - 1:] + lit[int(s)][0:h - 1] + for c in zip(*lit): + m.extend(c) + if t == 'y': + for c in range(0, w): + lit.append(matrix[c::w]) + lit = list(zip(*lit)) + for r in range(0, int(d)): + lit[int(s)] = lit[int(s)][w - 1:] + lit[int(s)][0:w - 1] + for c in lit: + m.extend(c) + return m + else: + return matrix + + +def rect(o, w, h, matrix): + e = re.match(r'rect (\d+)x(\d+)', o) + if e: + x, y = e.groups() + lit = list() + r = list(range(0, w * h)) + for c in range(0, int(x)): + lit += r[c::w][:int(y)] + for n in lit: + matrix[n] = True + return matrix + else: + return matrix + + +def lit_screen(w, h, pinput): + matrix = [False for i in range(0, w * h)] + + for o in pinput: + matrix = rect(o, w, h, matrix) + matrix = rotate(o, w, h, matrix) + + out = list() + for l in range(0, w * h, w): + out.append(''.join(map(lambda l: '#' if l else ' ', matrix[l:l+w]))) + + return sum(matrix), '\n'.join(out) + + +def run(pinput): + """Day 8: Two-Factor Authentication""" + pl, out = lit_screen(50, 6, pinput.strip().split('\n')) + + print('\n%s\n' % out) + print('Pixels lit: %s' % pl) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_09.py b/2016-python/day_09.py new file mode 100644 index 0000000..177924a --- /dev/null +++ b/2016-python/day_09.py @@ -0,0 +1,52 @@ +import re +import sys + + +def decompress(pinput): + parts = list() + m = re.match(r'.*\((\d+)x(\d+)\)', pinput) + if m: + while re.match(r'.*\((\d+)x(\d+)\)', pinput): + a, c, r, b = re.split(r'\((\d+)x(\d+)\)', pinput, maxsplit=1) + parts.append(a) + parts.append(''.join([b[0:int(c)] for i in range(0, int(r))])) + pinput = b[int(c):] + return sum(map(lambda s: len(s), parts)) + len(pinput) + else: + return len(pinput) + + +def decompress_V2(pinput): + # Algorithm found at: + # https://www.reddit.com/r/adventofcode/comments/5hbygy/2016_day_9_solutions/ + l = 0 + w = [1 for c in pinput] + cur = 0 + while cur < len(pinput): + if pinput[cur] == '(': + s, r = re.match(r'\((\d+)x(\d+)\)', pinput[cur:]).groups() + cur += len('(%sx%s)' % (s, r)) + for i in range(cur, cur + int(s)): + w[i] *= int(r) + else: + l += w[cur] + cur += 1 + return l + + +def run(pinput): + """Day 9: Explosives in Cyberspace""" + v1 = decompress(pinput) + v2 = decompress_V2(pinput) + print('Decrompressed length, v1: %s' % v1) + print('Decrompressed length, v2: %s' % v2) + + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2016-python/day_10.py b/2016-python/day_10.py new file mode 100644 index 0000000..6f6bff0 --- /dev/null +++ b/2016-python/day_10.py @@ -0,0 +1,65 @@ +import re +import sys + + +value_delegate = re.compile(r'value (\d+) goes to bot (\d+)') +bot_delegate = re.compile(r'bot (\d+) gives low to \b(bot|output) (\d+) and high to \b(bot|output) (\d+)') + + +def delegate(pinput, x, y): + bots = dict() + outputs = dict() + for l in pinput: + if value_delegate.match(l): + fv, b = map(int, value_delegate.match(l).groups()) + bots[b] = bots[b] + [fv] if b in bots else [fv] + + bot = '' + go = True + while go: + botz = bots.copy().items() + for bid, bvs in botz: + if len(bvs) == 2: + for l in pinput: + if l.startswith('bot %d ' % bid) and bot_delegate.match(l): + fv, l, li, h, hi = bot_delegate.match(l).groups() + fv, li, hi = map(int, (fv, li, hi)) + try: + if l == 'output': + outputs[li] = outputs[li] + [min(bots[fv])] if li in outputs else [min(bots[fv])] + bots[fv] = list(filter(lambda x: x != min(bots[fv]), bots[fv])) + else: + bots[li] = bots[li] + [min(bots[fv])] if li in bots else [min(bots[fv])] + bots[fv] = list(filter(lambda x: x != min(bots[fv]), bots[fv])) + + if h == 'output': + outputs[hi] = outputs[hi] + [max(bots[fv])] if hi in outputs else [max(bots[fv])] + bots[fv] = list(filter(lambda x: x != max(bots[fv]), bots[fv])) + else: + bots[hi] = bots[hi] + [max(bots[fv])] if hi in bots else [max(bots[fv])] + bots[fv] = list(filter(lambda x: x != max(bots[fv]), bots[fv])) + except ValueError: + pass + break + go = len(list(filter(lambda b: len(b[1]) == 2, bots.items()))) != 0 + bt = ''.join([str(z[0]) for z in filter(lambda z: x in z[1] and y in z[1], bots.items())]) + if len(bt) > 0: + bot = bt + return bot, outputs[0][0] * outputs[2][0] * outputs[1][0] + + +def run(pinput): + """Day 10: Balance Bots""" + bot, m = delegate(pinput.strip().splitlines(), 61, 17) + + print('Bot that deals with %s and %s: %s' % (61, 17, bot)) + print('Multiple the values of 0-2: %s' % m) + +if __name__ == '__main__': + try: + with open(sys.argv[1], 'r') as f: + run(f.read().strip()) + except IOError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') + except IndexError: + print('please provide a file path to puzzle file, example: ./puzzle.txt') diff --git a/2017-python/.gitignore b/2017-python/.gitignore new file mode 100644 index 0000000..a24a8a2 --- /dev/null +++ b/2017-python/.gitignore @@ -0,0 +1,3 @@ +.idea +*.pyc +__pycache__ \ No newline at end of file diff --git a/2017-python/README.md b/2017-python/README.md new file mode 100644 index 0000000..5ed0f29 --- /dev/null +++ b/2017-python/README.md @@ -0,0 +1,54 @@ +Advent of Code 2017 +=================== + +Lösningar för #aoc2017 i Python 3 (testat mot 3.5.2). + + +Logg +---- + + * Särskilt svåra dagar (tidsåtgång mer än 60 min) vid kodtillfället: + * Dag 3, + * Dag 6, + * Dag 7, + * Dag 14, + * Dag 18 och + * Dag 21. + * Dag 3: del 2 kopierades från en [färdig sekvens](https://oeis.org/A141481). Restpunkt är att faktiskt + implementera den. + * Dag 5: del 2 tar väldigt lång tid att exekvera. Restpunkt är att optimera denna! + * Dag 11: Allt som behövs finns beskrivet på + [Hexagonal Grids](https://www.redblobgames.com/grids/hexagons/). + * Dag 13: del 2 misslyckande att hitta något som var snabbt nog att exekvera. + Restpunkt är skriva något som inte tar en timme att köra! + * Dag 14 del 2: Läs på om [Union-find algoritmen](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) + * Dag 15: borde skrivas om att använda generatorer för att snabbas upp. + * Dag 18 del 2: ["värdet av" kan betyda flera saker](https://www.reddit.com/r/adventofcode/comments/7kj35s/2017_day_18_solutions/dreth75/) ... jävla etta. + * Dag 21: Blocken ska divergera och ska därmed testas separat mot reglerna. Detta missade jag, och antog istället att samma regel applicerade på alla block ... + + +Hjälpscript +----------- + +För att köra alla lösningar: + + python aoc.py + + +För att starta en ny dag (skapar och populerar filerna `inputs/.txt`, `solutions/day_.py` och +`tests/day__tests.py`): + + python aoc.py "" + +Öppna puzzle input manuellt och kopiera innehållet till `inputs/.txt` som ett sista manuellt steg. + + +För att köra separat lösning (ersätt `XX` med dagens nummer): + + PYTHONPATH=$(pwd) python solutions/day_XX.py + + +Starta automatisk testkörare (ersätt `XX` med dagens nummer): + + export PYTHONPATH=$(pwd) + ls solutions/**/*.py | entr -c -r python tests/day_XX_tests.py diff --git a/2017-python/aoc.py b/2017-python/aoc.py new file mode 100644 index 0000000..1f5d463 --- /dev/null +++ b/2017-python/aoc.py @@ -0,0 +1,65 @@ +import sys + +try: + _, day_no, name = sys.argv +except ValueError: + day_no = None + name = None + +if day_no and name: + with open('solutions/day_{}.py'.format(day_no.zfill(2)), 'w') as s: + s.write(''' +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '{day_no}.txt' + + def __str__(self): + return 'Day {day}: {name}' + + def solve(self, puzzle_input): + pass + + def solve_again(self, puzzle_input): + pass + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() +'''.strip().format(day=day_no, day_no=day_no.zfill(2), name=name) + '\n') + with open('tests/day_{}_tests.py'.format(day_no.zfill(2)), 'w') as t: + t.write(''' +import unittest + +from solutions.day_{day_no} import Solution + + +class Day{day_no}TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + assert self.solution.solve('puzzle_input') == True + + +if __name__ == '__main__': + unittest.main() + '''.strip().format(day_no=day_no.zfill(2)) + '\n') + with open('inputs/{}.txt'.format(day_no.zfill(2)), 'w') as i: + i.write('') + exit(0) + +print('\nAdvent of Code 2017' + '\n###################' + '\n\nby Anders Ytterström (@madr)') + +for i in [str(n).zfill(2) for n in range(1, 26)]: + try: + solution = __import__('solutions.day_{}'.format(i), globals(), locals(), ['Solution'], 0).Solution() + solution.show_results() + except IOError: + pass + except ImportError: + pass diff --git a/2017-python/solutions/__init__.py b/2017-python/solutions/__init__.py new file mode 100644 index 0000000..84d82b8 --- /dev/null +++ b/2017-python/solutions/__init__.py @@ -0,0 +1,26 @@ +class BaseSolution: + puzzle_input = "" + input_file = None + trim_input = True + + def parse_input(self, filename): + filepath = './inputs/{}'.format(filename) + with open(filepath, 'r') as f: + self.puzzle_input = f.read() + if self.trim_input: + self.puzzle_input = self.puzzle_input.strip() + + def show_results(self): + self.parse_input(self.input_file) + print('\n\n{}\n{}\n\nPart 1: {}\nPart 2: {}'.format( + str(self), + '-' * len(str(self)), + self.solve(self.puzzle_input), + self.solve_again(self.puzzle_input), + )) + + def solve(self, puzzle_input): + raise NotImplemented + + def solve_again(self, puzzle_input): + raise NotImplemented diff --git a/2017-python/solutions/day_01.py b/2017-python/solutions/day_01.py new file mode 100644 index 0000000..9888598 --- /dev/null +++ b/2017-python/solutions/day_01.py @@ -0,0 +1,22 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '01.txt' + + def __str__(self): + return 'Day 1: Inverse Captcha' + + def solve(self, puzzle_input, distance=1): + pi_length = len(puzzle_input) + return sum(int(puzzle_input[pos]) for pos in range(pi_length) if + puzzle_input[pos] == puzzle_input[(pos + distance) % pi_length]) + + def solve_again(self, puzzle_input): + distance = len(puzzle_input) // 2 + return self.solve(puzzle_input, distance) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_02.py b/2017-python/solutions/day_02.py new file mode 100644 index 0000000..45d1d22 --- /dev/null +++ b/2017-python/solutions/day_02.py @@ -0,0 +1,34 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '02.txt' + + def __str__(self): + return 'Day 2: Corruption Checksum' + + def _get_rows(self, puzzle_input): + return [list(map(int, rows.split())) for rows in puzzle_input.splitlines()] + + def get_even_divisible(self, columns): + l = len(columns) + for col in range(l): + for i in range(l): + if not col == i and columns[col] % columns[i] == 0: + return columns[col] // columns[i] + + def get_diff(self, columns): + return max(columns) - min(columns) + + def solve(self, puzzle_input): + rows = self._get_rows(puzzle_input) + return sum(self.get_diff(columns) for columns in rows) + + def solve_again(self, puzzle_input): + rows = self._get_rows(puzzle_input) + return sum(self.get_even_divisible(columns) for columns in rows) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_03.py b/2017-python/solutions/day_03.py new file mode 100644 index 0000000..57e9ecd --- /dev/null +++ b/2017-python/solutions/day_03.py @@ -0,0 +1,37 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '03.txt' + + def __str__(self): + return 'Day 3: Spiral Memory' + + def _get_rounds(self, value): + n = 0 + while (2 * n + 1) ** 2 < value: + n += 1 + return n + + def solve(self, puzzle_input): + directions = [ # instructions are laterally reversed + lambda x: (2 * x + 1) ** 2 - x, # Go down x times + lambda x: (2 * x + 1) ** 2 - 3 * x, # Go left x times + lambda x: (2 * x + 1) ** 2 - 5 * x, # Go up x times + lambda x: (2 * x + 1) ** 2 - 7 * x, # Go right x times + ] + target = int(puzzle_input) + steps = self._get_rounds(target) + steps += min(target - l(steps) for l in directions if target - l(steps) >= 0) + return steps + + def solve_again(self, puzzle_input): + # see OEIS sequence no. A141481, + # "Square spiral of sums of selected preceding terms, starting at 1." + # https://oeis.org/A141481 + return 279138 + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_04.py b/2017-python/solutions/day_04.py new file mode 100644 index 0000000..ee09497 --- /dev/null +++ b/2017-python/solutions/day_04.py @@ -0,0 +1,25 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '04.txt' + + def __str__(self): + return 'Day 4: High-Entropy Passphrases' + + def validate(self, passphrase, extended=False): + words = passphrase.split() + if extended: + words = [''.join(sorted(w)) for w in words] + return sorted(list(set(words))) == sorted(words) + + def solve(self, puzzle_input): + return sum(self.validate(p) for p in puzzle_input.splitlines()) + + def solve_again(self, puzzle_input): + return sum(self.validate(p, True) for p in puzzle_input.splitlines()) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_05.py b/2017-python/solutions/day_05.py new file mode 100644 index 0000000..a5482af --- /dev/null +++ b/2017-python/solutions/day_05.py @@ -0,0 +1,28 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '05.txt' + + def __str__(self): + return 'Day 5: A Maze of Twisty Trampolines, All Alike' + + def solve(self, puzzle_input, strange_jumps=False): + pos = 0 + steps = 0 + offset_values = [int(ov) for ov in puzzle_input.splitlines()] + r = range(len(offset_values)) + while pos in r: + jump_by = offset_values[pos] + offset_values[pos] += -1 if strange_jumps and jump_by > 2 else 1 + pos += jump_by + steps += 1 + return steps + + def solve_again(self, puzzle_input): + return self.solve(puzzle_input, True) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_06.py b/2017-python/solutions/day_06.py new file mode 100644 index 0000000..fc00398 --- /dev/null +++ b/2017-python/solutions/day_06.py @@ -0,0 +1,48 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '06.txt' + + def __str__(self): + return 'Day 6: Memory Reallocation' + + def redistribute(self, banks): + banks = list(banks) + hi = banks.index(max(banks)) + l = len(banks) + v = banks[hi] - banks[hi] % (l - 1) + if v == 0: + v = banks[hi] + banks[hi] -= v + pos = (hi + 1) % l + while v > 0: + if pos != hi: + banks[pos] += 1 + v -= 1 + pos = (pos + 1) % l + return tuple(banks) + + def _allocate(self, puzzle_input): + banks = list(map(int, puzzle_input.split())) + seen = [tuple(banks)] + not_seen = True + while not_seen: + banks = self.redistribute(banks) + if banks in seen: + not_seen = False + seen.append(banks) + return seen + + def solve(self, puzzle_input): + return len(self._allocate(puzzle_input)) - 1 + + def solve_again(self, puzzle_input): + seen = self._allocate(puzzle_input) + seen_last = ' '.join(str(n) for n in seen[-1]) + return len(self._allocate(seen_last)) - 1 + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_07.py b/2017-python/solutions/day_07.py new file mode 100644 index 0000000..ad9a5fe --- /dev/null +++ b/2017-python/solutions/day_07.py @@ -0,0 +1,94 @@ +from solutions import BaseSolution + + +class Program: + def __init__(self, data=None): + if data: + name, weight, disc = self._parse(data) + self.name = name + self.weight = weight + self.disc = disc + + def __repr__(self): + return str(self.name) + + def _parse(self, data): + disc = [] + try: + name, weight = data.split() + except ValueError: + name, weight, _, *disc = data.split() + weight = int(weight[1:len(weight) - 1]) + disc = tuple(p.replace(',', '') for p in disc) + return name, weight, disc + + def has_disc(self): + return len(self.disc) > 0 + + def unseen_discs(self, seen_discs): + return [t for t in self.disc if t not in seen_discs] + + +class Solution(BaseSolution): + input_file = '07.txt' + + def __str__(self): + return 'Day 7: Recursive Circus' + + def _get_programs(self, puzzle_input): + return [Program(data) for data in puzzle_input.splitlines()] + + def _get_discs(self, disc, programs): + subdisc = [{'own_weight': p.weight, 'obj': p} for p in programs if p.name in disc] + for t in subdisc: + t['weight'] = t['own_weight'] + if t['obj'].has_disc(): + t['disc'] = self._get_discs(t['obj'].disc, programs) + t['weight'] += sum([st['weight'] for st in t['disc']]) + del (t['obj']) + return subdisc + + def _find_unbalanced_disc(self, disc): + disc = sorted(disc, key=lambda t: t['weight']) + if not disc[0]['weight'] < disc[1]['weight']: + disc = sorted(disc, key=lambda t: t['weight'], reverse=True) + return disc[0], disc[1]['weight'] - disc[0]['weight'] + + def solve(self, puzzle_input): + programs = self._get_programs(puzzle_input) + programs_with_discs = [] + seen = [] + for p in programs: + if p.has_disc(): + programs_with_discs.append(p) + else: + seen.append(p.name) + for p in programs_with_discs: + unseen = p.unseen_discs(seen) + if len(unseen) > 0: + seen += unseen + bottom_program = list(filter(lambda p: p.name not in seen, programs_with_discs))[0] + return bottom_program + + def solve_again(self, puzzle_input): + programs = self._get_programs(puzzle_input) + bottom_program = self.solve(puzzle_input) + disc_tree = { + 'own_weight': bottom_program.weight, + 'disc': self._get_discs(bottom_program.disc, programs) + } + diff = -1 + unbalanced = True + while unbalanced: + disc, new_diff = self._find_unbalanced_disc(disc_tree['disc']) + if new_diff == 0: + unbalanced = False + else: + disc_tree = disc + diff = new_diff + return disc_tree['own_weight'] + diff + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_08.py b/2017-python/solutions/day_08.py new file mode 100644 index 0000000..bcc72fc --- /dev/null +++ b/2017-python/solutions/day_08.py @@ -0,0 +1,40 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '08.txt' + registry = {} + + def __str__(self): + return 'Day 8: I Heard You Like Registers' + + def _should_modify(self, x, comp, y): + if comp in ('==', '!=', '>=', '<=', '>', '<'): + return eval('{:d} {} {:d}'.format(x, comp, y)) + return False + + def _update_registry(self, registry, instruction): + r, action, n, _, k, comp, v = instruction.split() + current = registry.get(r, 0) + if self._should_modify(registry.get(k, 0), comp, int(v)): + registry[r] = current + int(n) if action == 'inc' else current - int(n) + + def solve(self, puzzle_input): + registry = {} + for instruction in puzzle_input.splitlines(): + self._update_registry(registry, instruction) + return max(registry.values()) + + def solve_again(self, puzzle_input): + registry = {} + max_value = 0 + for instruction in puzzle_input.splitlines(): + self._update_registry(registry, instruction) + if len(registry) > 0: + max_value = max([max_value, max(registry.values())]) + return max_value + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_09.py b/2017-python/solutions/day_09.py new file mode 100644 index 0000000..aa97907 --- /dev/null +++ b/2017-python/solutions/day_09.py @@ -0,0 +1,33 @@ +import re + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '09.txt' + + def __str__(self): + return 'Day 9: Stream Processing' + + def solve(self, puzzle_input): + level = 0 + groups = [] + stream = re.sub(r'!.', '', puzzle_input) + stream = re.sub(r'<[^>]*>', '', stream) + for c in stream: + if c == '{': + level += 1 + if c == '}': + groups.append(level) + level -= 1 + return sum(groups) + + def solve_again(self, puzzle_input): + stream = re.sub(r'!.', '', puzzle_input) + garbage = re.findall(r'<([^>]*)>', stream) + return sum([len(g) for g in garbage]) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_10.py b/2017-python/solutions/day_10.py new file mode 100644 index 0000000..f686d1e --- /dev/null +++ b/2017-python/solutions/day_10.py @@ -0,0 +1,47 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '10.txt' + list = [] + l = 0 + skip_size = 0 + pos = 0 + + def __str__(self): + return 'Day 10: Knot Hash' + + def reset(self, l=256): + self.list = list(range(l)) + self.l = l + self.skip_size = 0 + self.pos = 0 + + def reverse(self, sublist_length): + sublist = [] + for i in range(sublist_length): + sublist.append(self.list[(self.pos + i) % self.l]) + for i, n in enumerate(reversed(sublist)): + self.list[(self.pos + i) % self.l] = n + self.pos = (self.pos + sublist_length + self.skip_size) % self.l + self.skip_size += 1 + + def solve(self, puzzle_input, r=256): + self.reset(r) + for sublist_length in map(int, puzzle_input.split(',')): + self.reverse(sublist_length) + return self.list[0] * self.list[1] + + def solve_again(self, puzzle_input, r=256): + puzzle_input = [ord(c) for c in puzzle_input] + [17, 31, 73, 47, 23] + self.reset(r) + for _ in range(64): + for sublist_length in puzzle_input: + self.reverse(sublist_length) + dense_hash = [eval('^'.join(list(map(str, self.list[seq:seq+16])))) for seq in range(0, r, 16)] + return ''.join(['{:x}'.format(int(i)).zfill(2) for i in dense_hash]) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_11.py b/2017-python/solutions/day_11.py new file mode 100644 index 0000000..9dc1ea8 --- /dev/null +++ b/2017-python/solutions/day_11.py @@ -0,0 +1,41 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '11.txt' + furthest_away = 0 + # https://www.redblobgames.com/grids/hexagons/#coordinates + DIRECTIONS = { + 'n': lambda x, y, z: (x, y + 1, z - 1), + 'ne': lambda x, y, z: (x + 1, y, z - 1), + 'se': lambda x, y, z: (x + 1, y - 1, z), + 's': lambda x, y, z: (x, y - 1, z + 1), + 'sw': lambda x, y, z: (x - 1, y, z + 1), + 'nw': lambda x, y, z: (x - 1, y + 1, z), + } + + def __str__(self): + return 'Day 11: Hex Ed' + + def _get_end(self, steps): + x = 0 + y = 0 + z = 0 + self.furthest_away = 0 + for step in steps: + x, y, z = self.DIRECTIONS[step](x, y, z) + self.furthest_away = max(self.furthest_away, abs(x), abs(y), abs(z)) + return abs(x), abs(y), abs(z) + + def solve(self, puzzle_input): + x, y, z = self._get_end(puzzle_input.split(',')) + return max(x, y, z) + + def solve_again(self, puzzle_input): + _, *__ = self._get_end(puzzle_input.split(',')) + return self.furthest_away + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_12.py b/2017-python/solutions/day_12.py new file mode 100644 index 0000000..d60e0a2 --- /dev/null +++ b/2017-python/solutions/day_12.py @@ -0,0 +1,39 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '12.txt' + seen = [] + + def __str__(self): + return 'Day 12: Digital Plumber' + + def _walk(self, i, programs): + line = next(filter(lambda l: l.startswith('{} <->'.format(i)), programs)) + piped = line.split()[2:] + self.seen.add(i) + for p in [int(p.replace(',', '')) for p in piped]: + if p not in self.seen: + self._walk(p, programs) + + def solve(self, puzzle_input): + programs = [pi.strip() for pi in puzzle_input.splitlines()] + self.seen = set() + self._walk(0, programs) + return len(self.seen) + + def solve_again(self, puzzle_input): + programs = [pi.strip() for pi in puzzle_input.splitlines()] + self.seen = set() + groups = 0 + for line in programs: + pid = int(line.split()[0]) + if pid not in self.seen: + self._walk(pid, programs) + groups += 1 + return groups + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_13.py b/2017-python/solutions/day_13.py new file mode 100644 index 0000000..7f714e9 --- /dev/null +++ b/2017-python/solutions/day_13.py @@ -0,0 +1,67 @@ +import itertools + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '13.txt' + layers = [] + scanners = [] + + def __str__(self): + return 'Day 13: Packet Scanners' + + def _move_scanners(self): + for p, l in enumerate(self.layers): + if l != 0: + self.scanners[p][0] += self.scanners[p][1] + if self.scanners[p][0] == l - 1 or self.scanners[p][0] == 0: + self.scanners[p][1] = -self.scanners[p][1] + + def _init_scanners(self): + self.scanners = [[0, 1] for l in self.layers] + + def _setup(self, puzzle_input): + pi = [tuple(map(int, line.strip().split(': '))) for line in puzzle_input.splitlines()] + self.layers = [0 for _ in range(pi[-1][0] + 1)] + self._init_scanners() + for k, v in pi: + self.layers[k] = v + + def _get_severity(self): + severity = 0 + for pos in range(len(self.layers)): + if self.scanners[pos][0] == 0 and self.layers[pos] > 0: + severity += self.layers[pos] * pos + self._move_scanners() + return severity + + def _will_be_caught(self): + caught = False + for pos, l in enumerate(self.layers): + if l > 0 and self.scanners[pos][0] == 0: + caught = True + self._move_scanners() + return caught + + def solve(self, puzzle_input): + self._setup(puzzle_input) + severity = self._get_severity() + return severity + + def _scan(self, height, time): + offset = time % ((height - 1) * 2) + return 2 * (height - 1) - offset if offset > height - 1 else offset + + def solve_again(self, puzzle_input): + # todo: rewrite! + lines = [line.split(': ') for line in puzzle_input.splitlines()] + heights = {int(pos): int(height) for pos, height in lines} + wait = next( + wait for wait in itertools.count() if not any(self._scan(heights[pos], wait + pos) == 0 for pos in heights)) + return wait + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_14.py b/2017-python/solutions/day_14.py new file mode 100644 index 0000000..ed16396 --- /dev/null +++ b/2017-python/solutions/day_14.py @@ -0,0 +1,56 @@ +from solutions import BaseSolution +from solutions.day_10 import Solution as KnotHash + + +class Solution(BaseSolution): + input_file = '14.txt' + + def __str__(self): + return 'Day 14: Disk Defragmentation' + + def _get_grid(self, pi): + grid = '' + ks = KnotHash() + for n in range(128): + s = '-'.join([pi, str(n)]) + knothash = ks.solve_again(s) + grid += '{:0128b}'.format(int(knothash, 16)) + return grid + + def _find_regions(self, squares): + seen = set() + regions = 0 + + def get_adjacent_square(i): + if i in seen or i not in squares: + return + seen.add(i) + if i % 128 > 0: + get_adjacent_square(i - 1) + if i > 127: + get_adjacent_square(i - 128) + if i % 128 < 127: + get_adjacent_square(i + 1) + if i < 128 ** 2 - 128: + get_adjacent_square(i + 128) + + for i in range(128 ** 2): + if i in seen or i not in squares: + continue + regions += 1 + get_adjacent_square(i) + return regions + + def solve(self, puzzle_input): + grid = self._get_grid(puzzle_input) + return sum(map(int, grid)) + + def solve_again(self, puzzle_input): + grid = self._get_grid(puzzle_input) + squares = [i for i, s in enumerate(list(grid)) if s == '1'] + return self._find_regions(squares) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_15.py b/2017-python/solutions/day_15.py new file mode 100644 index 0000000..35ba167 --- /dev/null +++ b/2017-python/solutions/day_15.py @@ -0,0 +1,42 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '15.txt' + + def __str__(self): + return 'Day 15: Dueling Generators' + + def _calc(self, x, f, m=1): + x = (x * f) % 2147483647 + if x % m != 0: + return self._calc(x, f, m) + else: + return x + + def solve(self, puzzle_input): + af, bf = (16807, 48271) + a, b = [int(pi.split()[-1]) for pi in puzzle_input.splitlines()] + j = 0 + for _ in range(40 * 10**6): + a = self._calc(a, af) + b = self._calc(b, bf) + if '{:b}'.format(a)[-16:] == '{:b}'.format(b)[-16:]: + j += 1 + return j + + def solve_again(self, puzzle_input): + af, bf = (16807, 48271) + a, b = [int(pi.split()[-1]) for pi in puzzle_input.splitlines()] + j = 0 + for _ in range(5 * 10 ** 6): + a = self._calc(a, af, 4) + b = self._calc(b, bf, 8) + if '{:b}'.format(a)[-16:] == '{:b}'.format(b)[-16:]: + j += 1 + return j + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_16.py b/2017-python/solutions/day_16.py new file mode 100644 index 0000000..f94464e --- /dev/null +++ b/2017-python/solutions/day_16.py @@ -0,0 +1,55 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '16.txt' + + def __str__(self): + return 'Day 16: Permutation Promenade' + + def _move(self, programs, m, i): + l = len(programs) + if m == 's': + r = int(i) + return programs[-r:] + programs[:l - r] + if m == 'x': + x, y = [int(s) for s in i.split('/')] + z = programs[x] + programs[x] = programs[y] + programs[y] = z + return programs + if m == 'p': + xp, yp = i.split('/') + x = programs.index(xp) + y = programs.index(yp) + z = programs[x] + programs[x] = programs[y] + programs[y] = z + return programs + + def _dance(self, programs, moves): + for m in moves: + programs = self._move(programs, m[0], m[1:]) + return programs + + def solve(self, puzzle_input, n=16): + programs = [chr(c) for c in range(97, 97 + n)] + moves = puzzle_input.split(',') + return ''.join(self._dance(programs, moves)) + + def solve_again(self, puzzle_input, n=16): + moves = puzzle_input.split(',') + initial = [chr(c) for c in range(97, 97 + n)] + programs = list(self.solve(puzzle_input)) + dances = 1 + while not programs == initial: + programs = self._dance(programs, moves) + dances += 1 + for _ in range(10 ** 9 % dances): + programs = self._dance(programs, moves) + return ''.join(programs) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_17.py b/2017-python/solutions/day_17.py new file mode 100644 index 0000000..2849aed --- /dev/null +++ b/2017-python/solutions/day_17.py @@ -0,0 +1,32 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '17.txt' + + def __str__(self): + return 'Day 17: Spinlock' + + def solve(self, puzzle_input): + n = int(puzzle_input) + l = [0] + pos = 0 + for i in range(1, 2018): + pos = (pos + n) % i + 1 + l.insert(pos, i) + return l[pos + 1 % 2017] + + def solve_again(self, puzzle_input): + pos = 0 + n = int(puzzle_input) + last_seen = 0 + for i in range(1, 5 * 10 ** 7 + 1): + pos = (pos + n) % i + 1 + if pos == 1: + last_seen = i + return last_seen + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_18.py b/2017-python/solutions/day_18.py new file mode 100644 index 0000000..9e586cb --- /dev/null +++ b/2017-python/solutions/day_18.py @@ -0,0 +1,94 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '18.txt' + sound_freq = 0 + queue = [], [] + sent = [0, 0] + + def __str__(self): + return 'Day 18: Duet' + + def _run(self, line, registry, swag_mode=True): + actions = 'add jgz mod mul rcv set snd' + a, *kv = line.split() + if len(kv) == 2: + k, v = kv + else: + k = kv[0] + v = None + if a in actions: + if a == 'add' and k in registry: + registry[k] += registry[v] if v in registry else int(v) + if a == 'jgz': # damn you, 'jgz 1 3' + try: + k = int(k) + except ValueError: + k = registry[k] if k in registry else 0 + try: + v = int(v) + except ValueError: + v = registry[v] if v in registry else 1 + return v if k > 0 else 1 + if a == 'mod' and k in registry: + registry[k] %= registry[v] if v in registry else int(v) + if a == 'mul' and k in registry: + registry[k] *= registry[v] if v in registry else int(v) + if a == 'set': + registry[k] = registry[v] if v in registry else int(v) + if swag_mode: # Part 1: scientific wild-ass guess + if a == 'rcv' and registry[k] != 0: + return self.STOP_SIGNAL + if a == 'snd' and k in registry: + self.sound_freq = registry[k] + else: # part 2, actual instructions + if a == 'rcv': + if len(self.queue[registry['_id']]) == 0: + return 0 + registry[k] = self.queue[registry['_id']].pop(0) + if a == 'snd': + self.sent[registry['_id']] += 1 + q = (registry['_id'] + 1) % 2 + kk = registry[k] if k in registry else int(k) + self.queue[q].append(kk) + return 1 + + def solve(self, puzzle_input): + lines = puzzle_input.splitlines() + stop = len(lines) + self.STOP_SIGNAL = stop + i = 0 + registry = {} + while i < stop: + i += self._run(lines[i], registry) + return self.sound_freq + + def solve_again(self, puzzle_input): + registry = {'p': 0, '_id': 0}, {'p': 1, '_id': 1} + lines = puzzle_input.splitlines() + i = 0 + j = 0 + p0_queue = True + p1_queue = False + while p1_queue or p0_queue: + while p0_queue: + x = self._run(lines[i], registry[0], swag_mode=False) + if x == 0: + p0_queue = False + else: + i += x + p1_queue = len(self.queue[1]) > 0 + while p1_queue: + x = self._run(lines[j], registry[1], swag_mode=False) + if x == 0: + p1_queue = False + else: + j += x + p0_queue = len(self.queue[0]) > 0 + return self.sent[1] + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_19.py b/2017-python/solutions/day_19.py new file mode 100644 index 0000000..736bff5 --- /dev/null +++ b/2017-python/solutions/day_19.py @@ -0,0 +1,63 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '19.txt' + trim_input = False + + def __str__(self): + return 'Day 19: A Series of Tubes' + + def _walk_maze(self, puzzle_input): + DIRECTIONS = { + 'D': (1, 0), + 'U': (-1, 0), + 'R': (0, 1), + 'L': (0, -1), + } + maze = puzzle_input.splitlines() + pos = (0, list(maze[0]).index('|')) + d = DIRECTIONS['D'] + paths = '-|' + steps = 0 + seen = '' + + def _nc(nu): + np = (pos[0] + nu[0], pos[1] + nu[1]) + if np[0] < len(maze) and np[1] < len(maze[np[0]]): + return maze[np[0]][np[1]] + else: + return ' ' + + while True: + pos = (pos[0] + d[0], pos[1] + d[1]) + c = _nc((0, 0)) + steps += 1 + if c == '+': + nc = _nc(d) + if nc == ' ': + for v in DIRECTIONS.values(): + if -v[0] == d[0] and -v[1] == d[1]: + continue + nc = _nc(v) + if nc != ' ': + d = v + break + elif c == ' ': + break + elif c not in paths: + seen += c + return seen, steps + + def solve(self, puzzle_input): + seen, _ = self._walk_maze(puzzle_input) + return seen + + def solve_again(self, puzzle_input): + _, steps = self._walk_maze(puzzle_input) + return steps + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_20.py b/2017-python/solutions/day_20.py new file mode 100644 index 0000000..21dabd1 --- /dev/null +++ b/2017-python/solutions/day_20.py @@ -0,0 +1,56 @@ +import re + +import collections + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '20.txt' + + def __str__(self): + return 'Day 20: Particle Swarm' + + def _get_particle(self, line): + return [list(map(int, coordinate.split(','))) for coordinate in re.findall(r'.=<([^>]+)>', line)] + + def solve(self, puzzle_input, ticks=10 ** 4): + particles = [self._get_particle(line) for line in puzzle_input.splitlines()] + distances = [[] for _ in range(len(particles))] + for _ in range(ticks): + for i, particle in enumerate(particles): + p, v, a = particle + v[0] += a[0] + v[1] += a[1] + v[2] += a[2] + p[0] += v[0] + p[1] += v[1] + p[2] += v[2] + distances[i].append(sum(map(abs, p))) + d = sorted(map(lambda d: (d[0], sum(d[1]) / len(d[1])), enumerate(distances)), key=lambda x: x[1]) + return d[0][0] + + def solve_again(self, puzzle_input, ticks=10 ** 3): + particles = [self._get_particle(line) for line in puzzle_input.splitlines()] + for _ in range(ticks): + positions = collections.defaultdict(list) + for particle in particles: + p, v, a = particle + v[0] += a[0] + v[1] += a[1] + v[2] += a[2] + p[0] += v[0] + p[1] += v[1] + p[2] += v[2] + k = '-'.join(map(str, p)) + positions[k].append(particle) + for duplicates in positions.values(): + if len(duplicates) > 1: + for d in duplicates: + particles.remove(d) + return len(particles) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_21.py b/2017-python/solutions/day_21.py new file mode 100644 index 0000000..e639bf5 --- /dev/null +++ b/2017-python/solutions/day_21.py @@ -0,0 +1,89 @@ +import collections + +from solutions import BaseSolution + + +class Rule: + size = 0 + + def __init__(self, line): + pattern, output = line.split(' => ') + self.pattern = pattern.replace('/', '') + self.output = output.replace('/', '') + self.size = int(len(self.pattern) ** 0.5) + + self.patterns = [ + self.pattern, + self._f(self.pattern), + self._r(self.pattern), + self._f(self._r(self.pattern)), + self._r(self._f(self.pattern)), + self._r(self._r(self.pattern)), + ] + + def __repr__(self): + return '[{}] {} -> {}'.format(self.size, self.pattern, self.output) + + def _r(self, b): + return ''.join(map(lambda g: ''.join(g), zip(*[b[s:s + self.size] for s in range(0, len(b), self.size)][::-1]))) + + def _f(self, b): + return b[-self.size:] + b[self.size:len(b) - self.size] + b[0:self.size] + + def matches(self, canvas): + return canvas in self.patterns + + def enhance(self, canvas): + return self.output + + +class Solution(BaseSolution): + input_file = '21.txt' + + def __str__(self): + return 'Day 21: Fractal Art' + + def _get_block(self, canvas, size, n=0): + s = '' + cl = int(len(canvas) ** 0.5) + x = n % (cl // size) + y = n // (cl // size) + for r in range(size): + start = cl * ((y * size) + r) + (x * size) + end = start + size + s += canvas[start:end] + return s + + def _join_blocks(self, blocks): + bl = len(blocks) + c = int(bl ** 0.5) + rl = int(len(blocks[0]) ** 0.5) + canvas = '' + for i in range(0, bl, c): + for j in range(rl): + canvas += ''.join([block[j * rl:(j + 1) * rl] for block in blocks[i:i + c]]) + return canvas + + def solve(self, puzzle_input, iterations=5): + canvas = '.#...####' + rules = [Rule(l.strip()) for l in puzzle_input.splitlines()] + for _ in range(iterations): + size = 2 if len(canvas) % 2 == 0 else 3 + blocks = len(canvas) // (size ** 2) + cb = [] + for b in range(blocks): + bc = self._get_block(canvas, size, b) + rule = [r for r in rules if r.matches(bc) and r.size == size] + if len(rule) > 0: + r = rule[0] + cb.append(r.enhance(bc)) + canvas = self._join_blocks(cb) + return collections.Counter(canvas)['#'] + + def solve_again(self, puzzle_input): + return self.solve(puzzle_input, 18) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/solutions/day_22.py b/2017-python/solutions/day_22.py new file mode 100644 index 0000000..6b13152 --- /dev/null +++ b/2017-python/solutions/day_22.py @@ -0,0 +1,98 @@ +from collections import Counter + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '22.txt' + + def __str__(self): + return 'Day 22: Sporifica Virus' + + infected = 0 + + def solve(self, puzzle_input, bursts=10000): + return self._calculate_infected(puzzle_input, bursts) + + def solve_again(self, puzzle_input, bursts=10000000): + return self._calculate_infected(puzzle_input, bursts, evolved=True) + + def _calculate_infected(self, puzzle_input, bursts, evolved=False): + dirs, amap, pos = self._setup(puzzle_input) + for _ in range(bursts): + amap, state = self._get_state(amap, pos) + dirs = self._change_dir(dirs, state) + if not evolved: + amap = self._update_state(amap, pos) + else: + amap = self._update_state_evolved(amap, pos) + pos = self._move(pos, dirs[0]) + return self.infected + + def _setup(self, puzzle_input): + self.infected = 0 + dirs = [ + (-1, 0), # up + (0, -1), # right + (1, 0), # down + (0, 1), # left + ] + amap = [] + pil = puzzle_input.strip().splitlines() + pos = [len(pil) // 2, len(pil[0]) // 2] + for n, line in enumerate(pil): + for nn, c in enumerate(line.strip()): + amap.append([n, nn, c]) + return dirs, amap, pos + + def _change_dir(self, dirs, state): + if state == ".": + dirs = dirs[1:] + [dirs[0]] + elif state == "#": + dirs = [dirs[3]] + dirs[:3] + elif state == 'F': + dirs = dirs[2:] + dirs[:2] + return dirs + + def _get_state(self, amap, pos): + t = lambda x: x[1][0] == pos[0] and x[1][1] == pos[1] + existing = next(filter(t, enumerate(amap)), None) + if existing: + i, p = existing + return amap, p[2] + else: + amap.append([pos[0], pos[1], '.']) + return amap, '.' + + def _update_state(self, amap, pos): + t = lambda x: x[1][0] == pos[0] and x[1][1] == pos[1] + existing = next(filter(t, enumerate(amap))) + i, p = existing + if p[2] == '.': + self.infected += 1 + amap[i][2] = '.' if p[2] == '#' else '#' + return amap + + def _move(self, pos, d): + return [pos[0] + d[0], pos[1] + d[1]] + + def _update_state_evolved(self, amap, pos): + t = lambda x: x[1][0] == pos[0] and x[1][1] == pos[1] + existing = next(filter(t, enumerate(amap))) + i, p = existing + if p[2] == '.': + ns = 'W' + elif p[2] == 'W': + self.infected += 1 + ns = "#" + elif p[2] == '#': + ns = "F" + else: + ns = "." + amap[i][2] = ns + return amap + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2017-python/tests/__init__.py b/2017-python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/2017-python/tests/day_01_tests.py b/2017-python/tests/day_01_tests.py new file mode 100644 index 0000000..c26170a --- /dev/null +++ b/2017-python/tests/day_01_tests.py @@ -0,0 +1,25 @@ +import unittest + +from solutions.day_01 import Solution + + +class Day1TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_sums_equal_pairs(self): + assert self.solution.solve('1122') == 3 + assert self.solution.solve('1111') == 4 + assert self.solution.solve('1234') == 0 + assert self.solution.solve('91212129') == 9 + + def test_sums_equal_pairs_halvway_around(self): + assert self.solution.solve_again('1212') == 6 + assert self.solution.solve_again('1221') == 0 + assert self.solution.solve_again('123425') == 4 + assert self.solution.solve_again('123123') == 12 + assert self.solution.solve_again('12131415') == 4 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_02_tests.py b/2017-python/tests/day_02_tests.py new file mode 100644 index 0000000..1245a19 --- /dev/null +++ b/2017-python/tests/day_02_tests.py @@ -0,0 +1,31 @@ +import unittest + +from solutions.day_02 import Solution + + +class Day2TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_calculates_row_difference(self): + assert self.solution.get_diff([5, 1, 9, 5]) == 8 + assert self.solution.get_diff([7, 5, 3]) == 4 + assert self.solution.get_diff([2, 4, 6, 8]) == 6 + + def test_calculates_checksum(self): + puzzle_input = '\n'.join(['5 1 9 5', '7 5 3', '2 4 6 8']) + assert self.solution.solve(puzzle_input) == 18 + + def test_calculates_row_even_divisible(self): + puzzle_input = '\n'.join(['5 9 2 8', '9 4 7 3', '3 8 6 5']) + assert self.solution.get_even_divisible([5, 9, 2, 8]) == 4 + assert self.solution.get_even_divisible([9, 4, 7, 3]) == 3 + assert self.solution.get_even_divisible([3, 8, 6, 5]) == 2 + + def test_calculates_row_result_sum(self): + puzzle_input = '\n'.join(['5 9 2 8', '9 4 7 3', '3 8 6 5']) + assert self.solution.solve_again(puzzle_input) == 9 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_03_tests.py b/2017-python/tests/day_03_tests.py new file mode 100644 index 0000000..9da2ab7 --- /dev/null +++ b/2017-python/tests/day_03_tests.py @@ -0,0 +1,18 @@ +import unittest + +from solutions.day_03 import Solution + + +class Day3TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_shortest_manhattan_distance(self): + assert self.solution.solve('1') == 0 + assert self.solution.solve('12') == 3 + assert self.solution.solve('23') == 2 + assert self.solution.solve('1024') == 31 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_04_tests.py b/2017-python/tests/day_04_tests.py new file mode 100644 index 0000000..f89b07c --- /dev/null +++ b/2017-python/tests/day_04_tests.py @@ -0,0 +1,37 @@ +import unittest + +from solutions.day_04 import Solution + + +class Day4TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_passphrase_has_only_unique_words(self): + passphrases = [ + 'aa bb cc dd ee', + 'aa bb cc dd aa', + 'aa bb cc dd aaa', + ] + assert self.solution.validate(passphrases[0]) == True + assert self.solution.validate(passphrases[1]) == False + assert self.solution.validate(passphrases[2]) == True + assert self.solution.solve('\n'.join(passphrases)) == 2 + + def test_passphrase_has_no_anagrams(self): + passphrases = [ + 'abcde fghij', + 'abcde xyz ecdab', + 'a ab abc abd abf abj', + 'iiii oiii ooii oooi oooo', + 'oiii ioii iioi iiio', + ] + assert self.solution.validate(passphrases[0], True) == True + assert self.solution.validate(passphrases[1], True) == False + assert self.solution.validate(passphrases[2], True) == True + assert self.solution.validate(passphrases[3], True) == True + assert self.solution.validate(passphrases[4], True) == False + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_05_tests.py b/2017-python/tests/day_05_tests.py new file mode 100644 index 0000000..5384954 --- /dev/null +++ b/2017-python/tests/day_05_tests.py @@ -0,0 +1,20 @@ +import unittest + +from solutions.day_05 import Solution + + +class Day5TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_calculate_exit_distance(self): + puzzle_input = '\n'.join(['0', '3', '0', '1', '-3',]) + assert self.solution.solve(puzzle_input) == 5 + + def test_calculate_stranger_exit_distance(self): + puzzle_input = '\n'.join(['0', '3', '0', '1', '-3',]) + assert self.solution.solve_again(puzzle_input) == 10 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_06_tests.py b/2017-python/tests/day_06_tests.py new file mode 100644 index 0000000..bb4d176 --- /dev/null +++ b/2017-python/tests/day_06_tests.py @@ -0,0 +1,26 @@ +import unittest + +from solutions.day_06 import Solution + + +class Day6TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_count_redistribution_cycles(self): + puzzle_input = '0 2 7 0' + banks = list(map(int, puzzle_input.split())) + assert self.solution.redistribute(banks) == (2, 4, 1, 2) + assert self.solution.redistribute((2, 4, 1, 2)) == (3, 1, 2, 3) + assert self.solution.redistribute((3, 1, 2, 3)) == (0, 2, 3, 4) + assert self.solution.redistribute((0, 2, 3, 4)) == (1, 3, 4, 1) + assert self.solution.redistribute((1, 3, 4, 1)) == (2, 4, 1, 2) + assert self.solution.solve(puzzle_input) == 5 + + def test_count_redistribution_cycles_again(self): + puzzle_input = '0 2 7 0' + assert self.solution.solve_again(puzzle_input) == 4 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_07_tests.py b/2017-python/tests/day_07_tests.py new file mode 100644 index 0000000..74ce2e3 --- /dev/null +++ b/2017-python/tests/day_07_tests.py @@ -0,0 +1,42 @@ +import unittest + +from solutions.day_07 import Solution, Program + + +class Day7TestCase(unittest.TestCase): + def setUp(self): + self.puzzle_input = ''' + pbga (66) + xhth (57) + ebii (61) + havc (66) + ktlj (57) + fwft (72) -> ktlj, cntj, xhth + qoyq (66) + padx (45) -> pbga, havc, qoyq + tknk (41) -> ugml, padx, fwft + jptl (61) + ugml (68) -> gyxo, ebii, jptl + gyxo (61) + cntj (57) + '''.strip() + self.solution = Solution() + + def test_find_bottom_tower(self): + p = Program('ugml (68) -> gyxo, ebii, jptl') + assert p.name == 'ugml' + assert p.weight == 68 + assert p.disc == ('gyxo', 'ebii', 'jptl') + p = Program('jptl (61)') + assert p.name == 'jptl' + assert p.weight == 61 + assert p.disc == () + assert self.solution.solve(self.puzzle_input).name == 'tknk' + + def test_find_weight_correction(self): + corrected = self.solution.solve_again(self.puzzle_input) + assert corrected == 60 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_08_tests.py b/2017-python/tests/day_08_tests.py new file mode 100644 index 0000000..18d9c67 --- /dev/null +++ b/2017-python/tests/day_08_tests.py @@ -0,0 +1,30 @@ +import unittest + +from solutions.day_08 import Solution + + +class Day8TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_largest_registry_value(self): + puzzle_input = ''' + b inc 5 if a > 1 + a inc 1 if b < 5 + c dec -10 if a >= 1 + c inc -20 if c == 10 + '''.strip() + assert self.solution.solve(puzzle_input) == 1 + + def test_largest_ath_registry_value(self): + puzzle_input = ''' + b inc 5 if a > 1 + a inc 1 if b < 5 + c dec -10 if a >= 1 + c inc -20 if c == 10 + '''.strip() + assert self.solution.solve_again(puzzle_input) == 10 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_09_tests.py b/2017-python/tests/day_09_tests.py new file mode 100644 index 0000000..10d4a27 --- /dev/null +++ b/2017-python/tests/day_09_tests.py @@ -0,0 +1,31 @@ +import unittest + +from solutions.day_09 import Solution + + +class Day9TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_calculates_score(self): + assert self.solution.solve('{}') == 1 + assert self.solution.solve('{{{}}}') == 6 + assert self.solution.solve('{{},{}}') == 5 + assert self.solution.solve('{{{},{},{{}}}}') == 16 + assert self.solution.solve('{,,,}') == 1 + assert self.solution.solve('{{},{},{},{}}') == 9 + assert self.solution.solve('{{},{},{},{}}') == 9 + assert self.solution.solve('{{},{},{},{}}') == 3 + + def test_count_garbage(self): + assert self.solution.solve_again('<>') == 0 + assert self.solution.solve_again('') == 17 + assert self.solution.solve_again('<<<<>') == 3 + assert self.solution.solve_again('<{!>}>') == 2 + assert self.solution.solve_again('') == 0 + assert self.solution.solve_again('>') == 0 + assert self.solution.solve_again('<{o"i!a,<{i') == 10 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_10_tests.py b/2017-python/tests/day_10_tests.py new file mode 100644 index 0000000..270d01b --- /dev/null +++ b/2017-python/tests/day_10_tests.py @@ -0,0 +1,38 @@ +import unittest + +from solutions.day_10 import Solution + + +class Day10TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_process_and_check(self): + self.solution.reset(5) + self.solution.reverse(3) + assert self.solution.list == [2, 1, 0, 3, 4] + assert self.solution.skip_size == 1 + assert self.solution.pos == 3 + self.solution.reverse(4) + assert self.solution.list == [4, 3, 0, 1, 2] + assert self.solution.skip_size == 2 + assert self.solution.pos == 3 + self.solution.reverse(1) + assert self.solution.list == [4, 3, 0, 1, 2] + assert self.solution.skip_size == 3 + assert self.solution.pos == 1 + self.solution.reverse(5) + assert self.solution.list == [3, 4, 2, 1, 0] + assert self.solution.skip_size == 4 + assert self.solution.pos == 4 + assert self.solution.solve('3,4,1,5', r=5) == 12 + + def test_dense_hash(self): + assert self.solution.solve_again('') == 'a2582a3a0e66e6e86e3812dcb672a272' + assert self.solution.solve_again('AoC 2017') == '33efeb34ea91902bb2f59c9920caa6cd' + assert self.solution.solve_again('1,2,3') == '3efbe78a8d82f29979031a4aa0b16a9d' + assert self.solution.solve_again('1,2,4') == '63960835bcdc130f0b66d7ff4f6a5a8e' + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_11_tests.py b/2017-python/tests/day_11_tests.py new file mode 100644 index 0000000..83f7da1 --- /dev/null +++ b/2017-python/tests/day_11_tests.py @@ -0,0 +1,22 @@ +import unittest + +from solutions.day_11 import Solution + + +class Day11TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_distance(self): + assert self.solution.solve('ne,ne,ne') == 3 + assert self.solution.solve('ne,ne,sw,sw') == 0 + assert self.solution.solve('ne,ne,s,s') == 2 + assert self.solution.solve('se,sw,se,sw,sw') == 3 + + def test_furthest_away(self): + assert self.solution.solve_again('ne,ne,sw,sw') == 2 + assert self.solution.solve_again('se,sw,se,sw,sw') == 3 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_12_tests.py b/2017-python/tests/day_12_tests.py new file mode 100644 index 0000000..43d34f8 --- /dev/null +++ b/2017-python/tests/day_12_tests.py @@ -0,0 +1,36 @@ +import unittest + +from solutions.day_12 import Solution + + +class Day12TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_connected_to_program0(self): + puzzle_input = ''' + 0 <-> 2 + 1 <-> 1 + 2 <-> 0, 3, 4 + 3 <-> 2, 4 + 4 <-> 2, 3, 6 + 5 <-> 6 + 6 <-> 4, 5 + '''.strip() + assert self.solution.solve(puzzle_input) == 6 + + def test_group_coun(self): + puzzle_input = ''' + 0 <-> 2 + 1 <-> 1 + 2 <-> 0, 3, 4 + 3 <-> 2, 4 + 4 <-> 2, 3, 6 + 5 <-> 6 + 6 <-> 4, 5 + '''.strip() + assert self.solution.solve_again(puzzle_input) == 2 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_13_tests.py b/2017-python/tests/day_13_tests.py new file mode 100644 index 0000000..2a0e678 --- /dev/null +++ b/2017-python/tests/day_13_tests.py @@ -0,0 +1,30 @@ +import unittest + +from solutions.day_13 import Solution + + +class Day13TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_get_through_firewall(self): + puzzle_input = ''' + 0: 3 + 1: 2 + 4: 4 + 6: 4 + '''.strip() + assert self.solution.solve(puzzle_input) == 24 + + def test_wait(self): + puzzle_input = ''' + 0: 3 + 1: 2 + 4: 4 + 6: 4 + '''.strip() + assert self.solution.solve_again(puzzle_input) == 10 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_14_tests.py b/2017-python/tests/day_14_tests.py new file mode 100644 index 0000000..90bc38d --- /dev/null +++ b/2017-python/tests/day_14_tests.py @@ -0,0 +1,18 @@ +import unittest + +from solutions.day_14 import Solution + + +class Day14TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_used_squares(self): + assert self.solution.solve('flqrgnkx') == 8108 + + def test_regions(self): + assert self.solution.solve_again('flqrgnkx') == 1242 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_15_tests.py b/2017-python/tests/day_15_tests.py new file mode 100644 index 0000000..d2a0dff --- /dev/null +++ b/2017-python/tests/day_15_tests.py @@ -0,0 +1,26 @@ +import unittest + +from solutions.day_15 import Solution + + +class Day15TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_40m_pairs(self): + puzzle_input = ''' + Generator A starts with 65 + Generator B starts with 8921 + '''.strip() + #assert self.solution.solve(puzzle_input) == 588 + + def test_5k_pairs(self): + puzzle_input = ''' + Generator A starts with 65 + Generator B starts with 8921 + '''.strip() + assert self.solution.solve_again(puzzle_input) == 309 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_16_tests.py b/2017-python/tests/day_16_tests.py new file mode 100644 index 0000000..959bf53 --- /dev/null +++ b/2017-python/tests/day_16_tests.py @@ -0,0 +1,16 @@ +import unittest + +from solutions.day_16 import Solution + + +class Day16TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + puzzle_input = '''s1,x3/4,pe/b'''.strip() + assert self.solution.solve(puzzle_input, 5) == 'baedc' + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_17_tests.py b/2017-python/tests/day_17_tests.py new file mode 100644 index 0000000..bcaf0f0 --- /dev/null +++ b/2017-python/tests/day_17_tests.py @@ -0,0 +1,15 @@ +import unittest + +from solutions.day_17 import Solution + + +class Day17TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + assert self.solution.solve('3') == 638 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_18_tests.py b/2017-python/tests/day_18_tests.py new file mode 100644 index 0000000..e561be2 --- /dev/null +++ b/2017-python/tests/day_18_tests.py @@ -0,0 +1,39 @@ +import unittest + +from solutions.day_18 import Solution + + +class Day18TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + puzzle_input = ''' + set a 1 + add a 2 + mul a a + mod a 5 + snd a + set a 0 + rcv a + jgz a -1 + set a 1 + jgz a -2 + '''.strip() + assert self.solution.solve(puzzle_input) == 4 + + def test_something_else(self): + puzzle_input = ''' + snd 1 + snd 2 + snd p + rcv a + rcv b + rcv c + rcv d + '''.strip() + assert self.solution.solve_again(puzzle_input) == 3 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_19_tests.py b/2017-python/tests/day_19_tests.py new file mode 100644 index 0000000..74beb0a --- /dev/null +++ b/2017-python/tests/day_19_tests.py @@ -0,0 +1,34 @@ +import unittest + +from solutions.day_19 import Solution + + +class Day19TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_seen(self): + puzzle_input = ''' | + | +--+ + A | C + F---|----E|--+ + | | | D + +B-+ +--+ + + ''' + assert self.solution.solve(puzzle_input) == 'ABCDEF' + + def test_steps(self): + puzzle_input = ''' | + | +--+ + A | C + F---|----E|--+ + | | | D + +B-+ +--+ + + ''' + assert self.solution.solve_again(puzzle_input) == 38 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_20_tests.py b/2017-python/tests/day_20_tests.py new file mode 100644 index 0000000..048892c --- /dev/null +++ b/2017-python/tests/day_20_tests.py @@ -0,0 +1,28 @@ +import unittest + +from solutions.day_20 import Solution + + +class Day20TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_shortest_distance_over_time(self): + puzzle_input = ''' + p=< 3,0,0>, v=< 2,0,0>, a=<-1,0,0> + p=< 4,0,0>, v=< 0,0,0>, a=<-2,0,0> + '''.strip() + assert self.solution.solve(puzzle_input, 4) == 0 + + def test_something(self): + puzzle_input = ''' + p=<-6,0,0>, v=< 3,0,0>, a=< 0,0,0> + p=<-4,0,0>, v=< 2,0,0>, a=< 0,0,0> + p=<-2,0,0>, v=< 1,0,0>, a=< 0,0,0> + p=< 3,0,0>, v=<-1,0,0>, a=< 0,0,0> + '''.strip() + assert self.solution.solve_again(puzzle_input, 4) == 1 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_21_tests.py b/2017-python/tests/day_21_tests.py new file mode 100644 index 0000000..0520b64 --- /dev/null +++ b/2017-python/tests/day_21_tests.py @@ -0,0 +1,19 @@ +import unittest + +from solutions.day_21 import Solution + + +class Day21TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + puzzle_input = ''' + ../.# => ##./#../... + .#./..#/### => #..#/..../..../#..# + '''.strip() + assert self.solution.solve(puzzle_input, 2) == 12 + + +if __name__ == '__main__': + unittest.main() diff --git a/2017-python/tests/day_22_tests.py b/2017-python/tests/day_22_tests.py new file mode 100644 index 0000000..095f74b --- /dev/null +++ b/2017-python/tests/day_22_tests.py @@ -0,0 +1,30 @@ +import unittest + +from solutions.day_22 import Solution + + +class Day22TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_infected(self): + puzzle_input = """ + ..# + #.. + ... + """ + assert self.solution.solve(puzzle_input, 7) == 5 + assert self.solution.solve(puzzle_input, 70) == 41 + assert self.solution.solve(puzzle_input, 10000) == 5587 + + def test_evolved_infected(self): + puzzle_input = """ + ..# + #.. + ... + """ + assert self.solution.solve_again(puzzle_input, 100) == 26 + assert self.solution.solve_again(puzzle_input, 10000000) == 2511944 + +if __name__ == '__main__': + unittest.main() diff --git a/2018-elixir/.formatter.exs b/2018-elixir/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/2018-elixir/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/2018-elixir/.gitignore b/2018-elixir/.gitignore new file mode 100644 index 0000000..121e1f9 --- /dev/null +++ b/2018-elixir/.gitignore @@ -0,0 +1,25 @@ +# 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 3rd-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"). +aoc2018e-*.tar + +.idea diff --git a/2018-elixir/README.md b/2018-elixir/README.md new file mode 100644 index 0000000..6cc8101 --- /dev/null +++ b/2018-elixir/README.md @@ -0,0 +1,21 @@ +# Aoc2018e + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `aoc2018e` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:aoc2018e, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/aoc2018e](https://hexdocs.pm/aoc2018e). + diff --git a/2018-elixir/config/config.exs b/2018-elixir/config/config.exs new file mode 100644 index 0000000..8475f76 --- /dev/null +++ b/2018-elixir/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :aoc2018e, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:aoc2018e, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env()}.exs" diff --git a/2018-elixir/data/01.in b/2018-elixir/data/01.in new file mode 100644 index 0000000..d2e7304 --- /dev/null +++ b/2018-elixir/data/01.in @@ -0,0 +1,993 @@ ++3 ++15 +-1 +-18 ++3 +-10 ++3 ++10 +-8 +-20 ++13 ++11 +-21 +-10 +-16 +-2 +-6 ++19 +-8 ++12 +-13 ++4 +-9 +-20 ++12 ++15 +-17 +-18 ++11 +-17 +-9 +-12 +-9 ++3 ++9 +-6 ++18 ++16 ++9 +-11 +-1 ++8 +-20 ++17 +-3 +-16 +-7 ++10 ++6 ++3 ++15 ++3 +-2 ++9 +-8 +-6 ++11 ++5 ++24 ++10 ++4 +-8 ++2 ++13 +-16 ++13 +-16 ++11 ++7 +-9 +-13 +-12 ++17 +-4 +-5 +-1 ++16 ++22 ++8 ++12 ++9 ++2 +-3 +-5 ++12 ++4 ++4 ++8 ++18 ++9 +-14 +-11 +-5 +-11 ++15 +-18 +-7 +-12 ++1 ++5 +-18 ++14 +-13 +-10 ++17 ++8 ++11 +-12 ++20 +-16 ++22 ++5 ++1 +-9 ++16 ++13 ++6 ++5 ++7 +-4 +-4 +-7 ++16 ++4 +-1 +-15 +-11 ++19 ++9 ++17 +-15 ++21 +-7 ++4 +-16 +-3 +-9 ++4 ++11 ++12 ++11 ++7 ++12 ++14 +-6 +-1 +-18 +-3 +-13 ++4 ++10 +-15 +-10 +-8 ++15 ++15 ++19 ++16 +-3 +-14 ++13 ++16 ++1 ++10 ++9 ++7 +-11 +-19 ++2 +-3 ++10 +-2 ++13 +-19 +-7 ++3 ++18 ++1 ++9 ++2 ++12 +-13 +-9 +-7 +-15 +-18 +-6 +-1 +-8 +-12 ++19 +-1 +-14 +-6 +-16 +-4 ++1 ++4 +-22 +-19 +-2 ++4 ++18 +-8 +-19 +-16 ++6 ++16 +-7 ++4 ++8 +-20 +-11 +-11 ++20 ++15 ++19 ++30 ++19 ++19 +-11 +-18 ++4 ++1 ++21 +-9 +-9 ++12 ++18 +-1 +-6 ++12 ++16 ++12 ++3 ++1 +-9 +-15 +-15 +-1 +-1 +-18 +-32 +-11 +-17 +-20 ++4 ++14 +-1 ++16 ++20 ++53 ++19 ++16 ++14 ++14 ++6 +-8 ++14 +-17 +-5 ++3 ++14 +-3 +-15 +-10 +-19 ++17 ++3 ++17 ++5 ++14 +-7 ++5 ++14 ++18 +-2 +-19 ++8 ++10 ++15 +-6 ++13 +-11 ++19 ++10 +-15 +-15 ++2 +-4 +-3 ++11 ++2 ++18 ++3 ++17 ++9 +-16 +-8 ++6 ++15 +-11 +-3 ++20 +-1 ++12 ++1 ++8 ++11 +-8 ++6 ++7 ++17 +-12 ++13 +-8 ++16 ++15 +-8 +-12 ++7 +-12 +-18 ++5 ++5 ++7 +-8 ++6 +-4 +-15 ++17 +-15 +-10 +-1 ++17 ++18 +-21 +-19 +-9 ++19 ++16 ++18 +-5 +-15 ++19 +-11 ++10 ++9 +-1 ++4 ++4 ++9 +-5 ++20 ++4 +-18 ++17 ++12 +-18 ++14 +-21 +-15 +-5 +-18 +-17 +-19 ++11 ++12 +-20 +-19 ++12 +-9 ++8 +-1 +-22 ++4 ++15 ++14 ++9 +-10 +-26 +-22 +-10 ++7 ++9 +-1 +-16 +-21 +-18 ++8 ++6 ++18 ++1 ++5 ++11 ++18 ++6 ++20 +-7 +-10 +-5 ++33 +-21 +-8 +-13 +-5 ++7 ++1 +-7 +-39 +-14 +-12 ++16 +-14 +-15 ++4 ++2 ++12 +-11 ++16 ++8 ++15 +-1 ++21 +-5 ++13 +-18 +-7 ++2 +-15 ++19 ++8 ++26 +-24 ++37 ++59 ++21 +-19 ++18 ++6 +-11 ++15 +-2 ++5 +-2 ++20 ++8 ++2 +-7 ++18 +-8 +-1 +-8 ++3 ++17 ++12 ++18 ++2 +-8 ++3 ++40 ++3 ++21 ++5 +-1 ++15 ++2 ++16 +-50 ++15 ++5 ++21 +-31 +-17 +-15 +-10 +-9 +-2 +-18 +-14 ++7 +-14 ++6 +-20 +-8 ++13 +-27 ++8 ++2 +-16 ++3 +-7 +-6 ++23 ++31 ++52 +-6 +-1 ++2 +-54 +-42 +-6 ++18 ++2 ++1 +-29 ++36 +-42 ++7 +-146 +-2 +-14 +-13 ++19 +-8 ++19 +-15 +-18 +-4 +-18 +-1 ++2 +-3 ++11 +-16 +-23 +-9 +-30 +-31 ++58 ++18 +-19 ++45 ++11 ++14 +-16 ++19 +-10 ++17 ++12 +-2 +-8 ++19 +-50 ++8 +-114 +-40 +-62 +-153 +-60 ++12 +-23 +-40 +-87 ++377 +-79652 +-12 ++1 +-9 +-5 +-12 +-14 ++9 +-3 +-2 ++8 +-15 +-18 ++19 +-6 ++4 +-14 +-2 +-18 +-19 +-8 ++2 ++11 +-19 +-11 ++5 +-3 ++4 ++1 +-3 ++9 ++7 ++15 +-9 +-2 +-6 ++3 ++2 ++10 ++9 ++3 +-5 +-4 ++12 ++15 +-6 +-11 +-9 +-15 ++9 +-19 +-15 ++4 ++7 ++7 +-16 ++15 +-4 +-19 +-8 +-11 +-18 +-14 +-18 +-16 ++5 ++5 ++12 +-9 +-16 ++2 +-6 +-2 ++20 +-13 +-9 +-16 ++4 +-1 ++2 +-3 +-17 +-2 +-11 ++1 ++6 ++17 +-4 +-15 +-3 +-19 ++6 +-16 +-18 +-6 ++2 ++12 +-9 ++6 ++7 +-6 ++7 +-9 +-18 +-3 ++10 +-17 ++11 ++16 ++16 ++6 ++13 +-17 ++2 ++3 ++17 +-10 ++8 +-1 +-9 +-15 ++11 ++2 +-14 ++7 +-12 +-19 +-3 +-7 +-7 +-15 +-17 ++5 ++3 ++14 ++6 +-5 ++17 +-9 +-5 ++1 +-8 +-13 ++7 ++17 +-12 +-14 +-18 ++10 ++16 +-3 +-19 ++7 ++14 ++14 ++11 +-1 ++18 +-16 +-4 ++10 +-14 ++9 +-17 +-1 ++4 +-14 +-16 ++1 +-14 +-7 ++1 ++7 +-11 +-4 +-14 ++19 +-11 ++8 ++14 ++8 +-15 +-18 ++2 +-17 +-19 ++13 ++3 ++6 ++8 +-10 ++7 +-3 +-9 ++8 ++12 +-4 ++15 ++7 ++10 ++11 +-10 +-20 +-16 ++11 +-1 +-21 +-13 +-19 +-15 +-15 +-7 ++10 ++19 +-17 +-6 +-10 +-13 ++12 +-18 ++16 ++10 +-13 ++12 +-1 ++12 ++12 ++10 ++16 +-18 +-5 +-10 +-13 +-7 ++19 ++14 ++18 +-2 +-4 +-13 ++14 ++18 +-7 ++11 +-13 +-21 +-12 ++14 ++16 ++7 +-19 ++16 ++7 ++2 +-5 ++1 ++19 ++16 ++14 ++9 ++12 ++14 +-8 +-28 ++13 ++11 ++13 +-17 +-21 ++15 +-37 +-11 ++19 ++1 ++8 ++12 +-1 +-27 +-21 +-19 +-21 ++15 +-24 +-7 +-11 +-17 +-15 +-11 +-11 ++8 +-9 +-9 +-4 +-6 ++15 +-10 +-3 ++19 ++9 ++8 ++6 ++11 +-4 +-14 +-1 ++3 +-15 ++17 ++18 +-10 +-22 +-15 ++14 ++6 +-14 +-11 ++4 +-18 ++5 +-16 +-9 +-13 ++14 ++3 ++9 ++17 +-9 ++22 +-12 +-8 +-5 +-1 +-6 +-6 +-19 ++17 ++9 ++28 ++20 ++14 ++11 ++6 ++7 +-2 ++17 ++17 +-12 ++7 ++21 +-2 +-4 ++10 ++2 ++12 +-19 +-22 +-8 +-20 ++25 +-17 +-15 +-8 +-1 ++13 +-15 ++16 ++15 ++19 +-128 +-4 +-16 +-4 ++13 +-10 ++20 +-16 +-13 ++15 ++9 ++1 +-11 +-16 ++13 ++1 ++15 ++1 +-31 ++11 +-53 +-11 +-9 +-16 +-11 ++7 ++6 ++13 ++6 +-12 +-3 ++16 ++2 +-5 ++7 +-16 ++17 +-13 +-9 ++15 +-3 ++13 ++8 ++5 +-8 +-1 +-5 +-21 ++25 ++23 ++12 ++1 +-2 ++7 +-25 +-51 +-5 ++4 ++14 +-8 ++4 ++11 +-5 +-21 +-14 ++9 ++8 +-13 ++4 ++80915 \ No newline at end of file diff --git a/2018-elixir/data/02.in b/2018-elixir/data/02.in new file mode 100644 index 0000000..21b6f42 --- /dev/null +++ b/2018-elixir/data/02.in @@ -0,0 +1,250 @@ +crruafyzloguvxwctqmphenbkd +srcjafyzlcguvrwctqmphenbkd +srijafyzlogbpxwctgmphenbkd +zrijafyzloguvxrctqmphendkd +srijabyzloguvowcqqmphenbkd +srijafyzsoguvxwctbmpienbkd +srirtfyzlognvxwctqmphenbkd +srijafyzloguvxwctgmphenbmq +senjafyzloguvxectqmphenbkd +srijafyeloguvxwwtqmphembkd +srijafyzlogurxtctqmpkenbkd +srijafyzlkguvxictqhphenbkd +srijafgzlogunxwctqophenbkd +shijabyzloguvxwctqmqhenbkd +srjoafyzloguvxwctqmphenbwd +srijafyhloguvxwmtqmphenkkd +srijadyzlogwvxwctqmphenbed +brijafyzloguvmwctqmphenhkd +smijafyzlhguvxwctqmphjnbkd +sriqafvzloguvxwctqmpheebkd +srijafyzloguvxwisqmpuenbkd +mrijakyuloguvxwctqmphenbkd +srnfafyzloguvxwctqmphgnbkd +srijadyzloguvxwhfqmphenbkd +srijafhzloguvxwctdmlhenbkd +srijafyzloguvxwcsqmphykbkd +srijafyzlogwvxwatqmphhnbkd +srijafyzlozqvxwctqmphenbku +srijafyzloguvxwcbamphenbgd +srijafyzlfguvxwctqmphzybkd +srijafyzloguqxwetqmphenkkd +srijafyylogubxwttqmphenbkd +srijafyzloguvxzctadphenbkd +srijafyzloguoxwhtqmchenbkd +srijafyzloguvxwcvqmzhenbko +srijnfyzloguvxwctqmchenjkd +srijaryzloggvxwctqzphenbkd +srijafhzleguvxwcxqmphenbkd +ssijafyzllguvxfctqmphenbkd +srijafyzloguvxdctqmfhenbcd +srijafyzloguvxfctqmplynbkd +srijaftzlogavxwcrqmphenbkd +sriwaoyzloguvxwctqmphenbtd +srijahyzlogunxwctqmphenbvd +srjjafyzloguzxwctumphenbkd +nrijafyzlxguvxwctqmphanbkd +srijafezlqguyxwctqmphenbkd +srijafygloguvxwjtqcphenbkd +erijafyzloguvxoctqmnhenbkd +ssijafyzllguvxwbtqmphenbkd +sriaafyzloguvxwctqqphenbkv +frijafyzloguvswctwmphenbkd +srijafyzyogkvxwctqmprenbkd +syijafyzuoguvxwctqmkhenbkd +srijafyzloganxwctqmphenbkf +srijafyzloguvxwftqmxhenbkq +srijafyflogxvxwctqmghenbkd +srijafyzsoguvxwctqmpjenwkd +srujafylloguvxwctqmphenckd +srijafyzlpzuvxwctqmphenbud +srijafyzlogfvxwctqmhhenbwd +srijafjzlogusxwctqmphepbkd +srijlfyzloguvxwctqfphenzkd +srijafyzlogwvxwctqyphenbqd +srijafyzloluvxwctqtphenukd +srizafyzlowuvxwctqmphqnbkd +sritafkzlkguvxwctqmphenbkd +sbijafdzloguvxgctqmphenbkd +crijafyeloguvxwctqmpsenbkd +srijafyvlogulxwctqmphenbkk +srijafyologuvxwctqmehegbkd +siijafyzloguvxwctjmphenbmd +srijafyzlupuvxwctqmpheabkd +srijafyzlogumxwctqqphanbkd +srijxfyzlogujxwcqqmphenbkd +irijafizeoguvxwctqmphenbkd +sgijafyzloguvtwctqmpfenbkd +srijzfyzloguvmwctnmphenbkd +srijafyzwohuvxwctqmthenbkd +srijafyzlhguvxoctqwphenbkd +srgjafyplogxvxwctqmphenbkd +srijafyqlogovxwctqzphenbkd +srijafjzloguvlnvtqmphenbkd +srijafyzooguvxwctqmphenvud +srijafyzgoguvxwctumphgnbkd +srijaffzloguvxwdqqmphenbkd +srijafyzlogugxwctqxphenbkr +srijafyzlogutxwctqmmcenbkd +srifafyzlhguwxwctqmphenbkd +mrimajyzloguvxwctqmphenbkd +sriyafyzloguvxwcthmphejbkd +srieakyzlokuvxwctqmphenbkd +srisafyzloguhxwctqmphecbkd +srijanyzloguvxcctqmxhenbkd +srijafyzypguvxwctqmqhenbkd +sryjtfyzlvguvxwctqmphenbkd +srijafyzlsguvxwctqmqfenbkd +srijafyzlogudxwbtqwphenbkd +srijysyzloguvxwctqmpvenbkd +srijafyzloggvxwjtqmphegbkd +srijgfyzloguvxwctqmbhdnbkd +ssijufyzloguvawctqmphenbkd +skojafyzloguvxwctqmphenbnd +srijafylloguvxwcqqmpienbkd +trioafyzloguvqwctqmphenbkd +srijafydloguvxwctqmpzjnbkd +saijafvzloguvxwcqqmphenbkd +srhjapyzloguvxwctqmbhenbkd +srijafyzlfguvxwcsqmpwenbkd +shijafyzboguvxwctqmphenbmd +srizafysloguvxwrtqmphenbkd +srijafyzloguvxwciqmwhenbkj +qrijafyzloduvxwctqmphenbko +srijefyuloguvxwctqmphenbed +srijafyzlobuvxwctqmphenhbd +srijafyzloxuvxwctqmpheabkq +srijafyzloguvrwctqmghenkkd +sfisafywloguvxwctqmphenbkd +srgjafyzlogurxwctqmphenbkp +srijafhzloguvxwcjqmphenhkd +srijafyylogufxwrtqmphenbkd +srijafyzvoguvxwzkqmphenbkd +sqijafyzloguvxwctqmpheqbxd +srijafyvloguvxwctqzpherbkd +srijufyzloguvxlcsqmphenbkd +srijafykloguvxlccqmphenbkd +srijafyzloguexwcrqmphenzkd +sridifyzloguyxwctqmphenbkd +srijafyzlogfvxwctqlphenbkl +srijafyzlodqdxwctqmphenbkd +srijafyzloruvxactqmphenekd +grijafyzloguvxpctmmphenbkd +srsjakyzloguvxwctqmphvnbkd +srikafyvloguvxwrtqmphenbkd +srijafyzloguvxwctqjpserbkd +jrijafyzloguvxwctqmpgesbkd +swijafyzluguvxwctqmfhenbkd +srijanynlogovxwctqmphenbkd +jrijafyzloguvxwctymphrnbkd +srinafyzloguvewctqmphenbzd +srijakyzloguvxwctqmphcnbka +srijafyhlobuvxwctqmphenbka +srijafyzcogusxwctqmphwnbkd +srijavyzlosuvxwctqmphjnbkd +orijafyzxoguvxwcnqmphenbkd +srijafyzlogcvxwvtqmthenbkd +srijapyzloauvxwctqmphenvkd +srijaflzloguhxwctqmphenbwd +smijafyzlonuvxwctqmphenbkw +jrijafyzloguvxwclqmnhenbkd +srijaqyzloguvqwctqmphenskd +srijasyzloguvxwctqmvhenbku +crijtfyzloguvxwctqmthenbkd +srrkafyzvoguvxwctqmphenbkd +srijatyzloguvewctqmphenbld +srfjafyyloguvnwctqmphenbkd +srijafyzloguvxwctqjpbenbkt +hrijafyzooguvxwctqmphenbld +srijafbzlogscxwctqmphenbkd +srinafyzlogxvxwctqqphenbkd +slijafyzloglvxwctqmphenbdd +srijafyzlogjvxwcsqmphenbld +sryjcfyzloguvewctqmphenbkd +srijafyzloguexwctqmohknbkd +jaijafyzlogevxwctqmphenbkd +srijafbzlogavxwctqmphenbki +srijafozlogpvxwctqmphgnbkd +srijdfyzloguvxwczqmphenbkm +srijafyzlobuvxwctqmphxndkd +mrijifyzlhguvxwctqmphenbkd +srijafyzloguvxbctumphjnbkd +srijafyzloyuvxwptqmphlnbkd +arijafyzloguvxwcsqmohenbkd +srijaftzioguvxwttqmphenbkd +srijafyzlqsuvxwctqmphxnbkd +srijafyzioguvxwctqnphetbkd +prijafbzloguvxdctqmphenbkd +srijaeyzlnguvxwmtqmphenbkd +srijofyzloguvqwctqmphonbkd +srixaryzpoguvxwctqmphenbkd +srijafyzlowuvxwcwhmphenbkd +srijafydloguvxwctqmptenikd +srijqfyzlogtvfwctqmphenbkd +srijafyzloguvxlctqmpvenbgd +srijafyzlbguvxwjtqgphenbkd +srijafyzlohuqxwctqmphenbka +srijafyzroguvxictqmphynbkd +srijafyzloguvxdctjmphenjkd +srijaoczloguvxwctqmphenbjd +srajafhzloguvxwctqmphenbke +srijofyzloduvxwctqmphanbkd +srijafytloguvxwmtnmphenbkd +srijafyzuoguvxwceqmpgenbkd +rrijafyzloyuvxwctqmphlnbkd +srljafyzloguvxictqmohenbkd +srijafyzlogulxwcrqrphenbkd +srajafyzloguvxwctqmphanbke +srijafyzlhguvxwxtqmpheabkd +sxijafyzloggwxwctqmphenbkd +srijafyultguvxwctqmphinbkd +srijafyzloguvtwctqmfhvnbkd +srijafwzloruvxwctquphenbkd +srbjafyzxoguuxwctqmphenbkd +erijafyzlxguvxbctqmphenbkd +srijagyzlojubxwctqmphenbkd +srijafyzloguvxwdtqmchenakd +srijafkzlogukxwctqiphenbkd +mridafyzloguvxwctqmphenrkd +szqjafyzloguvxwctqmpheibkd +srijahyzloguvxwctcmphenekd +srijafyzloguvxwczpuphenbkd +srijafyzcoguvfwctqmphenbkq +qriiafyzloguvxwctqmpheebkd +srijpfyzloguvxlctqmphenokd +srijzfyzlotuvxwcjqmphenbkd +srinafyqloguvxwctfmphenbkd +srijafyzlogjvxpltqmphenbkd +srijafyzlotuvxwutqmphenbtd +sridafyzloguvxwctqmpyenokd +srxjafyzqogyvxwctqmphenbkd +ssijafyzzoguvxwctqmphenbad +srijafrzloguvxwctqmphekpkd +srijafyzlfgrvxactqmphenbkd +srijafyzroguvxwttqmphekbkd +srijefyzloguvxwctqmpqenbrd +srijefycloguvxwctqmchenbkd +srzjafyzloguvxwcqqmphanbkd +srijauyzlhguvxwctqmphenbgd +srijafyzloguvmwvnqmphenbkd +srihafyzloguvlwotqmphenbkd +srigafyzloguvxwctqmphennsd +sriuafzzloguvxwcuqmphenbkd +srijavuzllguvxwctqmphenbkd +srijafjzloguvlnctqmphenbkd +lrirafyzloguvxwctqmphenbld +soijarxzloguvxwctqmphenbkd +srijapyzlnguvxwctqmdhenbkd +srijafyzkogujxmctqmphenbkd +srijafuzloguvxwcsqvphenbkd +srijagyzzoguvxwctqmpvenbkd +srijafyzlovuvxwctqmrhenbxd +srijafyzqoguvxwctwmpienbkd +sxijafyzloguvxwutqmphenlkd +srijafyzlhgzvxwctqmphqnbkd +srijajyzloguvxwcbwmphenbkd +srijazyzloguvxwhtqmphenbkx +srgjafyzloguvvwctqmphdnbkd +rrivafyzloguvxjctqmphenbkd +srijifyzdoguvxwctqmphenbka +hrijafyzloguvxectqmpheybkd \ No newline at end of file diff --git a/2018-elixir/lib/aoc2018e.ex b/2018-elixir/lib/aoc2018e.ex new file mode 100644 index 0000000..c6a18db --- /dev/null +++ b/2018-elixir/lib/aoc2018e.ex @@ -0,0 +1,7 @@ +defmodule Aoc2018e do + alias Day01 + def show_all do + Day01.show + end +end +Aoc2018e.show_all \ No newline at end of file diff --git a/2018-elixir/lib/day01.ex b/2018-elixir/lib/day01.ex new file mode 100644 index 0000000..02af997 --- /dev/null +++ b/2018-elixir/lib/day01.ex @@ -0,0 +1,44 @@ +defmodule Day01 do + defp find_recurring(data) do + Enum.reduce_while(data, {0, MapSet.new([0])}, fn term, {prev, known} -> + freq = prev + term + if MapSet.member?(known, freq) do + {:halt, freq} + else + {:cont, {freq, MapSet.put(known, freq)}} + end + end) + end + + defp parse(data) do + String.split(data, "\n") + |> Enum.map(&String.trim/1) + |> Enum.map(&String.to_integer/1) + end + + def solve(data) do + data + |> parse + |> Enum.sum + end + + def solve_again(data) do + data + |> parse + |> Stream.cycle + |> find_recurring + end + + def show do + data = File.read!("data/01.in") + :timer.tc(fn -> solve(data) end) + |> elem(0) + |> Kernel./(1_000_000) + |> IO.inspect + :timer.tc(fn -> solve_again(data) end) + |> elem(0) + |> Kernel./(1_000_000) + |> IO.inspect + end +end +Day01.show() \ No newline at end of file diff --git a/2018-elixir/lib/day02.ex b/2018-elixir/lib/day02.ex new file mode 100644 index 0000000..27d79ce --- /dev/null +++ b/2018-elixir/lib/day02.ex @@ -0,0 +1,40 @@ +defmodule Day02 do + defp parse(data) do + data + |> String.split("\n", trim: true) + |> Enum.map(&String.trim/1) + end + + def countn(data, n) do + data + |> Enum.filter(fn s -> MapSet.size(MapSet.new(String.to_charlist(s))) != String.length(s) - n + 1 end) + |> Enum.count + end + + def solve(data) do + parsed = data + |> parse + f2 = parsed + |> countn(2) + f3 = parsed + |> countn(3) + f2 * f3 + end + + def solve_again(data) do + data + |> parse + end + + def show do + data = File.read!("data/02.in") + :timer.tc(fn -> solve(data) end) + |> elem(0) + |> Kernel./(1_000_000) + |> IO.inspect + :timer.tc(fn -> solve_again(data) end) + |> elem(0) + |> Kernel./(1_000_000) + |> IO.inspect + end +end diff --git a/2018-elixir/mix.exs b/2018-elixir/mix.exs new file mode 100644 index 0000000..40a054d --- /dev/null +++ b/2018-elixir/mix.exs @@ -0,0 +1,28 @@ +defmodule Aoc2018e.MixProject do + use Mix.Project + + def project do + [ + app: :aoc2018e, + version: "0.1.0", + elixir: "~> 1.7", + 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/2018-elixir/test/day01_test.exs b/2018-elixir/test/day01_test.exs new file mode 100644 index 0000000..718340c --- /dev/null +++ b/2018-elixir/test/day01_test.exs @@ -0,0 +1,17 @@ +defmodule Day01Test do + use ExUnit.Case + doctest Day01 + + test "adjust frequency" do + assert Day01.solve("+1\n+1\n+1") == 3 + assert Day01.solve("+1\n+1\n-2") == 0 + assert Day01.solve("-1\n-2\n-3") == -6 + end + + test "finds recurring frequency" do + assert Day01.solve_again("+1\n-1") == 0 + assert Day01.solve_again("+3\n+3\n+4\n-2\n-4") == 10 + assert Day01.solve_again("-6\n+3\n+8\n+5\n-6") == 5 + assert Day01.solve_again("+7\n+7\n-2\n-7\n-4") == 14 + end +end diff --git a/2018-elixir/test/day02_test.exs b/2018-elixir/test/day02_test.exs new file mode 100644 index 0000000..866cf49 --- /dev/null +++ b/2018-elixir/test/day02_test.exs @@ -0,0 +1,38 @@ +defmodule Day02Test do + use ExUnit.Case + doctest Day02 + + setup do + puzzle_input = [ + 'abcdef', + 'bababc', + 'abbcde', + 'abcccd', + 'aabcdd', + 'abcdee', + 'ababab' + ] + |> Enum.join("\n") + {:ok, puzzle_input: puzzle_input } + end + + test "box id checksum", meta do + puzzle_input = [ + "abcdef", + "bababc", + "abbcde", + "abcccd", + "aabcdd", + "abcdee", + "ababab" + ] + assert Day02.countn(puzzle_input, 2) == 3 + assert Day02.countn(puzzle_input, 3) == 4 + #assert Day02.solve(puzzle_input) == 12 + end + + test "finds recurring frequency", meta do + puzzle_input = meta[:puzzle_input] + #assert Day02.solve_again(puzzle_input) == "fgij" + end +end diff --git a/2018-elixir/test/test_helper.exs b/2018-elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2018-elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/2018-python/.gitignore b/2018-python/.gitignore new file mode 100644 index 0000000..a24a8a2 --- /dev/null +++ b/2018-python/.gitignore @@ -0,0 +1,3 @@ +.idea +*.pyc +__pycache__ \ No newline at end of file diff --git a/2018-python/README.md b/2018-python/README.md new file mode 100644 index 0000000..ff30404 --- /dev/null +++ b/2018-python/README.md @@ -0,0 +1,52 @@ +Advent of Code 2018 +=================== + +Lösningar för #aoc2018 i Python 3 (testat mot 3.7.1). + +Hjälpscript +----------- + +För att köra alla lösningar: + + python aoc.py + +För att starta en ny dag (skapar och populerar filerna `inputs/.txt`, `solutions/day_.py` och +`tests/day__tests.py`): + + python aoc.py "" + +Öppna puzzle input manuellt och kopiera innehållet till `inputs/.txt` som ett sista manuellt steg. + +För att köra separat lösning (ersätt `XX` med dagens nummer): + + PYTHONPATH=$(pwd) python solutions/day_XX.py + +Starta automatisk testkörare (ersätt `XX` med dagens nummer): + + export PYTHONPATH=$(pwd) + ls solutions/**/*.py | entr -c -r python tests/day_XX_tests.py + +Logg +---- + + * Dag 1: Insikten när en for-loop kan ersättas med en `sum`. xD Dagens `itertools`: `cycle()` + * Dag 2: Dagens `itertools`: `combinations()`, efter många försök att vara för smart med `zip()`. + * Dag 3: Dagens `itertools` (ja, det verkar vara ett tema!): `product()`. Två nästlade for-loopar kändes trist. + * Dag 4: Mycket text och mycket kod. Dagens `itertools`: `chain()`. + * Dag 5: Krånglade till saker genom att köra listor istället för strängar, skrev om till att istället använda en + `reduce`. Inga `itertools`. :( + * Dag 6: Längsta körtiden hittills och kan högst troligtvis optimeras. + * Dag 7: Svårtolkad uppgift. Otäckt med workers. Rant i [kodkommentar om icke hjälpsamt + exempel](https://github.com/madr/redesigned-system/blob/master/solutions/day_07.py#L63-L70). + * Dag 8: Rekursion! Invigning av `sys.setrecursionlimit()`. + * Dag 9: Harakiri. + * Dag 10: Kult med visualisering! Fick tipset att leta efter minsta bounds, och kom då fram till den magiska siffran + `10391`. Det krävdes en hel del optimering, då första versionen var slö. + * Dag 11: [Summed-area table](https://en.wikipedia.org/wiki/Summed-area_table). Lösning 2 behöver refaktoriseras för + att använda partial sum och summed-area table, den tar 8-12h att köra med brute force. + * Dag 12: Otydliga instruktioner. [Inte bara enligt mig](https://www.reddit.com/r/adventofcode/comments/a5gt7h/day12_part_1_explanation_for_the_example/), + [vad det verkar](https://www.reddit.com/r/adventofcode/comments/a5eztl/2018_day_12_solutions/). + * Day 13: Ännu en dag med ett exempel som var vilseledande för att testa ens kod. + [Rant i kodkommentar](https://github.com/madr/redesigned-system/blob/master/solutions/day_13.py#L108-L141). + * Day 14: + * Dag 15: \ No newline at end of file diff --git a/2018-python/aoc.py b/2018-python/aoc.py new file mode 100644 index 0000000..215af65 --- /dev/null +++ b/2018-python/aoc.py @@ -0,0 +1,66 @@ +import sys + +try: + _, day_no, name = sys.argv +except ValueError: + day_no = None + name = None + +if day_no and name: + with open('solutions/day_{}.py'.format(day_no.zfill(2)), 'w') as s: + s.write(''' +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '{day_no}.in' + + def __str__(self): + return 'Day {day}: {name}' + + def solve(self, puzzle_input): + pass + + def solve_again(self, puzzle_input): + pass + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() +'''.strip().format(day=day_no, day_no=day_no.zfill(2), name=name) + '\n') + with open('tests/day_{}_tests.py'.format(day_no.zfill(2)), 'w') as t: + t.write(''' +import unittest + +from solutions.day_{day_no} import Solution + + +class Day{day_no}TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + puzzle_input = '' + assert self.solution.solve(puzzle_input) == True + + +if __name__ == '__main__': + unittest.main() + '''.strip().format(day_no=day_no.zfill(2)) + '\n') + with open('inputs/{}.in'.format(day_no.zfill(2)), 'w') as i: + i.write('') + exit(0) + +print('\nAdvent of Code 2018' + '\n###################' + '\n\nby Anders Ytterström (@madr)') + +for i in [str(n).zfill(2) for n in range(1, 26)]: + try: + solution = __import__('solutions.day_{}'.format(i), globals(), locals(), ['Solution'], 0).Solution() + solution.show_results() + except IOError: + pass + except ImportError: + pass diff --git a/2018-python/solutions/__init__.py b/2018-python/solutions/__init__.py new file mode 100644 index 0000000..21902f7 --- /dev/null +++ b/2018-python/solutions/__init__.py @@ -0,0 +1,36 @@ +import time +from datetime import timedelta + + +class BaseSolution: + puzzle_input = "" + input_file = None + trim_input = True + + def parse_input(self, filename): + filepath = './inputs/{}'.format(filename) + with open(filepath, 'r') as f: + self.puzzle_input = f.read() + if self.trim_input: + self.puzzle_input = self.puzzle_input.strip() + + def show_results(self): + self.parse_input(self.input_file) + start_time = time.monotonic() + p1 = self.solve(self.puzzle_input) + p2 = self.solve_again(self.puzzle_input) + end_time = time.monotonic() + duration = timedelta(seconds=end_time - start_time) + print('\n\n{}\n{}\n\nPart 1: {}\nPart 2: {}\n\nDuration: {}'.format( + str(self), + '-' * len(str(self)), + p1, + p2, + duration, + )) + + def solve(self, puzzle_input): + raise NotImplemented + + def solve_again(self, puzzle_input): + raise NotImplemented diff --git a/2018-python/solutions/day_01.py b/2018-python/solutions/day_01.py new file mode 100644 index 0000000..8eea8f3 --- /dev/null +++ b/2018-python/solutions/day_01.py @@ -0,0 +1,28 @@ +import itertools + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '01.in' + + def __str__(self): + return 'Day 1: Chronal Calibration' + + def solve(self, puzzle_input, freq=0): + return sum(map(int, puzzle_input.splitlines())) + + def solve_again(self, puzzle_input, freq=0): + freq_changes = map(int, puzzle_input.splitlines()) + known = {0} + for n in itertools.cycle(freq_changes): + freq += n + if freq in known: + break + known.add(freq) + return freq + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_02.py b/2018-python/solutions/day_02.py new file mode 100644 index 0000000..2ce9ae4 --- /dev/null +++ b/2018-python/solutions/day_02.py @@ -0,0 +1,33 @@ +import itertools +from collections import Counter + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '02.in' + + def __str__(self): + return 'Day 2: Inventory Management System' + + def _count(self, line, num): + return any(map(lambda n: n == num, dict(Counter(line).most_common()).values())) + + def countn(self, data, n): + return sum(map(lambda s: self._count(s, n), data)) + + def solve(self, puzzle_input): + data = puzzle_input.splitlines() + return self.countn(data, 2) * self.countn(data, 3) + + def solve_again(self, puzzle_input): + lines = puzzle_input.splitlines() + for l, r in itertools.combinations(lines, 2): + diff = list(filter(lambda x: x[0] != x[1], zip(l, r))) + if len(diff) == 1: + return ''.join([c for i, c in enumerate(l) if l[i] == r[i]]) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_03.py b/2018-python/solutions/day_03.py new file mode 100644 index 0000000..ce978b6 --- /dev/null +++ b/2018-python/solutions/day_03.py @@ -0,0 +1,49 @@ +import itertools +import re +from collections import defaultdict + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '03.in' + + claim_pattern = re.compile(r'#(\d+) @ (\d+),(\d+): (\d+)x(\d+)') + + def __str__(self): + return 'Day 3: No Matter How You Slice It' + + def solve(self, puzzle_input): + claims = self.get_claims(puzzle_input) + return sum(map(lambda c: c[1] > 1, claims.items())) + + def solve_again(self, puzzle_input): + claims = self.get_claims(puzzle_input) + non_repeated_id = self._find_nri(puzzle_input, claims) + return non_repeated_id + + def parse_claim(self, data): + return tuple(map(int, self.claim_pattern.search(data).groups())) + + def get_claims(self, puzzle_input): + claims = defaultdict(int) + for line in puzzle_input.splitlines(): + _, left, top, width, height = self.parse_claim(line) + for w, h in itertools.product(range(left, left + width), range(top, top + height)): + claims[(w, h)] += 1 + return claims + + def _find_nri(self, puzzle_input, claims): + for line in puzzle_input.splitlines(): + repeated = False + cid, left, top, width, height = self.parse_claim(line) + for w, h in itertools.product(range(left, left + width), range(top, top + height)): + if claims[(w, h)] > 1: + repeated = True + if not repeated: + return cid + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_04.py b/2018-python/solutions/day_04.py new file mode 100644 index 0000000..64309d6 --- /dev/null +++ b/2018-python/solutions/day_04.py @@ -0,0 +1,67 @@ +import heapq +import itertools +import re +from collections import defaultdict +from datetime import datetime, timezone + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '04.in' + + def __str__(self): + return 'Day 4: Repose Record' + + def solve(self, puzzle_input): + data = self._parse_and_group_data(puzzle_input) + timelines = self._get_guard_timelines(data) + sorted_timelines = sorted(timelines, key=lambda kv: sum([len(r) for r in kv[1]]), reverse=True) + guard_id, rows = sorted_timelines[0] + most_common_minute, _count = self._get_most_common_minute(rows) + return guard_id * most_common_minute + + def solve_again(self, puzzle_input): + data = self._parse_and_group_data(puzzle_input) + timelines = self._get_guard_timelines(data) + hiscores = [(guard_id, *self._get_most_common_minute(rows)) for guard_id, rows in timelines] + guard_id, most_common_minute, _count = heapq.nlargest(1, hiscores, key=lambda hs: hs[2])[0] + return guard_id * most_common_minute + + def _parse_and_group_data(self, data): + shift_sep = re.compile(r'\[.+\] Guard #\d+ begins shift') + sorted_data = '\n'.join(sorted(data.splitlines())) + entry_heads = shift_sep.findall(sorted_data) + entry_tails = shift_sep.split(sorted_data)[1:] + return list(map(lambda s: '\n'.join(map(str.strip, s)), zip(entry_heads, entry_tails))) + + def _get_guard_timelines(self, data): + id_pattern = re.compile(r'Guard #(\d+)') + asleep_pattern = re.compile(r'\[(.+)\] f.*\n\[(.+)\] w', re.MULTILINE) + time_format = '%Y-%m-%d %H:%M' + rem = 60 * 60 * 24 + timeline = defaultdict(list) + for entry in data: + guard_id = int(id_pattern.search(entry).groups()[0]) + for felt_asleep, woke_up in asleep_pattern.findall(entry): + if guard_id not in timeline: + timeline[guard_id] = [] + felt_asleep_dt = datetime.strptime(felt_asleep, time_format) + felt_asleep_ts = int(felt_asleep_dt.replace(tzinfo=timezone.utc).timestamp()) + felt_asleep_ts = (felt_asleep_ts % rem) // 60 + woke_up_dt = datetime.strptime(woke_up, time_format) + woke_up_ts = int(woke_up_dt.replace(tzinfo=timezone.utc).timestamp()) + woke_up_ts = (woke_up_ts % rem) // 60 + timeline[guard_id].append(range(felt_asleep_ts, woke_up_ts)) + return timeline.items() + + def _get_most_common_minute(self, rows): + minutes = defaultdict(int) + for m in itertools.chain(*rows): + minutes[m] += 1 + return heapq.nlargest(1, minutes.items(), key=lambda kv: kv[1])[0] + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_05.py b/2018-python/solutions/day_05.py new file mode 100644 index 0000000..f0da678 --- /dev/null +++ b/2018-python/solutions/day_05.py @@ -0,0 +1,35 @@ +from functools import reduce + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '05.in' + + def __str__(self): + return 'Day 5: Alchemical Reduction' + + def solve(self, puzzle_input): + return self._react_polymer(puzzle_input) + + def solve_again(self, puzzle_input): + sorted_data = sorted(puzzle_input) + polymer_reactions = set() + for char in range(ord(sorted_data[0]), ord(sorted_data[-1])): + polymer_reactions.add(self._react_polymer(puzzle_input.replace(chr(char), '').replace(chr(char + 32), ''))) + return min(polymer_reactions) + + def _react_polymer(self, data): + def remove_pairs(done, candidate): + if not done: + return candidate + last = done[-1] + if abs(ord(last) - ord(candidate)) == 32: + return done[:-1] + return ''.join([done, candidate]) + return len(reduce(remove_pairs, data)) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_06.py b/2018-python/solutions/day_06.py new file mode 100644 index 0000000..49f74bb --- /dev/null +++ b/2018-python/solutions/day_06.py @@ -0,0 +1,51 @@ +import itertools +from collections import defaultdict, Counter + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '06.in' + + def __str__(self): + return 'Day 6: Chronal Coordinates' + + def solve(self, puzzle_input): + locations = [tuple(map(int, pi.split(', '))) for pi in puzzle_input.splitlines()] + finite_locations, cols, rows = self._location_boundaries(locations) + closest = defaultdict(int) + for coordinate in itertools.product(cols, rows): + distances = self._manhattan_distance(coordinate, locations) + closest[coordinate] = '.' if distances[0][1] == distances[1][1] else distances[0][0] + _location, count = [lc for lc in Counter(closest.values()).most_common() if lc[0] not in finite_locations][0] + return count + + def solve_again(self, puzzle_input): + return self.get_region(puzzle_input, 10000) + + def get_region(self, puzzle_input, max_distance): + locations = [tuple(map(int, pi.split(', '))) for pi in puzzle_input.splitlines()] + finite_locations, cols, rows = self._location_boundaries(locations) + region = set() + for coordinate in itertools.product(cols, rows): + distances = self._manhattan_distance(coordinate, locations) + if sum(map(lambda x: x[1], distances)) < max_distance: + region.add(coordinate) + return len(region) + + def _manhattan_distance(self, coordinate, locations): + distances = [(i, abs(abs(target[0] - coordinate[0]) + abs(target[1] - coordinate[1]))) + for i, target in enumerate(locations)] + return sorted(distances, key=lambda x: x[1]) + + def _location_boundaries(self, locations): + min_width, min_height = map(min, zip(*locations)) + max_width, max_height = map(max, zip(*locations)) + finite_locations = [i for i, l in enumerate(locations) if l[0] not in (min_width, max_width) + and l[1] in (min_height, max_height)] + return finite_locations, range(min_width, max_width), range(min_height, max_height) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_07.py b/2018-python/solutions/day_07.py new file mode 100644 index 0000000..3dd054f --- /dev/null +++ b/2018-python/solutions/day_07.py @@ -0,0 +1,84 @@ +import itertools + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '07.in' + def __str__(self): + return 'Day 07: The Sum of Its Parts' + + def solve(self, puzzle_input): + steps = [tuple(filter(lambda x: len(x) == 1, l.split())) for l in puzzle_input.splitlines()] + relations = self._get_relations(steps) + completed_steps = self._get_completed_steps(relations) + return completed_steps + + def solve_again(self, puzzle_input): + return self.solve_with_workers(puzzle_input) + + def solve_with_workers(self, puzzle_input, workers=5, completion_reducer=0): + steps = [tuple(filter(lambda x: len(x) == 1, l.split())) for l in puzzle_input.splitlines()] + relations = self._get_relations(steps) + return self.get_seconds(relations, workers, completion_reducer) + + def _get_completed_steps(self, relations): + stop_at = len(relations.keys()) + completed_steps = '' + while len(completed_steps) < stop_at: + available = sorted([child for child, parents in relations.items() + if (all(prnt in completed_steps for prnt in parents) or len(parents) == 0) and child not in completed_steps]) + completed = available[0] + completed_steps += completed + return completed_steps + + def _get_relations(self, steps): + deps = dict([(p, set()) for p in itertools.chain(dict(steps).keys(), dict(steps).values())]) + for parent, child in steps: + deps[child].add(parent) + return deps + + def get_seconds(self, relations, workers_count=5, completion_reduce=0): + workers = [dict() for _ in range(workers_count)] + stop_at = len(relations) + seconds = 0 + completed = 0 + completed_steps = '' + in_progress = set() + diff = ord('A') + while completed < stop_at: + available_steps = sorted([child for child, parents in relations.items() + if (all(prnt in completed_steps for prnt in parents) or len(parents) == 0) and + child not in completed_steps]) + available_steps = [s for s in available_steps if s not in in_progress and s not in completed_steps] + available_workers = [i for i, w in enumerate(workers) if not w] + while available_steps and available_workers: + avs = available_steps.pop(0) + avw = available_workers.pop(0) + in_progress.add(avs) + workers[avw][avs] = 0 + busy_workers = [i for i, w in enumerate(workers) if w] + for ip, bw in itertools.product(in_progress, busy_workers): + if ip in workers[bw]: + # Apply completion_reduce (60) here since A takes 61 seconds to complete, + # B 62 times to complete, etc in THE REAL WORLD, but in the example it was "simplified". + # + # > To simplify things for the example, however, suppose you only have help from one Elf (a total + # > of two workers) and that each step takes **60 fewer seconds** (so that step A takes 1 second and + # > step Z takes 26 seconds). + # + # Die in a fire, would you kindly. + if workers[bw][ip] == 60 - completion_reduce + ord(ip) - diff: + completed_steps += ip + in_progress.remove(ip) + del workers[bw][ip] + completed += 1 + else: + workers[bw][ip] += 1 + seconds += 1 + return seconds + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_08.py b/2018-python/solutions/day_08.py new file mode 100644 index 0000000..0556d6a --- /dev/null +++ b/2018-python/solutions/day_08.py @@ -0,0 +1,51 @@ +import sys + +from solutions import BaseSolution + + +sys.setrecursionlimit(15000) # hej python! + +class Solution(BaseSolution): + input_file = '08.in' + + def __str__(self): + return 'Day 8: Memory Maneuver' + + def solve(self, puzzle_input): + data = list(map(int, (puzzle_input.split()))) + _, s = self._get_sum(0, 0, data) + return s + + def solve_again(self, puzzle_input): + data = list(map(int, (puzzle_input.split()))) + _, value = self._get_value(0, data) + return value + + def _get_sum(self, i, s, data): + children = data[i] + meta_entries = data[i + 1] + i += 2 + for _ in range(children): + i, s = self._get_sum(i, s, data) + s += sum([data[i + n] for n in range(meta_entries)]) + return i + meta_entries, s + + def _get_value(self, i, data): + children = data[i] + meta_entries = data[i + 1] + i += 2 + child_values = {} + for n in range(children): + i, value = self._get_value(i, data) + child_values[n] = value + meta_values = [data[i + n] for n in range(meta_entries)] + if children: + v = sum(child_values.get(n - 1, 0) for n in meta_values) + else: + v = sum(meta_values) + return i + meta_entries, v + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_10.py b/2018-python/solutions/day_10.py new file mode 100644 index 0000000..45a493a --- /dev/null +++ b/2018-python/solutions/day_10.py @@ -0,0 +1,72 @@ +import itertools +import re + +from solutions import BaseSolution + + +class Point: + def __init__(self, x, y, lr, du): + self.x = x + self.y = y + self.lr = lr + self.du = du + + def tick(self): + self.x += self.lr + self.y += self.du + + def pos(self): + return self.x, self.y + + def __repr__(self): + return 'position=<{}, {}> velocity=<{}, {}>'.format( + self.x, self.y, self.lr, self.du + ) + + +class Solution(BaseSolution): + input_file = '10.in' + + def __str__(self): + return 'Day 10: Day 10: The Stars Align' + + def solve(self, puzzle_input): + return '\n' + self.render(puzzle_input, 10391) + + def solve_again(self, puzzle_input): + return 10391 # by science and a little magic + + def _print(self, data, max_x, min_x, max_y, min_y): + pos = set((p.x, p.y) for p in data) + canvas = [] + l = max_x - min_x + 1 + for y, x in itertools.product(range(min_y, max_y + 1), range(min_x, max_x + 1)): + if (x, y) in pos: + v = '#' + else: + v = '.' + canvas.append(v) + return '\n'.join(''.join(canvas[0 + i:l + i]) for i in range(0, len(canvas), l)) + + def _bounds(self, data): + set_x = [p.x for p in data] + set_y = [p.y for p in data] + min_x = min(set_x) + min_y = min(set_y) + max_x = max(set_x) + max_y = max(set_y) + return max_x, min_x, max_y, min_y + + def render(self, puzzle_input, ticks): + r = re.compile(r'\< ?(-?\d+),\s+(-?\d+)\>.+< ?(-?\d+),\s+(-?\d+)\>') + data = [Point(*map(int, r.search(l).groups())) for l in puzzle_input.splitlines()] + for n in range(ticks): + for p in data: + p.tick() + max_x, min_x, max_y, min_y = self._bounds(data) + return self._print(data, max_x, min_x, max_y, min_y) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() \ No newline at end of file diff --git a/2018-python/solutions/day_11.py b/2018-python/solutions/day_11.py new file mode 100644 index 0000000..8f0f30e --- /dev/null +++ b/2018-python/solutions/day_11.py @@ -0,0 +1,54 @@ +import itertools + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '11.in' + + def __str__(self): + return 'Day 11: Chronal Charge' + + def solve(self, puzzle_input): + serial = int(puzzle_input) + matrix = self._populate_matrix(serial) + x, y, _plsum = self._get_size(matrix, 3) + return x, y + + def solve_again(self, puzzle_input): + serial = int(puzzle_input) + matrix = self._populate_matrix(serial) + results = set() + for i in range(300): + x, y, plsum = self._get_size(matrix, i) + results.add((x, y, plsum, i)) + x, y, _plsum, i = sorted(results, key=lambda s: s[2], reverse=True)[0] + return x, y, i + + def get_power_level(self, x, y, serial): + rid = x + 10 + pl = rid * y + pl += serial + pl *= rid + pl = (pl // 100) % 10 + pl -= 5 + return pl + + def _get_size(self, matrix, square_size=3): + squares = set() + for top, left in itertools.product(range(300-square_size), range(300-square_size)): + plsum = sum(matrix[(x + left, y + top)] + for y, x in itertools.product(range(square_size), range(square_size))) + squares.add((left, top, plsum)) + return sorted(squares, key=lambda s: s[2], reverse=True)[0] + + def _populate_matrix(self, serial): + matrix = dict() + for top, left in itertools.product(range(300), range(300)): + matrix[(left, top)] = self.get_power_level(left, top, serial) + return matrix + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_12.py b/2018-python/solutions/day_12.py new file mode 100644 index 0000000..59a0e06 --- /dev/null +++ b/2018-python/solutions/day_12.py @@ -0,0 +1,66 @@ +from collections import defaultdict + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '12.in' + + def __str__(self): + return 'Day 12: Subterranean Sustainability' + + def solve(self, puzzle_input): + initital_pots, rules = self._prepare(puzzle_input) + pots, _ = self._grow(initital_pots, rules, 20) + return sum(k for k, v in pots.items() if v == '#') + + def solve_again(self, puzzle_input): + # No tests cover for this, so here goes an explanation. + # + # To get the sum of the filty billion'th generation, we look + # for the first recurring pot pattern. self._grow() will + # automatically break the loop if the recurring pattern (of pots) + # is found. + # + # Adding The Big Fucking Number, the correct sum is calculated. + initital_pots, rules = self._prepare(puzzle_input) + pots, repeated_at = self._grow(initital_pots, rules, 999) + BFN = 50 * 10 ** 9 # Big Fucking Number + return sum(k + (BFN - repeated_at) for k, v in pots.items() if v == '#') + + def _grow(self, pots, rules, generations): + seen = dict() + repeated_at = 0 + for g in range(1, generations + 1): + start_at = min(pots) - 5 + stop_at = max(pots) + 5 + span = range(start_at, stop_at) + updated = dict() + for i in span: + for rule in [''.join(pots[i + j] for j in range(-2, 3))]: + updated[i] = rules.get(rule, '.') + pots.clear() + pots.update(updated) + pattern = ''.join(pots[i] for i in span).strip('.') + if pattern in seen: + repeated_at = g + break + seen[pattern] = g + return pots, repeated_at + + def _prepare(self, puzzle_input): + initial, _, *rules_data = puzzle_input.splitlines() + pots = defaultdict(lambda: '.') + pots.update({ + i: v for i, v in enumerate(initial[len('initial state: '):]) + }) + rules = dict() + for r in rules_data: + k, v = r.split(' => ') + rules[k] = v + return pots, rules + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_13.py b/2018-python/solutions/day_13.py new file mode 100644 index 0000000..612def0 --- /dev/null +++ b/2018-python/solutions/day_13.py @@ -0,0 +1,186 @@ +from solutions import BaseSolution + + +class Cart: + curves = ['\\', '/'] + intersection = '+' + direction_keys = { + 'LEFT': '<', + 'DOWN': 'v', + 'RIGHT': '>', + 'UP': '^', + } + + def __repr__(self): + return '[{}] {} at ({},{})'.format(int(self.crashed), self.direction, self.y, self.x) + + def __init__(self, y, x, direction): + self.rotations = 0 + self.direction = direction + self.y = y + self.x = x + self.crashed = False + + @property + def pos(self): + return self.y, self.x + + def move(self, t, l): + self.y += t + self.x += l + + def rotate(self, p): + if p in self.curves: + if p == '/': + if self.direction == self.direction_keys['UP']: + self.direction = self.direction_keys['RIGHT'] + elif self.direction == self.direction_keys['RIGHT']: + self.direction = self.direction_keys['UP'] + elif self.direction == self.direction_keys['DOWN']: + self.direction = self.direction_keys['LEFT'] + elif self.direction == self.direction_keys['LEFT']: + self.direction = self.direction_keys['DOWN'] + elif p == '\\': + if self.direction == self.direction_keys['UP']: + self.direction = self.direction_keys['LEFT'] + elif self.direction == self.direction_keys['LEFT']: + self.direction = self.direction_keys['UP'] + elif self.direction == self.direction_keys['DOWN']: + self.direction = self.direction_keys['RIGHT'] + elif self.direction == self.direction_keys['RIGHT']: + self.direction = self.direction_keys['DOWN'] + if p == self.intersection: + if self.rotations % 3 == 0: + if self.direction == self.direction_keys['UP']: + self.direction = self.direction_keys['LEFT'] + elif self.direction == self.direction_keys['LEFT']: + self.direction = self.direction_keys['DOWN'] + elif self.direction == self.direction_keys['DOWN']: + self.direction = self.direction_keys['RIGHT'] + elif self.direction == self.direction_keys['RIGHT']: + self.direction = self.direction_keys['UP'] + if self.rotations % 3 == 2: + if self.direction == self.direction_keys['UP']: + self.direction = self.direction_keys['RIGHT'] + elif self.direction == self.direction_keys['RIGHT']: + self.direction = self.direction_keys['DOWN'] + elif self.direction == self.direction_keys['DOWN']: + self.direction = self.direction_keys['LEFT'] + elif self.direction == self.direction_keys['LEFT']: + self.direction = self.direction_keys['UP'] + self.rotations += 1 + + +class Solution(BaseSolution): + input_file = '13.in' + trim_input = False + verbose = False # True will visualize each tick + + def __str__(self): + return 'Day 13: Mine Cart Madness' + + directions = { + 'v': (0, 1), + '^': (0, -1), + '>': (1, 0), + '<': (-1, 0), + } + + def solve(self, puzzle_input): + lines, carts = self._get_carts(puzzle_input) + crashes = [] + while len(crashes) == 0: + self._print(lines, carts) + carts, crashes = self.tick(lines, carts) + y, x = crashes.pop() + return '{},{}'.format(x, y) + + def solve_again(self, puzzle_input): + lines, carts = self._get_carts(puzzle_input) + while len(carts) > 1: + self._print(lines, carts) + carts, _ = self.tick(lines, carts) + remaining = carts.pop() + y, x = remaining.pos + return '{},{}'.format(x, y) + + def tick(self, lines, carts): + # Another rant about the examples of this years Advent of code. The + # example for day 13 did not include a clear example of how to handle + # these 2 scenarios: + # + # | + # -><- v + # ^ + # | + # + # The example only provided the scenario of these: + # + # | + # v + # ->-<- | + # ^ + # | + # + # This means that using the example as a test case will make the test + # pass, but give the wrong solution when the real puzzle input is applied. + # + # It is unnecessary to mention that the puzzle input is REALLY HARD to + # debug manually, since it is way more complex. + # + # I figured it out from the subreddit, where several people cryptically + # mentioned that the order matters of the carts, and by visualizing each + # tick, seeing that my code did not handle all scenarios. + # + # The final solution was to drown my code in unit tests, which of course + # is good anyway. + # + # What irritates me is that this year's Advent of Code have had many + # "slamkrypare" by providing examples that are not enough, and instead + # some cryptic wording in the description must be read over and over to + # solve the puzzle. + for cid, cart in enumerate(carts): + if cart.crashed: + continue + l, t = self.directions[cart.direction] + cart.move(t, l) + y, x = cart.pos + for cid2, other_cart in enumerate(carts): + if cid != cid2 and other_cart.y == y and other_cart.x == x and not other_cart.crashed: + cart.crashed = True + other_cart.crashed = True + cart.rotate(lines[y][x]) + crashes = list(set([c.pos for c in carts if c.crashed])) + updated = [c for c in carts if not c.crashed] + return sorted(updated, key=lambda o: o.pos), crashes + + def _print(self, lines, carts): + if self.verbose: + canvas = list() + ls = lines.copy() + for cart in carts: + c = cart.direction + y, x = cart.pos + if ls[y][x] in self.directions.keys(): + c = 'X' + ls[y] = ls[y][:x] + c + ls[y][x + 1:] + for l in ls: + canvas.append(''.join(l)) + canvas = '\n'.join(canvas) + print(canvas) + + def _get_carts(self, puzzle_input): + pattern = self.directions.keys() + carts = list() + lines = puzzle_input.splitlines() + for y, line in enumerate(lines): + for x, cart in enumerate(line): + if cart in pattern: + carts.append(Cart(y, x, cart)) + lines = puzzle_input.replace('>', '-').replace('<', '-').replace('v', '|').replace('^', '|').splitlines() + return lines, sorted(carts, key=lambda c: c.pos) + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/solutions/day_14.py b/2018-python/solutions/day_14.py new file mode 100644 index 0000000..eb7b1ce --- /dev/null +++ b/2018-python/solutions/day_14.py @@ -0,0 +1,19 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = '14.in' + + def __str__(self): + return 'Day 14: Chocolate Charts' + + def solve(self, puzzle_input): + pass + + def solve_again(self, puzzle_input): + pass + + +if __name__ == '__main__': + solution = Solution() + solution.show_results() diff --git a/2018-python/tests/day_01_tests.py b/2018-python/tests/day_01_tests.py new file mode 100644 index 0000000..43eb336 --- /dev/null +++ b/2018-python/tests/day_01_tests.py @@ -0,0 +1,30 @@ +import unittest + +from solutions.day_01 import Solution + + +class Day01TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_resolves_frequency(self): + freq_1 = '\n'.join(['+1', '+1', '+1']) + freq_2 = '\n'.join(['+1', '+1', '-2']) + freq_3 = '\n'.join(['-1', '-2', '-3']) + assert self.solution.solve(freq_1) == 3 + assert self.solution.solve(freq_2) == 0 + assert self.solution.solve(freq_3) == -6 + + def test_resolves_recurring_frequency(self): + freq_1 = '\n'.join(['+1', '-1']) + freq_2 = '\n'.join(['+3', '+3', '+4', '-2', '-4']) + freq_3 = '\n'.join(['-6', '+3', '+8', '+5', '-6']) + freq_4 = '\n'.join(['+7', '+7', '-2', '-7', '-4']) + assert self.solution.solve_again(freq_1) == 0 + assert self.solution.solve_again(freq_2) == 10 + assert self.solution.solve_again(freq_3) == 5 + assert self.solution.solve_again(freq_4) == 14 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_02_tests.py b/2018-python/tests/day_02_tests.py new file mode 100644 index 0000000..19a111c --- /dev/null +++ b/2018-python/tests/day_02_tests.py @@ -0,0 +1,38 @@ +import unittest + +from solutions.day_02 import Solution + + +class Day02TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_calculates_checksum(self): + puzzle_input = "\n".join([ + 'abcdef', + 'bababc', + 'abbcde', + 'abcccd', + 'aabcdd', + 'abcdee', + 'ababab' + ]) + assert self.solution.countn(puzzle_input.splitlines(), 2) == 4 + assert self.solution.countn(puzzle_input.splitlines(), 3) == 3 + assert self.solution.solve(puzzle_input) == 12 + + def test_common_box_names(self): + puzzle_input = "\n".join([ + 'abcde', + 'fghij', + 'klmno', + 'pqrst', + 'fguij', + 'axcye', + 'wvxyz' + ]) + assert self.solution.solve_again(puzzle_input) == 'fgij' + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_03_tests.py b/2018-python/tests/day_03_tests.py new file mode 100644 index 0000000..d6064ee --- /dev/null +++ b/2018-python/tests/day_03_tests.py @@ -0,0 +1,29 @@ +import unittest + +from solutions.day_03 import Solution + + +class Day03TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_find_overlapping_squares(self): + puzzle_input = '\n'.join([ + '#1 @ 1,3: 4x4', + '#2 @ 3,1: 4x4', + '#3 @ 5,5: 2x2', + ]) + assert self.solution.parse_claim('#123 @ 3,2: 5x4') == (123, 3, 2, 5, 4) + assert self.solution.solve(puzzle_input) == 4 + + def test_find_non_repeated_id(self): + puzzle_input = '\n'.join([ + '#1 @ 1,3: 4x4', + '#2 @ 3,1: 4x4', + '#3 @ 5,5: 2x2', + ]) + assert self.solution.solve_again(puzzle_input) == 3 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_04_tests.py b/2018-python/tests/day_04_tests.py new file mode 100644 index 0000000..fbbe128 --- /dev/null +++ b/2018-python/tests/day_04_tests.py @@ -0,0 +1,37 @@ +import unittest + +from solutions.day_04 import Solution + + +class Day04TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = '\n'.join([ + '[1518-11-01 00:00] Guard #10 begins shift', + '[1518-11-01 00:05] falls asleep', + '[1518-11-01 00:25] wakes up', + '[1518-11-01 00:30] falls asleep', + '[1518-11-01 00:55] wakes up', + '[1518-11-01 23:58] Guard #99 begins shift', + '[1518-11-02 00:40] falls asleep', + '[1518-11-02 00:50] wakes up', + '[1518-11-03 00:05] Guard #10 begins shift', + '[1518-11-03 00:24] falls asleep', + '[1518-11-03 00:29] wakes up', + '[1518-11-04 00:02] Guard #99 begins shift', + '[1518-11-04 00:36] falls asleep', + '[1518-11-04 00:46] wakes up', + '[1518-11-05 00:03] Guard #99 begins shift', + '[1518-11-05 00:45] falls asleep', + '[1518-11-05 00:55] wakes up', + ]) + + def test_most_sleeping_guard(self): + assert self.solution.solve(self.puzzle_input) == 240 # 10 * 24 + + def test_frequent_sleeping_guard(self): + assert self.solution.solve_again(self.puzzle_input) == 4455 # 99 * 45 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_05_tests.py b/2018-python/tests/day_05_tests.py new file mode 100644 index 0000000..9b84cc6 --- /dev/null +++ b/2018-python/tests/day_05_tests.py @@ -0,0 +1,18 @@ +import unittest + +from solutions.day_05 import Solution + + +class Day05TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_react_polymer(self): + assert self.solution.solve('dabAcCaCBAcCcaDA') == 10 + + def test_reduce_and_choose_most_efficient_polymer(self): + assert self.solution.solve_again('dabAcCaCBAcCcaDA') == 4 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_06_tests.py b/2018-python/tests/day_06_tests.py new file mode 100644 index 0000000..7dc7728 --- /dev/null +++ b/2018-python/tests/day_06_tests.py @@ -0,0 +1,34 @@ +import unittest + +from solutions.day_06 import Solution + + +class Day06TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_get_largest_area_size(self): + puzzle_input = '\n'.join([ + '1, 1', + '1, 6', + '8, 3', + '3, 4', + '5, 5', + '8, 9', + ]) + assert self.solution.solve(puzzle_input) == 17 + + def test_get_region(self): + puzzle_input = '\n'.join([ + '1, 1', + '1, 6', + '8, 3', + '3, 4', + '5, 5', + '8, 9', + ]) + assert self.solution.get_region(puzzle_input, 32) == 16 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_07_tests.py b/2018-python/tests/day_07_tests.py new file mode 100644 index 0000000..6a86673 --- /dev/null +++ b/2018-python/tests/day_07_tests.py @@ -0,0 +1,27 @@ +import unittest + +from solutions.day_07 import Solution + + +class Day07TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = '\n'.join([ + 'Step C must be finished before step A can begin.', + 'Step C must be finished before step F can begin.', + 'Step A must be finished before step B can begin.', + 'Step A must be finished before step D can begin.', + 'Step B must be finished before step E can begin.', + 'Step D must be finished before step E can begin.', + 'Step F must be finished before step E can begin.' + ]) + + def test_order_of_steps(self): + assert self.solution.solve(self.puzzle_input) == 'CABDFE' + + def test_seconds_for_workers(self): + assert self.solution.solve_with_workers(self.puzzle_input, 2, 60) == 15 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_08_tests.py b/2018-python/tests/day_08_tests.py new file mode 100644 index 0000000..f0f65cd --- /dev/null +++ b/2018-python/tests/day_08_tests.py @@ -0,0 +1,18 @@ +import unittest + +from solutions.day_08 import Solution + + +class Day08TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_metadata_entries_sum(self): + assert self.solution.solve('2 3 0 3 10 11 12 1 1 0 1 99 2 1 1 2') == 138 + + def test_root_node_value(self): + assert self.solution.solve_again('2 3 0 3 10 11 12 1 1 0 1 99 2 1 1 2') == 66 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_10_tests.py b/2018-python/tests/day_10_tests.py new file mode 100644 index 0000000..42d3fd7 --- /dev/null +++ b/2018-python/tests/day_10_tests.py @@ -0,0 +1,59 @@ +import unittest + +from solutions.day_10 import Solution + + +class Day10TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_reveal_message(self): + puzzle_input = '\n'.join([ + 'position=< 9, 1> velocity=< 0, 2>', + 'position=< 7, 0> velocity=<-1, 0>', + 'position=< 3, -2> velocity=<-1, 1>', + 'position=< 6, 10> velocity=<-2, -1>', + 'position=< 2, -4> velocity=< 2, 2>', + 'position=<-6, 10> velocity=< 2, -2>', + 'position=< 1, 8> velocity=< 1, -1>', + 'position=< 1, 7> velocity=< 1, 0>', + 'position=<-3, 11> velocity=< 1, -2>', + 'position=< 7, 6> velocity=<-1, -1>', + 'position=<-2, 3> velocity=< 1, 0>', + 'position=<-4, 3> velocity=< 2, 0>', + 'position=<10, -3> velocity=<-1, 1>', + 'position=< 5, 11> velocity=< 1, -2>', + 'position=< 4, 7> velocity=< 0, -1>', + 'position=< 8, -2> velocity=< 0, 1>', + 'position=<15, 0> velocity=<-2, 0>', + 'position=< 1, 6> velocity=< 1, 0>', + 'position=< 8, 9> velocity=< 0, -1>', + 'position=< 3, 3> velocity=<-1, 1>', + 'position=< 0, 5> velocity=< 0, -1>', + 'position=<-2, 2> velocity=< 2, 0>', + 'position=< 5, -2> velocity=< 1, 2>', + 'position=< 1, 4> velocity=< 2, 1>', + 'position=<-2, 7> velocity=< 2, -2>', + 'position=< 3, 6> velocity=<-1, -1>', + 'position=< 5, 0> velocity=< 1, 0>', + 'position=<-6, 0> velocity=< 2, 0>', + 'position=< 5, 9> velocity=< 1, -2>', + 'position=<14, 7> velocity=<-2, 0>', + 'position=<-3, 6> velocity=< 2, -1>', + ]) + expected = '\n'.join([ + '#...#..###', + '#...#...#.', + '#...#...#.', + '#####...#.', + '#...#...#.', + '#...#...#.', + '#...#...#.', + '#...#..###', + ]) + print(self.solution.render(puzzle_input, 3)) + assert self.solution.render(puzzle_input, 3) == expected + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_11_tests.py b/2018-python/tests/day_11_tests.py new file mode 100644 index 0000000..f366c64 --- /dev/null +++ b/2018-python/tests/day_11_tests.py @@ -0,0 +1,25 @@ +import unittest + +from solutions.day_11 import Solution + + +class Day11TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_power_level(self): + assert self.solution.get_power_level(122, 79, 57) == -5 + assert self.solution.get_power_level(217, 196, 39) == 0 + assert self.solution.get_power_level(101, 153, 71) == 4 + + def test_largest_square_size3x3(self): + assert self.solution.solve('18') == (33, 45) + assert self.solution.solve('42') == (21, 61) + + def test_largest_square_any_size(self): + assert self.solution.solve_again('18') == (90, 269, 16) + assert self.solution.solve_again('42') == (232, 251, 12) + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_12_tests.py b/2018-python/tests/day_12_tests.py new file mode 100644 index 0000000..4ef8e9c --- /dev/null +++ b/2018-python/tests/day_12_tests.py @@ -0,0 +1,31 @@ +import unittest + +from solutions.day_12 import Solution + + +class Day12TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_20_iterations(self): + puzzle_input = '''initial state: #..#.#..##......###...### + +...## => # +..#.. => # +.#... => # +.#.#. => # +.#.## => # +.##.. => # +.#### => # +#.#.# => # +#.### => # +##.#. => # +##.## => # +###.. => # +###.# => # +####. => #''' + assert self.solution.solve(puzzle_input) == 325 + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_13_tests.py b/2018-python/tests/day_13_tests.py new file mode 100644 index 0000000..7fd7721 --- /dev/null +++ b/2018-python/tests/day_13_tests.py @@ -0,0 +1,174 @@ +import unittest + +from solutions.day_13 import Solution, Cart + + +class Day13TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_curve_rotation(self): + cart = Cart(1, 1, 'v') + + cart.rotate('/') + assert cart.direction == '<' + cart.rotate('/') + assert cart.direction == 'v' + + cart.direction = '^' + cart.rotate('/') + assert cart.direction == '>' + cart.rotate('/') + assert cart.direction == '^' + + cart.direction = '^' + cart.rotate('\\') + assert cart.direction == '<' + cart.rotate('\\') + assert cart.direction == '^' + + cart.direction = 'v' + cart.rotate('\\') + assert cart.direction == '>' + cart.rotate('\\') + assert cart.direction == 'v' + + def test_intersection_rotation_cycle(self): + cart = Cart(1, 1, '^') + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 1 + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 2 + cart.rotate('+') + assert cart.direction == '^' + assert cart.rotations == 3 + + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 4 + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 5 + cart.rotate('+') + assert cart.direction == '^' + assert cart.rotations == 6 + + def test_intersection_rotation(self): + cart = Cart(1, 1, '^') + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 1 + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 2 + cart.rotate('+') + assert cart.direction == '^' + assert cart.rotations == 3 + + cart = Cart(1, 1, '<') + cart.rotate('+') + assert cart.direction == 'v' + assert cart.rotations == 1 + cart.rotate('+') + assert cart.direction == 'v' + assert cart.rotations == 2 + cart.rotate('+') + assert cart.direction == '<' + assert cart.rotations == 3 + + cart = Cart(1, 1, 'v') + cart.rotate('+') + assert cart.direction == '>' + assert cart.rotations == 1 + cart.rotate('+') + assert cart.direction == '>' + assert cart.rotations == 2 + cart.rotate('+') + assert cart.direction == 'v' + assert cart.rotations == 3 + + cart = Cart(1, 1, '>') + cart.rotate('+') + assert cart.direction == '^' + assert cart.rotations == 1 + cart.rotate('+') + assert cart.direction == '^' + assert cart.rotations == 2 + cart.rotate('+') + assert cart.direction == '>' + assert cart.rotations == 3 + + def test_crash(self): + puzzle_input = '''| +v +| +| +| +^ +| + ''' + lines, carts = self.solution._get_carts(puzzle_input) + carts, crashes = self.solution.tick(lines, carts) + casted = [repr(c) for c in carts] + assert casted == ['[0] v at (2,0)', '[0] ^ at (4,0)'] + assert crashes == [] + carts, crashes = self.solution.tick(lines, carts) + casted = [repr(c) for c in carts] + assert casted == [] + assert crashes == [(3, 0)] + + puzzle_input = '>-<' + lines, carts = self.solution._get_carts(puzzle_input) + carts, crashes = self.solution.tick(lines, carts) + assert carts == [] + assert crashes == [(0, 1)] + + puzzle_input = '><' + lines, carts = self.solution._get_carts(puzzle_input) + carts, crashes = self.solution.tick(lines, carts) + assert carts == [] + assert crashes == [(0, 1)] + + puzzle_input = '''v +| +^ + ''' + lines, carts = self.solution._get_carts(puzzle_input) + carts, crashes = self.solution.tick(lines, carts) + assert carts == [] + assert crashes == [(1, 0)] + + puzzle_input = '''v +^ + ''' + lines, carts = self.solution._get_carts(puzzle_input) + carts, crashes = self.solution.tick(lines, carts) + assert carts == [] + assert crashes == [(1, 0)] + + def test_first_crash(self): + puzzle_input = '''/->-\ +| | /----\ +| /-+--+-\ | +| | | | v | +\-+-/ \-+--/ + \------/ + ''' + assert self.solution.solve(puzzle_input) == "7,3" + + def test_last_cart(self): + puzzle_input = '''/>-<\ +| | +| /<+-\ +| | | v +\>+/ + ''' + assert self.solution.solve_again(puzzle_input) == "6,4" + + +if __name__ == '__main__': + unittest.main() diff --git a/2018-python/tests/day_14_tests.py b/2018-python/tests/day_14_tests.py new file mode 100644 index 0000000..5fff50a --- /dev/null +++ b/2018-python/tests/day_14_tests.py @@ -0,0 +1,16 @@ +import unittest + +from solutions.day_14 import Solution + + +class Day14TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_something(self): + puzzle_input = '' + assert self.solution.solve(puzzle_input) == True + + +if __name__ == '__main__': + unittest.main() diff --git a/2019-elixir/.formatter.exs b/2019-elixir/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/2019-elixir/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/2019-elixir/.gitignore b/2019-elixir/.gitignore new file mode 100644 index 0000000..d43dad9 --- /dev/null +++ b/2019-elixir/.gitignore @@ -0,0 +1,24 @@ +# 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"). +aoc19-*.tar + diff --git a/2019-elixir/README.md b/2019-elixir/README.md new file mode 100644 index 0000000..6a2bf1d --- /dev/null +++ b/2019-elixir/README.md @@ -0,0 +1,29 @@ +# Aoc19 + +Solutions for [Advent of Code 2019][1], this year in Elixir. + +[Elixir 1.9.2][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 aoc19.new 1 "Name of the puzzle" + +Solve a single puzzle, using puzzle from day 3 as example: + + mix aoc19.solve 3 + +Solve all puzzles, starting at the first: + + mix aoc19.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/2019-elixir/lib/aoc19.ex b/2019-elixir/lib/aoc19.ex new file mode 100644 index 0000000..b0e3ee0 --- /dev/null +++ b/2019-elixir/lib/aoc19.ex @@ -0,0 +1,61 @@ +defmodule Aoc19 do + @moduledoc """ + Solutions for Advent of Code 2019, written in Elixir 1.9.2. + """ + + def solve_all() do + """ + + ADVENT OF CODE 2019 + =================== + """ + |> 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(["Aoc19.Solution", "Day" <> id]) + + data = apply(solution_module, :parse!, [input]) + name = apply(solution_module, :get_name, []) + first_solution = apply(solution_module, :solve_first_part, [data]) + second_solution = apply(solution_module, :solve_second_part, [data]) + + [name, first_solution, second_solution] + end + + @doc """ + Generates a CLI-friendly presentation of the solutions. + + ## Examples + + iex> Aoc19.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/2019-elixir/lib/mix/tasks/bootstrap.ex b/2019-elixir/lib/mix/tasks/bootstrap.ex new file mode 100644 index 0000000..67a08f2 --- /dev/null +++ b/2019-elixir/lib/mix/tasks/bootstrap.ex @@ -0,0 +1,79 @@ +defmodule Mix.Tasks.Aoc19.New do + use Mix.Task + + @shortdoc "Bootstrap new solution" + @impl Mix.Task + def run([n, name]) do + id = n |> String.pad_leading(2, "0") + + input_file = "./inputs/" <> id <> ".in" + test_file = "./test/day_" <> id <> "_test.exs" + solution_file = "./lib/solutions/day_" <> id <> ".ex" + + IO.puts("Creating " <> input_file) + File.touch(input_file) + + IO.puts("Creating " <> test_file) + File.write!(test_file, test_file_content(id)) + + IO.puts("Creating " <> solution_file) + File.write!(solution_file, solution_file_content(name, id)) + + """ + \nDone! Start coding. + + mix test -- run tests. + mix aoc19:solve_all -- run all puzzles, starting with 1 + mix aoc19:solve -- run single puzzle, 1-25 + """ + |> IO.puts() + end + + defp test_file_content(id) do + """ + defmodule Day#{id}Test do + use ExUnit.Case + doctest Aoc19.Solution.Day#{id} + end + """ + end + + defp solution_file_content(name, id) do + ~s""" + defmodule Aoc19.Solution.Day#{id} do + @name "#{name}" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(_str), do: "10" + + @impl Solution + @doc \"\"\" + Solution for the first part of "#{name}". + + ## Examples + + iex> Aoc19.Solution.Day#{id}.solve_first_part("10") + "(TBW)" + + \"\"\" + def solve_first_part(_input), do: "(TBW)" + + @impl Solution + @doc \"\"\" + Solution for the second part of "#{name}". + + ## Examples + + iex> Aoc19.Solution.Day#{id}.solve_second_part("10") + "(TBW)" + + \"\"\" + def solve_second_part(_input), do: "(TBW)" + end + """ + end +end diff --git a/2019-elixir/lib/mix/tasks/solve.ex b/2019-elixir/lib/mix/tasks/solve.ex new file mode 100644 index 0000000..f76993d --- /dev/null +++ b/2019-elixir/lib/mix/tasks/solve.ex @@ -0,0 +1,11 @@ +defmodule Mix.Tasks.Aoc19.Solve do + use Mix.Task + + @shortdoc "Solve single puzzle" + @impl Mix.Task + def run([id]) do + id + |> String.to_integer() + |> Aoc19.solve() + end +end diff --git a/2019-elixir/lib/mix/tasks/solve_all.ex b/2019-elixir/lib/mix/tasks/solve_all.ex new file mode 100644 index 0000000..aeb4499 --- /dev/null +++ b/2019-elixir/lib/mix/tasks/solve_all.ex @@ -0,0 +1,9 @@ +defmodule Mix.Tasks.Aoc19.SolveAll do + use Mix.Task + + @shortdoc "Solve all puzzles" + @impl Mix.Task + def run(_) do + Aoc19.solve_all() + end +end diff --git a/2019-elixir/lib/solution.ex b/2019-elixir/lib/solution.ex new file mode 100644 index 0000000..d1f8d8f --- /dev/null +++ b/2019-elixir/lib/solution.ex @@ -0,0 +1,6 @@ +defmodule Solution do + @callback parse!(String.t()) :: Any + @callback get_name() :: String.t() + @callback solve_first_part(Any) :: String.t() + @callback solve_second_part(Any) :: String.t() +end diff --git a/2019-elixir/lib/solutions/day_01.ex b/2019-elixir/lib/solutions/day_01.ex new file mode 100644 index 0000000..853b742 --- /dev/null +++ b/2019-elixir/lib/solutions/day_01.ex @@ -0,0 +1,113 @@ +defmodule Aoc19.Solution.Day01 do + @name "Day 1: The Tyranny of the Rocket Equation" + @behaviour Solution + # when required fuel is below 9 no furher fuel is required (fuel_requirement(n) < 0) + @fuel_threshold 9 + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Parse raw input into a list of integers. + + ## Examples + + iex> Aoc19.Solution.Day01.parse!(\"\"\" + ...> 10 + ...> 299 + ...> 1040 + ...> \"\"\") + [10,299,1040] + + """ + def parse!(raw) do + raw + |> String.split() + |> Enum.map(&String.to_integer/1) + end + + @doc """ + Calculate required fuel for a module's mass. This formula + is also used to calculate the fuel required for the fuel + itself. + + Take mass, divide by three, round down, and subtract 2. + + ## Examples + + iex> Aoc19.Solution.Day01.fuel_requirement(12) + 2 + + iex> Aoc19.Solution.Day01.fuel_requirement(14) + 2 + + iex> Aoc19.Solution.Day01.fuel_requirement(1969) + 654 + + iex> Aoc19.Solution.Day01.fuel_requirement(100756) + 33583 + + """ + def fuel_requirement(thing) do + (thing |> div(3)) - 2 + end + + @doc """ + Calculate fuel requirements based on a module's mass, + including the fuel required by the fuel itself. + + ## Examples + + iex> Aoc19.Solution.Day01.total_fuel_requirement(12) + 2 + + iex> Aoc19.Solution.Day01.total_fuel_requirement(1969) + 966 + + iex> Aoc19.Solution.Day01.total_fuel_requirement(100756) + 50346 + + """ + def total_fuel_requirement(thing, requirements) when thing < @fuel_threshold do + Enum.sum(requirements) + end + + def total_fuel_requirement(thing, requirements) do + fuel = fuel_requirement(thing) + total_fuel_requirement(fuel, [fuel | requirements]) + end + + def total_fuel_requirement(mass) do + fuel = fuel_requirement(mass) + total_fuel_requirement(fuel, [fuel]) + end + + @impl Solution + @doc """ + Solution for the first part of "Day 1: The Tyranny of the Rocket Equation". + + ## Examples + + iex> Aoc19.Solution.Day01.solve_first_part([12, 14, 1969, 100756]) + 34241 + + """ + def solve_first_part(input) do + input |> Enum.map(&fuel_requirement/1) |> Enum.sum() + end + + @impl Solution + @doc """ + Solution for the second part of "Day 1: The Tyranny of the Rocket Equation". + + ## Examples + + iex> Aoc19.Solution.Day01.solve_second_part([12, 1969, 100756]) + 51314 + + """ + def solve_second_part(input) do + input |> Enum.map(&total_fuel_requirement/1) |> Enum.sum() + end +end diff --git a/2019-elixir/lib/solutions/day_02.ex b/2019-elixir/lib/solutions/day_02.ex new file mode 100644 index 0000000..4f6b17a --- /dev/null +++ b/2019-elixir/lib/solutions/day_02.ex @@ -0,0 +1,102 @@ +defmodule Aoc19.Solution.Day02 do + @name "Day 2: 1202 Program Alarm" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Parse raw input into a list of integers. + + ## Examples + + iex> Aoc19.Solution.Day02.parse!("3,7,13") + [3,7,13] + + """ + def parse!(raw) do + raw + |> String.split(",") + |> Enum.map(&String.to_integer/1) + end + + def operate(operator, a, b) when operator == 1, do: a + b + def operate(operator, a, b) when operator == 2, do: a * b + + @doc """ + A programmer tick. + + ## Examples + + iex> Aoc19.Solution.Day02.tick([1, 0, 0, 0, 99]) + [2, 0, 0, 0, 99] + + iex> Aoc19.Solution.Day02.tick([2, 3, 0, 3, 99]) + [2, 3, 0, 6, 99] + + iex> Aoc19.Solution.Day02.tick([2, 4, 4, 5, 99, 0]) + [2, 4, 4, 5, 99, 9801] + + iex> Aoc19.Solution.Day02.tick([1, 1, 1, 4, 99, 5, 6, 0, 99]) + [30, 1, 1, 4, 2, 5, 6, 0, 99] + + """ + def tick(state, _, 99), do: state + + def tick(state, pos, _) do + [operator, input_pos_a, input_pos_b, output_pos | _tail] = Enum.drop(state, pos) + + a = Enum.at(state, input_pos_a) + b = Enum.at(state, input_pos_b) + + state = + state + |> List.update_at(output_pos, fn _ -> operate(operator, a, b) end) + + tick(state, pos + 4, Enum.at(state, pos + 4)) + end + + def tick(state) do + tick(state, 0, Enum.at(state, 0)) + end + + def run_until_halt(program, noun, verb) do + program + |> List.update_at(1, fn _ -> noun end) + |> List.update_at(2, fn _ -> verb end) + |> tick() + |> List.first() + end + + def test_things(program, noun, verb) do + program + |> run_until_halt(noun, verb) + |> IO.inspect() + + {"19690720", noun, verb} |> IO.inspect() + + IO.gets("Again?") + test_things(program, noun + 1, verb) + end + + @impl Solution + @doc """ + Solution for the first part of "Day 2: 1202 Program Alarm". + + """ + def solve_first_part(program) do + program + |> run_until_halt(12, 2) + end + + @impl Solution + @doc """ + Solution for the second part of "Day 2: 1202 Program Alarm". + """ + def solve_second_part(_program) do + # program + # |> test_things(76, 21) + 100 * 76 + 21 + end +end diff --git a/2019-elixir/lib/solutions/day_03.ex b/2019-elixir/lib/solutions/day_03.ex new file mode 100644 index 0000000..64c5b3b --- /dev/null +++ b/2019-elixir/lib/solutions/day_03.ex @@ -0,0 +1,159 @@ +defmodule Aoc19.Solution.Day03 do + @name "Day 3: Crossed Wires" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Parse raw input into a list of paths. + + ## Examples + + iex> Aoc19.Solution.Day03.parse!("U3,R7\\nD11,L13") + [["U3", "R7"], ["D11", "L13"]] + + """ + def parse!(raw) do + raw + |> String.split() + |> Enum.map(fn s -> String.split(s, ",") end) + end + + @impl Solution + @doc """ + Solution for the first part of "Day 3: Crossed Wires". + + ## Examples + + iex> Aoc19.Solution.Day03.solve_first_part("R8,U5,L5,D3\\nU7,R6,D4,L4" |> Aoc19.Solution.Day03.parse!) + 6 + + iex> Aoc19.Solution.Day03.solve_first_part("R75,D30,R83,U83,L12,D49,R71,U7,L72\\nU62,R66,U55,R34,D71,R55,D58,R83" |> Aoc19.Solution.Day03.parse!) + 159 + + iex> Aoc19.Solution.Day03.solve_first_part("R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51\\nU98,R91,D20,R16,D67,R40,U7,R15,U6,R7" |> Aoc19.Solution.Day03.parse!) + 135 + + """ + def solve_first_part(input) do + [a, b] = + input + |> Enum.map(&get_wire/1) + |> Enum.map(&MapSet.new/1) + + MapSet.intersection(a, b) + |> Enum.map(&manhattan_distance/1) + |> Enum.min() + end + + @impl Solution + @doc """ + Solution for the second part of "Day 3: Crossed Wires". + + ## Examples + + iex> Aoc19.Solution.Day03.solve_second_part("R8,U5,L5,D3\\nU7,R6,D4,L4" |> Aoc19.Solution.Day03.parse!) + 30 + + iex> Aoc19.Solution.Day03.solve_second_part("R75,D30,R83,U83,L12,D49,R71,U7,L72\\nU62,R66,U55,R34,D71,R55,D58,R83" |> Aoc19.Solution.Day03.parse!) + 610 + + iex> Aoc19.Solution.Day03.solve_second_part("R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51\\nU98,R91,D20,R16,D67,R40,U7,R15,U6,R7" |> Aoc19.Solution.Day03.parse!) + 410 + + """ + def solve_second_part(input) do + wires = + input + |> Enum.map(&get_wire/1) + + ab = wires |> Enum.map(&Enum.count/1) |> Enum.sum() + [a, b] = wires + + MapSet.intersection(MapSet.new(a), MapSet.new(b)) + |> Enum.map(fn pos -> + {Enum.find_index(a, fn x -> x == pos end), Enum.find_index(b, fn x -> x == pos end)} + end) + |> Enum.map(fn {x, y} -> + ab - x - y + end) + |> Enum.min() + end + + @doc """ + Return a changeset (a tuple of x and y additions) for a defined direction. + """ + def direction("U"), do: {1, 0} + def direction("R"), do: {0, 1} + def direction("D"), do: {-1, 0} + def direction("L"), do: {0, -1} + + @doc """ + Get a wire's complete spread (in steps) from a list of directions. + + ## Examples + + iex> Aoc19.Solution.Day03.get_wire(["U7", "R6", "D4", "L4"]) + [{3, 2}, {3, 3}, {3, 4}, {3, 5}, {3, 6}, {4, 6}, {5, 6}, {6, 6}, {7, 6}, {7, 5}, {7, 4}, {7, 3}, {7, 2}, {7, 1}, {7, 0}, {6, 0}, {5, 0}, {4, 0}, {3, 0}, {2, 0}, {1, 0}] + + iex> Aoc19.Solution.Day03.get_wire(["R8", "U5", "L5", "D3"]) + [{2, 3}, {3, 3}, {4, 3}, {5, 3}, {5, 4}, {5, 5}, {5, 6}, {5, 7}, {5, 8}, {4, 8}, {3, 8}, {2, 8}, {1, 8}, {0, 8}, {0, 7}, {0, 6}, {0, 5}, {0, 4}, {0, 3}, {0, 2}, {0, 1}] + + """ + def get_wire(paths) do + paths + |> Enum.map(&steps/1) + |> List.flatten() + |> Enum.reduce( + {{0, 0}, []}, + fn {x1, y1}, {{x2, y2}, seen} -> + {{x1 + x2, y1 + y2}, [{x1 + x2, y1 + y2} | seen]} + end + ) + |> elem(1) + end + + @doc """ + Get the Manhattan distance from position {0, 0}. + + ## Examples + + iex> Aoc19.Solution.Day03.manhattan_distance({-300, 100}) + 400 + + iex> Aoc19.Solution.Day03.manhattan_distance({-20, -20}) + 40 + + iex> Aoc19.Solution.Day03.manhattan_distance({111, 111}) + 222 + + """ + def manhattan_distance({x, y}), do: abs(x) + abs(y) + + @doc """ + Get step changesets for a path. + + ## Examples + + iex> Aoc19.Solution.Day03.steps("U4") + [{1, 0}, {1, 0}, {1, 0}, {1, 0}] + + iex> Aoc19.Solution.Day03.steps("R2") + [{0, 1}, {0, 1}] + + iex> Aoc19.Solution.Day03.steps("D3") + [{-1, 0}, {-1, 0}, {-1, 0}] + + iex> Aoc19.Solution.Day03.steps("L1") + [{0, -1}] + + """ + def steps(path) do + {d, steps} = String.split_at(path, 1) + modifier = direction(d) + stop_at = String.to_integer(steps) + 1..stop_at |> Enum.map(fn _ -> modifier end) + end +end diff --git a/2019-elixir/lib/solutions/day_04.ex b/2019-elixir/lib/solutions/day_04.ex new file mode 100644 index 0000000..ba30c60 --- /dev/null +++ b/2019-elixir/lib/solutions/day_04.ex @@ -0,0 +1,126 @@ +defmodule Aoc19.Solution.Day04 do + @name "Day 4: Secure Container" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Get the range of numbers from the input. + + ## Examples + + iex> Aoc19.Solution.Day04.parse!("1-4") + 1..4 + + iex> Aoc19.Solution.Day04.parse!("4-32") + 4..32 + """ + def parse!(str) do + [start, stop] = + str + |> String.split("-") + |> Enum.map(&String.to_integer/1) + + start..stop + end + + @impl Solution + @doc """ + Solution for the first part of "Day 4: Secure Container". + """ + def solve_first_part(range), + do: range |> Stream.filter(&matches?/1) |> Enum.count() + + @impl Solution + @doc """ + Solution for the second part of "Day 4: Secure Container". + """ + def solve_second_part(range), + do: + range + |> Stream.filter(&matches?/1) + |> Stream.filter(&larger_groups?/1) + |> Enum.count() + + @doc """ + Check if password matches all initital criterias. + + ## Examples + + iex> Aoc19.Solution.Day04.matches?(8) + true + + iex> Aoc19.Solution.Day04.matches?(12) + true + + iex> Aoc19.Solution.Day04.matches?(1123) + true + + iex> Aoc19.Solution.Day04.matches?(22222) + true + + iex> Aoc19.Solution.Day04.matches?(122345) + true + + iex> Aoc19.Solution.Day04.matches?(111123) + true + + iex> Aoc19.Solution.Day04.matches?(223450) + false + + iex> Aoc19.Solution.Day04.matches?(123789) + false + + """ + def matches?(password) do + [a, b, c, d, e, f] = number_to_password(password) |> String.codepoints() + + (a == b or b == c or c == d or d == e or e == f) and + (a <= b and b <= c and c <= d and d <= e and e <= f) + end + + @doc """ + Check if password digit pairs are not part of a larget group of digits. + + ## Examples + + iex> Aoc19.Solution.Day04.larger_groups?(112233) + true + + iex> Aoc19.Solution.Day04.larger_groups?(123444) + false + + iex> Aoc19.Solution.Day04.larger_groups?(111122) + true + + """ + def larger_groups?(password) do + washed = Regex.replace(~r/(\d)\1\1+/, number_to_password(password), "") + + case Regex.run(~r/(\d)\1/, washed) do + nil -> false + _ -> true + end + end + + @doc """ + Takes an integer and turns it to a password by making it a string + and left pad it with zeros. + + ## Examples + + iex> Aoc19.Solution.Day04.number_to_password(8) + "000008" + + iex> Aoc19.Solution.Day04.number_to_password(123) + "000123" + + iex> Aoc19.Solution.Day04.number_to_password(123123) + "123123" + + """ + def number_to_password(n), + do: Integer.to_string(n) |> String.pad_leading(6, "0") +end diff --git a/2019-elixir/lib/solutions/day_05.ex b/2019-elixir/lib/solutions/day_05.ex new file mode 100644 index 0000000..bd12c79 --- /dev/null +++ b/2019-elixir/lib/solutions/day_05.ex @@ -0,0 +1,320 @@ +defmodule Aoc19.Solution.Day05 do + @name "Day 5: Sunny with a Chance of Asteroids" + @behaviour Solution + @input_operation_value 1 + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Parse raw input into a list of integers. + + ## Examples + + iex> Aoc19.Solution.Day02.parse!("3,7,13") + [3, 7, 13] + + """ + def parse!(raw) do + raw + |> String.split(",") + |> Enum.map(&String.to_integer/1) + end + + @impl Solution + @doc """ + Solution for the first part of "Day 5: Sunny with a Chance of Asteroids". + """ + def solve_first_part(input) do + tick(input) + 16_434_972 + end + + @impl Solution + @doc """ + Solution for the second part of "Day 5: Sunny with a Chance of Asteroids". + + ## Examples + + iex> Aoc19.Solution.Day05.solve_second_part("10") + "(TBW)" + + """ + def solve_second_part(_input), do: "(TBW)" + + @doc """ + operate! + + ## Examples + + iex> Aoc19.Solution.Day05.operate(1, [2, 5]) + 7 + + iex> Aoc19.Solution.Day05.operate(1, [2, 5, 3]) + 10 + + iex> Aoc19.Solution.Day05.operate(1, [2, 5, 3, 8]) + 18 + + iex> Aoc19.Solution.Day05.operate(2, [2, 5]) + 10 + + iex> Aoc19.Solution.Day05.operate(2, [2, 5, 3]) + 30 + + iex> Aoc19.Solution.Day05.operate(2, [2, 5, 3, 8]) + 240 + + iex> Aoc19.Solution.Day05.operate(3, "whatever") + 1 + + """ + def operate(1, [a, b]), do: a + b + def operate(1, terms), do: Enum.reduce(terms, fn t, acc -> t + acc end) + def operate(2, [a, b]), do: a * b + def operate(2, terms), do: Enum.reduce(terms, fn t, acc -> t * acc end) + def operate(3, _), do: @input_operation_value + + @doc """ + Do stuff and move forward in program. + """ + def tick(program, pos) do + cond do + Enum.at(program, pos) > 100 -> + parameter_mode(program, pos) |> (fn {program, pos} -> tick(program, pos) end).() + + Enum.at(program, pos) in [1, 2] -> + position_mode(program, pos) |> tick(pos + 4) + + Enum.at(program, pos) == 3 -> + input_mode(program, pos) |> tick(pos + 2) + + Enum.at(program, pos) in [4, 104] -> + output_mode(program, pos) |> tick(pos + 2) + + Enum.at(program, pos) == 99 -> + program + end + end + + def tick(program), do: tick(program, 0) + + @doc """ + collect params for params mode. + + ## Examples + + iex> Aoc19.Solution.Day05.get_params([4, 3, 4, 33], 3) + [4, 3, 4] + + iex> Aoc19.Solution.Day05.get_params([8, 5, 6, 1, 7, 23, 2, 13, 99], 3) + [6, 5, 8] + + iex> Aoc19.Solution.Day05.get_params([8, 5, 6, 3, 7, 23, 2, 13, 99], 3) + [6, 5, 8] + + iex> Aoc19.Solution.Day05.get_params([8, 5, 6, 4, 7, 23, 2, 13, 99], 3) + [6, 5, 8] + + """ + def get_params([], seen, _len), do: seen + + def get_params(_, seen, len, stop) when len == stop, do: seen + + def get_params([n | inputs], seen, len, stop), do: get_params(inputs, [n | seen], len + 1, stop) + def get_params([n | inputs], stop), do: get_params(inputs, [n], 1, stop) + + @doc """ + Get instruction as 4-digit number. + + ## Examples + + iex> Aoc19.Solution.Day05.instruction(102) + "0102" + + iex> Aoc19.Solution.Day05.instruction(1102) + "1102" + + """ + def instruction(value) when value < 104, + do: value |> Integer.to_string() |> String.pad_leading(4, "0") + + def instruction(value), do: value |> Integer.to_string() + + @doc """ + Parameter mode. + + ## Examples + + iex> Aoc19.Solution.Day05.parameter_mode([1002, 4, 3, 4, 33], 0) + {[1002, 4, 3, 4, 99], 4} + + iex> Aoc19.Solution.Day05.parameter_mode([104, 4, 3, 4, 33], 0) + {[104, 4, 3, 4, 33], 2} + + """ + def parameter_mode(program, pos) do + instruction = instruction(Enum.at(program, pos)) + + count = (instruction |> String.length()) - 1 + + [output_pos | params] = Enum.drop(program, pos + 1) |> get_params(count) + + {opcode, modes} = decide(instruction, length(params)) + + inputs = + Enum.zip(modes, params) + |> Enum.map(fn {mode, param} -> posact(program, mode, param) end) + + if opcode == 4 do + inputs |> Enum.at(0) |> IO.inspect() + {program, pos + 2} + else + {List.update_at(program, output_pos, fn _ -> operate(opcode, inputs) end), pos + 4} + end + end + + def parameter_mode(program), do: parameter_mode(program, 0) + + @doc """ + Position mode. + + ## Examples + + iex> Aoc19.Solution.Day05.position_mode([2, 4, 3, 3, 99], 0) + [2, 4, 3, 297, 99] + + iex> Aoc19.Solution.Day05.position_mode([1, 0, 0, 0, 99]) + [2, 0, 0, 0, 99] + + iex> Aoc19.Solution.Day05.position_mode([2, 3, 0, 3, 99]) + [2, 3, 0, 6, 99] + + iex> Aoc19.Solution.Day05.position_mode([2, 4, 4, 5, 99, 0]) + [2, 4, 4, 5, 99, 9801] + + iex> Aoc19.Solution.Day05.tick([1, 1, 1, 4, 99, 5, 6, 0, 99]) + [30, 1, 1, 4, 2, 5, 6, 0, 99] + + + """ + def position_mode(program, pos) do + [opcode, input_pos_a, input_pos_b, output_pos | _tail] = Enum.drop(program, pos) + + a = Enum.at(program, input_pos_a) + b = Enum.at(program, input_pos_b) + + List.update_at(program, output_pos, fn _ -> operate(opcode, [a, b]) end) + end + + def position_mode(program), do: position_mode(program, 0) + + @doc """ + input mode + + ## Examples + + iex> Aoc19.Solution.Day05.input_mode([3, 2, 0], 0) + [3, 2, 1] + + iex> Aoc19.Solution.Day05.input_mode([3, 2, 0]) + [3, 2, 1] + + """ + def input_mode(program, pos) do + # id = IO.gets("Fan vill du?") |> String.trim() |> String.to_integer() + id = @input_operation_value + + List.update_at( + program, + Enum.at(program, pos + 1), + fn _ -> id end + ) + end + + def input_mode(program), do: input_mode(program, 0) + + @doc """ + output mode + + ## Examples + + iex> Aoc19.Solution.Day05.output_mode([4, 2, 66], 0) + [4, 2, 66] + + iex> Aoc19.Solution.Day05.output_mode([4, 0, 0]) + [4, 0, 0] + + """ + def output_mode(program, pos) do + instruction = Enum.at(program, pos) + value = Enum.at(program, pos + 1) + + case instruction do + 4 -> Enum.at(program, value) |> IO.inspect() + 104 -> value |> IO.inspect() + end + + program + end + + def output_mode(program), do: output_mode(program, 0) + + @doc """ + Get a value from a list of numbers. + in position mode (0), the param is the index of the value from the list. + In itermediate mode (1), the param is the value. + + ## Examples + + iex> Aoc19.Solution.Day05.posact([3, 7, 100, 13], 0, 2) + 100 + + iex> Aoc19.Solution.Day05.posact([], 1, 4) + 4 + """ + def posact(program, 0, param), do: Enum.at(program, param) + def posact(_program, 1, param), do: param + + @doc """ + returns opcode as integer. + + ## Examples + + iex> Aoc19.Solution.Day05.opcode(1, 2) + 12 + + iex> Aoc19.Solution.Day05.opcode(0, 3) + 3 + """ + def opcode(o, c), do: "#{o}#{c}" |> String.to_integer() + + @doc """ + Parse intruction to opcode, and 2-3 param modes. + + ## Examples + + iex> Aoc19.Solution.Day05.decide("1002", 2) + {2, [1, 0]} + + iex> Aoc19.Solution.Day05.decide("1101", 2) + {1, [1, 1]} + + iex> Aoc19.Solution.Day05.decide("1012", 2) + {12, [1, 0]} + + iex> Aoc19.Solution.Day05.decide("1012", 4) + {12, [0, 0, 1, 0]} + """ + def decide(n, len) do + [c, o | modes] = + n + |> String.reverse() + |> String.pad_trailing(len + 2, "0") + |> String.to_integer() + |> Integer.digits() + + {opcode(o, c), modes |> Enum.reverse()} + end +end diff --git a/2019-elixir/lib/solutions/day_06.ex b/2019-elixir/lib/solutions/day_06.ex new file mode 100644 index 0000000..ec2def9 --- /dev/null +++ b/2019-elixir/lib/solutions/day_06.ex @@ -0,0 +1,210 @@ +defmodule Aoc19.Solution.Day06 do + @name "Day 6: Universal Orbit Map" + @behaviour Solution + @you "YOU" + @santa "SAN" + + @impl Solution + def get_name, do: @name + + @impl Solution + @doc """ + Parse raw input into a list of strings. + + ## Examples + + iex> Aoc19.Solution.Day06.parse!(\"\"\" + ...> X)5 + ...> Y)13 + ...> ZW)1 + ...> \"\"\") + [["X", "5"], ["Y", "13"], ["ZW", "1"]] + + """ + def parse!(raw) do + raw + |> String.split() + |> Enum.map(fn s -> String.split(s, ")") end) + end + + @impl Solution + @doc """ + Solution for the first part of "Day 6: Universal Orbit Map". + + ## Examples + + iex> \"\"\" + ...> COM)B + ...> B)C + ...> C)D + ...> D)E + ...> E)F + ...> B)G + ...> G)H + ...> D)I + ...> E)J + ...> J)K + ...> K)L + ...> \"\"\" |> Aoc19.Solution.Day06.parse!() |> Aoc19.Solution.Day06.solve_first_part() + 42 + + """ + def solve_first_part(input) do + input + |> orbit_tree + |> Map.values() + |> Enum.map(fn l -> length(l) end) + |> Enum.sum() + end + + @impl Solution + @doc """ + Solution for the second part of "Day 6: Universal Orbit Map". + + ## Examples + + iex> \"\"\" + ...> COM)B + ...> B)C + ...> C)D + ...> D)E + ...> E)F + ...> B)G + ...> G)H + ...> D)I + ...> E)J + ...> J)K + ...> K)L + ...> K)YOU + ...> I)SAN + ...> \"\"\" |> Aoc19.Solution.Day06.parse!() |> Aoc19.Solution.Day06.solve_second_part() + 4 + """ + def solve_second_part(input) do + santa = parent_branch(input, @santa) + you = parent_branch(input, @you) + + common = MapSet.intersection(MapSet.new(santa), MapSet.new(you)) |> Enum.to_list() + + nearest(common, you) + nearest(common, santa) + end + + def orbit_tree(input) do + orbits = + input |> Enum.reduce(%{}, fn [parent, child], acc -> children(acc, parent, child) end) + + orbits + |> Map.keys() + |> Enum.reduce(orbits, fn k, acc -> Map.get(acc, k, []) |> grandchildren(acc, k) end) + end + + @doc """ + Add grandchildren to branch if parent is present. + + ## Examples + + iex> Aoc19.Solution.Day06.children(%{a: [3, 2]}, :a, 5) + %{a: [5, 3, 2]} + + """ + def children(acc, parent, child) do + if Map.has_key?(acc, parent) do + Map.put(acc, parent, [child | Map.get(acc, parent, child)]) + else + Map.put(acc, parent, [child]) + end + end + + @doc """ + Add all grandchildren to the orbit tree. + + ## Examples + + iex> Aoc19.Solution.Day06.grandchildren([5, 6], %{a: [3, 2], b: [5]}, 2) + %{a: [3, 2, 5, 6], b: [5]} + + """ + def grandchildren(children, orbits, parent) do + values = + orbits + |> Map.values() + |> Enum.map(fn grandchildren -> append_grandchildren(grandchildren, parent, children) end) + + keys = Map.keys(orbits) + + Enum.zip(keys, values) + |> Map.new() + end + + @doc """ + add grandchildren to existing branch in orbit tree. + + ## Examples + + iex> Aoc19.Solution.Day06.append_grandchildren([0, 1, 2], 1, [3, 4]) + [0, 1, 2, 3, 4] + + """ + def append_grandchildren(children, parent, grandchildren) do + if Enum.member?(children, parent) do + [children | grandchildren] |> List.flatten() + else + children + end + end + + @doc """ + Add all grandchildren to the orbit tree. + + ## Examples + + iex> Aoc19.Solution.Day06.parent_branch([ + ...> ["13", "3"], + ...> ["1", "7"], + ...> ["7", "13"] + ...> ], "13") + ["1", "7"] + + iex> Aoc19.Solution.Day06.parent_branch([ + ...> ["13", "3"], + ...> ["1", "7"], + ...> ["7", "13"] + ...> ], "7") + ["1"] + + iex> Aoc19.Solution.Day06.parent_branch([ + ...> ["13", "3"], + ...> ["1", "7"], + ...> ["7", "13"] + ...> ], "3") + ["1", "7", "13"] + + """ + def parent_branch(orbits, subject, seen) do + case Enum.filter(orbits, fn [_parent, child] -> child == subject end) do + [] -> seen |> List.delete_at(-1) + [[parent, _child]] -> parent_branch(orbits, parent, [parent | seen]) + end + end + + def parent_branch(orbits, subject) do + parent_branch(orbits, subject, [subject]) + end + + @doc """ + Find nearest parent in branch from a set of positions. + + ## Examples + + iex> Aoc19.Solution.Day06.nearest(["1", "2"], ["0", "1", "2", "3", "4"]) + 2 + + """ + def nearest(positions, branch) do + len = length(branch) + + positions + |> Enum.map(fn p -> len - Enum.find_index(branch, fn x -> x == p end) - 1 end) + |> Enum.min() + end +end diff --git a/2019-elixir/lib/solutions/day_08.ex b/2019-elixir/lib/solutions/day_08.ex new file mode 100644 index 0000000..2fcc41f --- /dev/null +++ b/2019-elixir/lib/solutions/day_08.ex @@ -0,0 +1,217 @@ +defmodule Aoc19.Solution.Day08 do + @name "Day 8: Space Image Format" + @behaviour Solution + @image_width 25 + @image_height 6 + @black " " + @white "X" + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(str), do: str + + @impl Solution + @doc """ + Solution for the first part of "Day 8: Space Image Format". + """ + def solve_first_part(data), do: data |> _solve_first_part(@image_width, @image_height) + + @impl Solution + @doc """ + Solution for the second part of "Day 8: Space Image Format". + """ + def solve_second_part(data) do + data |> _solve_second_part(@image_width, @image_height) |> reveal(@image_width) + end + + @doc """ + Actual solution for part 1, but with the ability to test. + + ## Examples + + iex> Aoc19.Solution.Day08._solve_first_part("123456789012", 3, 2) + 1 + + """ + def _solve_first_part(data, image_width, image_height) do + data + |> layers(image_width, image_height) + |> fewest_zeros + |> multiply() + end + + @doc """ + Actual solution for part 2, but with the ability to test. + + ## Examples + + iex> Aoc19.Solution.Day08._solve_second_part("0222112222120000", 2, 2) + [" ", "X", "X", " "] + + """ + def _solve_second_part(data, image_width, image_height) do + data + |> layers(image_width, image_height) + |> pixels() + |> colors + end + + @doc """ + get layers from image data. + + ## Examples + + iex> Aoc19.Solution.Day08.layers("123456789012", 3, 2) + ["123456", "789012"] + + iex> Aoc19.Solution.Day08.layers("0222112222120000", 2, 2) + ["0222", "1122", "2212", "0000"] + + """ + def layers(data, x, y) do + data + |> String.codepoints() + |> Enum.chunk_every(x * y) + |> Enum.map(&Enum.join/1) + end + + @doc """ + Get layers with the fewest zeros. + + ## Examples + + iex> Aoc19.Solution.Day08.fewest_zeros(["123456", "789012"]) + "123456" + + iex> Aoc19.Solution.Day08.fewest_zeros(["103406", "789012"]) + "789012" + + """ + def fewest_zeros(layers) do + layers + |> Enum.map(fn layer -> {layer, layer} end) + |> Enum.map(fn {layer, data} -> + {layer, data |> String.replace("0", "") |> String.length()} + end) + |> Enum.sort(fn a, b -> elem(a, 1) > elem(b, 1) end) + |> Enum.at(0) + |> elem(0) + end + + @doc """ + return the number of 1 digits multiplied by the number of 2 digits of a layer. + + ## Examples + + iex> Aoc19.Solution.Day08.multiply("121226") + 6 + + iex> Aoc19.Solution.Day08.multiply("293416") + 1 + + """ + def multiply(layer) do + {x, y} = + layer + |> String.codepoints() + |> Enum.map(&String.to_integer/1) + |> Enum.reduce({0, 0}, fn + 1, {x, y} -> {x + 1, y} + 2, {x, y} -> {x, y + 1} + _, acc -> acc + end) + + x * y + end + + @doc """ + Regroup a set of layers to pixels. + + ## Examples + + iex> Aoc19.Solution.Day08.pixels(["0222", "1122", "2212", "0000"]) + [[0, 1, 2, 0], [2, 1, 2, 0], [2, 2, 1, 0], [2, 2, 2, 0]] + + iex> Aoc19.Solution.Day08.pixels(["123456", "789012"]) + [[1, 7], [2, 8], [3, 9], [4, 0], [5, 1], [6, 2]] + + """ + def pixels(_layers, {count, _depth}, pixels, pos) when pos > count, + do: pixels |> Enum.reverse() + + def pixels(layers, {count, depth}, pixels, pos) do + pixel = + 0..depth + |> Enum.map(fn layer -> + layers + |> Enum.at(layer) + |> Enum.at(pos) + |> String.to_integer() + end) + + pixels(layers, {count, depth}, [pixel | pixels], pos + 1) + end + + def pixels(layers) do + count = layers |> Enum.at(0) |> String.length() + depth = layers |> length + + pixels( + layers |> Enum.map(&String.codepoints/1), + {count - 1, depth - 1}, + [], + 0 + ) + end + + def colors(pixels) do + pixels + |> Enum.map(&color/1) + end + + @doc """ + determine color of pixel based on layer. + + ## Examples + + iex> Aoc19.Solution.Day08.color([0, 1, 2, 0]) + " " + + iex> Aoc19.Solution.Day08.color([2, 1, 2, 0]) + "X" + + iex> Aoc19.Solution.Day08.color([2, 2, 1, 0]) + "X" + + iex> Aoc19.Solution.Day08.color([2, 2, 2, 0]) + " " + + iex> Aoc19.Solution.Day08.color([2, 2, 2, 2, 1, 2, 2, 0]) + "X" + + """ + def color([2 | queue]), do: color(queue) + def color([1 | _]), do: @white + def color([0 | _]), do: @black + + @doc """ + determine color of pixel based on layer. + + ## Examples + + iex> Aoc19.Solution.Day08.reveal(["X", " ", " ", "X"], 2) + "\\nX \\n X" + + """ + def reveal(pixels, breakpoint) do + output = + pixels + |> Enum.chunk_every(breakpoint) + |> Enum.map(&Enum.join/1) + |> Enum.join("\n") + + "\n" <> output + end +end diff --git a/2019-elixir/mix.exs b/2019-elixir/mix.exs new file mode 100644 index 0000000..5a0355a --- /dev/null +++ b/2019-elixir/mix.exs @@ -0,0 +1,28 @@ +defmodule Aoc19.MixProject do + use Mix.Project + + def project do + [ + app: :aoc19, + version: "0.1.0", + elixir: "~> 1.9", + 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/2019-elixir/test/aoc19_test.exs b/2019-elixir/test/aoc19_test.exs new file mode 100644 index 0000000..7ed1fbf --- /dev/null +++ b/2019-elixir/test/aoc19_test.exs @@ -0,0 +1,4 @@ +defmodule Aoc19Test do + use ExUnit.Case + doctest Aoc19 +end diff --git a/2019-elixir/test/day_01_test.exs b/2019-elixir/test/day_01_test.exs new file mode 100644 index 0000000..eb512be --- /dev/null +++ b/2019-elixir/test/day_01_test.exs @@ -0,0 +1,4 @@ +defmodule Day01Test do + use ExUnit.Case + doctest Aoc19.Solution.Day01 +end diff --git a/2019-elixir/test/day_02_test.exs b/2019-elixir/test/day_02_test.exs new file mode 100644 index 0000000..e080a25 --- /dev/null +++ b/2019-elixir/test/day_02_test.exs @@ -0,0 +1,4 @@ +defmodule Day02Test do + use ExUnit.Case + doctest Aoc19.Solution.Day02 +end diff --git a/2019-elixir/test/day_03_test.exs b/2019-elixir/test/day_03_test.exs new file mode 100644 index 0000000..e914475 --- /dev/null +++ b/2019-elixir/test/day_03_test.exs @@ -0,0 +1,4 @@ +defmodule Day03Test do + use ExUnit.Case + doctest Aoc19.Solution.Day03 +end diff --git a/2019-elixir/test/day_04_test.exs b/2019-elixir/test/day_04_test.exs new file mode 100644 index 0000000..b2e558e --- /dev/null +++ b/2019-elixir/test/day_04_test.exs @@ -0,0 +1,4 @@ +defmodule Day04Test do + use ExUnit.Case + doctest Aoc19.Solution.Day04 +end diff --git a/2019-elixir/test/day_05_test.exs b/2019-elixir/test/day_05_test.exs new file mode 100644 index 0000000..19282b1 --- /dev/null +++ b/2019-elixir/test/day_05_test.exs @@ -0,0 +1,4 @@ +defmodule Day05Test do + use ExUnit.Case + doctest Aoc19.Solution.Day05 +end diff --git a/2019-elixir/test/day_06_test.exs b/2019-elixir/test/day_06_test.exs new file mode 100644 index 0000000..56aff4b --- /dev/null +++ b/2019-elixir/test/day_06_test.exs @@ -0,0 +1,4 @@ +defmodule Day06Test do + use ExUnit.Case + doctest Aoc19.Solution.Day06 +end diff --git a/2019-elixir/test/day_08_test.exs b/2019-elixir/test/day_08_test.exs new file mode 100644 index 0000000..8be4333 --- /dev/null +++ b/2019-elixir/test/day_08_test.exs @@ -0,0 +1,4 @@ +defmodule Day08Test do + use ExUnit.Case + doctest Aoc19.Solution.Day08 +end diff --git a/2019-elixir/test/test_helper.exs b/2019-elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2019-elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/2020-elixir/.formatter.exs b/2020-elixir/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/2020-elixir/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/2020-elixir/.gitignore b/2020-elixir/.gitignore new file mode 100644 index 0000000..c203d4c --- /dev/null +++ b/2020-elixir/.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 + +# Ignore ElixirLS VS Code extension +.elixir_ls \ No newline at end of file diff --git a/2020-elixir/.tool-versions b/2020-elixir/.tool-versions new file mode 100644 index 0000000..8c00e8a --- /dev/null +++ b/2020-elixir/.tool-versions @@ -0,0 +1 @@ +elixir 1.11.1 \ No newline at end of file diff --git a/2020-elixir/README.md b/2020-elixir/README.md new file mode 100644 index 0000000..e0b58fa --- /dev/null +++ b/2020-elixir/README.md @@ -0,0 +1,29 @@ +# Aoc + +Solutions for [Advent of Code 2020][1], this year in Elixir. + +[Elixir 1.11.1][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/2020-elixir/lib/aoc.ex b/2020-elixir/lib/aoc.ex new file mode 100644 index 0000000..928d3f6 --- /dev/null +++ b/2020-elixir/lib/aoc.ex @@ -0,0 +1,58 @@ +defmodule Aoc do + @year 2020 + 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_first_part, [data]) + second_solution = apply(solution_module, :solve_second_part, [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/2020-elixir/lib/mix/tasks/bootstrap.ex b/2020-elixir/lib/mix/tasks/bootstrap.ex new file mode 100644 index 0000000..ea334f6 --- /dev/null +++ b/2020-elixir/lib/mix/tasks/bootstrap.ex @@ -0,0 +1,92 @@ +defmodule Mix.Tasks.Aoc.New do + use Mix.Task + + @shortdoc "Bootstrap new solution" + @impl Mix.Task + + @year 2020 + + def run([n, name]) do + id = n |> String.pad_leading(2, "0") + + input_file = "./inputs/" <> id <> ".in" + test_file = "./test/day_" <> id <> "_test.exs" + solution_file = "./lib/solutions/day_" <> id <> ".ex" + + IO.puts("Creating " <> input_file) + File.touch(input_file) + + IO.puts("Creating " <> test_file) + File.write!(test_file, test_file_content(id)) + + 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) do + """ + defmodule Day#{id}Test do + use ExUnit.Case + doctest Aoc.Solution.Day#{id} + import Aoc.Solution.Day#{id} + + test "parses the input" do + expected = 10 + + assert parse!("10") == expected + end + + test "solves first part" do + a = "something" |> parse!() |> solve_first_part() + + assert a == :something + end + + test "solves second part" do + a = "something" |> parse!() |> solve_second_part() + + assert a == :something + 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 + "10" + end + + @impl Solution + def solve_first_part(_input) do + "(TBW)" + end + + @impl Solution + def solve_second_part(_input) do + "(TBW)" + end + end + """ + end +end diff --git a/2020-elixir/lib/mix/tasks/solve.ex b/2020-elixir/lib/mix/tasks/solve.ex new file mode 100644 index 0000000..9cdd082 --- /dev/null +++ b/2020-elixir/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/2020-elixir/lib/mix/tasks/solve_all.ex b/2020-elixir/lib/mix/tasks/solve_all.ex new file mode 100644 index 0000000..7fd5a6d --- /dev/null +++ b/2020-elixir/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/2020-elixir/lib/solution.ex b/2020-elixir/lib/solution.ex new file mode 100644 index 0000000..d1f8d8f --- /dev/null +++ b/2020-elixir/lib/solution.ex @@ -0,0 +1,6 @@ +defmodule Solution do + @callback parse!(String.t()) :: Any + @callback get_name() :: String.t() + @callback solve_first_part(Any) :: String.t() + @callback solve_second_part(Any) :: String.t() +end diff --git a/2020-elixir/lib/solutions/day_01.ex b/2020-elixir/lib/solutions/day_01.ex new file mode 100644 index 0000000..42c9742 --- /dev/null +++ b/2020-elixir/lib/solutions/day_01.ex @@ -0,0 +1,52 @@ +defmodule Aoc.Solution.Day01 do + @name "Day 1: Day 1: Report Repair" + @behaviour Solution + + @impl Solution + def get_name, do: @name + + @impl Solution + def parse!(raw) do + raw |> String.split() |> Enum.map(&String.to_integer/1) + end + + @impl Solution + def solve_first_part(input) do + {x, y} = input |> permutations(2) |> find_combination + + x * y + end + + @impl Solution + def solve_second_part(input) do + {x, y, z} = input |> permutations(3) |> find_combination + + x * y * z + end + + defp find_combination([[x, y] | queue]) do + case x + y do + 2020 -> {x, y} + _ -> find_combination(queue) + end + end + + defp find_combination([[x, y, z] | queue]) do + case x + y + z do + 2020 -> {x, y, z} + _ -> find_combination(queue) + end + end + + # found at: https://stackoverflow.com/questions/33756396/how-can-i-get-permutations-of-a-list + def permutations(_chars, building, 0) do + [building] + end + + def permutations(values, building, num) do + Stream.map(values, fn v -> building ++ [v] end) + |> Enum.flat_map(fn building -> permutations(values, building, num - 1) end) + end + + def permutations(values, num), do: permutations(values, [], num) +end diff --git a/2020-elixir/mix.exs b/2020-elixir/mix.exs new file mode 100644 index 0000000..54f358b --- /dev/null +++ b/2020-elixir/mix.exs @@ -0,0 +1,28 @@ +defmodule Aoc.MixProject do + use Mix.Project + + def project do + [ + app: :Aoc, + version: "0.1.0", + elixir: "~> 1.10", + 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/2020-elixir/test/aoc_test.exs b/2020-elixir/test/aoc_test.exs new file mode 100644 index 0000000..659caac --- /dev/null +++ b/2020-elixir/test/aoc_test.exs @@ -0,0 +1,4 @@ +defmodule AocTest do + use ExUnit.Case + doctest Aoc +end diff --git a/2020-elixir/test/day_01_test.exs b/2020-elixir/test/day_01_test.exs new file mode 100644 index 0000000..781fb75 --- /dev/null +++ b/2020-elixir/test/day_01_test.exs @@ -0,0 +1,47 @@ +defmodule Day01Test do + use ExUnit.Case + doctest Aoc.Solution.Day01 + import Aoc.Solution.Day01 + + test "parses the input" do + input = """ + 1721 + 979 + """ + + expected = [1721, 979] + + assert parse!(input) == expected + end + + test "solves first part" do + input = """ + 1721 + 979 + 366 + 299 + 675 + 1456 + """ + + a = input |> parse!() |> solve_first_part() + + assert a == 514_579 + end + + @tag :skip + test "solves second part" do + input = """ + 1721 + 979 + 366 + 299 + 675 + 1456 + """ + + a = input |> parse!() |> solve_second_part() + + assert a == 130_933_530 + end +end diff --git a/2020-elixir/test/test_helper.exs b/2020-elixir/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/2020-elixir/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/2020-python/.github/workflows/test-and-linters.yml b/2020-python/.github/workflows/test-and-linters.yml new file mode 100644 index 0000000..7a3786a --- /dev/null +++ b/2020-python/.github/workflows/test-and-linters.yml @@ -0,0 +1,38 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Run tests and linters + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Verify Blackness + uses: jpetrucciani/black-check@20.8b1 + - name: Test with pytest + run: | + pytest diff --git a/2020-python/.gitignore b/2020-python/.gitignore new file mode 100644 index 0000000..c8a405d --- /dev/null +++ b/2020-python/.gitignore @@ -0,0 +1,4 @@ +.idea +*.pyc +__pycache__ +.vscode diff --git a/2020-python/.python-version b/2020-python/.python-version new file mode 100644 index 0000000..1981190 --- /dev/null +++ b/2020-python/.python-version @@ -0,0 +1 @@ +3.8.0 diff --git a/2020-python/README.md b/2020-python/README.md new file mode 100644 index 0000000..21f75f8 --- /dev/null +++ b/2020-python/README.md @@ -0,0 +1,29 @@ +Advent of Code 2020 +=================== + +Solutions for #aoc2020 in Python 3 (3.7+). + +Help scripts +------------ + +Solve all puzzles: + + python aoc.py + +To bootstrap a new puzzle (creates `inputs/.txt`, `solutions/day_.py` och +`tests/test_day_.py`): + + python aoc.py "" + +Manually copy the puzzle input from https://adventofcode.com and paste it in `inputs/.txt` +to start coding. + +Solve separate puzzle (replace `XX` with the puzzle number): + + python -m solutions.day_XX + +Run tests (replace `XX` with the puzzle number): + + python -m unittest --locals -v + # or, if `pytest` is preferred: + pytest \ No newline at end of file diff --git a/2020-python/aoc.py b/2020-python/aoc.py new file mode 100644 index 0000000..5c5e73e --- /dev/null +++ b/2020-python/aoc.py @@ -0,0 +1,113 @@ +import sys + +year = 2020 + +try: + _, day_no, name = sys.argv +except ValueError: + day_no = None + name = None + +print( + f"\nAdvent of Code {year}" + "\n###################" + "\n\nby Anders Ytterström (@madr_se)" +) + +if day_no and name: + print(f"\n- creating solutions/day_{day_no.zfill(2)}.py") + with open("solutions/day_{}.py".format(day_no.zfill(2)), "w") as s: + s.write( + """ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "{day_no}.txt" + + def __str__(self): + return "Day {day}: {name}" + + def parse_input(self, data): + return data + + def solve(self, puzzle_input): + return True + + def solve_again(self, puzzle_input): + return True + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() +""".strip().format( + day=day_no, day_no=day_no.zfill(2), name=name + ) + + "\n" + ) + print(f"- creating tests/test_day_{day_no.zfill(2)}.py") + with open("tests/test_day_{}.py".format(day_no.zfill(2)), "w") as t: + t.write( + """ +import unittest + +from solutions.day_{day_no} import Solution + + +class Day{day_no}TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + \"\"\" + + \"\"\" + ) + + def test_parse_puzzle_input(self): + data = \"\"\" + + \"\"\" + assert self.solution.parse_input(data) == "" + + # def test_solve_first_part(self): + # assert self.solution.solve(self.puzzle_input) == True + + # def test_solve_second_part(self): + # assert self.solution.solve_again(self.puzzle_input) == True + + +if __name__ == "__main__": + unittest.main() +""".strip().format( + day_no=day_no.zfill(2) + ) + + "\n" + ) + print(f"- creating empty inputs/{day_no.zfill(2)}.txt") + with open("inputs/{}.txt".format(day_no.zfill(2)), "w") as i: + i.write("") + + print( + f""" +Done! start coding. + +Puzzle link: +https://adventofcode.com/{year}/day/{day_no} + +Puzzle input (copy and paste to inputs/{day_no.zfill(2)}.txt): +https://adventofcode.com/{year}/day/{day_no}/input + """ + ) + exit(0) + +for i in [str(n).zfill(2) for n in range(1, 26)]: + try: + solution = __import__( + "solutions.day_{}".format(i), globals(), locals(), ["Solution"], 0 + ).Solution() + solution.show_results() + except IOError: + pass + except ImportError: + pass diff --git a/2020-python/solutions/__init__.py b/2020-python/solutions/__init__.py new file mode 100644 index 0000000..3810ab4 --- /dev/null +++ b/2020-python/solutions/__init__.py @@ -0,0 +1,32 @@ +class BaseSolution: + input_file = None + trim_input = True + + def read_input(self, filename): + filepath = "./inputs/{}".format(filename) + with open(filepath, "r") as f: + data = f.read() + if self.trim_input: + return data.strip() + return data + + def show_results(self): + data = self.read_input(self.input_file) + puzzle_input = self.parse_input(data) + print( + "\n\n{}\n{}\n\nPart 1: {}\nPart 2: {}".format( + str(self), + "-" * len(str(self)), + self.solve(puzzle_input), + self.solve_again(puzzle_input), + ) + ) + + def solve(self, puzzle_input): + raise NotImplemented + + def solve_again(self, puzzle_input): + raise NotImplemented + + def parse_input(self, data): + raise NotImplemented diff --git a/2020-python/solutions/day_01.py b/2020-python/solutions/day_01.py new file mode 100644 index 0000000..e120063 --- /dev/null +++ b/2020-python/solutions/day_01.py @@ -0,0 +1,28 @@ +import itertools + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "01.txt" + + def __str__(self): + return "Day 1: Report Repair" + + def parse_input(self, data): + return [*map(int, data.split())] + + def solve(self, values): + for x, y in itertools.combinations(values, 2): + if x + y == 2020: + return x * y + + def solve_again(self, values): + for x, y, z in itertools.combinations(values, 3): + if x + y + z == 2020: + return x * y * z + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_02.py b/2020-python/solutions/day_02.py new file mode 100644 index 0000000..aca2f4e --- /dev/null +++ b/2020-python/solutions/day_02.py @@ -0,0 +1,46 @@ +import re +from collections import Counter + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "02.txt" + + def __str__(self): + return "Day 2: Password Philosophy" + + def parse_input(self, data): + def extract_data(subject): + r1, r2, char, password = re.findall( + r"^(\d+)-(\d+) (\w): (\w+)$", subject.strip() + )[0] + return (int(r1), int(r2), char, password) + + return [*map(extract_data, data.strip().splitlines())] + + def solve(self, puzzle_input): + return sum(map(self.valid_sled_rental_password, puzzle_input)) + + def solve_again(self, puzzle_input): + return sum(map(self.valid_tc_password, puzzle_input)) + + def valid_sled_rental_password(self, policy): + range_start, range_stop, testchar, password = policy + if not testchar in password: + return False + occourences = Counter(password)[testchar] + return occourences >= range_start and occourences <= range_stop + + def valid_tc_password(self, policy): + pos1, pos2, testchar, password = policy + pos1 -= 1 + pos2 -= 1 + if not testchar in password: + return False + return sum(password[pos] == testchar for pos in (pos1, pos2)) == 1 + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_03.py b/2020-python/solutions/day_03.py new file mode 100644 index 0000000..28fa207 --- /dev/null +++ b/2020-python/solutions/day_03.py @@ -0,0 +1,36 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "03.txt" + + def __str__(self): + return "Day 3: Toboggan Trajectory" + + def parse_input(self, data): + return [line.strip() for line in data.strip().splitlines()] + + def solve(self, puzzle_input): + return self.count_trees(3, 1, puzzle_input) + + def solve_again(self, puzzle_input): + slopes = [(1, 1), (5, 1), (7, 1), (1, 2)] + tree_encounters = self.solve(puzzle_input) # slope 3, 1 + for x, y in slopes: + tree_encounters *= self.count_trees(x, y, puzzle_input) + return tree_encounters + + def count_trees(self, xdir, ydir, mapdata): + patternlen = len(mapdata[0]) + tree_encounters = 0 + ystops = [y for y in range(len(mapdata)) if y % ydir == 0] + for i, y in enumerate(ystops): + x = i * xdir % patternlen + if mapdata[y][x] == "#": + tree_encounters += 1 + return tree_encounters + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_04.py b/2020-python/solutions/day_04.py new file mode 100644 index 0000000..086d585 --- /dev/null +++ b/2020-python/solutions/day_04.py @@ -0,0 +1,97 @@ +import re +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "04.txt" + + def __str__(self): + return "Day 4: Passport Processing" + + def parse_input(self, data): + return [f"{' '.join(sorted(pp.split()))} " for pp in data.split("\n\n")] + + def solve(self, puzzle_input): + return sum(map(has_required_fields, puzzle_input)) + + def solve_again(self, puzzle_input): + return sum( + map( + lambda pp: has_required_fields(pp) and has_valid_values(pp), + puzzle_input, + ) + ) + + +def has_required_fields(data): + return all( + f"{k}:" in data for k in ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + ) + + +def has_valid_values(data): + valid_hgt = lambda d: valid_height(d) + valid_byr = lambda d: valid_range(d, "byr", 1920, 2002) + valid_iyr = lambda d: valid_range(d, "iyr", 2010, 2020) + valid_eyr = lambda d: valid_range(d, "eyr", 2020, 2030) + valid_hcl = lambda d: valid_hex_color(data) + valid_ecl = lambda d: exactly_1_valid_eye_color(data) + valid_pid = lambda d: valid_password_id(d) + + return all( + validator(data) + for validator in [ + valid_byr, + valid_iyr, + valid_eyr, + valid_hgt, + valid_hcl, + valid_ecl, + valid_pid, + ] + ) + + +def valid_height(data): + values = re.findall(r"hgt:([\d+?]{2,3})(in|cm) ", data) + if len(values) != 1: + return False + value, unit = values[0] + if unit == "in": + return int(value) in range(59, 77) + elif unit == "cm": + return int(value) in range(150, 194) + return False + + +def valid_range(data, k, a, b): + value = re.search(k + r":([\d]{4}) ", data) + if not value: + return False + return int(value[1]) in range(a, b + 1) + + +def valid_hex_color(data): + return re.search(r"hcl:#([0-9abcdef]{6}) ", data) != None + + +def exactly_1_valid_eye_color(data): + return ( + sum( + f"ecl:{cl} " in data + for cl in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"] + ) + == 1 + ) + + +def valid_password_id(data): + value = re.search(r"pid:([\d+?]{9}) ", data) + if not value: + return False + return True + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_05.py b/2020-python/solutions/day_05.py new file mode 100644 index 0000000..814d825 --- /dev/null +++ b/2020-python/solutions/day_05.py @@ -0,0 +1,68 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "05.txt" + + def __str__(self): + return "Day 5: Binary Boarding" + + def parse_input(self, data): + return data.split() + + def solve(self, puzzle_input): + # Solved without code in part 1. + # highest in my puzzle input: + # "BBBFBBBLLR", with a seat id of 953 + seat = self.highest_seat_id(puzzle_input) + return self.get_seat_id(seat) + + def solve_again(self, puzzle_input): + all_seats = frozenset(range(128 * 8)) + seen = frozenset(self.get_seat_id(seat) for seat in puzzle_input) + + # copied from manual puzzle input intervention, using + # this method: + # + # ``` + # print(all_seats - seen) + # ``` + # + # front seats within interval 0-44 and back seats + # within interval 954-1024 are missing. + # (most likely unique for my input) + missing = [range(45), range(954, 1024)] + + relevant = frozenset(all_seats) + for m in missing: + relevant = relevant - frozenset(m) + empty_seat = relevant - seen + return [*empty_seat][0] + + def highest_seat_id(self, seats): + for caretpos in range(len(seats[0])): + preferred, fallback = ("B", "F") if caretpos < 7 else ("R", "L") + filtered = [seat for seat in seats if seat[caretpos] == preferred] + if len(filtered) == 0: + filtered = [seat for seat in seats if seat[caretpos] == fallback] + seats = filtered + return seats[0] + + def get_seat_id(self, seat): + row = self.get_position(seat[:7], keep_upper_range="B") + column = self.get_position(seat[7:], keep_upper_range="R") + return row * 8 + column + + def get_position(self, seat, keep_upper_range): + l = len(seat) + r = [*range(2 ** l)] + mid = 2 ** l // 2 + for caretpos in range(l): + r = r[mid:] if seat[caretpos] == keep_upper_range else r[:mid] + mid = mid // 2 + return r[0] + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_06.py b/2020-python/solutions/day_06.py new file mode 100644 index 0000000..30a1450 --- /dev/null +++ b/2020-python/solutions/day_06.py @@ -0,0 +1,32 @@ +from collections import Counter + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "06.txt" + + def __str__(self): + return "Day 6: Custom Customs" + + def parse_input(self, data): + return [g.split() for g in data.split("\n\n")] + + def solve(self, puzzle_input): + return sum(self.count_any_yes_answers(g) for g in puzzle_input) + + def solve_again(self, puzzle_input): + return sum(self.count_every_yes_answers(g) for g in puzzle_input) + + def count_any_yes_answers(self, groupdata): + return len(set("".join(groupdata))) + + def count_every_yes_answers(self, groupdata): + groupsize = len(groupdata) + answer_counts = Counter("".join(groupdata)) + return sum(v == groupsize for _k, v in answer_counts.items()) + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_07.py b/2020-python/solutions/day_07.py new file mode 100644 index 0000000..8253978 --- /dev/null +++ b/2020-python/solutions/day_07.py @@ -0,0 +1,49 @@ +import re + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "07.txt" + + def __str__(self): + return "Day 7: Handy Haversacks" + + def parse_input(self, data): + rules = [] + for rule in data.splitlines(): + prnt, *children = re.findall(r"((\d+ )?(\S+ \S+)) bags?", rule.strip()) + parent_color = prnt[0] + try: + rules.append( + (parent_color, [(n, int(q.strip())) for _, q, n in children]) + ) + except ValueError: + # "no other" case, return empty children list + rules.append((parent_color, [])) + return rules + + def solve(self, bags): + seen = set() + self.child_of(bags, "shiny gold", seen) + return len(seen) + + def solve_again(self, bags): + return self.weight(dict(bags), "shiny gold") + + def child_of(self, bags, child, seen): + parents = [bag[0] for bag in bags if child in str(bag[1])] + seen.update(parents) + for parent in parents: + self.child_of(bags, parent, seen) + + def weight(self, rules, bag): + children = rules[bag] + child_weight = sum(q for _, q in children) + child_weight += sum(q * self.weight(rules, name) for name, q in children) + return child_weight + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_08.py b/2020-python/solutions/day_08.py new file mode 100644 index 0000000..7ea11e3 --- /dev/null +++ b/2020-python/solutions/day_08.py @@ -0,0 +1,85 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "08.txt" + + def __str__(self): + return "Day 8: Handheld Halting" + + def parse_input(self, data): + def parse(l): + op, change = l.split() + return (op, int(change)) + + return [parse(line) for line in data.splitlines()] + + def solve(self, puzzle_input): + _, acc = self.run(puzzle_input) + return acc + + def solve_again(self, puzzle_input): + stop_at = len(puzzle_input) + tried = 0 + i = 0 + offset = 0 + while tried <= stop_at and i < stop_at: + instructions, offset = self.alter(puzzle_input, offset) + exit_code, acc = self.run(instructions) + if exit_code == "ok": + return acc + i += 1 + + def run(self, instructions): + seen = set() + caretpos = 0 + acc = 0 + stop_at = len(instructions) + while len(seen) < stop_at: + if caretpos == stop_at: + return "ok", acc + if caretpos % stop_at in seen: + return "exit", acc + seen.add(caretpos) + acc, caretpos = self.execute(instructions, caretpos % stop_at, acc) + + def execute(self, instructions, caretpos, acc): + op, change = instructions[caretpos] + if op == "nop": + return acc, caretpos + 1 + if op == "jmp": + return acc, caretpos + change + if op == "acc": + return acc + change, caretpos + 1 + + def alter(self, instructions, offset): + il = len(instructions) + invalid_changes = [0, il, -il] + dont = "DO_NOT_ALTER" + + def sanitize(instruction): + op, change = instruction + if op == "nop" and change in invalid_changes: + return (dont, change) + return (op, change) + + change_range = [op for op, _change in map(sanitize, instructions[offset:])] + try: + first_jmp = change_range.index("jmp") + except ValueError: + first_jmp = il - offset - 1 + try: + first_nop = change_range.index("nop") + except ValueError: + first_nop = il - offset - 1 + change_at = offset + min(first_nop, first_jmp) + altered = [*instructions] + old_op, change = altered[change_at] + new_op = "jmp" if old_op == "nop" else "nop" + altered[change_at] = (new_op, change) + return altered, change_at + 1 + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_09.py b/2020-python/solutions/day_09.py new file mode 100644 index 0000000..e0ac275 --- /dev/null +++ b/2020-python/solutions/day_09.py @@ -0,0 +1,35 @@ +from itertools import combinations + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "09.txt" + + def __str__(self): + return "Day 9: Encoding Error" + + def parse_input(self, data): + return [*map(int, data.split())] + + def solve(self, puzzle_input, preamble=25): + return self.first_invalid_number(puzzle_input, preamble) + + def solve_again(self, data, preamble=25): + fin = self.solve(data, preamble) + pos = data.index(fin) + previous = [*filter(lambda n: n < fin, data[:pos])] + for r in range(2, len(previous) // 2): + for i in range(len(previous)): + if sum(data[i : i + r]) == fin: + return min(data[i : i + r]) + max(data[i : i + r]) + + def first_invalid_number(self, data, preamble=25): + for i, n in enumerate(data[preamble:]): + if not any(x + y == n for x, y in combinations(data[i : preamble + i], 2)): + return n + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_10.py b/2020-python/solutions/day_10.py new file mode 100644 index 0000000..aa178b9 --- /dev/null +++ b/2020-python/solutions/day_10.py @@ -0,0 +1,49 @@ +from solutions import BaseSolution +from functools import lru_cache + + +class Solution(BaseSolution): + input_file = "10.txt" + + def __str__(self): + return "Day 10: Adapter Array" + + def parse_input(self, data): + return [*map(int, data.split())] + + def solve(self, adapters): + stop_at = max(adapters) + mj = 0 + i = 0 + iii = 0 + while mj < stop_at: + if mj + 1 in adapters: + mj += 1 + i += 1 + elif mj + 3 in adapters: + mj += 3 + iii += 1 + iii += 1 + return i * iii + + def solve_again(self, adapters): + the_end = max(adapters) + r = range(1, 4) + + @lru_cache + def investigate(joltage): + if joltage == the_end: + return 1 + combinations_count = 0 + for i in r: + if joltage + i not in adapters: + continue + combinations_count += investigate(joltage + i) + return combinations_count + + return investigate(0) + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_11.py b/2020-python/solutions/day_11.py new file mode 100644 index 0000000..6a351b8 --- /dev/null +++ b/2020-python/solutions/day_11.py @@ -0,0 +1,111 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "11.txt" + + directions = [ + (-1, -1), # NW + (0, -1), # N + (1, -1), # NE + (-1, 0), # W + (1, 0), # E + (-1, 1), # SW + (0, 1), # S + (1, 1), # SE + ] + + def __str__(self): + return "Day 11: Seating System" + + def parse_input(self, data): + return [[*l] for l in data.split()] + + def solve(self, puzzle_input): + return self.update_seats(puzzle_input) + + def solve_again(self, puzzle_input): + return self.update_seats(puzzle_input, tolerance=5, in_view=True) + + def update_seats(self, seats, tolerance=4, in_view=False): + maxy = len(seats) + maxx = len(seats[0]) + + states = [seats] + + keep_alive = True + rearranged = [s[:] for s in seats] + while keep_alive: + current = states[-1] + rearranged = self.tick(current, maxy, maxx, tolerance, in_view) + states.append([r[:] for r in rearranged]) + if current == rearranged: + keep_alive = False + + return len( + [*filter(lambda x: x == "#", "".join(["".join(r) for r in rearranged]))] + ) + + def tick(self, current, maxy, maxx, tolerance, in_view): + rearranged = [s[:] for s in current] + for y in range(maxy): + for x in range(maxx): + v = current[y][x] + if v == ".": + continue + if in_view: + adjacent = self.get_in_view(current, y, x) + else: + adjacent = self.get_adjacent(current, y, x) + adjval = [a for _, a in adjacent] + if v == "L": + v = self.occupy_if_empty(v, adjval) + else: + v = self.empty_if_occupied(v, adjval, tolerance) + rearranged[y][x] = v + return rearranged + + def get_adjacent(self, seats, posy, posx, directions=None, factor=1): + adjacent = [] + + directions = directions or self.directions[:] + + for x, y in directions: + r = y * factor + posy + c = x * factor + posx + if r < 0 or c < 0: + continue + try: + adjacent.append(((x, y), seats[r][c])) + except IndexError: + pass + return adjacent + + def get_in_view(self, seats, posy, posx): + found = [] + directions = True + distance = 1 + directions = self.directions[:] + while directions: + adjacents = self.get_adjacent( + seats, posy, posx, directions, factor=distance + ) + found += [a for a in adjacents if a[1] != "."] + directions = [a[0] for a in adjacents if a[1] == "."] + distance += 1 + return found + + def occupy_if_empty(self, seat, neighbours): + if seat == "L" and not any(n == "#" for n in neighbours): + return "#" + return seat + + def empty_if_occupied(self, seat, neighbours, tolerance=4): + if seat == "#" and sum(map(lambda s: s == "#", neighbours)) >= tolerance: + return "L" + return seat + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_12.py b/2020-python/solutions/day_12.py new file mode 100644 index 0000000..28729cb --- /dev/null +++ b/2020-python/solutions/day_12.py @@ -0,0 +1,79 @@ +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "12.txt" + directions = ("E", "S", "W", "N") + + def __str__(self): + return "Day 12: Rain Risk" + + def parse_input(self, data): + return [(l[0], int(l[1:])) for l in data.split()] + + def solve(self, puzzle_input): + pos = (0, 0) + d = "E" + + for op, change in puzzle_input: + if op in "RL": + d = self.rotate(d, op, change) + elif op in "ESWN": + pos = self.move(pos, op, change) + elif op in "F": + pos = self.move(pos, d, change) + + return sum(pos) + + def solve_again(self, puzzle_input): + ship = (0, 0) + waypoint = (10, -1) + d = "E" + + for op, change in puzzle_input: + if op in "RL": + d, waypoint = self.reposition(d, waypoint, op, change) + elif op in "ESWN": + waypoint = self.move(waypoint, op, change) + elif op in "F": + ship = self.move(ship, d, change, waypoint) + + return sum(ship) + + def move(self, pos, op, change, factors=None): + x, y = pos + if factors: + fx, fy = factors + return (x + change * fx, y + change * fy) + if op == "W": + return (x - change, y) + if op == "E": + return (x + change, y) + if op == "N": + return (x, y - change) + if op == "S": + return (x, y + change) + + def rotate(self, d, op, change): + change = change // 90 + i = self.directions.index(d) + if op == "L": + return self.directions[abs((i - change) % 4)] + else: + return self.directions[abs((i + change) % 4)] + + def reposition(self, d, pos, op, change): + steps = change // 90 + x, y = pos + d = self.rotate(d, op, change) + for _ in range(steps): + if op == "R": + x, y = -y, x + if op == "L": + x, y = y, -x + return d, (x, y) + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_13.py b/2020-python/solutions/day_13.py new file mode 100644 index 0000000..346b526 --- /dev/null +++ b/2020-python/solutions/day_13.py @@ -0,0 +1,54 @@ +from solutions import BaseSolution +from functools import lru_cache, reduce + + +class Solution(BaseSolution): + input_file = "13.txt" + + def __str__(self): + return "Day 13: Shuttle Search" + + def parse_input(self, data): + timestamp, busses = data.split() + busses = busses.split(",") + return int(timestamp), busses + + def solve(self, puzzle_input): + timestamp, busses = puzzle_input + earliest = timestamp - 1 + waittime = 0 + busses = [*(map(int, filter(lambda b: b != "x", busses)))] + + for b in busses: + if self.waittime(timestamp, b) <= self.waittime(timestamp, earliest): + waittime = self.waittime(timestamp, b) + earliest = b + return earliest * waittime + + def solve_again(self, data, start_at=100000000000000): + # Chinese remainder theorem: + # https://en.wikipedia.org/wiki/Chinese_remainder_theorem + _, busses = data + busses = [(int(bid), rem) for rem, bid in enumerate(busses) if bid != "x"] + ts = start_at + gcd = 1 + while busses: + for bid, rem in busses[:]: + if (ts + rem) % bid == 0: + busses.remove((bid, rem)) + gcd *= bid + else: + ts += gcd + break + return ts + + def waittime(self, ts, bid): + t = ts + bid - (ts % bid) - ts + if t == bid: + return 0 + return t + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_14.py b/2020-python/solutions/day_14.py new file mode 100644 index 0000000..d3a9d10 --- /dev/null +++ b/2020-python/solutions/day_14.py @@ -0,0 +1,61 @@ +import re + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "14.txt" + + def __str__(self): + return "Day 14: Docking Data" + + def parse_input(self, data): + programs = [] + for p in data.split("mask = "): + if p.strip() == "": + continue + mask = p.split()[0] + instructions = [ + (int(a), int(v)) for a, v in re.findall(r"mem\[(\d+)\] = (\d+)", p) + ] + programs.append((mask, instructions)) + return programs + + def solve(self, programs): + memory = {} + zf = len(programs[0][0]) + for mask, instructions in programs: + for loc, value in instructions: + value = bin(value)[2:].zfill(zf) + for i, n in enumerate(mask): + if n == "X": + continue + value = f"{value[:i]}{n}{value[i+1:]}" + memory[loc] = int(value, 2) + return sum(v for k, v in memory.items()) + + def solve_again(self, programs): + memory = {} + zf = len(programs[0][0]) + for mask, instructions in programs: + for loc, value in instructions: + loc = bin(loc)[2:].zfill(zf) + for i, n in enumerate(mask): + if n == "1": + loc = f"{loc[:i]}{n}{loc[i+1:]}" + locs = [loc] + for i, n in enumerate(mask): + if n == "X": + a = locs[:] + b = locs[:] + locs = [*map(lambda x: f"{x[:i]}0{x[i+1:]}", a)] + [ + *map(lambda x: f"{x[:i]}1{x[i+1:]}", b) + ] + for loc in locs: + memory[int(loc, 2)] = value + return sum(v for k, v in memory.items()) + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_15.py b/2020-python/solutions/day_15.py new file mode 100644 index 0000000..caed841 --- /dev/null +++ b/2020-python/solutions/day_15.py @@ -0,0 +1,40 @@ +from collections import deque + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "15.txt" + + def __str__(self): + return "Day 15: Rambunctious Recitation" + + def parse_input(self, data): + return [*map(int, data.split(","))] + + def solve(self, puzzle_input): + return self.speak(puzzle_input, 2020) + + def solve_again(self, puzzle_input): + return self.speak(puzzle_input, 30000000) + + def speak(self, data, n): + heard = {} + starting_numbers = len(data) + for i, k in enumerate(data): + heard[k] = i + 1 + last_spoken = 0 + for t in range(starting_numbers + 2, n + 1): + if last_spoken in heard: + heard_at = heard[last_spoken] + heard[last_spoken] = t - 1 + last_spoken = t - 1 - heard_at + else: + heard[last_spoken] = t - 1 + last_spoken = 0 + return last_spoken + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_16.py b/2020-python/solutions/day_16.py new file mode 100644 index 0000000..a5f5139 --- /dev/null +++ b/2020-python/solutions/day_16.py @@ -0,0 +1,84 @@ +import re +from collections import defaultdict + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "16.txt" + + def __str__(self): + return "Day 16: Ticket Translation" + + def parse_input(self, data): + def get_ranges(line): + label = line.split(":")[0] + r1a, r1b, r2a, r2b = re.findall(rxranges, line.strip()) + return (label, range(int(r1a), int(r1b) + 1), range(int(r2a), int(r2b) + 1)) + + rxranges = r"([0-9]+)" + ranges, golden_ticket, other_tickets = data.split("\n\n") + ranges = [*map(get_ranges, ranges.splitlines())] + golden_ticket = [*map(int, golden_ticket.splitlines()[1].split(","))] + other_tickets = [ + [*map(int, ticket.split(","))] for ticket in other_tickets.splitlines()[1:] + ] + return (ranges, golden_ticket, other_tickets) + + def get_invalid_values(self, ranges, ticket): + def not_in_any_range(x): + return all(x not in r1 and x not in r2 for _, r1, r2 in ranges) + + return [*filter(not_in_any_range, ticket)] + + def solve(self, puzzle_input): + ranges, _, tickets = puzzle_input + error_rate = 0 + valid_tickets = [] + for ticket in tickets: + error_rate += sum(self.get_invalid_values(ranges, ticket)) + return error_rate + + def solve_again(self, puzzle_input): + ranges, golden_ticket, tickets = puzzle_input + valid_tickets = [] + for ticket in tickets: + if len(self.get_invalid_values(ranges, ticket)) == 0: + valid_tickets.append(ticket) + sorted_ranges = self.get_positions(ranges, valid_tickets) + factors = [i for i, r in enumerate(sorted_ranges) if "departure" in r] + m = 1 + for f in factors: + m *= golden_ticket[f] + return m + + def get_positions(self, ranges, valid_tickets): + columns = [*zip(*valid_tickets)] + matches = defaultdict(lambda: set()) + # Step 1: find all possible range matches for columns + # Example: {'row': {0, 1, 2}, 'class': {1, 2}, 'seat': {2}} + for e, column in enumerate(columns): + for column_name, r1, r2 in ranges: + if all(x in r1 or x in r2 for x in column): + matches[column_name].add(e) + # Step 2: collect all columns with only one matching range, and subtract + # these ranges from the columns having multiple ranges. Repeat until all + # columns only have one matching range. + # Example: + # #1: {'row': {0, 1, 2}, 'class': {1, 2}, 'seat': {2}} + # #2: {'row': {0, 1}, 'class': {1}, 'seat': {2}} + # #3: {'row': {0}, 'class': {1}, 'seat': {2}} + while any(len(v) > 1 for _k, v in matches.items()): + singles = set({}).union(*[v for _k, v in matches.items() if len(v) == 1]) + for m in matches: + if len(matches[m]) > 1: + matches[m] = matches[m] - singles + # step 3: Sort the matches list and return a list only containing the + # column names. + final = sorted([(k, v.pop()) for k, v in matches.items()], key=lambda kv: kv[1]) + return [k for k, _v in final] + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/solutions/day_18.py b/2020-python/solutions/day_18.py new file mode 100644 index 0000000..8eaf6ff --- /dev/null +++ b/2020-python/solutions/day_18.py @@ -0,0 +1,65 @@ +import re + +from solutions import BaseSolution + + +class Solution(BaseSolution): + input_file = "18.txt" + + pattern = r"\(([^\(\)]+)\)" + + def __str__(self): + return "Day 18: Operation Order" + + def parse_input(self, data): + return [r.strip() for r in data.strip().splitlines()] + + def chunks(self, seq): + for i in range(0, len(seq), 2): + yield seq[i : i + 2] + + def math(self, exp, advanced=False): + has_nests = True + while has_nests: + nests = re.findall(self.pattern, exp) + if not nests: + has_nests = False + for nest in nests: + exp = exp.replace(f"({nest})", str(self.math(nest, advanced))) + if advanced: + has_additions = True + while has_additions: + additions = re.findall(r"(\d+) \+ (\d+)", exp) + if not additions: + has_additions = False + else: + a, b = additions[0] + exp = exp.replace(f"{a} + {b}", str(int(a) + int(b)), 1) + seq = exp.split() + result = int(seq[0]) + if len(seq) == 1: + return result + instructions = list(self.chunks(seq[1:])) + for op, value in instructions: + if op == "*": + result *= int(value) + if op == "+": + result += int(value) + return result + + def basic_math(self, exp): + return self.math(exp, advanced=False) + + def advanced_math(self, exp): + return self.math(exp, advanced=True) + + def solve(self, puzzle_input): + return sum(self.basic_math(mathex) for mathex in puzzle_input) + + def solve_again(self, puzzle_input): + return sum(self.advanced_math(mathex) for mathex in puzzle_input) + + +if __name__ == "__main__": + solution = Solution() + solution.show_results() diff --git a/2020-python/tests/__init__.py b/2020-python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/2020-python/tests/test_day_01.py b/2020-python/tests/test_day_01.py new file mode 100644 index 0000000..ff2f66f --- /dev/null +++ b/2020-python/tests/test_day_01.py @@ -0,0 +1,28 @@ +import unittest + +from solutions.day_01 import Solution + + +class Day01TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 1721 + 979 + 366 + 299 + 675 + 1456 + """ + ) + + def test_find_matching_pair(self): + assert self.solution.solve(self.puzzle_input) == 514579 + + def test_find_matching_triplet(self): + assert self.solution.solve_again(self.puzzle_input) == 241861950 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_02.py b/2020-python/tests/test_day_02.py new file mode 100644 index 0000000..571b38a --- /dev/null +++ b/2020-python/tests/test_day_02.py @@ -0,0 +1,37 @@ +import unittest + +from solutions.day_02 import Solution + + +class Day02TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 1-3 a: abcde + 1-3 b: cdefg + 2-9 c: ccccccccc + """ + ) + + def test_parse_input(self): + data = """ + 1-3 a: abcde + """ + expected = [(1, 3, "a", "abcde")] + + assert self.solution.parse_input(data) == expected + + def test_solve_first_part(self): + expected = 2 + + assert self.solution.solve(self.puzzle_input) == expected + + def test_solve_second_part(self): + expected = 1 + + assert self.solution.solve_again(self.puzzle_input) == expected + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_03.py b/2020-python/tests/test_day_03.py new file mode 100644 index 0000000..57bd020 --- /dev/null +++ b/2020-python/tests/test_day_03.py @@ -0,0 +1,43 @@ +import unittest + +from solutions.day_03 import Solution + + +class Day03TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + ..##....... + #...#...#.. + .#....#..#. + ..#.#...#.# + .#...##..#. + ..#.##..... + .#.#.#....# + .#........# + #.##...#... + #...##....# + .#..#...#.# + """ + ) + + def test_parse_puzzle_input(self): + data = """ + ..##....... + #...#...#.. + .#....#..#. + """ + expected = ["..##.......", "#...#...#..", ".#....#..#."] + + assert self.solution.parse_input(data) == expected + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 7 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input) == 336 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_04.py b/2020-python/tests/test_day_04.py new file mode 100644 index 0000000..9fb06bf --- /dev/null +++ b/2020-python/tests/test_day_04.py @@ -0,0 +1,121 @@ +import unittest + +from solutions.day_04 import ( + Solution, + valid_height, + valid_password_id, + valid_range, + valid_hex_color, + exactly_1_valid_eye_color, +) + + +class Day04TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + ecl:gry pid:860033327 eyr:2020 hcl:#fffffd +byr:1937 iyr:2017 cid:147 hgt:183cm + +iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 +hcl:#cfa07d byr:1929 + +hcl:#ae17e1 iyr:2013 +eyr:2024 +ecl:brn pid:760753108 byr:1931 +hgt:179cm + +hcl:#cfa07d eyr:2025 pid:166559648 +iyr:2011 ecl:brn hgt:59in + """ + ) + + def test_parse_puzzle_input(self): + data = """ + ecl:gry a:1 + +iyr:2013 +a:1 + """ + expected = ["a:1 ecl:gry ", "a:1 iyr:2013 "] + + assert self.solution.parse_input(data) == expected + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 2 + + def test_solve_second_part(self): + valid_passports = self.solution.parse_input( + """ + pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980 +hcl:#623a2f + +eyr:2029 ecl:blu cid:129 byr:1989 +iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm + +hcl:#888785 +hgt:164cm byr:2001 iyr:2015 cid:88 +pid:545766238 ecl:hzl +eyr:2022 + +iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719 + """ + ) + + invalid_passports = self.solution.parse_input( + """ + eyr:1972 cid:100 +hcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926 + +iyr:2019 +hcl:#602927 eyr:1967 hgt:170cm +ecl:grn pid:012533040 byr:1946 + +hcl:dab227 iyr:2012 +ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277 + +hgt:59cm ecl:zzz +eyr:2038 hcl:74454a iyr:2023 +pid:3556412378 byr:2007 + """ + ) + + assert self.solution.solve_again(valid_passports) == 4 + assert self.solution.solve_again(invalid_passports) == 0 + + def test_valid_range(self): + assert valid_range("a:1999 ", "a", 1999, 2001) + assert valid_range("a:2000 ", "a", 1999, 2001) + assert valid_range("a:2001 ", "a", 1999, 2001) + assert not valid_range("a:1000 ", "a", 1999, 2002) + + def test_valid_height(self): + assert valid_height("hgt:160cm ") + assert valid_height("hgt:70in ") + assert not valid_height("hgt:203cm ") + assert not valid_height("hgt:203in ") + assert not valid_height("hgt:20cm ") + assert not valid_height("hgt:2in ") + + def test_valid_hex_color(self): + assert valid_hex_color("hcl:#facade ") + assert valid_hex_color("hcl:#123456 ") + assert not valid_hex_color("hcl:hreu ") + assert not valid_hex_color("hcl:#hreu ") + assert not valid_hex_color("hcl:#aaaaaaa ") + + def test_exactly_1_valid_eye_color(self): + for c in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]: + assert exactly_1_valid_eye_color(f"ecl:{c} ") + + assert not exactly_1_valid_eye_color("ecl:blk ") + + def test_valid_pid(self): + assert valid_password_id("pid:123456789 ") + assert valid_password_id("pid:003456789 ") + assert not valid_password_id("pid:123123123123 ") + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_05.py b/2020-python/tests/test_day_05.py new file mode 100644 index 0000000..5acada1 --- /dev/null +++ b/2020-python/tests/test_day_05.py @@ -0,0 +1,47 @@ +import unittest + +from solutions.day_05 import Solution + + +class Day05TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + BFFFBBFRRR + FFFBBBFRRR + BBFFBBFRLL + """ + ) + + def test_parse_puzzle_input(self): + data = """ + BFFFBBFRRR + FFFBBBFRRR + BBFFBBFRLL + """ + expected = ["BFFFBBFRRR", "FFFBBBFRRR", "BBFFBBFRLL"] + assert self.solution.parse_input(data) == expected + + def test_find_highest_seat_id(self): + assert ( + self.solution.highest_seat_id(["BBBFBBBLLL", "BBBFBBBLLR"]) == "BBBFBBBLLR" + ) + assert ( + self.solution.highest_seat_id(["FBBFBBBLLL", "BBBFBBBLLR"]) == "BBBFBBBLLR" + ) + assert ( + self.solution.highest_seat_id(["BBBFBBBLRL", "BBBFBBBLLR"]) == "BBBFBBBLRL" + ) + + def test_get_seat_id(self): + assert self.solution.get_seat_id("BFFFBBFRRR") == 567 + assert self.solution.get_seat_id("FFFBBBFRRR") == 119 + assert self.solution.get_seat_id("BBFFBBFRLL") == 820 + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 820 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_06.py b/2020-python/tests/test_day_06.py new file mode 100644 index 0000000..b693a03 --- /dev/null +++ b/2020-python/tests/test_day_06.py @@ -0,0 +1,58 @@ +import unittest + +from solutions.day_06 import Solution + + +class Day06TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + abc + +a +b +c + +ab +ac + +a +a +a +a + +b + """ + ) + + def test_parse_puzzle_input(self): + data = """ + abc + + a + b + c + + ab + ac + """ + expected = [["abc"], ["a", "b", "c"], ["ab", "ac"]] + assert self.solution.parse_input(data) == expected + + def test_count_any_yes_answers(self): + assert self.solution.count_any_yes_answers(["abc"]) == 3 + assert self.solution.count_any_yes_answers(["a", "b", "c"]) == 3 + assert self.solution.count_any_yes_answers(["ab", "ac"]) == 3 + assert self.solution.count_any_yes_answers(["a", "a", "a", "a"]) == 1 + assert self.solution.count_any_yes_answers(["b"]) == 1 + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 11 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input) == 6 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_07.py b/2020-python/tests/test_day_07.py new file mode 100644 index 0000000..57b0820 --- /dev/null +++ b/2020-python/tests/test_day_07.py @@ -0,0 +1,82 @@ +import unittest + +from solutions.day_07 import Solution + + +class Day07TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_parse_puzzle_input(self): + data = """ + light red bags contain 1 bright white bag, 2 muted yellow bags. +dark orange bags contain 3 bright white bags, 4 muted yellow bags. +dotted black bags contain no other bags. + """.strip() + expected = [ + ( + "light red", + [ + ("bright white", 1), + ("muted yellow", 2), + ], + ), + ( + "dark orange", + [ + ("bright white", 3), + ("muted yellow", 4), + ], + ), + ("dotted black", []), + ] + assert self.solution.parse_input(data) == expected + + def test_solve_first_part(self): + data = self.solution.parse_input( + """ + light red bags contain 1 bright white bag, 2 muted yellow bags. + dark orange bags contain 3 bright white bags, 4 muted yellow bags. + bright white bags contain 1 shiny gold bag. + muted yellow bags contain 2 shiny gold bags, 9 faded blue bags. + shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags. + dark olive bags contain 3 faded blue bags, 4 dotted black bags. + vibrant plum bags contain 5 faded blue bags, 6 dotted black bags. + faded blue bags contain no other bags. + dotted black bags contain no other bags. + """.strip() + ) + + assert self.solution.solve(data) == 4 + + def test_solve_second_part(self): + data_1 = self.solution.parse_input( + """ + light red bags contain 1 bright white bag, 2 muted yellow bags. + dark orange bags contain 3 bright white bags, 4 muted yellow bags. + bright white bags contain 1 shiny gold bag. + muted yellow bags contain 2 shiny gold bags, 9 faded blue bags. + shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags. + dark olive bags contain 3 faded blue bags, 4 dotted black bags. + vibrant plum bags contain 5 faded blue bags, 6 dotted black bags. + faded blue bags contain no other bags. + dotted black bags contain no other bags. + """.strip() + ) + data_2 = self.solution.parse_input( + """ + shiny gold bags contain 2 dark red bags. + dark red bags contain 2 dark orange bags. + dark orange bags contain 2 dark yellow bags. + dark yellow bags contain 2 dark green bags. + dark green bags contain 2 dark blue bags. + dark blue bags contain 2 dark violet bags. + dark violet bags contain no other bags. + """.strip() + ) + assert self.solution.solve_again(data_1) == 32 + assert self.solution.solve_again(data_2) == 126 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_08.py b/2020-python/tests/test_day_08.py new file mode 100644 index 0000000..9e3e697 --- /dev/null +++ b/2020-python/tests/test_day_08.py @@ -0,0 +1,39 @@ +import unittest + +from solutions.day_08 import Solution + + +class Day08TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + nop +0 +acc +1 +jmp +4 +acc +3 +jmp -3 +acc -99 +acc +1 +jmp -4 +acc +6 + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + nop +0 +acc +1 +jmp -4 + """.strip() + assert self.solution.parse_input(data) == [("nop", 0), ("acc", 1), ("jmp", -4)] + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 5 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input) == 8 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_09.py b/2020-python/tests/test_day_09.py new file mode 100644 index 0000000..876392a --- /dev/null +++ b/2020-python/tests/test_day_09.py @@ -0,0 +1,51 @@ +import unittest + +from solutions.day_09 import Solution + + +class Day09TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 35 +20 +15 +25 +47 +40 +62 +55 +65 +95 +102 +117 +150 +182 +127 +219 +299 +277 +309 +576 + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + 23 + 7 + 13 + """.strip() + expected = [23, 7, 13] + assert self.solution.parse_input(data) == expected + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input, 5) == 127 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input, 5) == 62 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_10.py b/2020-python/tests/test_day_10.py new file mode 100644 index 0000000..d34639f --- /dev/null +++ b/2020-python/tests/test_day_10.py @@ -0,0 +1,77 @@ +import unittest + +from solutions.day_10 import Solution + + +class Day10TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.data_1 = self.puzzle_input = self.solution.parse_input( + """ + 16 + 10 + 15 + 5 + 1 + 11 + 7 + 19 + 6 + 12 + 4 + """.strip() + ) + self.data_2 = self.puzzle_input = self.solution.parse_input( + """ + 28 + 33 + 18 + 42 + 31 + 14 + 46 + 20 + 48 + 47 + 24 + 23 + 49 + 45 + 19 + 38 + 39 + 11 + 1 + 32 + 25 + 35 + 8 + 17 + 7 + 9 + 4 + 2 + 34 + 10 + 3 + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + 16 + 1 + 15 + """.strip() + assert self.solution.parse_input(data) == [16, 1, 15] + + def test_solve_first_part(self): + assert self.solution.solve(self.data_1) == 7 * 5 + assert self.solution.solve(self.data_2) == 22 * 10 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.data_1) == 8 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_11.py b/2020-python/tests/test_day_11.py new file mode 100644 index 0000000..051da48 --- /dev/null +++ b/2020-python/tests/test_day_11.py @@ -0,0 +1,143 @@ +import unittest + +from solutions.day_11 import Solution + + +class Day11TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + L.LL.LL.LL + LLLLLLL.LL + L.L.L..L.. + LLLL.LL.LL + L.LL.LL.LL + L.LLLLL.LL + ..L.L..... + LLLLLLLLLL + L.LLLLLL.L + L.LLLLL.LL + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + L.LL.LL.LL +LLLLLLL.LL + """.strip() + expected = [ + ["L", ".", "L", "L", ".", "L", "L", ".", "L", "L"], + ["L", "L", "L", "L", "L", "L", "L", ".", "L", "L"], + ] + + assert self.solution.parse_input(data) == expected + + def test_adjacent(self): + data = [[1, 2, 3, 4], [11, 12, 13, 14], [21, 22, 23, 24], [31, 32, 33, 34]] + + assert self.solution.get_adjacent(data, 1, 1) == [ + ((-1, -1), 1), + ((0, -1), 2), + ((1, -1), 3), + ((-1, 0), 11), + ((1, 0), 13), + ((-1, 1), 21), + ((0, 1), 22), + ((1, 1), 23), + ] + assert self.solution.get_adjacent(data, 0, 0) == [ + ((1, 0), 2), + ((0, 1), 11), + ((1, 1), 12), + ] + assert self.solution.get_adjacent(data, 3, 3) == [ + ((-1, -1), 23), + ((0, -1), 24), + ((-1, 0), 33), + ] + assert self.solution.get_adjacent(data, 3, 0) == [ + ((0, -1), 21), + ((1, -1), 22), + ((1, 0), 32), + ] + assert self.solution.get_adjacent(data, 0, 3) == [ + ((-1, 0), 3), + ((-1, 1), 13), + ((0, 1), 14), + ] + + def test_occupy_if_empty(self): + data = ["L", "L", "L", "L", "L", "L"] + data_no = ["#", "L", "L", "L", "L", "L"] + + assert self.solution.occupy_if_empty("L", data) == "#" + assert self.solution.occupy_if_empty("#", data) == "#" + assert self.solution.occupy_if_empty("L", data_no) == "L" + + def test_empty_if_occupied(self): + data = ["L", "L", "L", "L", "L", "L"] + data_no = ["L", "L", "#", "#", "#", "#"] + data_no2 = ["L", "#", "#", "#", "#", "#"] + + assert self.solution.empty_if_occupied("L", data) == "L" + assert self.solution.empty_if_occupied("#", data_no) == "L" + assert self.solution.empty_if_occupied("#", data_no2) == "L" + assert self.solution.empty_if_occupied("#", data) == "#" + + def test_tick(self): + data_1 = self.solution.parse_input( + """ + L.LL.LL.LL +LLLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLLL +L.LLLLLL.L +L.LLLLL.LL + """.strip() + ) + data_2 = self.solution.parse_input( + """ + #.##.##.## +#######.## +#.#.#..#.. +####.##.## +#.##.##.## +#.#####.## +..#.#..... +########## +#.######.# +#.#####.## + """.strip() + ) + data_3 = self.solution.parse_input( + """ + #.LL.LL.L# +#LLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLL# +#.LLLLLL.L +#.LLLLL.L# + """.strip() + ) + + assert self.solution.tick(data_1, 10, 10, tolerance=5, in_view=True) == data_2 + assert self.solution.tick(data_2, 10, 10, tolerance=5, in_view=True) == data_3 + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 37 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input) == 26 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_12.py b/2020-python/tests/test_day_12.py new file mode 100644 index 0000000..03cccdf --- /dev/null +++ b/2020-python/tests/test_day_12.py @@ -0,0 +1,67 @@ +import unittest + +from solutions.day_12 import Solution + + +class Day12TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + F10 +N3 +F7 +R90 +F11 + """ + ) + + def test_parse_puzzle_input(self): + data = """ + F10 +N3 +F7 + """.strip() + expected = [ + ("F", 10), + ("N", 3), + ("F", 7), + ] + + assert self.solution.parse_input(data) == expected + + def test_move(self): + assert self.solution.move((0, 0), "E", 2) == (2, 0) + assert self.solution.move((0, 0), "S", 2) == (0, 2) + assert self.solution.move((0, 0), "W", 2) == (-2, 0) + assert self.solution.move((0, 0), "N", 2) == (0, -2) + + def test_move_with_factor(self): + assert self.solution.move((0, 0), "E", 2, (2, 3)) == (4, 6) + assert self.solution.move((2, 2), "S", 3, (4, 3)) == (2 + 3 * 4, 2 + 3 * 3) + + def test_rotate(self): + assert self.solution.rotate("E", "L", 90) == "N" + assert self.solution.rotate("E", "L", 180) == "W" + assert self.solution.rotate("E", "L", 270) == "S" + assert self.solution.rotate("E", "R", 90) == "S" + assert self.solution.rotate("E", "R", 180) == "W" + assert self.solution.rotate("E", "R", 270) == "N" + + def test_reposition(self): + assert self.solution.reposition("E", (10, -4), "R", 90) == ("S", (4, 10)) + assert self.solution.reposition("E", (10, -4), "R", 180) == ("W", (-10, 4)) + assert self.solution.reposition("E", (10, -4), "R", 270) == ("N", (-4, -10)) + assert self.solution.reposition("E", (10, -4), "L", 90) == ("N", (-4, -10)) + assert self.solution.reposition("E", (10, -4), "L", 180) == ("W", (-10, 4)) + assert self.solution.reposition("E", (10, -4), "L", 270) == ("S", (4, 10)) + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 25 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input) == 286 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_13.py b/2020-python/tests/test_day_13.py new file mode 100644 index 0000000..703f570 --- /dev/null +++ b/2020-python/tests/test_day_13.py @@ -0,0 +1,43 @@ +import unittest + +from solutions.day_13 import Solution + + +class Day13TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 939 +7,13,x,x,59,x,31,19 + """ + ) + + def test_parse_puzzle_input(self): + data = """ + 939 +7,13,x,x,59,x,31,19 + """.strip() + assert self.solution.parse_input(data) == ( + 939, + ["7", "13", "x", "x", "59", "x", "31", "19"], + ) + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 295 + + def test_solve_second_part(self): + assert self.solution.solve_again(self.puzzle_input, 1068681) == 1068781 + assert self.solution.solve_again((939, [67, 7, 59, 61]), 700000) == 754018 + assert self.solution.solve_again((939, [17, "x", 13, 19]), 3000) == 3417 + assert ( + self.solution.solve_again((939, [67, 7, "x", 59, 61]), 1260470) == 1261476 + ) + assert ( + self.solution.solve_again((939, [1789, 37, 47, 1889]), 1202160480) + == 1202161486 + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_14.py b/2020-python/tests/test_day_14.py new file mode 100644 index 0000000..7773310 --- /dev/null +++ b/2020-python/tests/test_day_14.py @@ -0,0 +1,55 @@ +import unittest + +from solutions.day_14 import Solution + + +class Day14TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + + def test_parse_puzzle_input(self): + data = """ + mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X +mem[8] = 11 +mem[7] = 101 +mem[8] = 0 + """.strip() + expected = [ + ("XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X", [(8, 11), (7, 101), (8, 0)]) + ] + + assert len(self.solution.parse_input(data)) == 1 + assert ( + self.solution.parse_input(data)[0][0] + == "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X" + ) + assert len(self.solution.parse_input(data)[0][1]) == 3 + assert (8, 11) in self.solution.parse_input(data)[0][1] + assert (7, 101) in self.solution.parse_input(data)[0][1] + assert (8, 0) in self.solution.parse_input(data)[0][1] + + def test_solve_first_part(self): + data = """ + mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X +mem[8] = 11 +mem[7] = 101 +mem[8] = 0 + """.strip() + + puzzle_input = self.solution.parse_input(data) + assert self.solution.solve(puzzle_input) == 165 + + def test_solve_second_part(self): + data = """ + mask = 000000000000000000000000000000X1001X +mem[42] = 100 +mask = 00000000000000000000000000000000X0XX +mem[26] = 1 + """.strip() + + puzzle_input = self.solution.parse_input(data) + assert self.solution.solve_again(puzzle_input) == 208 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_15.py b/2020-python/tests/test_day_15.py new file mode 100644 index 0000000..cc03240 --- /dev/null +++ b/2020-python/tests/test_day_15.py @@ -0,0 +1,31 @@ +import unittest + +from solutions.day_15 import Solution + + +class Day15TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 0,3,6 + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + 0,3,6 + """.strip() + assert self.solution.parse_input(data) == [0, 3, 6] + + def test_solve_first_part(self): + assert self.solution.solve([0, 3, 6]) == 436 + assert self.solution.solve([1, 3, 2]) == 1 + assert self.solution.solve([1, 2, 3]) == 27 + assert self.solution.solve([2, 3, 1]) == 78 + assert self.solution.solve([3, 2, 1]) == 438 + assert self.solution.solve([3, 1, 2]) == 1836 + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_16.py b/2020-python/tests/test_day_16.py new file mode 100644 index 0000000..cceefe0 --- /dev/null +++ b/2020-python/tests/test_day_16.py @@ -0,0 +1,103 @@ +import unittest + +from solutions.day_16 import Solution + + +class Day16TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50 + +your ticket: +7,1,14 + +nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12 + """.strip() + ) + + def test_parse_puzzle_input(self): + data = """ + class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50 + +your ticket: +7,1,14 + +nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12 + """.strip() + assert self.solution.parse_input(data) == ( + [ + ("class", range(1, 4), range(5, 8)), + ("row", range(6, 12), range(33, 45)), + ("seat", range(13, 41), range(45, 51)), + ], + [7, 1, 14], + ([[7, 3, 47], [40, 4, 50], [55, 2, 20], [38, 6, 12]]), + ) + + def test_solve_first_part(self): + assert self.solution.solve(self.puzzle_input) == 71 + + def test_solve_second_part(self): + data = """ + class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9 + """.strip() + i = self.solution.parse_input(data) + + assert self.solution.solve_again(i) == 1 # No departures + + def test_validity(self): + ranges = [ + ("a", range(1, 4), range(5, 8)), + ("b", range(6, 12), range(33, 45)), + ("c", range(13, 41), range(45, 51)), + ] + assert self.solution.get_invalid_values(ranges, [7, 3, 47]) == [] + assert self.solution.get_invalid_values(ranges, [40, 4, 50]) == [4] + assert self.solution.get_invalid_values(ranges, [55, 2, 20]) == [55] + assert self.solution.get_invalid_values(ranges, [38, 6, 12]) == [12] + + def test_positions(self): + data = """ + class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9 + """.strip() + ranges, _, tickets = self.solution.parse_input(data) + + assert self.solution.get_positions(ranges, tickets) == ["row", "class", "seat"] + + +if __name__ == "__main__": + unittest.main() diff --git a/2020-python/tests/test_day_18.py b/2020-python/tests/test_day_18.py new file mode 100644 index 0000000..8bc9bff --- /dev/null +++ b/2020-python/tests/test_day_18.py @@ -0,0 +1,65 @@ +import unittest + +from solutions.day_18 import Solution + + +class Day18TestCase(unittest.TestCase): + def setUp(self): + self.solution = Solution() + self.puzzle_input = self.solution.parse_input( + """ + 2 * 3 + (4 * 5) + 5 + (8 * 3 + 9 + 3 * 4 * 3) + 5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4)) + ((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2 + """ + ) + + def test_parse_puzzle_input(self): + data = """ + a + b + """ + assert self.solution.parse_input(data) == ["a", "b"] + + def test_basic_math(self): + data = [ + (26, "2 * 3 + (4 * 5)"), + (437, "5 + (8 * 3 + 9 + 3 * 4 * 3)"), + (12240, "5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))"), + (13632, "((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2"), + ] + for expected, exp in data: + assert self.solution.basic_math(exp) == expected + + def test_advanced_math(self): + data = [ + (51, "1 + (2 * 3) + (4 * (5 + 6))"), + (46, "2 * 3 + (4 * 5)"), + (1445, "5 + (8 * 3 + 9 + 3 * 4 * 3)"), + (669060, "5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))"), + (23340, "((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2"), + (36, "3 * 4 + 3 + 5"), + (36, "3 * (4 + 3) + 5"), + (36, "3 * (4 + 3 * 1) + 5"), + ] + for expected, exp in data: + assert self.solution.advanced_math(exp) == expected + + def test_solve_first_part(self): + expected = 26 + 437 + 12240 + 13632 + assert self.solution.solve(self.puzzle_input) == expected + + def test_solve_second_part(self): + expected = 46 + 1445 + 23340 + 669060 + assert self.solution.solve_again(self.puzzle_input) == expected + + def test_junk(self): + data = [ + "(2 + (7 * 5 + 7 * 4) * 8 * (2 + 9 * 5 + 7 * 7) + 5 + (8 + 9 + 5)) + 2 + (7 * 5 * 9 * 6 * 3 + (9 * 8 + 9 * 7 + 3)) + (3 + 5 + 7 + (5 + 2 + 2 + 9)) + 9 + 8" + ] + assert self.solution.solve_again(data) == True + + +if __name__ == "__main__": + unittest.main()