From 595fc3a51455a597c5f76f6ad6b0871911c13cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Sat, 14 Dec 2024 16:23:03 +0000 Subject: [PATCH] chore: import old 2019, 2020 and 2021 solutions into the monorepo (#31) * imported 2021 solutions into the monorepo * imported 2020 solutions into the monorepo * imported 2019 solutions into the monorepo --- .github/badges/completion2024.json | 7 + 2019/day01/Cargo.toml | 23 + 2019/day01/src/lib.rs | 135 +++ 2019/day01/src/main.rs | 40 + 2019/day02/Cargo.toml | 23 + 2019/day02/src/lib.rs | 176 +++ 2019/day02/src/main.rs | 34 + 2019/day03/Cargo.toml | 24 + 2019/day03/src/lib.rs | 399 +++++++ 2019/day03/src/main.rs | 34 + 2019/day04/Cargo.toml | 24 + 2019/day04/src/lib.rs | 258 ++++ 2019/day04/src/main.rs | 28 + 2019/day05/Cargo.toml | 24 + 2019/day05/src/lib.rs | 537 +++++++++ 2019/day05/src/main.rs | 32 + 2019/day05/src/utils.rs | 32 + 2019/day06/Cargo.toml | 23 + 2019/day06/src/lib.rs | 243 ++++ 2019/day06/src/main.rs | 42 + 2019/day07/Cargo.toml | 25 + 2019/day07/src/intcode_machine.rs | 504 ++++++++ 2019/day07/src/lib.rs | 210 ++++ 2019/day07/src/main.rs | 27 + 2019/day07/src/utils.rs | 32 + 2019/day08/Cargo.toml | 24 + 2019/day08/src/lib.rs | 171 +++ 2019/day08/src/main.rs | 42 + 2019/day09/Cargo.toml | 24 + 2019/day09/src/intcode_machine.rs | 659 +++++++++++ 2019/day09/src/lib.rs | 58 + 2019/day09/src/main.rs | 27 + 2019/day09/src/utils.rs | 93 ++ 2020/day01/Cargo.toml | 24 + 2020/day01/src/lib.rs | 79 ++ 2020/day01/src/main.rs | 31 + 2020/day02/Cargo.toml | 23 + 2020/day02/src/lib.rs | 179 +++ 2020/day02/src/main.rs | 31 + 2020/day03/Cargo.toml | 23 + 2020/day03/src/lib.rs | 225 ++++ 2020/day03/src/main.rs | 31 + 2020/day04/Cargo.toml | 23 + 2020/day04/src/lib.rs | 112 ++ 2020/day04/src/main.rs | 31 + 2020/day04/src/passport/eye_color.rs | 50 + 2020/day04/src/passport/hair_color.rs | 65 ++ 2020/day04/src/passport/height.rs | 121 ++ 2020/day04/src/passport/mod.rs | 190 +++ 2020/day04/src/passport/passport_id.rs | 55 + 2020/day05/Cargo.toml | 24 + 2020/day05/src/lib.rs | 157 +++ 2020/day05/src/main.rs | 31 + 2020/day06/Cargo.toml | 23 + 2020/day06/src/lib.rs | 124 ++ 2020/day06/src/main.rs | 31 + 2020/day07/Cargo.toml | 23 + 2020/day07/src/lib.rs | 301 +++++ 2020/day07/src/main.rs | 32 + 2020/day08/Cargo.toml | 23 + 2020/day08/src/lib.rs | 298 +++++ 2020/day08/src/main.rs | 32 + 2020/day09/Cargo.toml | 24 + 2020/day09/src/lib.rs | 106 ++ 2020/day09/src/main.rs | 33 + 2020/day10/Cargo.toml | 24 + 2020/day10/src/lib.rs | 152 +++ 2020/day10/src/main.rs | 32 + 2020/day11/Cargo.toml | 23 + 2020/day11/src/lib.rs | 343 ++++++ 2020/day11/src/main.rs | 35 + 2020/day12/Cargo.toml | 23 + 2020/day12/src/lib.rs | 254 ++++ 2020/day12/src/main.rs | 32 + 2020/day13/Cargo.toml | 23 + 2020/day13/src/lib.rs | 198 ++++ 2020/day13/src/main.rs | 31 + 2020/day14/Cargo.toml | 23 + 2020/day14/src/lib.rs | 212 ++++ 2020/day14/src/main.rs | 32 + 2020/day15/Cargo.toml | 23 + 2020/day15/src/lib.rs | 247 ++++ 2020/day15/src/main.rs | 30 + 2020/day16/Cargo.toml | 23 + 2020/day16/src/lib.rs | 266 +++++ 2020/day16/src/main.rs | 32 + 2020/day17/Cargo.toml | 24 + 2020/day17/src/lib.rs | 218 ++++ 2020/day17/src/main.rs | 32 + 2020/day18/Cargo.toml | 23 + 2020/day18/src/lib.rs | 272 +++++ 2020/day18/src/main.rs | 32 + 2020/day19/Cargo.toml | 23 + 2020/day19/src/lib.rs | 281 +++++ 2020/day19/src/main.rs | 32 + 2020/day20/Cargo.toml | 23 + 2020/day20/src/lib.rs | 1036 +++++++++++++++++ 2020/day20/src/main.rs | 32 + 2020/day21/Cargo.toml | 23 + 2020/day21/src/lib.rs | 166 +++ 2020/day21/src/main.rs | 32 + 2020/day22/Cargo.toml | 23 + 2020/day22/src/lib.rs | 261 +++++ 2020/day22/src/main.rs | 32 + 2020/day23/Cargo.toml | 23 + 2020/day23/src/lib.rs | 366 ++++++ 2020/day23/src/main.rs | 26 + 2020/day24/Cargo.toml | 26 + 2020/day24/src/lib.rs | 236 ++++ 2020/day24/src/main.rs | 32 + 2020/day25/Cargo.toml | 23 + 2020/day25/src/lib.rs | 112 ++ 2020/day25/src/main.rs | 23 + 2021/day01/Cargo.toml | 24 + 2021/day01/src/lib.rs | 62 + 2021/day01/src/main.rs | 26 + 2021/day02/Cargo.toml | 23 + 2021/day02/src/lib.rs | 148 +++ 2021/day02/src/main.rs | 26 + 2021/day03/Cargo.toml | 23 + 2021/day03/src/lib.rs | 147 +++ 2021/day03/src/main.rs | 21 + 2021/day04/Cargo.toml | 23 + 2021/day04/src/lib.rs | 309 +++++ 2021/day04/src/main.rs | 22 + 2021/day05/Cargo.toml | 23 + 2021/day05/src/lib.rs | 213 ++++ 2021/day05/src/main.rs | 22 + 2021/day06/Cargo.toml | 23 + 2021/day06/src/lib.rs | 80 ++ 2021/day06/src/main.rs | 27 + 2021/day07/Cargo.toml | 23 + 2021/day07/src/lib.rs | 82 ++ 2021/day07/src/main.rs | 27 + 2021/day08/Cargo.toml | 23 + 2021/day08/src/lib.rs | 218 ++++ 2021/day08/src/main.rs | 22 + 2021/day09/Cargo.toml | 23 + 2021/day09/src/lib.rs | 237 ++++ 2021/day09/src/main.rs | 22 + 2021/day10/Cargo.toml | 23 + 2021/day10/src/lib.rs | 274 +++++ 2021/day10/src/main.rs | 22 + 2021/day11/Cargo.toml | 23 + 2021/day11/src/lib.rs | 219 ++++ 2021/day11/src/main.rs | 22 + 2021/day12/Cargo.toml | 23 + 2021/day12/src/lib.rs | 267 +++++ 2021/day12/src/main.rs | 22 + 2021/day13/Cargo.toml | 23 + 2021/day13/src/lib.rs | 264 +++++ 2021/day13/src/main.rs | 22 + 2021/day14/Cargo.toml | 24 + 2021/day14/src/lib.rs | 222 ++++ 2021/day14/src/main.rs | 22 + 2021/day15/Cargo.toml | 24 + 2021/day15/src/lib.rs | 193 +++ 2021/day15/src/main.rs | 22 + 2021/day16/Cargo.toml | 25 + 2021/day16/src/lib.rs | 430 +++++++ 2021/day16/src/main.rs | 22 + 2021/day17/Cargo.toml | 23 + 2021/day17/src/lib.rs | 162 +++ 2021/day17/src/main.rs | 22 + 2021/day18/Cargo.toml | 24 + 2021/day18/src/lib.rs | 576 +++++++++ 2021/day18/src/main.rs | 22 + 2021/day19/Cargo.toml | 24 + 2021/day19/src/lib.rs | 645 ++++++++++ 2021/day19/src/main.rs | 22 + 2021/day20/Cargo.toml | 23 + 2021/day20/src/lib.rs | 241 ++++ 2021/day20/src/main.rs | 22 + 2021/day21/Cargo.toml | 23 + 2021/day21/src/lib.rs | 379 ++++++ 2021/day21/src/main.rs | 22 + 2021/day22/Cargo.toml | 24 + 2021/day22/src/intersection.rs | 62 + 2021/day22/src/lib.rs | 486 ++++++++ 2021/day22/src/main.rs | 22 + 2021/day24/Cargo.toml | 23 + 2021/day24/src/alu/instruction.rs | 83 ++ 2021/day24/src/alu/mod.rs | 19 + 2021/day24/src/alu/operand.rs | 87 ++ 2021/day24/src/chunk.rs | 83 ++ 2021/day24/src/lib.rs | 112 ++ 2021/day24/src/main.rs | 22 + 2022/day01/Cargo.toml | 7 +- 2022/day01/src/lib.rs | 1 - 2022/day02/Cargo.toml | 7 +- 2022/day02/src/lib.rs | 1 - 2022/day03/Cargo.toml | 7 +- 2022/day03/src/lib.rs | 1 - 2022/day04/Cargo.toml | 7 +- 2022/day04/src/lib.rs | 1 - 2022/day04/src/types.rs | 1 - 2022/day05/Cargo.toml | 7 +- 2022/day05/src/lib.rs | 1 - 2022/day05/src/types.rs | 1 - 2022/day06/Cargo.toml | 7 +- 2022/day06/src/lib.rs | 1 - 2022/day07/Cargo.toml | 7 +- 2022/day07/src/lib.rs | 1 - 2022/day08/Cargo.toml | 7 +- 2022/day08/src/lib.rs | 1 - 2022/day10/Cargo.toml | 7 +- 2022/day10/src/lib.rs | 1 - 2022/day11/Cargo.toml | 7 +- 2022/day11/src/lib.rs | 1 - 2023/day01/Cargo.toml | 7 +- 2023/day01/src/lib.rs | 1 - 2023/day01/src/types.rs | 3 +- 2023/day02/Cargo.toml | 7 +- 2023/day02/src/lib.rs | 1 - 2023/day03/Cargo.toml | 7 +- 2023/day03/src/helpers.rs | 5 +- 2023/day03/src/lib.rs | 1 - 2023/day04/Cargo.toml | 7 +- 2023/day04/src/lib.rs | 1 - 2023/day05/Cargo.toml | 7 +- 2023/day05/src/lib.rs | 1 - 2024/day01/Cargo.toml | 7 +- 2024/day01/src/lib.rs | 1 - 2024/day02/Cargo.toml | 7 +- 2024/day02/src/lib.rs | 1 - 2024/day03/Cargo.toml | 7 +- 2024/day03/src/common.rs | 1 - 2024/day03/src/lib.rs | 1 - 2024/day04/Cargo.toml | 7 +- 2024/day04/src/common.rs | 1 - 2024/day04/src/lib.rs | 1 - 2024/day05/Cargo.toml | 7 +- 2024/day05/src/lib.rs | 1 - Cargo.toml | 68 ++ README.md | 29 +- aoc-solution-derive/Cargo.toml | 7 +- aoc-solution/Cargo.toml | 7 +- clippy.toml | 1 + common/Cargo.toml | 7 +- common/src/legacy.rs | 331 ++++++ common/src/lib.rs | 4 + solution-runner/Cargo.toml | 76 +- solution-runner/src/main.rs | 57 + tools/aoc-init/Cargo.toml | 7 +- tools/aoc-init/src/main.rs | 1 + .../template/{{year}}/day{{day}}/Cargo.toml | 7 +- .../template/{{year}}/day{{day}}/src/lib.rs | 1 - 247 files changed, 20153 insertions(+), 61 deletions(-) create mode 100644 .github/badges/completion2024.json create mode 100644 2019/day01/Cargo.toml create mode 100644 2019/day01/src/lib.rs create mode 100644 2019/day01/src/main.rs create mode 100644 2019/day02/Cargo.toml create mode 100644 2019/day02/src/lib.rs create mode 100644 2019/day02/src/main.rs create mode 100644 2019/day03/Cargo.toml create mode 100644 2019/day03/src/lib.rs create mode 100644 2019/day03/src/main.rs create mode 100644 2019/day04/Cargo.toml create mode 100644 2019/day04/src/lib.rs create mode 100644 2019/day04/src/main.rs create mode 100644 2019/day05/Cargo.toml create mode 100644 2019/day05/src/lib.rs create mode 100644 2019/day05/src/main.rs create mode 100644 2019/day05/src/utils.rs create mode 100644 2019/day06/Cargo.toml create mode 100644 2019/day06/src/lib.rs create mode 100644 2019/day06/src/main.rs create mode 100644 2019/day07/Cargo.toml create mode 100644 2019/day07/src/intcode_machine.rs create mode 100644 2019/day07/src/lib.rs create mode 100644 2019/day07/src/main.rs create mode 100644 2019/day07/src/utils.rs create mode 100644 2019/day08/Cargo.toml create mode 100644 2019/day08/src/lib.rs create mode 100644 2019/day08/src/main.rs create mode 100644 2019/day09/Cargo.toml create mode 100644 2019/day09/src/intcode_machine.rs create mode 100644 2019/day09/src/lib.rs create mode 100644 2019/day09/src/main.rs create mode 100644 2019/day09/src/utils.rs create mode 100644 2020/day01/Cargo.toml create mode 100644 2020/day01/src/lib.rs create mode 100644 2020/day01/src/main.rs create mode 100644 2020/day02/Cargo.toml create mode 100644 2020/day02/src/lib.rs create mode 100644 2020/day02/src/main.rs create mode 100644 2020/day03/Cargo.toml create mode 100644 2020/day03/src/lib.rs create mode 100644 2020/day03/src/main.rs create mode 100644 2020/day04/Cargo.toml create mode 100644 2020/day04/src/lib.rs create mode 100644 2020/day04/src/main.rs create mode 100644 2020/day04/src/passport/eye_color.rs create mode 100644 2020/day04/src/passport/hair_color.rs create mode 100644 2020/day04/src/passport/height.rs create mode 100644 2020/day04/src/passport/mod.rs create mode 100644 2020/day04/src/passport/passport_id.rs create mode 100644 2020/day05/Cargo.toml create mode 100644 2020/day05/src/lib.rs create mode 100644 2020/day05/src/main.rs create mode 100644 2020/day06/Cargo.toml create mode 100644 2020/day06/src/lib.rs create mode 100644 2020/day06/src/main.rs create mode 100644 2020/day07/Cargo.toml create mode 100644 2020/day07/src/lib.rs create mode 100644 2020/day07/src/main.rs create mode 100644 2020/day08/Cargo.toml create mode 100644 2020/day08/src/lib.rs create mode 100644 2020/day08/src/main.rs create mode 100644 2020/day09/Cargo.toml create mode 100644 2020/day09/src/lib.rs create mode 100644 2020/day09/src/main.rs create mode 100644 2020/day10/Cargo.toml create mode 100644 2020/day10/src/lib.rs create mode 100644 2020/day10/src/main.rs create mode 100644 2020/day11/Cargo.toml create mode 100644 2020/day11/src/lib.rs create mode 100644 2020/day11/src/main.rs create mode 100644 2020/day12/Cargo.toml create mode 100644 2020/day12/src/lib.rs create mode 100644 2020/day12/src/main.rs create mode 100644 2020/day13/Cargo.toml create mode 100644 2020/day13/src/lib.rs create mode 100644 2020/day13/src/main.rs create mode 100644 2020/day14/Cargo.toml create mode 100644 2020/day14/src/lib.rs create mode 100644 2020/day14/src/main.rs create mode 100644 2020/day15/Cargo.toml create mode 100644 2020/day15/src/lib.rs create mode 100644 2020/day15/src/main.rs create mode 100644 2020/day16/Cargo.toml create mode 100644 2020/day16/src/lib.rs create mode 100644 2020/day16/src/main.rs create mode 100644 2020/day17/Cargo.toml create mode 100644 2020/day17/src/lib.rs create mode 100644 2020/day17/src/main.rs create mode 100644 2020/day18/Cargo.toml create mode 100644 2020/day18/src/lib.rs create mode 100644 2020/day18/src/main.rs create mode 100644 2020/day19/Cargo.toml create mode 100644 2020/day19/src/lib.rs create mode 100644 2020/day19/src/main.rs create mode 100644 2020/day20/Cargo.toml create mode 100644 2020/day20/src/lib.rs create mode 100644 2020/day20/src/main.rs create mode 100644 2020/day21/Cargo.toml create mode 100644 2020/day21/src/lib.rs create mode 100644 2020/day21/src/main.rs create mode 100644 2020/day22/Cargo.toml create mode 100644 2020/day22/src/lib.rs create mode 100644 2020/day22/src/main.rs create mode 100644 2020/day23/Cargo.toml create mode 100644 2020/day23/src/lib.rs create mode 100644 2020/day23/src/main.rs create mode 100644 2020/day24/Cargo.toml create mode 100644 2020/day24/src/lib.rs create mode 100644 2020/day24/src/main.rs create mode 100644 2020/day25/Cargo.toml create mode 100644 2020/day25/src/lib.rs create mode 100644 2020/day25/src/main.rs create mode 100644 2021/day01/Cargo.toml create mode 100644 2021/day01/src/lib.rs create mode 100644 2021/day01/src/main.rs create mode 100644 2021/day02/Cargo.toml create mode 100644 2021/day02/src/lib.rs create mode 100644 2021/day02/src/main.rs create mode 100644 2021/day03/Cargo.toml create mode 100644 2021/day03/src/lib.rs create mode 100644 2021/day03/src/main.rs create mode 100644 2021/day04/Cargo.toml create mode 100644 2021/day04/src/lib.rs create mode 100644 2021/day04/src/main.rs create mode 100644 2021/day05/Cargo.toml create mode 100644 2021/day05/src/lib.rs create mode 100644 2021/day05/src/main.rs create mode 100644 2021/day06/Cargo.toml create mode 100644 2021/day06/src/lib.rs create mode 100644 2021/day06/src/main.rs create mode 100644 2021/day07/Cargo.toml create mode 100644 2021/day07/src/lib.rs create mode 100644 2021/day07/src/main.rs create mode 100644 2021/day08/Cargo.toml create mode 100644 2021/day08/src/lib.rs create mode 100644 2021/day08/src/main.rs create mode 100644 2021/day09/Cargo.toml create mode 100644 2021/day09/src/lib.rs create mode 100644 2021/day09/src/main.rs create mode 100644 2021/day10/Cargo.toml create mode 100644 2021/day10/src/lib.rs create mode 100644 2021/day10/src/main.rs create mode 100644 2021/day11/Cargo.toml create mode 100644 2021/day11/src/lib.rs create mode 100644 2021/day11/src/main.rs create mode 100644 2021/day12/Cargo.toml create mode 100644 2021/day12/src/lib.rs create mode 100644 2021/day12/src/main.rs create mode 100644 2021/day13/Cargo.toml create mode 100644 2021/day13/src/lib.rs create mode 100644 2021/day13/src/main.rs create mode 100644 2021/day14/Cargo.toml create mode 100644 2021/day14/src/lib.rs create mode 100644 2021/day14/src/main.rs create mode 100644 2021/day15/Cargo.toml create mode 100644 2021/day15/src/lib.rs create mode 100644 2021/day15/src/main.rs create mode 100644 2021/day16/Cargo.toml create mode 100644 2021/day16/src/lib.rs create mode 100644 2021/day16/src/main.rs create mode 100644 2021/day17/Cargo.toml create mode 100644 2021/day17/src/lib.rs create mode 100644 2021/day17/src/main.rs create mode 100644 2021/day18/Cargo.toml create mode 100644 2021/day18/src/lib.rs create mode 100644 2021/day18/src/main.rs create mode 100644 2021/day19/Cargo.toml create mode 100644 2021/day19/src/lib.rs create mode 100644 2021/day19/src/main.rs create mode 100644 2021/day20/Cargo.toml create mode 100644 2021/day20/src/lib.rs create mode 100644 2021/day20/src/main.rs create mode 100644 2021/day21/Cargo.toml create mode 100644 2021/day21/src/lib.rs create mode 100644 2021/day21/src/main.rs create mode 100644 2021/day22/Cargo.toml create mode 100644 2021/day22/src/intersection.rs create mode 100644 2021/day22/src/lib.rs create mode 100644 2021/day22/src/main.rs create mode 100644 2021/day24/Cargo.toml create mode 100644 2021/day24/src/alu/instruction.rs create mode 100644 2021/day24/src/alu/mod.rs create mode 100644 2021/day24/src/alu/operand.rs create mode 100644 2021/day24/src/chunk.rs create mode 100644 2021/day24/src/lib.rs create mode 100644 2021/day24/src/main.rs create mode 100644 clippy.toml create mode 100644 common/src/legacy.rs diff --git a/.github/badges/completion2024.json b/.github/badges/completion2024.json new file mode 100644 index 0000000..26d656f --- /dev/null +++ b/.github/badges/completion2024.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "label": "2024", + "message": "10/50", + "color": "red", + "style": "for-the-badge" +} diff --git a/2019/day01/Cargo.toml b/2019/day01/Cargo.toml new file mode 100644 index 0000000..291f774 --- /dev/null +++ b/2019/day01/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day01_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day01_2019" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day01/src/lib.rs b/2019/day01/src/lib.rs new file mode 100644 index 0000000..0a0cc8a --- /dev/null +++ b/2019/day01/src/lib.rs @@ -0,0 +1,135 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +#[derive(Aoc)] +pub struct Day01; + +struct FuelCalculator {} + +impl FuelCalculator { + fn required_fuel(mass: u64) -> Option { + match (mass as f64 / 3.0).floor() - 2.0 { + f if f <= 0.0 => None, + f => Some(f as u64), + } + } +} + +trait Fuelable { + fn calculate_base_required_fuel(&self) -> u64; + fn calculate_total_required_fuel(&self) -> u64; +} + +pub struct Module { + mass: u64, +} + +impl Module { + fn new(mass: u64) -> Self { + Self { mass } + } +} + +impl Fuelable for Module { + fn calculate_base_required_fuel(&self) -> u64 { + FuelCalculator::required_fuel(self.mass).unwrap() + } + + fn calculate_total_required_fuel(&self) -> u64 { + let f = FuelCalculator::required_fuel; + std::iter::successors(f(self.mass), |x| f(*x)).sum() + } +} + +struct FuelUpper {} + +impl FuelUpper { + fn determine_total_required_base_fuel(fuelables: &[F]) -> u64 { + fuelables + .iter() + .map(|f| f.calculate_base_required_fuel()) + .sum() + } + + fn determine_total_required_fuel(fuelables: &[F]) -> u64 { + fuelables + .iter() + .map(|f| f.calculate_total_required_fuel()) + .sum() + } +} + +pub fn input_to_modules(inputs: Vec) -> Vec { + inputs + .iter() + .map(|i| i.parse::().unwrap()) + .map(Module::new) + .collect() +} + +pub fn do_part1(inputs_modules: &[Module]) { + let required_base_fuel = FuelUpper::determine_total_required_base_fuel(inputs_modules); + println!("Part 1 answer: {}", required_base_fuel); +} + +pub fn do_part2(input_modules: &[Module]) { + let required_total_fuel = FuelUpper::determine_total_required_fuel(input_modules); + println!("Part 2 answer: {}", required_total_fuel); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn module_calculates_correct_base_fuel_for_mass_of_12() { + assert_eq!(2, Module::new(12).calculate_base_required_fuel(),); + } + + #[test] + fn module_calculates_correct_base_fuel_for_mass_of_14() { + assert_eq!(2, Module::new(14).calculate_base_required_fuel()); + } + + #[test] + fn module_calculates_correct_base_fuel_for_mass_of_1969() { + assert_eq!(654, Module::new(1969).calculate_base_required_fuel()); + } + + #[test] + fn module_calculates_correct_base_fuel_for_mass_of_100756() { + assert_eq!(33583, Module::new(100_756).calculate_base_required_fuel()); + } + + #[test] + fn module_calculates_correct_total_fuel_for_mass_of_14() { + assert_eq!(2, Module::new(14).calculate_total_required_fuel()); + } + + #[test] + fn module_calculates_correct_total_fuel_for_mass_of_1969() { + assert_eq!(966, Module::new(1969).calculate_total_required_fuel()); + } + + #[test] + fn module_calculates_correct_total_fuel_for_mass_of_100756() { + assert_eq!(50346, Module::new(100_756).calculate_total_required_fuel()); + } +} diff --git a/2019/day01/src/main.rs b/2019/day01/src/main.rs new file mode 100644 index 0000000..dcb0da1 --- /dev/null +++ b/2019/day01/src/main.rs @@ -0,0 +1,40 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day01_2019::{do_part1, do_part2, input_to_modules}; +use std::fs::File; +use std::io::{BufRead, BufReader}; + +fn read_input_file(path: &str) -> Vec { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + let mut inputs = vec![]; + for line in reader.lines() { + inputs.push(line.unwrap()); + } + + inputs +} + +fn main() { + let day1_input = read_input_file("inputs/2019/day01"); + let modules = input_to_modules(day1_input); + do_part1(&modules); + do_part2(&modules); +} diff --git a/2019/day02/Cargo.toml b/2019/day02/Cargo.toml new file mode 100644 index 0000000..781dcd5 --- /dev/null +++ b/2019/day02/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day02_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day02_2019" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2019/day02/src/lib.rs b/2019/day02/src/lib.rs new file mode 100644 index 0000000..71841f7 --- /dev/null +++ b/2019/day02/src/lib.rs @@ -0,0 +1,176 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +#[derive(Aoc)] +pub struct Day02; + +// The below code could be made slightly nicer by introducing Tape type and defining methods on it. +// And also by properly defining OpCodes and operations on them +struct IntcodeMachine { + tape: Vec, + head_position: usize, + output: usize, +} + +enum State { + Running, + Halted, + Error, +} + +impl IntcodeMachine { + fn new(tape: Vec) -> Self { + Self { + tape, + head_position: 0, + output: 0, + } + } + + fn run(&mut self) -> usize { + loop { + match self.step_through() { + State::Running => {} // continue execution + State::Halted => return self.output, + State::Error => panic!("intcode machine ended in an invalid state"), + } + self.advance_head(); + } + } + + fn advance_head(&mut self) { + assert!(self.head_position + 4 < self.tape.len()); + self.head_position += 4 + } + + fn step_through(&mut self) -> State { + match self.tape[self.head_position] { + 1 => { + self.add_op(); + State::Running + } + 2 => { + self.mul_op(); + State::Running + } + 99 => { + self.halt_op(); + State::Halted + } + _ => State::Error, + } + } + + fn add_op(&mut self) { + let input1_idx = self.tape[self.head_position + 1]; + let input2_idx = self.tape[self.head_position + 2]; + let output_idx = self.tape[self.head_position + 3]; + self.tape[output_idx] = self.tape[input1_idx] + self.tape[input2_idx]; + } + + fn mul_op(&mut self) { + let input1_idx = self.tape[self.head_position + 1]; + let input2_idx = self.tape[self.head_position + 2]; + let output_idx = self.tape[self.head_position + 3]; + self.tape[output_idx] = self.tape[input1_idx] * self.tape[input2_idx]; + } + + fn halt_op(&mut self) { + self.output = self.tape[0] + } +} + +fn prepare_tape(input_tape: Vec, subs: (usize, usize)) -> Vec { + input_tape + .iter() + .cloned() + .take(1) + .chain(vec![subs.0, subs.1]) + .chain(input_tape.iter().cloned().skip(3)) + .collect() +} + +pub fn do_part1(input: Vec) { + let prepared_tape = prepare_tape(input, (12, 2)); + println!( + "Part 1 answer: {}", + IntcodeMachine::new(prepared_tape).run() + ); +} + +pub fn do_part2(input: Vec) { + // bruteforce possible noun, verb pairs + // an alternative would be to reverse engineer the machine execution + // or implement something like SAT solver + // But even puzzle authors imply you should just try to bruteforce + let part2_answer_vec: Vec = (0..99) + .flat_map(|noun| (0..99).map(move |verb| (noun, verb))) + .map(|noun_verb_pair| { + let machine_input = prepare_tape(input.clone(), noun_verb_pair); + (noun_verb_pair, IntcodeMachine::new(machine_input).run()) + }) + .skip_while(|(_, output)| *output != 19_690_720) + .map(|(noun_verb_pair, _)| 100 * noun_verb_pair.0 + noun_verb_pair.1) + .take(1) + .collect(); + + println!("Part 2 answer: {:?}", part2_answer_vec.first().unwrap()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod intcode_machine { + use super::*; + + #[test] + fn produces_expected_output_for_tiny_input_with_opcode1() { + assert_eq!(2, IntcodeMachine::new(vec![1, 0, 0, 0, 99]).run()) + } + + #[test] + fn produces_expected_output_for_tiny_input_with_opcode2() { + assert_eq!(2, IntcodeMachine::new(vec![2, 3, 0, 3, 99]).run()) + } + + #[test] + fn produces_expected_output_for_average_size_input() { + assert_eq!(2, IntcodeMachine::new(vec![2, 4, 4, 5, 99, 0]).run()) + } + + #[test] + fn produces_expected_output_for_longer_input() { + assert_eq!( + 30, + IntcodeMachine::new(vec![1, 1, 1, 4, 99, 5, 6, 0, 99]).run() + ) + } + + #[test] + fn produces_expected_output_for_a_lengthy_input() { + assert_eq!( + 3500, + IntcodeMachine::new(vec![1, 9, 10, 3, 2, 3, 11, 0, 99, 30, 40, 50]).run() + ) + } + } +} diff --git a/2019/day02/src/main.rs b/2019/day02/src/main.rs new file mode 100644 index 0000000..6d103fe --- /dev/null +++ b/2019/day02/src/main.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day02_2019::{do_part1, do_part2}; +use std::fs; + +fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .split(',') + .map(|s| s.parse::().unwrap()) + .collect() +} + +fn main() { + let day2_input = read_input_file("inputs/2019/day01"); + do_part1(day2_input.clone()); + do_part2(day2_input.clone()); +} diff --git a/2019/day03/Cargo.toml b/2019/day03/Cargo.toml new file mode 100644 index 0000000..967539c --- /dev/null +++ b/2019/day03/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day03_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day03_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day03/src/lib.rs b/2019/day03/src/lib.rs new file mode 100644 index 0000000..245e80b --- /dev/null +++ b/2019/day03/src/lib.rs @@ -0,0 +1,399 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; +use std::cmp::{max, min}; + +#[derive(Aoc)] +pub struct Day03; + +#[derive(Debug, PartialEq, Clone)] +enum PointAxisTranslation { + Up(i64), + Right(i64), + Down(i64), + Left(i64), +} + +impl PointAxisTranslation { + fn from_str(raw: &str) -> Option { + let mut chars_iter = raw.chars(); + let direction = chars_iter.next()?; + + let value_str: String = chars_iter.collect(); + let value = value_str.parse::(); + match value { + Err(_) => None, + Ok(val) => match direction { + 'U' => Some(PointAxisTranslation::Up(val)), + 'R' => Some(PointAxisTranslation::Right(val)), + 'D' => Some(PointAxisTranslation::Down(val)), + 'L' => Some(PointAxisTranslation::Left(val)), + _ => None, + }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct Point { + x: i64, + y: i64, +} + +impl Point { + #[allow(dead_code)] + fn new(x: i64, y: i64) -> Self { + Point { x, y } + } + + fn translate_on_axis(&self, translation: PointAxisTranslation) -> Self { + match translation { + PointAxisTranslation::Up(val) => Point { + x: self.x, + y: self.y + val, + }, + PointAxisTranslation::Right(val) => Point { + x: self.x + val, + y: self.y, + }, + PointAxisTranslation::Down(val) => Point { + x: self.x, + y: self.y - val, + }, + PointAxisTranslation::Left(val) => Point { + x: self.x - val, + y: self.y, + }, + } + } + + fn origin() -> Self { + Point { x: 0, y: 0 } + } + + fn manhattan_distance_to_origin(&self) -> i64 { + self.x.abs() + self.y.abs() + } + + fn distance_to(&self, other: Self) -> i64 { + (self.x - other.x).abs() + (self.y - other.y).abs() + } + + // Note: this method assumes that we already determined the point is an actual intersection + // so that it's guaranteed to be collinear + fn is_on_segment(&self, segment: &WireSegment) -> bool { + let r1 = min(segment.start.x, segment.end.x)..=max(segment.start.x, segment.end.x); + let r2 = min(segment.start.y, segment.end.y)..=max(segment.start.y, segment.end.y); + + r1.contains(&self.x) && r2.contains(&self.y) + } +} + +#[derive(Debug, Clone)] +struct WireSegment { + start: Point, + end: Point, +} + +impl WireSegment { + fn new(start: Point, end: Point) -> Self { + WireSegment { start, end } + } + + fn len(&self) -> i64 { + self.start.distance_to(self.end) + } + + fn intersection(&self, other: &Self) -> Option { + let a1 = self.end.y - self.start.y; + let b1 = self.start.x - self.end.x; + let c1 = a1 * self.start.x + b1 * self.start.y; + + let a2 = other.end.y - other.start.y; + let b2 = other.start.x - other.end.x; + let c2 = a2 * other.start.x + b2 * other.start.y; + + let delta = a1 * b2 - a2 * b1; + match delta { + 0 => None, + _ => { + let potential_intersection = Point { + x: (b2 * c1 - b1 * c2) / delta, + y: (a1 * c2 - a2 * c1) / delta, + }; + + if potential_intersection.is_on_segment(self) + && potential_intersection.is_on_segment(other) + { + Some(potential_intersection) + } else { + None + } + } + } + } +} + +#[derive(Clone)] +pub struct Wire { + segments: Vec, +} + +impl Wire { + pub fn new_from_raw(raw_str: &str) -> Self { + let origin = Point::origin(); + let points: Vec<_> = vec![origin] // we need to start our sequence with the origin + .into_iter() + .chain( + raw_str + .split(',') + .map(|s| PointAxisTranslation::from_str(s).unwrap()) // if it panics during unwrap it means we got invalid input so there's nothing sensible we can do anyway + .scan(origin, |curr_point, translation| { + let new_point = curr_point.translate_on_axis(translation); + *curr_point = new_point; + // TODO: unnecessary copy + Some(new_point) + }), + ) + .collect(); + + let segments = points + .into_iter() + .tuple_windows() + .map(|(p1, p2)| WireSegment::new(p1, p2)) + .collect(); + + Self { segments } + } + + fn retrace_steps(&self, point: &Point) -> i64 { + // while not on segment, add segment len + // then if on segment, add distance from seg start to point + let intersection_segment_index = self + .segments + .iter() + .position(|seg| point.is_on_segment(seg)) + .unwrap(); // if it panics, something weird must have happened... + + let full_segments_distance: i64 = self + .segments + .iter() + .take(intersection_segment_index) + .map(|seg| seg.len()) + .sum(); + + full_segments_distance + point.distance_to(self.segments[intersection_segment_index].start) + } + + fn all_intersections(&self, other: &Self) -> Vec { + self.segments + .iter() + .flat_map(|w1_seg| other.segments.iter().map(move |w2_seg| (w1_seg, w2_seg))) + .filter_map(|(w1_seg, w2_seg)| w1_seg.intersection(w2_seg)) + .collect() + } + + fn closest_intersection_to_origin(&self, other: &Self) -> Point { + // all intersections + let origin = Point::origin(); + self.all_intersections(other) + .into_iter() + .filter(|p| p != &origin) // we don't want origin itself + .map(|p| (p, p.manhattan_distance_to_origin())) + .min_by(|(_, d1), (_, d2)| d1.cmp(d2)) + .unwrap() + .0 + // we don't care about distance itself, only the coordinates + } + + fn least_step_intersection_distance(&self, other: &Self) -> i64 { + // all intersections + let origin = Point::origin(); + self.all_intersections(other) + .into_iter() + .filter(|p| p != &origin) // we don't want origin itself + .map(|p| self.retrace_steps(&p) + other.retrace_steps(&p)) + .min() + .unwrap() + } +} + +pub fn do_part1(input_wires: Vec) { + assert_eq!(2, input_wires.len()); // as per specs + let closest_intersection_dist = input_wires[0] + .closest_intersection_to_origin(&input_wires[1]) + .manhattan_distance_to_origin(); + println!("Part 1 answer: {}", closest_intersection_dist); +} + +pub fn do_part2(input_wires: Vec) { + assert_eq!(2, input_wires.len()); // as per specs + let least_steps = input_wires[0].least_step_intersection_distance(&input_wires[1]); + println!("Part 2 answer: {}", least_steps); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_correctly_determines_closest_intersection_for_first_input() { + let wire1 = Wire::new_from_raw("R8,U5,L5,D3"); + let wire2 = Wire::new_from_raw("U7,R6,D4,L4"); + + assert_eq!( + 6, + wire1 + .closest_intersection_to_origin(&wire2) + .manhattan_distance_to_origin() + ) + } + + #[test] + fn it_correctly_determines_closest_intersection_for_second_input() { + let wire1 = Wire::new_from_raw("R75,D30,R83,U83,L12,D49,R71,U7,L72"); + let wire2 = Wire::new_from_raw("U62,R66,U55,R34,D71,R55,D58,R83"); + + assert_eq!( + 159, + wire1 + .closest_intersection_to_origin(&wire2) + .manhattan_distance_to_origin() + ) + } + + #[test] + fn it_correctly_determines_closest_intersection_for_third_input() { + let wire1 = Wire::new_from_raw("R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51"); + let wire2 = Wire::new_from_raw("U98,R91,D20,R16,D67,R40,U7,R15,U6,R7"); + + assert_eq!( + 135, + wire1 + .closest_intersection_to_origin(&wire2) + .manhattan_distance_to_origin() + ) + } + + #[test] + fn it_correctly_determines_closest_intersection_steps_for_first_input() { + let wire1 = Wire::new_from_raw("R8,U5,L5,D3"); + let wire2 = Wire::new_from_raw("U7,R6,D4,L4"); + + assert_eq!(30, wire1.least_step_intersection_distance(&wire2)) + } + + #[test] + fn it_correctly_determines_closest_intersection_steps_for_second_input() { + let wire1 = Wire::new_from_raw("R75,D30,R83,U83,L12,D49,R71,U7,L72"); + let wire2 = Wire::new_from_raw("U62,R66,U55,R34,D71,R55,D58,R83"); + + assert_eq!(610, wire1.least_step_intersection_distance(&wire2)) + } + + #[test] + fn it_correctly_determines_closest_intersection_steps_for_third_input() { + let wire1 = Wire::new_from_raw("R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51"); + let wire2 = Wire::new_from_raw("U98,R91,D20,R16,D67,R40,U7,R15,U6,R7"); + + assert_eq!(410, wire1.least_step_intersection_distance(&wire2)) + } + + #[cfg(test)] + mod segment_intersection { + use super::*; + + #[test] + fn it_correctly_detects_intersection() { + let l1 = WireSegment::new(Point::new(4, 0), Point::new(6, 10)); + let l2 = WireSegment::new(Point::new(0, 3), Point::new(10, 7)); + assert_eq!(Point::new(5, 5), l1.intersection(&l2).unwrap()) + } + + #[test] + fn it_correctly_detects_no_intersection() { + let l1 = WireSegment::new(Point::new(0, 0), Point::new(1, 1)); + let l2 = WireSegment::new(Point::new(1, 2), Point::new(4, 5)); + assert_eq!(None, l1.intersection(&l2)) + } + + #[test] + fn it_correctly_detects_no_intersection_for_parallel_lines() { + let l1 = WireSegment::new(Point::new(0, 0), Point::new(1, 1)); + let l2 = WireSegment::new(Point::new(0, 1), Point::new(1, 2)); + assert_eq!(None, l1.intersection(&l2)) + } + + #[test] + fn it_correctly_detects_no_intersection_outside_segments_even_if_infinite_lines_would_have_intersected( + ) { + let l1 = WireSegment::new(Point::new(0, 0), Point::new(1, 1)); + let l2 = WireSegment::new(Point::new(2, 3), Point::new(3, 2)); + assert_eq!(None, l1.intersection(&l2)) + } + } + + #[cfg(test)] + mod point_axis_translation { + use super::*; + + #[test] + fn it_returns_valid_up_translation() { + assert_eq!( + PointAxisTranslation::Up(10), + PointAxisTranslation::from_str("U10").unwrap() + ); + } + + #[test] + fn it_returns_valid_right_translation() { + assert_eq!( + PointAxisTranslation::Right(10), + PointAxisTranslation::from_str("R10").unwrap() + ); + } + + #[test] + fn it_returns_valid_down_translation() { + assert_eq!( + PointAxisTranslation::Down(10), + PointAxisTranslation::from_str("D10").unwrap() + ); + } + + #[test] + fn it_returns_valid_left_translation() { + assert_eq!( + PointAxisTranslation::Left(10), + PointAxisTranslation::from_str("L10").unwrap() + ); + } + + #[test] + fn it_returns_none_for_invalid_translations() { + if let Some(_) = PointAxisTranslation::from_str("Z10") { + panic!("expected nothing!") + } + if let Some(_) = PointAxisTranslation::from_str("Z1Y0") { + panic!("expected nothing!") + } + } + } +} diff --git a/2019/day03/src/main.rs b/2019/day03/src/main.rs new file mode 100644 index 0000000..abc4e2e --- /dev/null +++ b/2019/day03/src/main.rs @@ -0,0 +1,34 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day03_2019::{do_part1, do_part2, Wire}; +use std::fs; + +fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .split('\n') + .map(Wire::new_from_raw) + .collect() +} + +fn main() { + let wires = read_input_file("inputs/2019/day03"); + do_part1(wires.clone()); + do_part2(wires); +} diff --git a/2019/day04/Cargo.toml b/2019/day04/Cargo.toml new file mode 100644 index 0000000..e9f01c7 --- /dev/null +++ b/2019/day04/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day04_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day04_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day04/src/lib.rs b/2019/day04/src/lib.rs new file mode 100644 index 0000000..4603845 --- /dev/null +++ b/2019/day04/src/lib.rs @@ -0,0 +1,258 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; + +#[derive(Aoc)] +pub struct Day04; + +// this solution is again, not the most optimal, but that's not the point +// the aim of those exercises is to improve my Rust understanding and playing with iterators. + +#[derive(Debug)] +pub struct PasswordRange { + min: usize, + max: usize, +} + +impl PasswordRange { + pub fn new(input: &str) -> Self { + let semiparsed_input_vec: Vec<_> = input + .splitn(2, '-') + .map(|x| x.parse::().unwrap()) + .collect(); + assert_eq!(2, semiparsed_input_vec.len()); + Self { + min: semiparsed_input_vec[0], + max: semiparsed_input_vec[1], + } + } +} + +struct Password { + val: usize, + val_digits: Vec, +} + +impl Password { + fn new(val: usize) -> Self { + Password { + val, + val_digits: Password::val_to_digits_vec(val), + } + } + + fn val_to_digits_vec(val: usize) -> Vec { + let mut digits = Vec::new(); + let mut n = val; + while n > 9 { + digits.push(n % 10); + n /= 10; + } + digits.push(n); + digits.reverse(); + digits + } + + fn is_n_digit_long(&self, n: u32) -> bool { + self.val >= 10usize.pow(n - 1) && self.val <= (10usize.pow(n) - 1) + } + + #[allow(dead_code)] + fn is_within_range(&self, range: &PasswordRange) -> bool { + self.val >= range.min && self.val <= range.max + } + + fn has_same_adjacent_pair(&self) -> bool { + self.val_digits + .iter() + .tuple_windows() + .any(|(d1, d2)| d1 == d2) + } + + fn has_strict_adjacent_pair(&self) -> bool { + self.val_digits + .iter() + .map(|d| (d, 1)) // map each digit to an initial count of 1 ... + .coalesce(|(d1, n), (d2, m)| { + // ... then merge counts for identical chars + if d1 == d2 { + Ok((d1, n + m)) + } else { + Err(((d1, n), (d2, m))) + } + }) + .any(|(_, count)| count == 2) // check if there are any groups of size 2 + // that was ambiguous. Initially I was looking for group of even length because that's what I understood from the question + } + + fn is_not_decreasing(&self) -> bool { + !self + .val_digits + .iter() + .tuple_windows() + .map(|(d1, d2)| d1 <= d2) + .any(|p| !p) + } +} + +struct PasswordCombinations {} + +impl PasswordCombinations { + fn part1_determine(range: &PasswordRange) -> usize { + (range.min..=range.max) // this satisfies is_within_range + .map(Password::new) + .filter(|pass| pass.is_n_digit_long(6)) + .filter(|pass| pass.has_same_adjacent_pair()) + .filter(|pass| pass.is_not_decreasing()) + .map(|_| 1) + .sum() + } + + fn part2_determine(range: &PasswordRange) -> usize { + (range.min..=range.max) // this satisfies is_within_range + .map(Password::new) + .filter(|pass| pass.is_n_digit_long(6)) + // .filter(|pass| pass.has_same_adjacent_pair()) + .filter(|pass| pass.is_not_decreasing()) + .filter(|pass| pass.has_strict_adjacent_pair()) + .map(|_| 1) + .sum() + } +} + +pub fn do_part1(pwrange: &PasswordRange) { + let valid_pass_count = PasswordCombinations::part1_determine(pwrange); + println!("Part 1 answer: {}", valid_pass_count); +} + +pub fn do_part2(pwrange: &PasswordRange) { + let valid_pass_count = PasswordCombinations::part2_determine(pwrange); + println!("Part 2 answer: {}", valid_pass_count); +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn detects_112233_as_valid_part2_password() { + let pass = Password::new(112_233); + assert!(pass.is_n_digit_long(6)); + assert!(pass.is_not_decreasing()); + assert!(pass.has_strict_adjacent_pair()); + } + + #[test] + fn detects_123444_as_invalid_part2_password() { + let pass = Password::new(123_444); + assert!(pass.is_n_digit_long(6)); + assert!(pass.is_not_decreasing()); + assert!(!pass.has_strict_adjacent_pair()); + } + + #[test] + fn detects_111122_as_valid_part2_password() { + let pass = Password::new(111_122); + assert!(pass.is_n_digit_long(6)); + assert!(pass.is_not_decreasing()); + assert!(pass.has_strict_adjacent_pair()); + } + + #[cfg(test)] + mod password_validators { + use super::*; + + #[test] + fn number_of_digits_works_as_expected() { + let pass1 = Password::new(1000); + let pass2 = Password::new(1500); + let pass3 = Password::new(9999); + let pass4 = Password::new(999); + let pass5 = Password::new(10000); + + assert!(pass1.is_n_digit_long(4)); + assert!(pass2.is_n_digit_long(4)); + assert!(pass3.is_n_digit_long(4)); + assert!(!pass4.is_n_digit_long(4)); + assert!(!pass5.is_n_digit_long(4)); + } + + #[test] + fn range_works_as_expected() { + let pass1 = Password::new(1000); + let pass2 = Password::new(1500); + let pass3 = Password::new(9999); + let pass4 = Password::new(999); + let pass5 = Password::new(10000); + + let range = PasswordRange::new("1000-9999"); + assert!(pass1.is_within_range(&range)); + assert!(pass2.is_within_range(&range)); + assert!(pass3.is_within_range(&range)); + assert!(!pass4.is_within_range(&range)); + assert!(!pass5.is_within_range(&range)); + } + + #[test] + fn adjacent_pairs_work_as_expected() { + let pass1 = Password::new(1); + let pass2 = Password::new(12345); + let pass3 = Password::new(11234); + let pass4 = Password::new(12234); + let pass5 = Password::new(12344); + let pass6 = Password::new(12134); + + assert!(!pass1.has_same_adjacent_pair()); + assert!(!pass2.has_same_adjacent_pair()); + assert!(pass3.has_same_adjacent_pair()); + assert!(pass4.has_same_adjacent_pair()); + assert!(pass5.has_same_adjacent_pair()); + assert!(!pass6.has_same_adjacent_pair()); + } + + #[test] + fn non_decreasing_works_as_expected() { + let pass1 = Password::new(1000); + let pass2 = Password::new(1111); + let pass3 = Password::new(1234); + let pass4 = Password::new(1212); + + assert!(!pass1.is_not_decreasing()); + assert!(pass2.is_not_decreasing()); + assert!(pass3.is_not_decreasing()); + assert!(!pass4.is_not_decreasing()); + } + + #[test] + fn string_digit_pair_works_as_expected() { + let pass1 = Password::new(1222); + let pass2 = Password::new(12_345_678); + let pass3 = Password::new(123_345_678); + let pass4 = Password::new(123_444_567); + let pass5 = Password::new(111_122); + + assert!(!pass1.has_strict_adjacent_pair()); + assert!(!pass2.has_strict_adjacent_pair()); + assert!(pass3.has_strict_adjacent_pair()); + assert!(!pass4.has_strict_adjacent_pair()); + assert!(pass5.has_strict_adjacent_pair()); + } + } +} diff --git a/2019/day04/src/main.rs b/2019/day04/src/main.rs new file mode 100644 index 0000000..4fac63a --- /dev/null +++ b/2019/day04/src/main.rs @@ -0,0 +1,28 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day04_2019::{do_part1, do_part2, PasswordRange}; + +const DAY4_INPUT: &str = "402328-864247"; + +fn main() { + let input = DAY4_INPUT; + let pwrange = PasswordRange::new(input); + do_part1(&pwrange); + do_part2(&pwrange) +} diff --git a/2019/day05/Cargo.toml b/2019/day05/Cargo.toml new file mode 100644 index 0000000..a4feddc --- /dev/null +++ b/2019/day05/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day05_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day05_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day05/src/lib.rs b/2019/day05/src/lib.rs new file mode 100644 index 0000000..1438126 --- /dev/null +++ b/2019/day05/src/lib.rs @@ -0,0 +1,537 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +pub mod utils; + +#[derive(Aoc)] +pub struct Day05; + +const ADD_OP_CODE: isize = 1; +const MUL_OP_CODE: isize = 2; +const INPUT_OP_CODE: isize = 3; +const OUTPUT_OP_CODE: isize = 4; +const JMP_TRUE_OP_CODE: isize = 5; +const JMP_FALSE_OP_CODE: isize = 6; +const LESS_THAN_OP_CODE: isize = 7; +const EQUALS_OP_CODE: isize = 8; +const HALT_OP_CODE: isize = 99; + +const POSITION_MODE: usize = 0; +const IMMEDIATE_MODE: usize = 1; + +// TODO: in hindsight stdout and stdio should have been injected with dependency injection to be +// able to actually test OP3 and OP4 + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParamMode { + Position, + Immediate, +} + +impl TryFrom for ParamMode { + type Error = (); + + fn try_from(value: usize) -> Result { + use ParamMode::*; + + match value { + POSITION_MODE => Ok(Position), + IMMEDIATE_MODE => Ok(Immediate), + _ => Err(()), + } + } +} + +type HeadPositionUpdate = usize; + +#[derive(Debug)] +pub enum OpCodeExecutionError { + TapeError, + InvalidOpArguments, + ExecutionFailure, + ExecutionFinished, +} + +impl From for OpCodeExecutionError { + fn from(_: TapeError) -> Self { + OpCodeExecutionError::TapeError + } +} + +enum OpCode { + Add(Vec), + Mul(Vec), + In, + Out(Vec), + Jt(Vec), + Jf(Vec), + Lt(Vec), + Eq(Vec), + Halt, + #[allow(dead_code)] + Er(isize), +} + +impl OpCode { + fn execute( + &self, + tape: &mut Tape, + head_position: usize, + ) -> Result { + use OpCode::*; + match self { + Add(param_modes) => self.execute_add(tape, head_position, param_modes.clone()), + Mul(param_modes) => self.execute_mul(tape, head_position, param_modes.clone()), + Jt(param_modes) => self.execute_jump_true(tape, head_position, param_modes.clone()), + Jf(param_modes) => self.execute_jump_false(tape, head_position, param_modes.clone()), + Lt(param_modes) => self.execute_less_than(tape, head_position, param_modes.clone()), + Eq(param_modes) => self.execute_equals(tape, head_position, param_modes.clone()), + + In => self.execute_input(tape, head_position), + Out(param_modes) => self.execute_output(tape, head_position, param_modes.clone()), + + Halt => Err(OpCodeExecutionError::ExecutionFinished), + Er(_) => Err(OpCodeExecutionError::ExecutionFailure), + } + } + + fn mode_tape_read( + &self, + tape: &Tape, + tape_idx: usize, + param_mode: ParamMode, + ) -> Result { + let literal_value = tape.read(tape_idx)?; + match param_mode { + ParamMode::Position => { + if literal_value < 0 { + Err(OpCodeExecutionError::InvalidOpArguments) + } else { + Ok(tape.read(literal_value as usize)?) + } + } + ParamMode::Immediate => Ok(literal_value), + } + } + + fn execute_add( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let result = self.mode_tape_read(tape, head_position + 1, param_modes[0])? + + self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + let output_idx = tape.read(head_position + 3)?; + tape.write(output_idx as usize, result)?; + + Ok(head_position + 4) + } + + fn execute_mul( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let result = self.mode_tape_read(tape, head_position + 1, param_modes[0])? + * self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + let output_idx = tape.read(head_position + 3)?; + tape.write(output_idx as usize, result)?; + + Ok(head_position + 4) + } + + fn execute_less_than( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param1 = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let param2 = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + let store_target = tape.read(head_position + 3)?; + + if param1 < param2 { + tape.write(store_target as usize, 1)?; + } else { + tape.write(store_target as usize, 0)?; + } + + Ok(head_position + 4) + } + + fn execute_jump_true( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let jump_target = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + if param != 0 { + Ok(jump_target as usize) + } else { + Ok(head_position + 3) + } + } + + fn execute_jump_false( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let jump_target = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + if param == 0 { + Ok(jump_target as usize) + } else { + Ok(head_position + 3) + } + } + + fn execute_equals( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param1 = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let param2 = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + let store_target = tape.read(head_position + 3)?; + + if param1 == param2 { + tape.write(store_target as usize, 1)?; + } else { + tape.write(store_target as usize, 0)?; + } + + Ok(head_position + 4) + } + + fn execute_input( + &self, + tape: &mut Tape, + head_position: usize, + ) -> Result { + let output_idx = tape.read(head_position + 1)?; + + // Read the user input + println!("Provide the system required input..."); + let mut buffer = String::new(); + std::io::stdin().read_line(&mut buffer).unwrap(); + let input_value = buffer.trim().parse::().unwrap(); + + tape.write(output_idx as usize, input_value)?; + + Ok(head_position + 2) + } + + fn execute_output( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let output_val = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + println!("Test result: {}", output_val); + Ok(head_position + 2) + } +} + +impl From for OpCode { + fn from(code: isize) -> Self { + use OpCode::*; + + // make sure the opcode itself is positive, otherwise we have an invalid execution + if code < 0 { + return Er(code); + } + + let digits = utils::num_to_digits_vec(code as usize); + + let mut opcode_digits: Vec<_> = std::iter::repeat(0) + .chain(digits.clone()) + .rev() + .take(2) + .collect(); + opcode_digits.reverse(); + let op_code_value = utils::digits_vec_to_num(&opcode_digits); + + let num_args = match op_code_value as isize { + ADD_OP_CODE => 3, + MUL_OP_CODE => 3, + JMP_TRUE_OP_CODE => 2, + JMP_FALSE_OP_CODE => 2, + LESS_THAN_OP_CODE => 3, + EQUALS_OP_CODE => 3, + INPUT_OP_CODE => 0, + OUTPUT_OP_CODE => 1, + HALT_OP_CODE => 0, + _ => 0, + }; + + let param_modes_vec: Vec<_> = std::iter::repeat(0) + .chain(digits) + .rev() + .skip(2) + .take(num_args) + .map(|x| ParamMode::try_from(x).unwrap()) + .collect(); + + match op_code_value as isize { + ADD_OP_CODE => Add(param_modes_vec), + MUL_OP_CODE => Mul(param_modes_vec), + JMP_TRUE_OP_CODE => Jt(param_modes_vec), + JMP_FALSE_OP_CODE => Jf(param_modes_vec), + LESS_THAN_OP_CODE => Lt(param_modes_vec), + EQUALS_OP_CODE => Eq(param_modes_vec), + INPUT_OP_CODE => In, + OUTPUT_OP_CODE => Out(param_modes_vec), + HALT_OP_CODE => Halt, + _ => Er(code), + } + } +} + +#[derive(Debug)] +pub enum TapeError { + WriteOutOfRangeError, + ReadOutOfRangeError, +} + +pub struct Tape(Vec); + +impl Tape { + pub fn new(input: Vec) -> Self { + Tape(input) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn write(&mut self, position: usize, value: isize) -> Result<(), TapeError> { + if self.0.len() < position { + return Err(TapeError::WriteOutOfRangeError); + } + + self.0[position] = value; + Ok(()) + } + + fn read(&self, position: usize) -> Result { + if self.0.len() < position { + return Err(TapeError::ReadOutOfRangeError); + } + + Ok(self.0[position]) + } +} + +#[derive(Debug)] +pub enum IntcodeMachineError { + TapeOutOfBoundsError, + ExecutionFailure, +} + +impl From for IntcodeMachineError { + fn from(_: TapeError) -> Self { + IntcodeMachineError::TapeOutOfBoundsError + } +} + +impl From for IntcodeMachineError { + fn from(_: OpCodeExecutionError) -> Self { + IntcodeMachineError::ExecutionFailure + } +} + +pub struct IntcodeMachine { + tape: Tape, + head_position: usize, + output: isize, +} + +impl IntcodeMachine { + pub fn new(tape: Tape) -> Self { + IntcodeMachine { + tape, + head_position: 0, + output: 0, + } + } + + fn update_head(&mut self, val: HeadPositionUpdate) -> Result<(), IntcodeMachineError> { + // check if new head is within 0..tape.len() + if !(0..self.tape.len()).contains(&val) { + return Err(IntcodeMachineError::TapeOutOfBoundsError); + } + + self.head_position = val; + Ok(()) + } + + pub fn run(&mut self) -> Result { + loop { + let op = OpCode::from(self.tape.read(self.head_position)?); + let head_update = match op.execute(&mut self.tape, self.head_position) { + Err(err) => { + return match err { + OpCodeExecutionError::ExecutionFinished => { + self.output = self.tape.read(0)?; + Ok(self.output) + } + _ => Err(IntcodeMachineError::ExecutionFailure), + } + } + Ok(head_update) => head_update, + }; + + self.update_head(head_update)?; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn machine_works_on_negative_values() { + assert_eq!( + 1101, + IntcodeMachine::new(Tape::new(vec![1101, 100, -1, 4, 0])) + .run() + .unwrap() + ); + } + + #[cfg(test)] + mod day2_intcode_machine_reimplementation { + use super::*; + + #[test] + fn produces_expected_output_for_tiny_input_with_opcode1() { + assert_eq!( + 2, + IntcodeMachine::new(Tape::new(vec![1, 0, 0, 0, 99])) + .run() + .unwrap() + ) + } + + #[test] + fn produces_expected_output_for_tiny_input_with_opcode2() { + assert_eq!( + 2, + IntcodeMachine::new(Tape::new(vec![2, 3, 0, 3, 99])) + .run() + .unwrap() + ) + } + + #[test] + fn produces_expected_output_for_average_size_input() { + assert_eq!( + 2, + IntcodeMachine::new(Tape::new(vec![2, 4, 4, 5, 99, 0])) + .run() + .unwrap() + ) + } + + #[test] + fn produces_expected_output_for_longer_input() { + assert_eq!( + 30, + IntcodeMachine::new(Tape::new(vec![1, 1, 1, 4, 99, 5, 6, 0, 99])) + .run() + .unwrap() + ) + } + + #[test] + fn produces_expected_output_for_a_lengthy_input() { + assert_eq!( + 3500, + IntcodeMachine::new(Tape::new(vec![1, 9, 10, 3, 2, 3, 11, 0, 99, 30, 40, 50])) + .run() + .unwrap() + ) + } + + // #[test] + // fn produces_expected_output_for_day2_input() { + // let mut day2_tape = Tape::new(read_input_file("inputs/2019/day02")); + // // do the substitutions + // day2_tape.0[1] = 12; + // day2_tape.0[2] = 2; + // assert_eq!(4_138_687, IntcodeMachine::new(day2_tape).run().unwrap()) + // } + } + + #[cfg(test)] + mod opcode_parsing { + use super::*; + + #[test] + fn works_for_basic_addition() { + match OpCode::from(1) { + OpCode::Add(param_vec) => { + assert_eq!(ParamMode::Position, param_vec[0]); + assert_eq!(ParamMode::Position, param_vec[1]); + assert_eq!(ParamMode::Position, param_vec[2]); + } + + _ => panic!("expected Add"), + } + } + + #[test] + fn works_for_basic_addition_with_zero_prefix() { + match OpCode::from(101) { + OpCode::Add(param_vec) => { + assert_eq!(ParamMode::Immediate, param_vec[0]); + assert_eq!(ParamMode::Position, param_vec[1]); + assert_eq!(ParamMode::Position, param_vec[2]); + } + _ => panic!("expected Add"), + } + } + + #[test] + fn work_for_addition_with_implicit_mode() { + match OpCode::from(1101) { + OpCode::Add(param_vec) => { + assert_eq!(ParamMode::Immediate, param_vec[0]); + assert_eq!(ParamMode::Immediate, param_vec[1]); + assert_eq!(ParamMode::Position, param_vec[2]); + } + _ => panic!("expected Add"), + } + } + } +} diff --git a/2019/day05/src/main.rs b/2019/day05/src/main.rs new file mode 100644 index 0000000..d75a4c6 --- /dev/null +++ b/2019/day05/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day05_2019::utils::read_input_file; +use day05_2019::{IntcodeMachine, Tape}; + +fn run_machine(tape: Tape) { + // answer will be printed (as per specs) to output (here STDOUT) + // part1 requires input of 1, part2 of 5 + println!("When asked for input, provide '1' when executing part1 and '5' when executing part2"); + IntcodeMachine::new(tape).run().unwrap(); +} + +fn main() { + let tape = Tape::new(read_input_file("inputs/2019/day05")); + run_machine(tape); +} diff --git a/2019/day05/src/utils.rs b/2019/day05/src/utils.rs new file mode 100644 index 0000000..3b4a365 --- /dev/null +++ b/2019/day05/src/utils.rs @@ -0,0 +1,32 @@ +use std::fs; + +pub fn num_to_digits_vec(val: usize) -> Vec { + let mut digits = Vec::new(); + let mut n = val; + while n > 9 { + digits.push(n % 10); + n /= 10; + } + digits.push(n); + digits.reverse(); + digits +} + +pub fn digits_vec_to_num(digits: &[usize]) -> usize { + digits + .iter() + .cloned() + .reduce(|acc, x| match acc { + 0 => x, + n => n * 10 + x, + }) + .unwrap() +} + +pub fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .split(',') + .map(|s| s.parse::().unwrap()) + .collect() +} diff --git a/2019/day06/Cargo.toml b/2019/day06/Cargo.toml new file mode 100644 index 0000000..fc75e47 --- /dev/null +++ b/2019/day06/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day06_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day06_2019" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day06/src/lib.rs b/2019/day06/src/lib.rs new file mode 100644 index 0000000..d934548 --- /dev/null +++ b/2019/day06/src/lib.rs @@ -0,0 +1,243 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; + +#[derive(Aoc)] +pub struct Day06; + +#[derive(Debug)] +pub struct OrbitalMap { + global_center_of_mass: Orbit, +} + +impl OrbitalMap { + pub fn new(raw_orbits: Vec) -> Self { + let mut orbit_directory = OrbitalMap::combine_raw_orbits(raw_orbits); + // start with "COM" + // if it doesn't exist, panic, because it MUST exist + let mut com = orbit_directory.remove("COM").unwrap(); + com.extract_orbiting_object_details(&mut orbit_directory); + + OrbitalMap { + global_center_of_mass: com, + } + } + + fn combine_raw_orbits(raw_orbits: Vec) -> HashMap { + let mut orbit_directory: HashMap = HashMap::new(); + + for orbit in raw_orbits { + match orbit_directory.get_mut(&orbit.center_of_mass_name) { + Some(orb) => { + orb.combine(orbit); + } + None => { + orbit_directory.insert(orbit.center_of_mass_name.clone(), orbit); + } + } + } + + orbit_directory + } + + fn total_orbit_count(&self) -> usize { + self.global_center_of_mass.orbit_count(1) + } + + fn num_transfers(&self, source_name: &str, destination_name: &str) -> usize { + // get path from root to those, list all the nodes alongside + // find the common node + // then dist from common to either + let source_path = self.depth_first_search(source_name); + let dest_path = self.depth_first_search(destination_name); + + let intersection = source_path + .iter() + .zip(dest_path.iter()) + .position(|(se, de)| se != de) + .unwrap_or(source_path.len()); + + (source_path.len() - intersection) + (dest_path.len() - intersection) + } + + // it's more likely that our target will be deep in the tree rather than close to the root + fn depth_first_search(&self, target_name: &str) -> Vec<&str> { + self.global_center_of_mass + .find_dfs(target_name, vec![]) + .unwrap() + } +} + +#[derive(Debug)] +pub struct Orbit { + center_of_mass_name: String, + orbiting_objects: Vec, +} + +impl Orbit { + fn new(name: String) -> Self { + Orbit { + center_of_mass_name: name, + orbiting_objects: vec![], + } + } + + fn orbit_count(&self, chain_length: usize) -> usize { + self.orbiting_objects + .iter() + .map(|orb| chain_length + orb.orbit_count(chain_length + 1)) + .sum() + } + + fn add_orbiting_object(&mut self, orbiting_object: Orbit) { + self.orbiting_objects.push(orbiting_object); + } + + fn extract_orbiting_object_details(&mut self, orbit_directory: &mut HashMap) { + for orbiting_object in &mut self.orbiting_objects { + match orbit_directory.remove(&orbiting_object.center_of_mass_name) { + None => { + // it's a leaf node so we don't need to do anything + } + Some(orbit_details) => { + orbiting_object.orbiting_objects = orbit_details.orbiting_objects; + orbiting_object.extract_orbiting_object_details(orbit_directory); + } + } + } + } + + fn combine(&mut self, other: Orbit) { + // make sure we are actually trying to combine right objects + assert_eq!(self.center_of_mass_name, other.center_of_mass_name); + + self.orbiting_objects.extend(other.orbiting_objects); + } + + fn find_dfs<'a>( + &'a self, + target_name: &str, + current_path: Vec<&'a str>, + ) -> Option> { + for orb in &self.orbiting_objects { + let mut new_path = current_path.clone(); + new_path.push(&self.center_of_mass_name); + + if orb.center_of_mass_name == target_name { + return Some(new_path); + } else { + match orb.find_dfs(target_name, new_path) { + None => (), + Some(path) => return Some(path), + } + } + } + + None + } +} + +pub fn parse_orbits(raw_orbits: Vec) -> Vec { + raw_orbits + .into_iter() + .map(|raw_orbit| { + let object_names: Vec<_> = raw_orbit.split(')').collect(); + assert_eq!(2, object_names.len()); + let mut main_orbit = Orbit::new(String::from(object_names[0])); + let orbiting_object = Orbit::new(String::from(object_names[1])); + main_orbit.add_orbiting_object(orbiting_object); + + main_orbit + }) + .collect() +} + +pub fn do_part1(orbital_map: &OrbitalMap) { + println!("Part 1 answer: {}", orbital_map.total_orbit_count()); +} + +pub fn do_part2(orbital_map: &OrbitalMap) { + println!("Part 1 answer: {}", orbital_map.num_transfers("YOU", "SAN")); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_orbital_map_returns_correct_number_of_orbits() { + let sample_orbits = vec![ + String::from("COM)B"), + String::from("B)C"), + String::from("C)D"), + String::from("D)E"), + String::from("E)F"), + String::from("B)G"), + String::from("G)H"), + String::from("D)I"), + String::from("E)J"), + String::from("J)K"), + String::from("K)L"), + ]; + let raw_orbits = parse_orbits(sample_orbits); + let orbital_map = OrbitalMap::new(raw_orbits); + + assert_eq!(42, orbital_map.total_orbit_count()); + } + + #[test] + fn sample_orbital_map_returns_correct_number_of_orbital_transfers() { + let sample_orbits = vec![ + String::from("COM)B"), + String::from("B)C"), + String::from("C)D"), + String::from("D)E"), + String::from("E)F"), + String::from("B)G"), + String::from("G)H"), + String::from("D)I"), + String::from("E)J"), + String::from("J)K"), + String::from("K)L"), + String::from("K)YOU"), + String::from("I)SAN"), + ]; + let raw_orbits = parse_orbits(sample_orbits); + let orbital_map = OrbitalMap::new(raw_orbits); + + assert_eq!(4, orbital_map.num_transfers("YOU", "SAN")); + } + + #[test] + fn sample_2_orbital_map_returns_correct_number_of_orbital_transfers() { + let sample_orbits = vec![ + String::from("COM)A"), + String::from("A)B"), + String::from("B)C"), + String::from("C)D"), + String::from("A)YOU"), + String::from("D)SAN"), + ]; + let raw_orbits = parse_orbits(sample_orbits); + let orbital_map = OrbitalMap::new(raw_orbits); + + assert_eq!(3, orbital_map.num_transfers("YOU", "SAN")); + } +} diff --git a/2019/day06/src/main.rs b/2019/day06/src/main.rs new file mode 100644 index 0000000..1018910 --- /dev/null +++ b/2019/day06/src/main.rs @@ -0,0 +1,42 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day06_2019::{do_part1, do_part2, parse_orbits, OrbitalMap}; +use std::fs::File; +use std::io::{BufRead, BufReader}; + +fn read_input_file(path: &str) -> Vec { + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + let mut inputs = vec![]; + for line in reader.lines() { + inputs.push(line.unwrap()); + } + + inputs +} + +fn main() { + let raw_day6_input = read_input_file("inputs/2019/day06"); + let raw_orbits = parse_orbits(raw_day6_input); + let orbital_map = OrbitalMap::new(raw_orbits); + + do_part1(&orbital_map); + do_part2(&orbital_map); +} diff --git a/2019/day07/Cargo.toml b/2019/day07/Cargo.toml new file mode 100644 index 0000000..c3ec775 --- /dev/null +++ b/2019/day07/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "day07_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day07_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } +permutohedron = "0.2.4" + +[lints] +workspace = true diff --git a/2019/day07/src/intcode_machine.rs b/2019/day07/src/intcode_machine.rs new file mode 100644 index 0000000..ede4ce7 --- /dev/null +++ b/2019/day07/src/intcode_machine.rs @@ -0,0 +1,504 @@ +use std::convert::TryFrom; +use std::io::{BufRead, Write}; + +use crate::utils; + +const ADD_OP_CODE: isize = 1; +const MUL_OP_CODE: isize = 2; +const INPUT_OP_CODE: isize = 3; +const OUTPUT_OP_CODE: isize = 4; +const JMP_TRUE_OP_CODE: isize = 5; +const JMP_FALSE_OP_CODE: isize = 6; +const LESS_THAN_OP_CODE: isize = 7; +const EQUALS_OP_CODE: isize = 8; +const HALT_OP_CODE: isize = 99; + +const POSITION_MODE: usize = 0; +const IMMEDIATE_MODE: usize = 1; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParamMode { + Position, + Immediate, +} + +impl TryFrom for ParamMode { + type Error = (); + + fn try_from(value: usize) -> Result { + use ParamMode::*; + + match value { + POSITION_MODE => Ok(Position), + IMMEDIATE_MODE => Ok(Immediate), + _ => Err(()), + } + } +} + +type HeadPositionUpdate = usize; + +#[derive(Debug)] +enum OpCodeExecutionError { + TapeError, + InvalidOpArguments, + ExecutionFailure, + ExecutionFinished, + InputFailure, +} + +impl From for OpCodeExecutionError { + fn from(_: TapeError) -> Self { + OpCodeExecutionError::TapeError + } +} + +enum OpCode { + Add(Vec), + Mul(Vec), + In, + Out(Vec), + Jt(Vec), + Jf(Vec), + Lt(Vec), + Eq(Vec), + Halt, + #[allow(dead_code)] + Er(isize), +} + +impl OpCode { + fn execute( + &self, + tape: &mut Tape, + head_position: usize, + reader: &mut R, + writer: &mut W, + ) -> Result + where + R: BufRead, + W: Write, + { + use OpCode::*; + match self { + Add(param_modes) => self.execute_add(tape, head_position, param_modes.clone()), + Mul(param_modes) => self.execute_mul(tape, head_position, param_modes.clone()), + Jt(param_modes) => self.execute_jump_true(tape, head_position, param_modes.clone()), + Jf(param_modes) => self.execute_jump_false(tape, head_position, param_modes.clone()), + Lt(param_modes) => self.execute_less_than(tape, head_position, param_modes.clone()), + Eq(param_modes) => self.execute_equals(tape, head_position, param_modes.clone()), + + In => self.execute_input(tape, head_position, reader), + Out(param_modes) => { + self.execute_output(tape, head_position, param_modes.clone(), writer) + } + + Halt => Err(OpCodeExecutionError::ExecutionFinished), + Er(_) => Err(OpCodeExecutionError::ExecutionFailure), + } + } + + fn mode_tape_read( + &self, + tape: &Tape, + tape_idx: usize, + param_mode: ParamMode, + ) -> Result { + let literal_value = tape.read(tape_idx)?; + match param_mode { + ParamMode::Position => { + if literal_value < 0 { + Err(OpCodeExecutionError::InvalidOpArguments) + } else { + Ok(tape.read(literal_value as usize)?) + } + } + ParamMode::Immediate => Ok(literal_value), + } + } + + fn execute_add( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let result = self.mode_tape_read(tape, head_position + 1, param_modes[0])? + + self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + let output_idx = tape.read(head_position + 3)?; + tape.write(output_idx as usize, result)?; + + Ok(head_position + 4) + } + + fn execute_mul( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let result = self.mode_tape_read(tape, head_position + 1, param_modes[0])? + * self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + let output_idx = tape.read(head_position + 3)?; + tape.write(output_idx as usize, result)?; + + Ok(head_position + 4) + } + + fn execute_less_than( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param1 = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let param2 = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + let store_target = tape.read(head_position + 3)?; + + if param1 < param2 { + tape.write(store_target as usize, 1)?; + } else { + tape.write(store_target as usize, 0)?; + } + + Ok(head_position + 4) + } + + fn execute_jump_true( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let jump_target = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + if param != 0 { + Ok(jump_target as usize) + } else { + Ok(head_position + 3) + } + } + + fn execute_jump_false( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let jump_target = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + + if param == 0 { + Ok(jump_target as usize) + } else { + Ok(head_position + 3) + } + } + + fn execute_equals( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + ) -> Result { + let param1 = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + let param2 = self.mode_tape_read(tape, head_position + 2, param_modes[1])?; + let store_target = tape.read(head_position + 3)?; + + if param1 == param2 { + tape.write(store_target as usize, 1)?; + } else { + tape.write(store_target as usize, 0)?; + } + + Ok(head_position + 4) + } + + fn execute_input( + &self, + tape: &mut Tape, + head_position: usize, + mut reader: R, + ) -> Result + where + R: BufRead, + { + let output_idx = tape.read(head_position + 1)?; + + let mut buffer = String::new(); + reader.read_line(&mut buffer).unwrap(); + let input_value = match buffer.trim().parse::() { + Ok(val) => val, + _ => return Err(OpCodeExecutionError::InputFailure), + }; + + tape.write(output_idx as usize, input_value)?; + + Ok(head_position + 2) + } + + fn execute_output( + &self, + tape: &mut Tape, + head_position: usize, + param_modes: Vec, + mut writer: W, + ) -> Result + where + W: Write, + { + let output_val = self.mode_tape_read(tape, head_position + 1, param_modes[0])?; + write!(&mut writer, "{}", output_val).unwrap(); + Ok(head_position + 2) + } +} + +impl From for OpCode { + fn from(code: isize) -> Self { + use OpCode::*; + + // make sure the opcode itself is positive, otherwise we have an invalid execution + if code < 0 { + return Er(code); + } + + let digits = utils::num_to_digits_vec(code as usize); + + let mut opcode_digits: Vec<_> = std::iter::repeat(0) + .chain(digits.clone()) + .rev() + .take(2) + .collect(); + opcode_digits.reverse(); + let op_code_value = utils::digits_vec_to_num(&opcode_digits); + + let num_args = match op_code_value as isize { + ADD_OP_CODE => 3, + MUL_OP_CODE => 3, + JMP_TRUE_OP_CODE => 2, + JMP_FALSE_OP_CODE => 2, + LESS_THAN_OP_CODE => 3, + EQUALS_OP_CODE => 3, + INPUT_OP_CODE => 0, + OUTPUT_OP_CODE => 1, + HALT_OP_CODE => 0, + _ => 0, + }; + + let param_modes_vec: Vec<_> = std::iter::repeat(0) + .chain(digits) + .rev() + .skip(2) + .take(num_args) + .map(|x| ParamMode::try_from(x).unwrap()) + .collect(); + + match op_code_value as isize { + ADD_OP_CODE => Add(param_modes_vec), + MUL_OP_CODE => Mul(param_modes_vec), + JMP_TRUE_OP_CODE => Jt(param_modes_vec), + JMP_FALSE_OP_CODE => Jf(param_modes_vec), + LESS_THAN_OP_CODE => Lt(param_modes_vec), + EQUALS_OP_CODE => Eq(param_modes_vec), + INPUT_OP_CODE => In, + OUTPUT_OP_CODE => Out(param_modes_vec), + HALT_OP_CODE => Halt, + _ => Er(code), + } + } +} + +#[derive(Debug)] +enum TapeError { + WriteOutOfRangeError, + ReadOutOfRangeError, +} + +#[derive(Debug, Clone)] +pub struct Tape(Vec); + +impl Tape { + pub fn new(input: Vec) -> Self { + Tape(input) + } + + fn len(&self) -> usize { + self.0.len() + } + + fn write(&mut self, position: usize, value: isize) -> Result<(), TapeError> { + if self.0.len() < position { + return Err(TapeError::WriteOutOfRangeError); + } + + self.0[position] = value; + Ok(()) + } + + fn read(&self, position: usize) -> Result { + if self.0.len() < position { + return Err(TapeError::ReadOutOfRangeError); + } + + Ok(self.0[position]) + } +} + +#[derive(Debug)] +pub enum IntcodeMachineError { + TapeOutOfBoundsError, + ExecutionFailure, + InputFailure(State), +} + +impl From for IntcodeMachineError { + fn from(_: TapeError) -> Self { + IntcodeMachineError::TapeOutOfBoundsError + } +} + +impl From for IntcodeMachineError { + fn from(_: OpCodeExecutionError) -> Self { + IntcodeMachineError::ExecutionFailure + } +} + +#[derive(Debug, Clone)] +pub struct State { + tape: Tape, + head_position: usize, +} + +impl State { + pub fn new_from_tape(tape: Tape) -> Self { + State { + tape, + head_position: 0, + } + } +} + +impl Default for State { + fn default() -> Self { + State { + tape: Tape(Vec::new()), + head_position: 0, + } + } +} + +pub struct IntcodeMachine +where + R: BufRead, + W: Write, +{ + tape: Tape, + head_position: usize, + input: R, + output: W, +} + +impl IntcodeMachine +where + R: BufRead, + W: Write, +{ + pub(crate) fn new(tape: Tape, reader: R, writer: W) -> Self { + IntcodeMachine { + tape, + head_position: 0, + input: reader, + output: writer, + } + } + + pub fn load_state(state: State, reader: R, writer: W) -> Self { + IntcodeMachine { + tape: state.tape, + head_position: state.head_position, + input: reader, + output: writer, + } + } + + pub fn dump_state(&self) -> State { + State { + tape: self.tape.clone(), + head_position: self.head_position, + } + } + + fn update_head(&mut self, val: HeadPositionUpdate) -> Result<(), IntcodeMachineError> { + // check if new head is within 0..tape.len() + if !(0..self.tape.len()).contains(&val) { + return Err(IntcodeMachineError::TapeOutOfBoundsError); + } + + self.head_position = val; + Ok(()) + } + + pub(crate) fn run(&mut self) -> Result { + loop { + let op = OpCode::from(self.tape.read(self.head_position)?); + let head_update = match op.execute( + &mut self.tape, + self.head_position, + &mut self.input, + &mut self.output, + ) { + Err(err) => match err { + OpCodeExecutionError::ExecutionFinished => { + return Ok(self.tape.read(0)?); + } + OpCodeExecutionError::InputFailure => { + return Err(IntcodeMachineError::InputFailure(self.dump_state())) + } + _ => { + return Err(IntcodeMachineError::ExecutionFailure); + } + }, + Ok(head_update) => head_update, + }; + + self.update_head(head_update)?; + } + } +} + +#[cfg(test)] +mod tests { + + // #[test] + // fn injecting_io_works_for_day_5_input_part1() { + // let tape = Tape::new(utils::read_input_file("inputs/2019/day05")); + // + // let input = b"1"; + // let mut output = Vec::new(); + // + // IntcodeMachine::new(tape, &input[..], &mut output) + // .run() + // .unwrap(); + // + // let output = String::from_utf8(output).unwrap().parse::().unwrap(); + // assert_eq!(13_210_611, output); + // } + // + // #[test] + // fn injecting_io_works_for_day_5_input_part2() { + // let tape = Tape::new(utils::read_input_file("inputs/2019/day05")); + // + // let input = b"5"; + // let mut output = Vec::new(); + // + // IntcodeMachine::new(tape, &input[..], &mut output) + // .run() + // .unwrap(); + // + // let output = String::from_utf8(output).unwrap().parse::().unwrap(); + // assert_eq!(584_126, output); + // } +} diff --git a/2019/day07/src/lib.rs b/2019/day07/src/lib.rs new file mode 100644 index 0000000..d7455aa --- /dev/null +++ b/2019/day07/src/lib.rs @@ -0,0 +1,210 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::intcode_machine::{IntcodeMachine, IntcodeMachineError, State, Tape}; +use aoc_solution::Aoc; +use itertools::enumerate; +use permutohedron::LexicalPermutation; + +pub mod intcode_machine; +pub mod utils; + +#[derive(Aoc)] +pub struct Day07; + +struct AmplifierPhaseSequence(Vec); + +impl AmplifierPhaseSequence { + fn new(v: Vec) -> Self { + assert_eq!(5, v.len()); + Self(v) + } + + fn test_sequence_with_state_dump(&self, tape: Tape) -> (isize, Vec) { + let mut output_signal = 0; + let mut amp_states = vec![State::default(); 5]; + + for (i, phase_seq) in enumerate(&self.0) { + let input_string = format!("{}\n{}\n", phase_seq, output_signal); + let input = input_string.as_bytes(); + let mut output_reader = Vec::new(); + + match IntcodeMachine::new(tape.clone(), input, &mut output_reader).run() { + Ok(_) => (), + Err(IntcodeMachineError::InputFailure(state)) => amp_states[i] = state, + _ => panic!("unexpected machine failure"), + }; + + output_signal = String::from_utf8(output_reader) + .unwrap() + .parse::() + .unwrap(); + } + + (output_signal, amp_states) + } + + fn test_sequence(&self, tape: Tape) -> isize { + let (output_signal, _) = self.test_sequence_with_state_dump(tape); + output_signal + } + + fn test_feedback_sequence(&self, tape: Tape) -> isize { + let (mut output_signal, mut amp_states) = self.test_sequence_with_state_dump(tape); + + // main feedback loop + let mut i = 0; + loop { + let input_string = format!("{}\n", output_signal); + let input = input_string.as_bytes(); + let mut output_reader = Vec::new(); + + let machine_output = + IntcodeMachine::load_state(amp_states[i].clone(), input, &mut output_reader).run(); + + // get any outputs + output_signal = String::from_utf8(output_reader) + .unwrap() + .parse::() + .unwrap(); + + match machine_output { + // if amp E halted, return + Ok(_) => { + if i == 4 { + break; + } + } + Err(IntcodeMachineError::InputFailure(state)) => amp_states[i] = state, // save current memory dump to resume later + _ => panic!("unexpected machine failure"), + }; + + i = (i + 1) % 5; + } + + output_signal + } +} + +pub fn do_part1(tape: Tape) { + let mut highest_signal = 0; + + let mut data = [0, 1, 2, 3, 4]; + let mut permutations = Vec::new(); + loop { + permutations.push(data.to_vec()); + if !data.next_permutation() { + break; + } + } + + for perm in permutations { + println!("testing permutation: {:?}", perm); + let amp_seq = AmplifierPhaseSequence::new(perm); + let amp_out = amp_seq.test_sequence(tape.clone()); + if amp_out > highest_signal { + highest_signal = amp_out; + } + } + + println!("Part 1 answer: {}", highest_signal); +} + +pub fn do_part2(tape: Tape) { + let mut highest_signal = 0; + + let mut data = [5, 6, 7, 8, 9]; + let mut permutations = Vec::new(); + loop { + permutations.push(data.to_vec()); + if !data.next_permutation() { + break; + } + } + + for perm in permutations { + println!("testing permutation: {:?}", perm); + let amp_seq = AmplifierPhaseSequence::new(perm); + let amp_out = amp_seq.test_feedback_sequence(tape.clone()); + if amp_out > highest_signal { + highest_signal = amp_out; + } + } + + println!("Part 2 answer: {}", highest_signal); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_produces_thruster_signal_43210_from_seq_43210_with_sample_input() { + let amp_seq = AmplifierPhaseSequence::new(vec![4, 3, 2, 1, 0]); + let tape = Tape::new(vec![ + 3, 15, 3, 16, 1002, 16, 10, 16, 1, 16, 15, 15, 4, 15, 99, 0, 0, + ]); + + assert_eq!(43210, amp_seq.test_sequence(tape)); + } + + #[test] + fn it_produces_thruster_signal_54321_from_seq_01234_with_sample_input() { + let amp_seq = AmplifierPhaseSequence::new(vec![0, 1, 2, 3, 4]); + let tape = Tape::new(vec![ + 3, 23, 3, 24, 1002, 24, 10, 24, 1002, 23, -1, 23, 101, 5, 23, 23, 1, 24, 23, 23, 4, 23, + 99, 0, 0, + ]); + + assert_eq!(54321, amp_seq.test_sequence(tape)); + } + + #[test] + fn it_produces_thruster_signal_65210_from_seq_10432_with_sample_input() { + let amp_seq = AmplifierPhaseSequence::new(vec![1, 0, 4, 3, 2]); + let tape = Tape::new(vec![ + 3, 31, 3, 32, 1002, 32, 10, 32, 1001, 31, -2, 31, 1007, 31, 0, 33, 1002, 33, 7, 33, 1, + 33, 31, 31, 1, 32, 31, 31, 4, 31, 99, 0, 0, 0, + ]); + + assert_eq!(65210, amp_seq.test_sequence(tape)); + } + + #[test] + fn it_produces_thruster_signal_139629729_from_feedback_seq_98765_with_sample_input() { + let amp_seq = AmplifierPhaseSequence::new(vec![9, 8, 7, 6, 5]); + let tape = Tape::new(vec![ + 3, 26, 1001, 26, -4, 26, 3, 27, 1002, 27, 2, 27, 1, 27, 26, 27, 4, 27, 1001, 28, -1, + 28, 1005, 28, 6, 99, 0, 0, 5, + ]); + + assert_eq!(139_629_729, amp_seq.test_feedback_sequence(tape)); + } + + #[test] + fn it_produces_thruster_signal_18216_from_feedback_seq_97856_with_sample_input() { + let amp_seq = AmplifierPhaseSequence::new(vec![9, 7, 8, 5, 6]); + let tape = Tape::new(vec![ + 3, 52, 1001, 52, -5, 52, 3, 53, 1, 52, 56, 54, 1007, 54, 5, 55, 1005, 55, 26, 1001, 54, + -5, 54, 1105, 1, 12, 1, 53, 54, 53, 1008, 54, 0, 55, 1001, 55, 1, 55, 2, 53, 55, 53, 4, + 53, 1001, 56, -1, 56, 1005, 56, 6, 99, 0, 0, 0, 0, 10, + ]); + + assert_eq!(18216, amp_seq.test_feedback_sequence(tape)); + } +} diff --git a/2019/day07/src/main.rs b/2019/day07/src/main.rs new file mode 100644 index 0000000..7ca2172 --- /dev/null +++ b/2019/day07/src/main.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day07_2019::intcode_machine::Tape; +use day07_2019::{do_part1, do_part2, utils}; + +fn main() { + let tape = Tape::new(utils::read_input_file("inputs/2019/day07")); + + do_part1(tape.clone()); + do_part2(tape); +} diff --git a/2019/day07/src/utils.rs b/2019/day07/src/utils.rs new file mode 100644 index 0000000..3b4a365 --- /dev/null +++ b/2019/day07/src/utils.rs @@ -0,0 +1,32 @@ +use std::fs; + +pub fn num_to_digits_vec(val: usize) -> Vec { + let mut digits = Vec::new(); + let mut n = val; + while n > 9 { + digits.push(n % 10); + n /= 10; + } + digits.push(n); + digits.reverse(); + digits +} + +pub fn digits_vec_to_num(digits: &[usize]) -> usize { + digits + .iter() + .cloned() + .reduce(|acc, x| match acc { + 0 => x, + n => n * 10 + x, + }) + .unwrap() +} + +pub fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .split(',') + .map(|s| s.parse::().unwrap()) + .collect() +} diff --git a/2019/day08/Cargo.toml b/2019/day08/Cargo.toml new file mode 100644 index 0000000..31cac78 --- /dev/null +++ b/2019/day08/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day08_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day08_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day08/src/lib.rs b/2019/day08/src/lib.rs new file mode 100644 index 0000000..cf77c8d --- /dev/null +++ b/2019/day08/src/lib.rs @@ -0,0 +1,171 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use core::fmt; +use std::fmt::Display; + +#[derive(Aoc)] +pub struct Day08; + +const TRANSPARENT_PIXEL: usize = 2; + +#[derive(Clone)] +pub struct Image { + height: usize, + width: usize, + layers: Vec, +} + +impl Image { + pub fn new(height: usize, width: usize) -> Self { + Image { + height, + width, + layers: Vec::new(), + } + } + + pub fn area(&self) -> usize { + self.height * self.width + } + + pub fn add_layer(&mut self, layer_data: Vec) { + assert_eq!(self.area(), layer_data.len()); + let layer = Layer::new(layer_data, self.height, self.width); + self.layers.push(layer); + } + + fn layer_id_with_fewest_zeroes(&self) -> usize { + let (i, _) = self + .layers + .iter() + .enumerate() + .map(|(i, layer)| (i, layer.digit_count(0))) + .min_by(|(_, c1), (_, c2)| c1.cmp(c2)) + .unwrap(); + + i + } + + fn layer_by_id(&self, id: usize) -> &Layer { + &self.layers[id] + } + + fn cover_all_layers(self) -> Layer { + self.layers + .into_iter() + .rev() + .reduce(|bottom, top| bottom.cover(&top)) + .unwrap() + } +} + +// for each height there's a vec of width data +#[derive(Debug, PartialEq, Clone)] +struct Layer(Vec>); + +impl Display for Layer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use std::char; + + for width in self.0.iter() { + let width_as_chars = width + .iter() + .map(|&d| char::from_digit(d as u32, 10).unwrap()) + .collect::(); + writeln!(f, "{width_as_chars}")?; + } + Ok(()) + } +} + +impl Layer { + fn new(layer_data: Vec, height: usize, width: usize) -> Self { + let widths: Vec> = layer_data.chunks(width).map(|w| w.to_vec()).collect(); + assert_eq!(height, widths.len()); + Layer(widths) + } + + #[allow(dead_code)] + fn transparent(height: usize, width: usize) -> Self { + Layer::new( + std::iter::repeat(TRANSPARENT_PIXEL) + .take(height * width) + .collect(), + height, + width, + ) + } + + fn digit_count(&self, digit: usize) -> usize { + assert!(digit <= 9); + self.0 + .iter() + .flat_map(|width| width.iter()) + .filter(|&&d| d == digit) + .count() + } + + fn cover(&self, top: &Layer) -> Self { + let resultant_layer_data: Vec<_> = self + .0 + .iter() + .flat_map(|width| width.iter()) + .zip(top.0.iter().flat_map(|width| width.iter())) + .map(|(&orig, &cover)| { + if cover == TRANSPARENT_PIXEL { + orig + } else { + cover + } + }) + .collect(); + + Layer::new(resultant_layer_data, self.0.len(), self.0[0].len()) + } +} + +pub fn do_part2(image: Image) { + let visible_data = image.cover_all_layers(); + println!("Part 2 answer: \n{}", visible_data); +} + +pub fn do_part1(image: Image) { + let layer_id = image.layer_id_with_fewest_zeroes(); + let layer = image.layer_by_id(layer_id); + let x = layer.digit_count(1) * layer.digit_count(2); + + println!("Part 1 answer: {}", x); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn covering_layer_with_transparent_layer_doesnt_change_the_layer() { + let height = 3; + let width = 3; + + let layer = Layer::new(vec![0, 1, 2, 2, 1, 0, 0, 1, 0], height, width); + let transparent = Layer::transparent(height, width); + + assert_eq!(layer, layer.cover(&transparent)) + } +} diff --git a/2019/day08/src/main.rs b/2019/day08/src/main.rs new file mode 100644 index 0000000..d7b3684 --- /dev/null +++ b/2019/day08/src/main.rs @@ -0,0 +1,42 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day08_2019::{do_part1, do_part2, Image}; +use std::fs; + +fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .chars() + .map(|c| c.to_digit(10).unwrap() as usize) + .collect() +} + +fn main() { + let mut image = Image::new(6, 25); + + let input = read_input_file("inputs/2019/day08"); + let layer_chunks = input.chunks(image.area()); + + for layer_data in layer_chunks { + image.add_layer(layer_data.to_vec()); + } + + do_part1(image.clone()); + do_part2(image); +} diff --git a/2019/day09/Cargo.toml b/2019/day09/Cargo.toml new file mode 100644 index 0000000..7d98576 --- /dev/null +++ b/2019/day09/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day09_2019" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day09_2019" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true diff --git a/2019/day09/src/intcode_machine.rs b/2019/day09/src/intcode_machine.rs new file mode 100644 index 0000000..d2b72d0 --- /dev/null +++ b/2019/day09/src/intcode_machine.rs @@ -0,0 +1,659 @@ +use std::convert::TryFrom; +use std::io::{BufRead, Write}; + +use crate::utils; + +const ADD_OP_CODE: isize = 1; +const MUL_OP_CODE: isize = 2; +const INPUT_OP_CODE: isize = 3; +const OUTPUT_OP_CODE: isize = 4; +const JMP_TRUE_OP_CODE: isize = 5; +const JMP_FALSE_OP_CODE: isize = 6; +const LESS_THAN_OP_CODE: isize = 7; +const EQUALS_OP_CODE: isize = 8; +const RLT_BASE_OFFSET_OP_CODE: isize = 9; +const HALT_OP_CODE: isize = 99; + +const POSITION_MODE: usize = 0; +const IMMEDIATE_MODE: usize = 1; +const RELATIVE_MODE: usize = 2; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParamMode { + Position, + Immediate, + Relative, +} + +// TODO: replace usize with u64 and isize with i64 due to ever changing specs + +impl TryFrom for ParamMode { + type Error = (); + + fn try_from(value: usize) -> Result { + use ParamMode::*; + + match value { + POSITION_MODE => Ok(Position), + IMMEDIATE_MODE => Ok(Immediate), + RELATIVE_MODE => Ok(Relative), + _ => Err(()), + } + } +} + +type HeadPositionUpdate = usize; + +#[derive(Debug)] +enum OpCodeExecutionError { + TapeError, + ExecutionFailure, + ExecutionFinished, + InputFailure, +} + +impl From for OpCodeExecutionError { + fn from(_: TapeError) -> Self { + OpCodeExecutionError::TapeError + } +} + +#[derive(Debug)] +enum OpCode { + Add(Vec), + Mul(Vec), + In(Vec), + Out(Vec), + Jt(Vec), + Jf(Vec), + Lt(Vec), + Eq(Vec), + Rbo(Vec), + Halt, + #[allow(dead_code)] + Er(isize), +} + +impl From for OpCode { + fn from(code: isize) -> Self { + use OpCode::*; + + // make sure the opcode itself is positive, otherwise we have an invalid execution + if code < 0 { + return Er(code); + } + + let digits = utils::num_to_digits_vec(code as usize); + + let mut opcode_digits: Vec<_> = std::iter::repeat(0) + .chain(digits.clone()) + .rev() + .take(2) + .collect(); + opcode_digits.reverse(); + let op_code_value = utils::digits_vec_to_num(&opcode_digits); + + let num_args = match op_code_value as isize { + ADD_OP_CODE => 3, + MUL_OP_CODE => 3, + JMP_TRUE_OP_CODE => 2, + JMP_FALSE_OP_CODE => 2, + LESS_THAN_OP_CODE => 3, + EQUALS_OP_CODE => 3, + INPUT_OP_CODE => 1, + OUTPUT_OP_CODE => 1, + RLT_BASE_OFFSET_OP_CODE => 1, + HALT_OP_CODE => 0, + _ => 0, + }; + + let param_modes_vec: Vec<_> = std::iter::repeat(0) + .chain(digits) + .rev() + .skip(2) + .take(num_args) + .map(|x| ParamMode::try_from(x).unwrap()) + .collect(); + + match op_code_value as isize { + ADD_OP_CODE => Add(param_modes_vec), + MUL_OP_CODE => Mul(param_modes_vec), + JMP_TRUE_OP_CODE => Jt(param_modes_vec), + JMP_FALSE_OP_CODE => Jf(param_modes_vec), + LESS_THAN_OP_CODE => Lt(param_modes_vec), + EQUALS_OP_CODE => Eq(param_modes_vec), + INPUT_OP_CODE => In(param_modes_vec), + OUTPUT_OP_CODE => Out(param_modes_vec), + RLT_BASE_OFFSET_OP_CODE => Rbo(param_modes_vec), + HALT_OP_CODE => Halt, + _ => Er(code), + } + } +} + +#[derive(Debug)] +enum TapeError { + WriteOutOfRange, + ReadOutOfRange, + WriteInImmediateMode, +} + +#[derive(Debug, Clone)] +pub struct Tape(Vec); + +impl Tape { + pub fn new(input: Vec) -> Self { + Tape(input) + } + + fn resize(&mut self, lower_bound: usize) { + self.0.resize(lower_bound, 0); + } + + fn len(&self) -> usize { + self.0.len() + } + + fn write(&mut self, position: usize, value: isize) { + if position >= self.0.len() { + // according to day09 specs, write should always succeed (unless to negative index) + self.resize(position + 1); + } + + self.0[position] = value; + } + + fn read(&mut self, position: usize) -> isize { + if position >= self.len() { + // according to day09 specs, read should always succeed (unless on negative index) + self.resize(position + 1); + } + + self.0[position] + } + + fn mode_read( + &mut self, + position: usize, + relative_base: isize, + param_mode: ParamMode, + ) -> Result { + let literal_value = self.read(position); + match param_mode { + ParamMode::Position => { + if literal_value < 0 { + Err(TapeError::ReadOutOfRange) + } else { + Ok(self.read(literal_value as usize)) + } + } + ParamMode::Relative => { + if (literal_value + relative_base) < 0 { + Err(TapeError::ReadOutOfRange) + } else { + Ok(self.read((literal_value + relative_base) as usize)) + } + } + + ParamMode::Immediate => Ok(literal_value), + } + } + + fn mode_write( + &mut self, + position: usize, + relative_base: isize, + param_mode: ParamMode, + value: isize, + ) -> Result<(), TapeError> { + match param_mode { + ParamMode::Position => { + self.write(position, value); + Ok(()) + } + ParamMode::Relative => { + if position as isize + relative_base < 0 { + return Err(TapeError::WriteOutOfRange); + } + self.write((position as isize + relative_base) as usize, value); + Ok(()) + } + + ParamMode::Immediate => Err(TapeError::WriteInImmediateMode), + } + } +} + +#[derive(Debug)] +pub enum IntcodeMachineError { + TapeOutOfBoundsError, + ExecutionFailure, + InputFailure(State), +} + +impl From for IntcodeMachineError { + fn from(_: TapeError) -> Self { + IntcodeMachineError::TapeOutOfBoundsError + } +} + +impl From for IntcodeMachineError { + fn from(_: OpCodeExecutionError) -> Self { + IntcodeMachineError::ExecutionFailure + } +} + +#[derive(Debug, Clone)] +pub struct State { + tape: Tape, + relative_base: isize, + head_position: usize, +} + +impl State { + pub fn new_from_tape(tape: Tape) -> Self { + State { + tape, + relative_base: 0, + head_position: 0, + } + } +} + +impl Default for State { + fn default() -> Self { + State { + tape: Tape(Vec::new()), + relative_base: 0, + head_position: 0, + } + } +} + +pub struct IntcodeMachine +where + R: BufRead, + W: Write, +{ + tape: Tape, + head_position: usize, + relative_base: isize, + + input: R, + output: W, +} + +impl IntcodeMachine +where + R: BufRead, + W: Write, +{ + pub(crate) fn new(tape: Tape, reader: R, writer: W) -> Self { + IntcodeMachine { + tape, + head_position: 0, + relative_base: 0, + input: reader, + output: writer, + } + } + + pub fn load_state(state: State, reader: R, writer: W) -> Self { + IntcodeMachine { + tape: state.tape, + head_position: state.head_position, + relative_base: state.relative_base, + input: reader, + output: writer, + } + } + + pub fn dump_state(&self) -> State { + State { + tape: self.tape.clone(), + relative_base: self.relative_base, + head_position: self.head_position, + } + } + + fn update_head(&mut self, val: HeadPositionUpdate) -> Result<(), IntcodeMachineError> { + // check if new head is within 0..tape.len() + if !(0..self.tape.len()).contains(&val) { + return Err(IntcodeMachineError::TapeOutOfBoundsError); + } + + self.head_position = val; + Ok(()) + } + + fn execute_add( + &mut self, + param_modes: Vec, + ) -> Result { + let result = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])? + + self.tape.mode_read( + self.head_position + 2, + self.relative_base, + param_modes[1], + )?; + + let output_idx = self.tape.read(self.head_position + 3); + self.tape.mode_write( + output_idx as usize, + self.relative_base, + param_modes[2], + result, + )?; + + Ok(self.head_position + 4) + } + + fn execute_mul( + &mut self, + param_modes: Vec, + ) -> Result { + let result = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])? + * self.tape.mode_read( + self.head_position + 2, + self.relative_base, + param_modes[1], + )?; + + let output_idx = self.tape.read(self.head_position + 3); + self.tape.mode_write( + output_idx as usize, + self.relative_base, + param_modes[2], + result, + )?; + + Ok(self.head_position + 4) + } + + fn execute_less_than( + &mut self, + param_modes: Vec, + ) -> Result { + let param1 = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + let param2 = + self.tape + .mode_read(self.head_position + 2, self.relative_base, param_modes[1])?; + let store_target = self.tape.read(self.head_position + 3); + + if param1 < param2 { + self.tape + .mode_write(store_target as usize, self.relative_base, param_modes[2], 1)?; + } else { + self.tape + .mode_write(store_target as usize, self.relative_base, param_modes[2], 0)?; + } + + Ok(self.head_position + 4) + } + + fn execute_jump_true( + &mut self, + param_modes: Vec, + ) -> Result { + let param = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + let jump_target = + self.tape + .mode_read(self.head_position + 2, self.relative_base, param_modes[1])?; + + if param != 0 { + Ok(jump_target as usize) + } else { + Ok(self.head_position + 3) + } + } + + fn execute_jump_false( + &mut self, + param_modes: Vec, + ) -> Result { + let param = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + let jump_target = + self.tape + .mode_read(self.head_position + 2, self.relative_base, param_modes[1])?; + + if param == 0 { + Ok(jump_target as usize) + } else { + Ok(self.head_position + 3) + } + } + + fn execute_equals( + &mut self, + param_modes: Vec, + ) -> Result { + let param1 = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + let param2 = + self.tape + .mode_read(self.head_position + 2, self.relative_base, param_modes[1])?; + let store_target = self.tape.read(self.head_position + 3); + + if param1 == param2 { + self.tape + .mode_write(store_target as usize, self.relative_base, param_modes[2], 1)?; + } else { + self.tape + .mode_write(store_target as usize, self.relative_base, param_modes[2], 0)?; + } + + Ok(self.head_position + 4) + } + + fn execute_adjust_relative_base( + &mut self, + param_modes: Vec, + ) -> Result { + let param = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + + self.relative_base += param; + Ok(self.head_position + 2) + } + + fn execute_input( + &mut self, + param_modes: Vec, + ) -> Result { + let output_idx = self.tape.read(self.head_position + 1); + + let mut buffer = String::new(); + self.input.read_line(&mut buffer).unwrap(); + let input_value = match buffer.trim().parse::() { + Ok(val) => val, + _ => return Err(OpCodeExecutionError::InputFailure), + }; + + self.tape.mode_write( + output_idx as usize, + self.relative_base, + param_modes[0], + input_value, + )?; + + Ok(self.head_position + 2) + } + + fn execute_output( + &mut self, + param_modes: Vec, + ) -> Result { + let output_val = + self.tape + .mode_read(self.head_position + 1, self.relative_base, param_modes[0])?; + writeln!(&mut self.output, "{}", output_val).unwrap(); + + Ok(self.head_position + 2) + } + + fn execute_op(&mut self, op: OpCode) -> Result { + use OpCode::*; + match op { + Add(param_modes) => self.execute_add(param_modes), + Mul(param_modes) => self.execute_mul(param_modes), + Jt(param_modes) => self.execute_jump_true(param_modes), + Jf(param_modes) => self.execute_jump_false(param_modes), + Lt(param_modes) => self.execute_less_than(param_modes), + Eq(param_modes) => self.execute_equals(param_modes), + Rbo(param_modes) => self.execute_adjust_relative_base(param_modes), + In(param_modes) => self.execute_input(param_modes), + Out(param_modes) => self.execute_output(param_modes.clone()), + + Halt => Err(OpCodeExecutionError::ExecutionFinished), + Er(_) => Err(OpCodeExecutionError::ExecutionFailure), + } + } + + pub(crate) fn run(&mut self) -> Result { + loop { + let op = OpCode::from(self.tape.read(self.head_position)); + let head_update = match self.execute_op(op) { + Err(err) => match err { + OpCodeExecutionError::ExecutionFinished => { + return Ok(self.tape.read(0)); + } + OpCodeExecutionError::InputFailure => { + return Err(IntcodeMachineError::InputFailure(self.dump_state())); + } + _ => { + return Err(IntcodeMachineError::ExecutionFailure); + } + }, + Ok(head_update) => head_update, + }; + + self.update_head(head_update)?; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // #[test] + // fn intcode_machine_still_works_for_day2_part1() { + // let mut day2_tape = Tape::new(utils::read_input_file("inputs/2019/day02")); + // day2_tape.write(1, 12); + // day2_tape.write(2, 2); + // + // let dummy_in = b""; + // let mut dummy_out = Vec::new(); + // assert_eq!( + // 4_138_687, + // IntcodeMachine::new(day2_tape, &dummy_in[..], &mut dummy_out) + // .run() + // .unwrap() + // ) + // } + + // #[test] + // fn injecting_io_works_for_day_5_input_part1() { + // let tape = Tape::new(utils::read_input_file("inputs/2019/day05")); + // + // let input = b"1"; + // let mut output = Vec::new(); + // + // IntcodeMachine::new(tape, &input[..], &mut output) + // .run() + // .unwrap(); + // + // let output = utils::parse_multiple_utf8_num_repr_lns(&output) + // .last() + // .unwrap() + // .to_owned(); + // assert_eq!(13_210_611, output); + // } + // + // #[test] + // fn injecting_io_works_for_day_5_input_part2() { + // let tape = Tape::new(utils::read_input_file("inputs/2019/day05")); + // + // let input = b"5"; + // let mut output = Vec::new(); + // + // IntcodeMachine::new(tape, &input[..], &mut output) + // .run() + // .unwrap(); + // + // let output = utils::parse_multiple_utf8_num_repr_lns(&output) + // .last() + // .unwrap() + // .to_owned(); + // assert_eq!(584_126, output); + // } + #[test] + fn example_1_outputs_itself() { + let tape_input = vec![ + 109, 1, 204, -1, 1001, 100, 1, 100, 1008, 100, 16, 101, 1006, 101, 0, 99, + ]; + let tape = Tape::new(tape_input.clone()); + + let dummy_in = b""; + let mut dummy_out = Vec::new(); + + IntcodeMachine::new(tape, &dummy_in[..], &mut dummy_out) + .run() + .unwrap(); + + let full_out = utils::parse_multiple_utf8_num_repr_lns(&dummy_out); + assert_eq!(tape_input, full_out); + } + + #[test] + fn example_2_outputs_16_digit_number() { + let tape = Tape::new(vec![1102, 34_915_192, 34_915_192, 7, 4, 7, 99, 0]); + + let dummy_in = b""; + let mut dummy_out = Vec::new(); + + IntcodeMachine::new(tape, &dummy_in[..], &mut dummy_out) + .run() + .unwrap(); + + assert_eq!( + 1_219_070_632_396_864, + utils::parse_multiple_utf8_num_repr_lns(&dummy_out) + .last() + .unwrap() + .to_owned() + ) + } + + #[test] + fn example_3_outputs_1125899906842624() { + let tape = Tape::new(vec![104, 1_125_899_906_842_624, 99]); + + let dummy_in = b""; + let mut dummy_out = Vec::new(); + + IntcodeMachine::new(tape, &dummy_in[..], &mut dummy_out) + .run() + .unwrap(); + + assert_eq!( + 1_125_899_906_842_624, + utils::parse_multiple_utf8_num_repr_lns(&dummy_out) + .last() + .unwrap() + .to_owned() + ); + } +} diff --git a/2019/day09/src/lib.rs b/2019/day09/src/lib.rs new file mode 100644 index 0000000..0482a7d --- /dev/null +++ b/2019/day09/src/lib.rs @@ -0,0 +1,58 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::intcode_machine::{IntcodeMachine, Tape}; +use aoc_solution::Aoc; + +pub mod intcode_machine; +pub mod utils; + +#[derive(Aoc)] +pub struct Day09; + +pub fn do_part1(tape: Tape) { + let fake_input = b"1"; + let mut output = Vec::new(); + + IntcodeMachine::new(tape, &fake_input[..], &mut output) + .run() + .unwrap(); + + let parsed_output = utils::parse_multiple_utf8_num_repr_lns(&output) + .last() + .unwrap() + .to_owned(); + + println!("{:?}", parsed_output); +} + +pub fn do_part2(tape: Tape) { + let fake_input = b"2"; + let mut output = Vec::new(); + + IntcodeMachine::new(tape, &fake_input[..], &mut output) + .run() + .unwrap(); + + let parsed_output = utils::parse_multiple_utf8_num_repr_lns(&output) + .last() + .unwrap() + .to_owned(); + + println!("{:?}", parsed_output); +} diff --git a/2019/day09/src/main.rs b/2019/day09/src/main.rs new file mode 100644 index 0000000..e1b3317 --- /dev/null +++ b/2019/day09/src/main.rs @@ -0,0 +1,27 @@ +// Copyright 2019-2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day09_2019::intcode_machine::Tape; +use day09_2019::{do_part1, do_part2, utils}; + +fn main() { + let tape = Tape::new(utils::read_input_file("inputs/2019/day09")); + + do_part1(tape.clone()); + do_part2(tape); +} diff --git a/2019/day09/src/utils.rs b/2019/day09/src/utils.rs new file mode 100644 index 0000000..d86d53b --- /dev/null +++ b/2019/day09/src/utils.rs @@ -0,0 +1,93 @@ +use std::fs; + +pub fn num_to_digits_vec(val: usize) -> Vec { + let mut digits = Vec::new(); + let mut n = val; + while n > 9 { + digits.push(n % 10); + n /= 10; + } + digits.push(n); + digits.reverse(); + digits +} + +pub fn digits_vec_to_num(digits: &[usize]) -> usize { + digits + .iter() + .cloned() + .reduce(|acc, x| match acc { + 0 => x, + n => n * 10 + x, + }) + .unwrap() +} + +pub fn utf8_dec_num_repr_to_num(utf8_dec_digits: &[u8]) -> isize { + let mut possible_sign = utf8_dec_digits.iter().peekable(); + if possible_sign.peek().unwrap() == &&45 { + 0 - digits_vec_to_num( + &utf8_dec_digits + .iter() + .skip(1) + .map(|&d| (d - 48) as usize) + .collect::>(), + ) as isize + } else { + digits_vec_to_num( + &utf8_dec_digits + .iter() + .map(|&d| (d - 48) as usize) + .collect::>(), + ) as isize + } +} + +pub fn parse_multiple_utf8_num_repr_lns(utf8_dec_digits_nums: &[u8]) -> Vec { + utf8_dec_digits_nums + .split(|d| d == &10) + .filter(|ds| !ds.is_empty()) + .map(utf8_dec_num_repr_to_num) + .collect() +} + +pub fn read_input_file(path: &str) -> Vec { + fs::read_to_string(path) + .unwrap() + .split(',') + .map(|s| s.parse::().unwrap()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn utf8_dec_num_repr_to_num_works_for_positive_values() { + assert_eq!(42, utf8_dec_num_repr_to_num(&[52, 50])) + } + + #[test] + fn utf8_dec_num_repr_to_num_works_for_negative_values() { + assert_eq!(-42, utf8_dec_num_repr_to_num(&[45, 52, 50])) + } + + #[test] + fn recovering_multiple_written_values() { + let mut out_vec = Vec::new(); + + writeln!(&mut out_vec, "{}", &42).unwrap(); + writeln!(&mut out_vec, "{}", &-56).unwrap(); + writeln!(&mut out_vec, "{}", &6432).unwrap(); + writeln!(&mut out_vec, "{}", &-432).unwrap(); + writeln!(&mut out_vec, "{}", &-32432).unwrap(); + writeln!(&mut out_vec, "{}", &0).unwrap(); + + assert_eq!( + vec![42, -56, 6432, -432, -32432, 0], + parse_multiple_utf8_num_repr_lns(&out_vec) + ) + } +} diff --git a/2020/day01/Cargo.toml b/2020/day01/Cargo.toml new file mode 100644 index 0000000..7879c83 --- /dev/null +++ b/2020/day01/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day01_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day01_2020" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day01/src/lib.rs b/2020/day01/src/lib.rs new file mode 100644 index 0000000..01b7ba8 --- /dev/null +++ b/2020/day01/src/lib.rs @@ -0,0 +1,79 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; + +#[derive(Aoc)] +pub struct Day01; + +pub fn part1(input: Vec) -> Option { + // if you really want to be fancy about it, you could sort the whole thing first, + // then be smart about choosing second value, like if v1 + v2 > 2020, don't bother + // checking anything above v2. But current approach works well enough + // and cutting edge performance is not a requirement. + + for pair in input.iter().tuple_combinations::<(_, _)>() { + if pair.0 + pair.1 == 2020 { + return Some(pair.0 * pair.1); + } + } + + None +} + +pub fn part2(input: Vec) -> Option { + for triplet in input.iter().tuple_combinations::<(_, _, _)>() { + if triplet.0 + triplet.1 + triplet.2 == 2020 { + return Some(triplet.0 * triplet.1 * triplet.2); + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![1721, 979, 366, 299, 675, 1456]; + let expected = 514579; + + assert_eq!(expected, part1(input).unwrap()) + } + + #[test] + fn part1_fails_on_invalid_input() { + assert!(part1(vec![1, 2, 3]).is_none()) + } + + #[test] + fn part2_sample_input() { + let input = vec![1721, 979, 366, 299, 675, 1456]; + let expected = 241861950; + + assert_eq!(expected, part2(input).unwrap()) + } + + #[test] + fn part2_fails_on_invalid_input() { + assert!(part2(vec![1, 2, 3]).is_none()) + } +} diff --git a/2020/day01/src/main.rs b/2020/day01/src/main.rs new file mode 100644 index 0000000..b928c10 --- /dev/null +++ b/2020/day01/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day01_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day01").expect("failed to read input file"); + let part1_result = part1(input.clone()).expect("failed to solve part1"); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day02/Cargo.toml b/2020/day02/Cargo.toml new file mode 100644 index 0000000..7ce4f42 --- /dev/null +++ b/2020/day02/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day02_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day02_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day02/src/lib.rs b/2020/day02/src/lib.rs new file mode 100644 index 0000000..4868a66 --- /dev/null +++ b/2020/day02/src/lib.rs @@ -0,0 +1,179 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::{bail, Context}; +use aoc_solution::Aoc; +use std::str::FromStr; + +#[derive(Aoc)] +pub struct Day02; + +#[derive(Debug)] +pub struct Policy { + lower_bound: usize, + upper_bound: usize, + character: char, +} + +impl FromStr for Policy { + type Err = anyhow::Error; + + fn from_str(raw_policy: &str) -> Result { + let mut raw_policy = raw_policy.to_string(); + // final character is a `:` so we can discard it + // if this fails, it means provided string was empty + raw_policy.pop().context("malformed policy")?; + + // we are left with `lowerbound-upperbound character` + let split: Vec<_> = raw_policy.split_whitespace().collect(); + if split.len() != 2 { + bail!("malformed policy") + } + + let chars_raw: Vec<_> = split[1].chars().collect(); + if chars_raw.len() != 1 { + bail!("malformed policy") + } + + let character = chars_raw.first().context("malformed policy")?.to_owned(); + + let bound_split: Vec<_> = split[0].split('-').collect(); + if bound_split.len() != 2 { + bail!("malformed policy") + } + + let lower_bound = bound_split[0].parse()?; + let upper_bound = bound_split[1].parse()?; + + Ok(Policy { + lower_bound, + upper_bound, + character, + }) + } +} + +impl Policy { + fn verify_password_part1(&self, password: &Password) -> bool { + let chars = password.chars(); + let count = chars.filter(|c| c == &self.character).count(); + count >= self.lower_bound && count <= self.upper_bound + } + + fn verify_password_part2(&self, password: &Password) -> bool { + let chars: Vec<_> = password.chars().collect(); + + let first_char = chars[self.lower_bound - 1]; + let second_char = chars[self.upper_bound - 1]; + + (first_char == self.character) ^ (second_char == self.character) + } +} + +type Password<'a> = &'a str; + +// input is formatted as follows: +// `lowerbound-upperbound character: password` +// for example: `1-3 a: abcde` +// note that final space separates policy from password +fn parse_into_policy_password(input_line: &str) -> Option<(Policy, Password)> { + let split: Vec<_> = input_line.split_whitespace().collect(); + + if split.len() != 3 { + return None; + } + + // we know there will be 2 chunks in policy due to the described structure + let policy_raw = [split[0], split[1]].join(" "); + let password = split[2]; + let policy = Policy::from_str(&policy_raw).ok()?; + + Some((policy, password)) +} + +pub fn part1(input: Vec) -> Option { + let mut valid_count = 0; + for policy_password in input.iter().map(|input| parse_into_policy_password(input)) { + let (policy, password) = policy_password?; + if policy.verify_password_part1(&password) { + valid_count += 1; + } + } + Some(valid_count) +} + +pub fn part2(input: Vec) -> Option { + let mut valid_count = 0; + for policy_password in input.iter().map(|input| parse_into_policy_password(input)) { + let (policy, password) = policy_password?; + if policy.verify_password_part2(&password) { + valid_count += 1; + } + } + Some(valid_count) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "1-3 a: abcde".to_string(), + "1-3 b: cdefg".to_string(), + "2-9 c: ccccccccc".to_string(), + ]; + let expected = 2; + + assert_eq!(expected, part1(input).unwrap()) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "1-3 a: abcde".to_string(), + "1-3 b: cdefg".to_string(), + "2-9 c: ccccccccc".to_string(), + ]; + let expected = 1; + + assert_eq!(expected, part2(input).unwrap()) + } + + #[cfg(test)] + mod policy_parsing { + use super::*; + + #[test] + fn returns_err_on_malformed_policies() { + assert!(Policy::from_str("1- 3 a:").is_err()); + assert!(Policy::from_str("1-3 ab:").is_err()); + assert!(Policy::from_str("1-2-3 a:").is_err()); + assert!(Policy::from_str("1-a a:").is_err()); + assert!(Policy::from_str("a-3 a:").is_err()); + assert!(Policy::from_str("a-3 a").is_err()); + assert!(Policy::from_str("a-3 :").is_err()); + } + + #[test] + fn returns_none_on_malformed_policy_passwords() { + assert!(parse_into_policy_password("1-3 a: abcde foo").is_none()); + } + } +} diff --git a/2020/day02/src/main.rs b/2020/day02/src/main.rs new file mode 100644 index 0000000..ff2b15f --- /dev/null +++ b/2020/day02/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day02_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day02").expect("failed to read input file"); + let part1_result = part1(input.clone()).expect("failed to solve part1"); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day03/Cargo.toml b/2020/day03/Cargo.toml new file mode 100644 index 0000000..cda34f8 --- /dev/null +++ b/2020/day03/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day03_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day03_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day03/src/lib.rs b/2020/day03/src/lib.rs new file mode 100644 index 0000000..6dd93c6 --- /dev/null +++ b/2020/day03/src/lib.rs @@ -0,0 +1,225 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::bail; +use aoc_solution::Aoc; +use std::borrow::Borrow; + +#[derive(Aoc)] +pub struct Day03; + +const EMPTY_STATE_SYMBOL: char = '.'; +const TREE_STATE_SYMBOL: char = '#'; +const PART1_SLOPE: (usize, usize) = (3, 1); + +type Position = (usize, usize); + +#[derive(Clone, Copy)] +pub enum Location { + Empty, + Tree, +} + +impl Location { + fn is_tree(&self) -> bool { + matches!(self, Location::Tree) + } +} + +impl TryFrom for Location { + type Error = anyhow::Error; + + fn try_from(value: char) -> Result { + match value { + EMPTY_STATE_SYMBOL => Ok(Location::Empty), + TREE_STATE_SYMBOL => Ok(Location::Tree), + _ => bail!("invalid location"), + } + } +} + +struct Row(Vec); + +impl<'a> TryFrom<&'a String> for Row { + type Error = anyhow::Error; + + fn try_from(raw: &'a String) -> Result { + let chars = raw.chars(); + let size_hint = chars.size_hint(); + let mut row = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0)); + for location in raw.chars().map(|char| char.try_into()) { + row.push(location?); + } + + Ok(Row(row)) + } +} + +struct Grid(Vec); + +impl<'a> TryFrom<&'a [String]> for Grid { + type Error = anyhow::Error; + + fn try_from(raw: &'a [String]) -> Result { + let mut rows = Vec::with_capacity(raw.len()); + for raw_row in raw { + rows.push(raw_row.try_into()?); + } + + Ok(Grid(rows)) + } +} + +impl std::ops::Index for Grid { + type Output = Location; + + fn index(&self, index: Position) -> &Self::Output { + let (x, y) = index; + + self.0[y].0[x].borrow() + } +} + +impl Grid { + // TODO: what's the idiomatic way of creating a parameterized iterator? + fn into_iterator(self, slope: (usize, usize)) -> GridIntoIterator { + GridIntoIterator { + slope, + position: (0, 0), + grid: self, + } + } + + fn len(&self) -> usize { + self.0.len() + } + + fn row_len(&self) -> usize { + if self.0.is_empty() { + 0 + } else { + self.0[0].0.len() + } + } +} + +struct GridIntoIterator { + slope: (usize, usize), + position: Position, + grid: Grid, +} + +// as per part1 specs +impl Iterator for GridIntoIterator { + type Item = Location; + + fn next(&mut self) -> Option { + if self.position.1 >= self.grid.len() { + None + } else { + let location = self.grid[self.position]; + self.position = ( + (self.position.0 + self.slope.0) % self.grid.row_len(), + self.position.1 + self.slope.1, + ); + Some(location) + } + } +} + +pub fn part1(input: Vec) -> Option { + Some( + Grid::try_from(input.as_slice()) + .ok()? + .into_iterator(PART1_SLOPE) + .filter(|location| location.is_tree()) + .count(), + ) +} + +pub fn part2(input: Vec) -> Option { + let slopes = vec![(1usize, 1usize), (3, 1), (5, 1), (7, 1), (1, 2)]; + // can't do it with a single iterator due to trying to catch errors with `?` : ( + let mut running_total = 1; + for slope in slopes { + running_total *= Grid::try_from(input.as_slice()) + .ok()? + .into_iterator(slope) + .filter(|location| location.is_tree()) + .count(); + } + + Some(running_total) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "..##.......".to_string(), + "#...#...#..".to_string(), + ".#....#..#.".to_string(), + "..#.#...#.#".to_string(), + ".#...##..#.".to_string(), + "..#.##.....".to_string(), + ".#.#.#....#".to_string(), + ".#........#".to_string(), + "#.##...#...".to_string(), + "#...##....#".to_string(), + ".#..#...#.#".to_string(), + ]; + + let expected = 7; + + assert_eq!(expected, part1(input).unwrap()) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "..##.......".to_string(), + "#...#...#..".to_string(), + ".#....#..#.".to_string(), + "..#.#...#.#".to_string(), + ".#...##..#.".to_string(), + "..#.##.....".to_string(), + ".#.#.#....#".to_string(), + ".#........#".to_string(), + "#.##...#...".to_string(), + "#...##....#".to_string(), + ".#..#...#.#".to_string(), + ]; + + let expected = 336; + + assert_eq!(expected, part2(input).unwrap()) + } + + #[test] + fn fails_to_parse_invalid_location() { + assert!(Location::try_from('a').is_err()) + } + + #[test] + fn grid_returns_0_row_if_empty() { + assert_eq!(0, Grid(Vec::new()).row_len()) + } +} diff --git a/2020/day03/src/main.rs b/2020/day03/src/main.rs new file mode 100644 index 0000000..c1abfda --- /dev/null +++ b/2020/day03/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day03_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day03").expect("failed to read input file"); + let part1_result = part1(input.clone()).expect("failed to solve part1"); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day04/Cargo.toml b/2020/day04/Cargo.toml new file mode 100644 index 0000000..bac2165 --- /dev/null +++ b/2020/day04/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day04_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day04_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day04/src/lib.rs b/2020/day04/src/lib.rs new file mode 100644 index 0000000..bc35b2e --- /dev/null +++ b/2020/day04/src/lib.rs @@ -0,0 +1,112 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::passport::{Passport, RawPassport}; +use aoc_solution::Aoc; + +mod passport; + +#[derive(Aoc)] +pub struct Day04; + +pub fn part1(input: Vec) -> usize { + input + .iter() + .map(RawPassport::try_from) + .filter(Result::is_ok) + .count() +} + +pub fn part2(input: Vec) -> usize { + input + .iter() + .map(RawPassport::try_from) + .filter(Result::is_ok) + .map(|raw_pass| Passport::try_from(raw_pass.unwrap())) + .filter(Result::is_ok) + .filter(|pass| pass.as_ref().unwrap().validate()) + .count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "ecl:gry pid:860033327 eyr:2020 hcl:#fffffd +byr:1937 iyr:2017 cid:147 hgt:183cm" + .to_string(), + "iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 +hcl:#cfa07d byr:1929" + .to_string(), + "hcl:#ae17e1 iyr:2013 +eyr:2024 +ecl:brn pid:760753108 byr:1931 +hgt:179cm" + .to_string(), + "hcl:#cfa07d eyr:2025 pid:166559648 +iyr:2011 ecl:brn hgt:59in +" + .to_string(), + ]; + + let expected = 2; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "eyr:1972 cid:100 +hcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926" + .to_string(), + "iyr:2019 +hcl:#602927 eyr:1967 hgt:170cm +ecl:grn pid:012533040 byr:1946" + .to_string(), + "hcl:dab227 iyr:2012 +ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277" + .to_string(), + "hgt:59cm ecl:zzz +eyr:2038 hcl:74454a iyr:2023 +pid:3556412378 byr:2007" + .to_string(), + "pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980 +hcl:#623a2f" + .to_string(), + "eyr:2029 ecl:blu cid:129 byr:1989 +iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm" + .to_string(), + "hcl:#888785 +hgt:164cm byr:2001 iyr:2015 cid:88 +pid:545766238 ecl:hzl +eyr:2022" + .to_string(), + "iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719 +" + .to_string(), + ]; + + let expected = 4; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day04/src/main.rs b/2020/day04/src/main.rs new file mode 100644 index 0000000..bec2521 --- /dev/null +++ b/2020/day04/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day04_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day04") + .expect("failed to read input file"); + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day04/src/passport/eye_color.rs b/2020/day04/src/passport/eye_color.rs new file mode 100644 index 0000000..74f5d9d --- /dev/null +++ b/2020/day04/src/passport/eye_color.rs @@ -0,0 +1,50 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::bail; + +const COLOR_AMBER_ABBREVIATION: &str = "amb"; +const COLOR_BLUE_ABBREVIATION: &str = "blu"; +const COLOR_BROWN_ABBREVIATION: &str = "brn"; +const COLOR_GRAY_ABBREVIATION: &str = "gry"; +const COLOR_GREEN_ABBREVIATION: &str = "grn"; +const COLOR_HAZEL_ABBREVIATION: &str = "hzl"; +const COLOR_OTHER_ABBREVIATION: &str = "oth"; + +pub(super) enum EyeColor { + Amber, + Blue, + Brown, + Gray, + Green, + Hazel, + Other, +} + +impl<'a> TryFrom<&'a str> for EyeColor { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + match value { + COLOR_AMBER_ABBREVIATION => Ok(Self::Amber), + COLOR_BLUE_ABBREVIATION => Ok(Self::Blue), + COLOR_BROWN_ABBREVIATION => Ok(Self::Brown), + COLOR_GRAY_ABBREVIATION => Ok(Self::Gray), + COLOR_GREEN_ABBREVIATION => Ok(Self::Green), + COLOR_HAZEL_ABBREVIATION => Ok(Self::Hazel), + COLOR_OTHER_ABBREVIATION => Ok(Self::Other), + _ => bail!("invalid colour"), + } + } +} diff --git a/2020/day04/src/passport/hair_color.rs b/2020/day04/src/passport/hair_color.rs new file mode 100644 index 0000000..dbfaaae --- /dev/null +++ b/2020/day04/src/passport/hair_color.rs @@ -0,0 +1,65 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(super) struct HairColor(String); + +impl> From for HairColor { + fn from(str: S) -> Self { + HairColor(str.into()) + } +} + +impl HairColor { + pub(super) fn validate_in_passport(&self) -> bool { + if !self.0.is_ascii() || self.0.len() != 7 { + return false; + } + + for (i, char) in self.0.chars().enumerate() { + if i == 0 { + if char != '#' { + return false; + } else { + continue; + } + } else { + match char { + '0'..='9' | 'a'..='f' => continue, + _ => return false, + } + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hair_color_validation() { + let valid = vec!["#123abc", "#123abf"]; + let invalid = vec!["", "#123abz", "123abcd"]; + + for valid in valid { + assert!(HairColor(valid.to_string()).validate_in_passport()) + } + + for invalid in invalid { + assert!(!HairColor(invalid.to_string()).validate_in_passport()) + } + } +} diff --git a/2020/day04/src/passport/height.rs b/2020/day04/src/passport/height.rs new file mode 100644 index 0000000..22169e9 --- /dev/null +++ b/2020/day04/src/passport/height.rs @@ -0,0 +1,121 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::bail; + +const HEIGHT_METRIC_UNIT: &str = "cm"; +const HEIGHT_IMPERIAL_UNIT: &str = "in"; + +pub(super) enum Height { + Metric(usize), + Imperial(usize), +} + +impl<'a> TryFrom<&'a str> for Height { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + // we need at very least 3 characters - 2 for unit and one for value + if !value.is_ascii() || value.len() < 3 { + bail!("invalid height") + } + + let mut chars: Vec<_> = value.chars().collect(); + let unit = [chars.pop().unwrap(), chars.pop().unwrap()] + .iter() + .rev() + .collect::(); + let value = chars.iter().collect::().parse()?; + + match &*unit { + HEIGHT_METRIC_UNIT => Ok(Height::Metric(value)), + HEIGHT_IMPERIAL_UNIT => Ok(Height::Imperial(value)), + _ => bail!("invalid height"), + } + } +} + +impl Height { + fn value(&self) -> usize { + match self { + Height::Metric(value) => *value, + Height::Imperial(value) => *value, + } + } + + fn is_metric(&self) -> bool { + matches!(self, Height::Metric(_)) + } + + fn is_imperial(&self) -> bool { + matches!(self, Height::Imperial(_)) + } + + pub(super) fn validate_in_passport(&self) -> bool { + let value = self.value(); + if self.is_metric() && !(150..=193).contains(&value) { + return false; + } + if self.is_imperial() && !(59..=76).contains(&value) { + return false; + } + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn height_parsing() { + let height = Height::try_from("60in").unwrap(); + assert!(height.is_imperial()); + assert_eq!(60, height.value()); + + let height = Height::try_from("60cm").unwrap(); + assert!(height.is_metric()); + assert_eq!(60, height.value()); + + assert!(Height::try_from("60c").is_err()); + assert!(Height::try_from("60inch").is_err()); + assert!(Height::try_from("a60cm").is_err()); + assert!(Height::try_from("60").is_err()); + } + + #[test] + fn passport_validation() { + let good_metric = [150, 170, 193]; + let bad_metric = [149, 194, 0, 2000]; + + let good_imperial = [59, 65, 67]; + let bad_imperial = [58, 77, 0, 321]; + + for good in good_metric.iter() { + assert!(Height::Metric(*good).validate_in_passport()) + } + + for bad in bad_metric.iter() { + assert!(!Height::Metric(*bad).validate_in_passport()) + } + + for good in good_imperial.iter() { + assert!(Height::Imperial(*good).validate_in_passport()) + } + + for bad in bad_imperial.iter() { + assert!(!Height::Imperial(*bad).validate_in_passport()) + } + } +} diff --git a/2020/day04/src/passport/mod.rs b/2020/day04/src/passport/mod.rs new file mode 100644 index 0000000..0eb5e4c --- /dev/null +++ b/2020/day04/src/passport/mod.rs @@ -0,0 +1,190 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use self::eye_color::EyeColor; +use self::hair_color::HairColor; +use self::height::Height; +use self::passport_id::PassportId; +use anyhow::bail; +use std::collections::HashMap; + +mod eye_color; +mod hair_color; +mod height; +mod passport_id; + +const BIRTH_YEAR_ABBREVIATION: &str = "byr"; +const ISSUE_YEAR_ABBREVIATION: &str = "iyr"; +const EXPIRATION_YEAR_ABBREVIATION: &str = "eyr"; +const HEIGHT_ABBREVIATION: &str = "hgt"; +const HAIR_COLOR_ABBREVIATION: &str = "hcl"; +const EYE_COLOR_ABBREVIATION: &str = "ecl"; +const PASSPORT_ID_ABBREVIATION: &str = "pid"; +const COUNTRY_ID_ABBREVIATION: &str = "cid"; + +pub(crate) struct Passport { + birth_year: usize, + issue_year: usize, + expiration_year: usize, + height: Height, + hair_color: HairColor, + #[allow(dead_code)] // we don't use this field for anything + eye_color: EyeColor, + passport_id: PassportId, + #[allow(dead_code)] // we don't use this field for anything + country_id: Option, +} + +impl TryFrom for Passport { + type Error = anyhow::Error; + + fn try_from(value: RawPassport) -> Result { + Ok(Passport { + birth_year: value.birth_year, + issue_year: value.issue_year, + expiration_year: value.expiration_year, + height: Height::try_from(&*value.height)?, + hair_color: HairColor::from(value.hair_color), + eye_color: EyeColor::try_from(&*value.eye_color)?, + passport_id: PassportId::from(value.passport_id), + country_id: value.country_id, + }) + } +} + +impl Passport { + pub(crate) fn validate(&self) -> bool { + if self.birth_year < 1920 || self.birth_year > 2002 { + return false; + } + + if self.issue_year < 2010 || self.issue_year > 2020 { + return false; + } + + if self.expiration_year < 2020 || self.expiration_year > 2030 { + return false; + } + + if !self.height.validate_in_passport() { + return false; + } + + if !self.hair_color.validate_in_passport() { + return false; + } + + if !self.passport_id.validate_in_passport() { + return false; + } + + true + } +} + +pub(crate) struct RawPassport { + birth_year: usize, + issue_year: usize, + expiration_year: usize, + height: String, + hair_color: String, + eye_color: String, + passport_id: String, + country_id: Option, +} + +impl<'a> TryFrom<&'a String> for RawPassport { + type Error = anyhow::Error; + + fn try_from(value: &'a String) -> Result { + let fields = value.split_ascii_whitespace(); + + let mut mandatory_passport_fields = HashMap::new(); + let mut country_id = None; + + for field in fields { + let name_value = field.split(':').collect::>(); + if name_value.len() != 2 { + bail!("invalid field {field}"); + } + + let name = name_value[0]; + let value = name_value[1]; + + match name { + BIRTH_YEAR_ABBREVIATION => { + mandatory_passport_fields.insert(BIRTH_YEAR_ABBREVIATION, value); + } + ISSUE_YEAR_ABBREVIATION => { + mandatory_passport_fields.insert(ISSUE_YEAR_ABBREVIATION, value); + } + EXPIRATION_YEAR_ABBREVIATION => { + mandatory_passport_fields.insert(EXPIRATION_YEAR_ABBREVIATION, value); + } + HEIGHT_ABBREVIATION => { + mandatory_passport_fields.insert(HEIGHT_ABBREVIATION, value); + } + HAIR_COLOR_ABBREVIATION => { + mandatory_passport_fields.insert(HAIR_COLOR_ABBREVIATION, value); + } + EYE_COLOR_ABBREVIATION => { + mandatory_passport_fields.insert(EYE_COLOR_ABBREVIATION, value); + } + PASSPORT_ID_ABBREVIATION => { + mandatory_passport_fields.insert(PASSPORT_ID_ABBREVIATION, value); + } + COUNTRY_ID_ABBREVIATION => country_id = Some(value), + _ => { + bail!("unknown field") + } + } + } + + if mandatory_passport_fields.len() != 7 { + bail!("missing fields"); + } + + Ok(RawPassport { + birth_year: mandatory_passport_fields + .get(BIRTH_YEAR_ABBREVIATION) + .unwrap() + .parse()?, + issue_year: mandatory_passport_fields + .get(ISSUE_YEAR_ABBREVIATION) + .unwrap() + .parse()?, + expiration_year: mandatory_passport_fields + .get(EXPIRATION_YEAR_ABBREVIATION) + .unwrap() + .parse()?, + height: mandatory_passport_fields + .get(HEIGHT_ABBREVIATION) + .unwrap() + .parse()?, + hair_color: mandatory_passport_fields + .get(HAIR_COLOR_ABBREVIATION) + .unwrap() + .parse()?, + eye_color: mandatory_passport_fields + .get(EYE_COLOR_ABBREVIATION) + .unwrap() + .parse()?, + passport_id: mandatory_passport_fields + .get(PASSPORT_ID_ABBREVIATION) + .unwrap() + .parse()?, + country_id: country_id.and_then(|id| id.parse().ok()), + }) + } +} diff --git a/2020/day04/src/passport/passport_id.rs b/2020/day04/src/passport/passport_id.rs new file mode 100644 index 0000000..ced9804 --- /dev/null +++ b/2020/day04/src/passport/passport_id.rs @@ -0,0 +1,55 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub(super) struct PassportId(String); + +impl> From for PassportId { + fn from(str: S) -> Self { + PassportId(str.into()) + } +} + +impl PassportId { + pub(super) fn validate_in_passport(&self) -> bool { + // it must be numeric of size 9 + if !self.0.is_ascii() || self.0.len() != 9 { + return false; + } + for char in self.0.chars() { + if !char.is_numeric() { + return false; + } + } + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn passport_id_validation() { + let valid = vec!["000000001", "600304001"]; + let invalid = vec!["", "0123456789", "00000001", "aaaaaaaaa"]; + + for valid in valid { + assert!(PassportId(valid.to_string()).validate_in_passport()) + } + + for invalid in invalid { + assert!(!PassportId(invalid.to_string()).validate_in_passport()) + } + } +} diff --git a/2020/day05/Cargo.toml b/2020/day05/Cargo.toml new file mode 100644 index 0000000..4d3eb18 --- /dev/null +++ b/2020/day05/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day05_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day05_2020" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day05/src/lib.rs b/2020/day05/src/lib.rs new file mode 100644 index 0000000..5690263 --- /dev/null +++ b/2020/day05/src/lib.rs @@ -0,0 +1,157 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; + +#[derive(Aoc)] +pub struct Day05; + +const HIGH_BIT_ROW: char = 'B'; +const LOW_BIT_ROW: char = 'F'; +const HIGH_BIT_COLUMN: char = 'R'; +const LOW_BIT_COLUMN: char = 'L'; + +#[derive(Debug)] +pub struct MalformedSeat; + +struct Seat { + row: u8, + // restricted from 0 to 127, i.e. 7 bit value + column: u8, // restricted from 0 to 7, i.e. 3 bit value +} + +impl Seat { + fn id(&self) -> usize { + self.row as usize * 8 + self.column as usize + } +} + +impl<'a> TryFrom<&'a String> for Seat { + type Error = MalformedSeat; + + fn try_from(value: &'a String) -> Result { + if !value.is_ascii() || value.len() != 10 { + return Err(MalformedSeat); + } + + let mut row = 0; + let mut column = 0; + + let (row_raw, column_raw) = value.split_at(7); + for (i, row_raw_char) in row_raw.chars().rev().enumerate() { + if row_raw_char == HIGH_BIT_ROW { + row |= 1 << i + } else if row_raw_char != LOW_BIT_ROW { + return Err(MalformedSeat); + } + } + + for (i, column_raw_char) in column_raw.chars().rev().enumerate() { + if column_raw_char == HIGH_BIT_COLUMN { + column |= 1 << i + } else if column_raw_char != LOW_BIT_COLUMN { + return Err(MalformedSeat); + } + } + + Ok(Seat { row, column }) + } +} + +pub fn part1(input: Vec) -> Option { + input + .iter() + .map(Seat::try_from) + .filter(Result::is_ok) + .map(|seat| seat.unwrap().id()) + .max() +} + +pub fn part2(input: Vec) -> Option { + let mut seat_ids: Vec<_> = input + .iter() + .map(Seat::try_from) + .filter(Result::is_ok) + .map(|seat| seat.unwrap().id()) + .collect(); + + seat_ids.sort_unstable(); + let mut gaps = Vec::new(); + for (&prev_seat_id, &next_seat_id) in seat_ids.iter().tuple_windows() { + if prev_seat_id + 1 != next_seat_id { + gaps.push(prev_seat_id + 1); + } + } + + if gaps.len() != 1 && !gaps.is_empty() { + eprintln!("found multiple possible seat locations! - {:?}", gaps); + None + } else { + gaps.pop() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn seat_parsing() { + let seat1 = Seat::try_from(&"BFFFBBFRRR".to_string()).unwrap(); + assert_eq!(seat1.row, 70); + assert_eq!(seat1.column, 7); + assert_eq!(seat1.id(), 567); + + let seat2 = Seat::try_from(&"FFFBBBFRRR".to_string()).unwrap(); + assert_eq!(seat2.row, 14); + assert_eq!(seat2.column, 7); + assert_eq!(seat2.id(), 119); + + let seat3 = Seat::try_from(&"BBFFBBFRLL".to_string()).unwrap(); + assert_eq!(seat3.row, 102); + assert_eq!(seat3.column, 4); + assert_eq!(seat3.id(), 820); + } + + #[test] + fn sample_part1_input() { + let input = vec![ + "BFFFBBFRRR".to_string(), + "FFFBBBFRRR".to_string(), + "BBFFBBFRLL".to_string(), + ]; + + let expected = 820; + + assert_eq!(expected, part1(input).unwrap()); + } + + #[test] + fn sample_part2_input() { + let input = vec![ + "BFFFBBFRRR".to_string(), // 567 + "BFFFBBFRRL".to_string(), // 566 + "BFFFBBFRLL".to_string(), // 564 + ]; + + let expected = 565; + + assert_eq!(expected, part2(input).unwrap()); + } +} diff --git a/2020/day05/src/main.rs b/2020/day05/src/main.rs new file mode 100644 index 0000000..14d96ff --- /dev/null +++ b/2020/day05/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day05_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day05").expect("failed to read input file"); + let part1_result = part1(input.clone()).expect("failed to solve part1"); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day06/Cargo.toml b/2020/day06/Cargo.toml new file mode 100644 index 0000000..c32ac69 --- /dev/null +++ b/2020/day06/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day06_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day06_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day06/src/lib.rs b/2020/day06/src/lib.rs new file mode 100644 index 0000000..b12c29b --- /dev/null +++ b/2020/day06/src/lib.rs @@ -0,0 +1,124 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; + +#[derive(Aoc)] +pub struct Day06; + +struct Group { + size: usize, + answers: HashMap, +} + +impl<'a> From<&'a String> for Group { + fn from(answers_raw: &'a String) -> Self { + let mut answers = HashMap::new(); + + let group_answers: Vec<_> = answers_raw.split_ascii_whitespace().collect(); + let size = group_answers.len(); + + group_answers + .into_iter() + .flat_map(|answer| answer.chars()) + .for_each(|char| *answers.entry(char).or_insert(0) += 1); + + Group { size, answers } + } +} + +impl Group { + fn len(&self) -> usize { + self.answers.len() + } + + fn all_yes(&self) -> usize { + self.answers + .iter() + .filter(|(_, &count)| count == self.size) + .count() + } +} + +pub fn part1(input: Vec) -> usize { + input + .iter() + .map(|group_answers| Group::from(group_answers).len()) + .sum() +} + +pub fn part2(input: Vec) -> usize { + input + .iter() + .map(|group_answers| Group::from(group_answers).all_yes()) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "abc".to_string(), + "a +b +c" + .to_string(), + "ab +ac" + .to_string(), + "a +a +a +a" + .to_string(), + "b".to_string(), + ]; + + let expected = 11; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "abc".to_string(), + "a +b +c" + .to_string(), + "ab +ac" + .to_string(), + "a +a +a +a" + .to_string(), + "b".to_string(), + ]; + + let expected = 6; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day06/src/main.rs b/2020/day06/src/main.rs new file mode 100644 index 0000000..fb73d29 --- /dev/null +++ b/2020/day06/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day06_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day06") + .expect("failed to read input file"); + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day07/Cargo.toml b/2020/day07/Cargo.toml new file mode 100644 index 0000000..3bc84c0 --- /dev/null +++ b/2020/day07/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day07_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day07_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day07/src/lib.rs b/2020/day07/src/lib.rs new file mode 100644 index 0000000..b7022c7 --- /dev/null +++ b/2020/day07/src/lib.rs @@ -0,0 +1,301 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::fmt::{self, Debug, Formatter}; +use std::rc::Rc; + +#[derive(Aoc)] +pub struct Day07; + +const EMPTY_BAG: &str = "no other"; +const TARGET_BAG: &str = "shiny gold"; + +struct Part1Traversal<'a> { + visited: HashSet, + graph: &'a BagGraph, +} + +impl<'a> Part1Traversal<'a> { + fn new(graph: &'a BagGraph) -> Self { + Part1Traversal { + visited: HashSet::new(), + graph, + } + } + + fn visit_entry(&mut self, entry: &Rc>) { + for parent in &entry.borrow().parents { + if self.visited.insert(parent.borrow().name.clone()) { + self.visit_entry(parent); + } + } + } + + fn get_unique_parents(&mut self, bag_name: &str) -> HashSet { + let entry = self.graph.nodes.get(bag_name).unwrap(); + + for parent in &entry.borrow().parents { + self.visited.insert(parent.borrow().name.clone()); + self.visit_entry(parent) + } + + self.visited.clone() + } +} + +struct Part2Traversal<'a> { + graph: &'a BagGraph, +} + +fn p2_visit_entry(entry: &Rc>) -> usize { + let mut branch_bags = 1; + + for child in &entry.borrow().inner { + branch_bags += child.count * p2_visit_entry(&child.bag); + } + + branch_bags +} + +impl<'a> Part2Traversal<'a> { + fn new(graph: &'a BagGraph) -> Self { + Part2Traversal { graph } + } + + fn visit_entry(&mut self, entry: &Rc>) -> usize { + p2_visit_entry(entry) + } + + fn get_total_bags(&mut self, bag_name: &str) -> usize { + let entry = self.graph.nodes.get(bag_name).unwrap(); + self.visit_entry(entry) - 1 + } +} + +#[derive(Default)] +pub struct BagGraph { + nodes: HashMap>>, +} + +impl BagGraph { + fn new() -> Self { + Default::default() + } + + fn insert_rule(&mut self, bag_name: String, inner: Vec) { + let mut inner_bags = Vec::with_capacity(inner.len()); + let mut inserted_inner_bags = Vec::with_capacity(inner.len()); + + for (inner_bag_count, inner_bag_name) in inner { + // inner rcs + let node = self + .nodes + .entry(inner_bag_name.clone()) + .or_insert_with(|| Rc::new(RefCell::new(Bag::new(inner_bag_name)))); + + inner_bags.push(BagInner { + count: inner_bag_count, + bag: Rc::clone(node), + }); + + inserted_inner_bags.push(Rc::clone(node)); + } + + let parent = self + .nodes + .entry(bag_name.clone()) + .or_insert_with(|| Rc::new(RefCell::new(Bag::new(bag_name.clone())))); + + // it can't have been set before + debug_assert!(parent.borrow().inner.is_empty()); + parent.borrow_mut().set_inner(inner_bags); + + for inner_bag in inserted_inner_bags { + inner_bag.borrow_mut().add_parent(Rc::clone(parent)) + } + } +} + +impl Debug for BagGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "BAG GRAPH")?; + for (name, children) in self.nodes.iter() { + writeln!(f, "{}:", name)?; + for child in &children.borrow().inner { + writeln!(f, "\t{} x {}", child.count, child.bag.borrow().name)?; + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct Bag { + // each bag has a name/colour + name: String, + // and can have multiple other bags inside it + inner: Vec, + // and can have multiple parents + parents: Vec>>, +} + +impl Bag { + fn new(name: String) -> Self { + Bag { + name, + inner: Vec::new(), + parents: Vec::new(), + } + } + + fn add_parent(&mut self, parent: Rc>) { + self.parents.push(parent) + } + + fn set_inner(&mut self, inner: Vec) { + self.inner = inner + } +} + +type BagInnerRaw = (usize, String); + +struct BagInner { + count: usize, + bag: Rc>, +} + +impl Debug for BagInner { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{} x {}", self.count, self.bag.borrow().name) + } +} + +fn into_count_and_name(raw_bag: &str) -> Option { + if raw_bag.starts_with(EMPTY_BAG) { + return None; + } + let whitespace_split: Vec<_> = raw_bag.split_ascii_whitespace().collect(); + // we must at least have a count and name + assert!(whitespace_split.len() >= 2); + let count = whitespace_split[0] + .parse() + .expect("failed to parse bag count!"); + + // final chunk is word 'bag' or 'bags' and we don't care about it + let name = whitespace_split[1..whitespace_split.len() - 1].join(" "); + Some((count, name)) +} + +fn parse_rule(rule: &str) -> (String, Vec) { + let split: Vec<_> = rule.split("contain").collect(); + let bag_name = split[0].trim().trim_end_matches(" bags").to_owned(); + + let inner_bags = split[1].split([',', '.']); + let raw_inner = inner_bags + .map(str::trim) + .filter(|bag| !bag.is_empty()) + .filter_map(into_count_and_name) + .collect(); + + (bag_name, raw_inner) +} + +pub fn part1(input: Vec) -> usize { + let mut graph = BagGraph::new(); + input + .iter() + .map(|rule| parse_rule(rule)) + .for_each(|rule| graph.insert_rule(rule.0, rule.1)); + + Part1Traversal::new(&graph) + .get_unique_parents(TARGET_BAG) + .len() +} + +pub fn part2(input: Vec) -> usize { + let mut graph = BagGraph::new(); + input + .iter() + .map(|rule| parse_rule(rule)) + .for_each(|rule| graph.insert_rule(rule.0, rule.1)); + + Part2Traversal::new(&graph).get_total_bags(TARGET_BAG) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_part1_input() { + let input = vec![ + "light red bags contain 1 bright white bag, 2 muted yellow bags.".to_string(), + "dark orange bags contain 3 bright white bags, 4 muted yellow bags.".to_string(), + "bright white bags contain 1 shiny gold bag.".to_string(), + "muted yellow bags contain 2 shiny gold bags, 9 faded blue bags.".to_string(), + "shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags.".to_string(), + "dark olive bags contain 3 faded blue bags, 4 dotted black bags.".to_string(), + "vibrant plum bags contain 5 faded blue bags, 6 dotted black bags.".to_string(), + "faded blue bags contain no other bags.".to_string(), + "dotted black bags contain no other bags.".to_string(), + ]; + + let expected = 4; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn sample_part2_input1() { + let input = vec![ + "light red bags contain 1 bright white bag, 2 muted yellow bags.".to_string(), + "dark orange bags contain 3 bright white bags, 4 muted yellow bags.".to_string(), + "bright white bags contain 1 shiny gold bag.".to_string(), + "muted yellow bags contain 2 shiny gold bags, 9 faded blue bags.".to_string(), + "shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags.".to_string(), + "dark olive bags contain 3 faded blue bags, 4 dotted black bags.".to_string(), + "vibrant plum bags contain 5 faded blue bags, 6 dotted black bags.".to_string(), + "faded blue bags contain no other bags.".to_string(), + "dotted black bags contain no other bags.".to_string(), + ]; + + let expected = 32; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn sample_part2_input2() { + let input = vec![ + "shiny gold bags contain 2 dark red bags.".to_string(), + "dark red bags contain 2 dark orange bags.".to_string(), + "dark orange bags contain 2 dark yellow bags.".to_string(), + "dark yellow bags contain 2 dark green bags.".to_string(), + "dark green bags contain 2 dark blue bags.".to_string(), + "dark blue bags contain 2 dark violet bags.".to_string(), + "dark violet bags contain no other bags.".to_string(), + ]; + + let expected = 126; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day07/src/main.rs b/2020/day07/src/main.rs new file mode 100644 index 0000000..f22266d --- /dev/null +++ b/2020/day07/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day07_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day07").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day08/Cargo.toml b/2020/day08/Cargo.toml new file mode 100644 index 0000000..ab8d50d --- /dev/null +++ b/2020/day08/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day08_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day08_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day08/src/lib.rs b/2020/day08/src/lib.rs new file mode 100644 index 0000000..a02a5b6 --- /dev/null +++ b/2020/day08/src/lib.rs @@ -0,0 +1,298 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::bail; +use aoc_solution::Aoc; +use std::collections::HashSet; +use std::fmt::{self, Debug, Formatter}; + +#[derive(Aoc)] +pub struct Day08; + +const ACC_OPCODE: &str = "acc"; +const JMP_OPCODE: &str = "jmp"; +const NOP_OPCODE: &str = "nop"; + +#[derive(Clone, Eq, PartialEq)] +pub enum Opcode { + Acc, + Jump, + Nop, +} + +impl Debug for Opcode { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Opcode::Acc => write!(f, "{}", ACC_OPCODE), + Opcode::Jump => write!(f, "{}", JMP_OPCODE), + Opcode::Nop => write!(f, "{}", NOP_OPCODE), + } + } +} + +impl<'a> TryFrom<&'a str> for Opcode { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + match value { + ACC_OPCODE => Ok(Opcode::Acc), + JMP_OPCODE => Ok(Opcode::Jump), + NOP_OPCODE => Ok(Opcode::Nop), + other => bail!("invalid opcode {other}"), + } + } +} + +struct Instruction { + opcode: Opcode, + // currently there's only a single operand, so until requirements change, keep it like this + operand: isize, +} + +impl Debug for Instruction { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?} {}", self.opcode, self.operand) + } +} + +impl<'a> TryFrom<&'a String> for Instruction { + type Error = anyhow::Error; + + fn try_from(value: &'a String) -> Result { + let code_operand: Vec<_> = value.split_ascii_whitespace().collect(); + if code_operand.len() != 2 { + bail!("instruction too short") + } + let opcode = Opcode::try_from(code_operand[0])?; + + let operand = code_operand[1].parse()?; + + Ok(Instruction { opcode, operand }) + } +} + +enum ExecutionError { + LoopDetected(isize), +} + +impl ExecutionError { + fn into_inner(self) -> isize { + match self { + ExecutionError::LoopDetected(final_accumulator) => final_accumulator, + } + } +} + +struct Computer { + // currently there's only a single register, so until requirements change, keep it like this + accumulator: isize, + instruction_pointer: usize, + // used to look for cycles + executed_instructions: HashSet, +} + +impl Computer { + fn new() -> Computer { + Computer { + accumulator: 0, + instruction_pointer: 0, + executed_instructions: Default::default(), + } + } + + fn reset(&mut self) { + self.accumulator = 0; + self.instruction_pointer = 0; + self.executed_instructions = HashSet::new(); + } + + fn execute_instruction(&mut self, instruction: &Instruction) { + match instruction.opcode { + Opcode::Nop => self.instruction_pointer += 1, + Opcode::Acc => { + self.accumulator += instruction.operand; + self.instruction_pointer += 1; + } + Opcode::Jump => { + self.instruction_pointer = + (self.instruction_pointer as isize + instruction.operand) as usize + } + } + } + + fn execute_program(&mut self, program: &[Instruction]) -> Result { + loop { + if self + .executed_instructions + .contains(&self.instruction_pointer) + { + // we run into an execution loop + return Err(ExecutionError::LoopDetected(self.accumulator)); + } + + // we have terminated + if self.instruction_pointer == program.len() { + return Ok(self.accumulator); + } + + self.executed_instructions.insert(self.instruction_pointer); + let next_instruction = &program[self.instruction_pointer]; + self.execute_instruction(next_instruction); + } + } + + fn determine_non_halting_set(&mut self, program: &[Instruction]) -> HashSet { + if self.execute_program(program).is_ok() { + panic!("program did not loop!") + } + let non_halting_set = self.executed_instructions.clone(); + self.reset(); + non_halting_set + } +} + +fn parse_as_instructions(raw: &[String]) -> Vec { + raw.iter() + .map(|raw_instruction| { + Instruction::try_from(raw_instruction).expect("failed to parse instruction") + }) + .collect() +} + +pub fn part1(input: Vec) -> isize { + let instructions = parse_as_instructions(&input); + match Computer::new().execute_program(&instructions) { + Err(err) => err.into_inner(), + Ok(_) => panic!("managed to terminate in part1!"), + } +} + +fn substitute_instruction(instruction: &mut Instruction) -> bool { + match instruction.opcode { + Opcode::Nop => { + instruction.opcode = Opcode::Jump; + true + } + Opcode::Jump => { + instruction.opcode = Opcode::Nop; + true + } + _ => false, + } +} + +pub fn part2(input: Vec) -> Option { + let mut instructions = parse_as_instructions(&input); + let mut computer = Computer::new(); + // there is no point in trying to substitute instructions that were never executed in the + // looping case + let non_halting_set = computer.determine_non_halting_set(&instructions); + for non_halting in non_halting_set.iter() { + // attempt substitution of any non-halting instructions in the program + // but don't run the program if we run into an 'acc' + if substitute_instruction(&mut instructions[*non_halting]) { + if let Ok(termination_result) = computer.execute_program(&instructions) { + return Some(termination_result); + } else { + computer.reset(); + substitute_instruction(&mut instructions[*non_halting]); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "nop +0".to_string(), + "acc +1".to_string(), + "jmp +4".to_string(), + "acc +3".to_string(), + "jmp -3".to_string(), + "acc -99".to_string(), + "acc +1".to_string(), + "jmp -4".to_string(), + "acc +6".to_string(), + ]; + + let expected = 5; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "nop +0".to_string(), + "acc +1".to_string(), + "jmp +4".to_string(), + "acc +3".to_string(), + "jmp -3".to_string(), + "acc -99".to_string(), + "acc +1".to_string(), + "jmp -4".to_string(), + "acc +6".to_string(), + ]; + + let expected = 8; + + assert_eq!(expected, part2(input).unwrap()) + } + + #[test] + fn instruction_formatting() { + assert_eq!( + "jmp 2", + format!( + "{:?}", + Instruction { + opcode: Opcode::Jump, + operand: 2 + } + ) + ); + + assert_eq!( + "nop -3", + format!( + "{:?}", + Instruction { + opcode: Opcode::Nop, + operand: -3 + } + ) + ); + + assert_eq!( + "acc 5", + format!( + "{:?}", + Instruction { + opcode: Opcode::Acc, + operand: 5 + } + ) + ) + } +} diff --git a/2020/day08/src/main.rs b/2020/day08/src/main.rs new file mode 100644 index 0000000..8a207e0 --- /dev/null +++ b/2020/day08/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day08_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day08").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day09/Cargo.toml b/2020/day09/Cargo.toml new file mode 100644 index 0000000..b5738f3 --- /dev/null +++ b/2020/day09/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day09_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day09_2020" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day09/src/lib.rs b/2020/day09/src/lib.rs new file mode 100644 index 0000000..fc5cc7f --- /dev/null +++ b/2020/day09/src/lib.rs @@ -0,0 +1,106 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; + +#[derive(Aoc)] +pub struct Day09; + +fn is_valid(preamble: &[usize], value: usize) -> bool { + for pair in preamble.iter().tuple_combinations::<(_, _)>() { + if pair.0 + pair.1 == value { + return true; + } + } + + false +} + +pub fn part1(input: Vec, window_size: usize) -> Option { + for (i, val) in input.iter().enumerate().skip(window_size) { + if !is_valid(&input[i - window_size..i], *val) { + return Some(*val); + } + } + + None +} + +fn check_slice(slice: &[usize], target: usize) -> Option> { + let mut running_total = 0; + let mut used_values = Vec::new(); + for val in slice { + running_total += *val; + used_values.push(*val); + match running_total { + n if n == target => return Some(used_values), + n if n > target => return None, + _ => (), + } + } + None +} + +pub fn part2(input: Vec, window_size: usize) -> Option { + let invalid_number = part1(input.clone(), window_size)?; + + // even though it might seem we try every single possible starting index, + // this loop will never go beyond to where invalid number was found. + // also we're passing just a pointer to `check_slice` so it's fine to pass it to the whole + // underlying slice as we will only iterate through tiny part of it each time + for i in 0..input.len() { + if let Some(set) = check_slice(&input[i..], invalid_number) { + debug_assert!(set.len() >= 2); + let min = set.iter().min()?; + let max = set.iter().max()?; + return Some(min + max); + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + 35, 20, 15, 25, 47, 40, 62, 55, 65, 95, 102, 117, 150, 182, 127, 219, 299, 277, 309, + 576, + ]; + + let expected = 127; + + assert_eq!(expected, part1(input, 5).unwrap()); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + 35, 20, 15, 25, 47, 40, 62, 55, 65, 95, 102, 117, 150, 182, 127, 219, 299, 277, 309, + 576, + ]; + + let expected = 62; + + assert_eq!(expected, part2(input, 5).unwrap()); + } +} diff --git a/2020/day09/src/main.rs b/2020/day09/src/main.rs new file mode 100644 index 0000000..0cc79d9 --- /dev/null +++ b/2020/day09/src/main.rs @@ -0,0 +1,33 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// legacy code + +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day09_2020::{part1, part2}; + +const PART1_WINDOW_SIZE: usize = 25; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day09").expect("failed to read input file"); + + let part1_result = part1(input.clone(), PART1_WINDOW_SIZE).expect("failed to solve part1"); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input, PART1_WINDOW_SIZE).expect("failed to solve part2"); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day10/Cargo.toml b/2020/day10/Cargo.toml new file mode 100644 index 0000000..262c434 --- /dev/null +++ b/2020/day10/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day10_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day10_2020" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day10/src/lib.rs b/2020/day10/src/lib.rs new file mode 100644 index 0000000..575f582 --- /dev/null +++ b/2020/day10/src/lib.rs @@ -0,0 +1,152 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use itertools::Itertools; +use std::collections::HashMap; + +#[derive(Aoc)] +pub struct Day10; + +pub fn part1(input: Vec) -> usize { + // for larger input it might have been more efficient to temporarily store it in a HashSet, + // but considering the input has less than 100 values, the performance penalty of cloning and + // sorting the input slice is negligible + + // adding value of 0 indicating the jolt value of the outlet + let mut adapters = input + .iter() + .copied() + .chain(std::iter::once(0)) + .collect_vec(); + adapters.sort_unstable(); + + let mut one_jolt_diffs = 0; + let mut three_jolt_diffs = 1; // there's the final 3 jolt difference to the device + adapters.iter().tuple_windows().for_each(|(a, b)| { + if *b == a + 1 { + one_jolt_diffs += 1; + } + + if *b == a + 3 { + three_jolt_diffs += 1 + } + }); + + one_jolt_diffs * three_jolt_diffs +} + +#[inline] +fn path_cost( + node: usize, + connection_map: &HashMap>, + cost_map: &mut HashMap, +) -> usize { + // if we have value in cache - return it + if let Some(cost) = cost_map.get(&node) { + return *cost; + } + // if it has any children, it's the sum of the cost of the children + if let Some(children) = connection_map.get(&node) { + let cost = children + .iter() + .map(|child| path_cost(*child, connection_map, cost_map)) + .sum(); + cost_map.insert(node, cost); + cost + } else { + 1 + } +} + +pub fn part2(input: Vec) -> usize { + // note that device always only has a single valid parent, i.e. highest adapter + let mut valid_adapters = input + .iter() + .copied() + .chain(std::iter::once(0)) + .collect_vec(); + valid_adapters.sort_unstable(); + + let mut connection_map = HashMap::with_capacity(valid_adapters.len() - 1); + + // for each adapter + 'outer: for (i, adapter) in valid_adapters.iter().enumerate() { + // find it's children, i.e. other adapters that can be connected to it + for potential_child in &valid_adapters[i + 1..] { + if *adapter + 3 >= *potential_child { + let children = connection_map.entry(*adapter).or_insert_with(Vec::new); + children.push(*potential_child); + } else { + // vec is sorted so we won't find anything there + continue 'outer; + } + } + } + + let mut cost_map = HashMap::with_capacity(connection_map.len()); + + path_cost(0, &connection_map, &mut cost_map) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input1() { + let input = vec![16, 10, 15, 5, 1, 11, 7, 19, 6, 12, 4]; + + let expected = 35; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part1_sample_input2() { + let input = vec![ + 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, + ]; + + let expected = 220; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input1() { + let input = vec![16, 10, 15, 5, 1, 11, 7, 19, 6, 12, 4]; + + let expected = 8; + + assert_eq!(expected, part2(input)) + } + + #[test] + fn part2_sample_input2() { + let input = vec![ + 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, + ]; + + let expected = 19208; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day10/src/main.rs b/2020/day10/src/main.rs new file mode 100644 index 0000000..9df454f --- /dev/null +++ b/2020/day10/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day10_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day10").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day11/Cargo.toml b/2020/day11/Cargo.toml new file mode 100644 index 0000000..1df7404 --- /dev/null +++ b/2020/day11/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day11_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day11_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day11/src/lib.rs b/2020/day11/src/lib.rs new file mode 100644 index 0000000..d7ee1a7 --- /dev/null +++ b/2020/day11/src/lib.rs @@ -0,0 +1,343 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::fmt::{self, Display, Formatter}; +use std::mem; +use std::ops::{Index, IndexMut}; + +#[derive(Aoc)] +pub struct Day11; + +const EMPTY_SEAT: char = 'L'; +const OCCUPIED_SEAT: char = '#'; +const FLOOR: char = '.'; + +#[derive(Debug, Clone, PartialEq, Copy)] +pub enum Seat { + Empty, + Occupied, + Floor, +} + +impl From for Seat { + fn from(value: char) -> Self { + match value { + EMPTY_SEAT => Seat::Empty, + OCCUPIED_SEAT => Seat::Occupied, + FLOOR => Seat::Floor, + v => panic!("invalid seat state {}", v), + } + } +} + +impl From for char { + fn from(seat: Seat) -> Self { + match seat { + Seat::Empty => EMPTY_SEAT, + Seat::Occupied => OCCUPIED_SEAT, + Seat::Floor => FLOOR, + } + } +} + +impl Seat { + fn swap(&mut self) { + mem::swap( + self, + &mut match self { + Seat::Empty => Seat::Occupied, + Seat::Occupied => Seat::Empty, + Seat::Floor => Seat::Floor, + }, + ); + } + + fn is_floor(&self) -> bool { + matches!(self, Seat::Floor) + } + + fn is_empty(&self) -> bool { + matches!(self, Seat::Empty) + } + + fn is_occupied(&self) -> bool { + matches!(self, Seat::Occupied) + } +} + +type SeatRow = Vec; +type SeatPosition = (usize, usize); + +#[derive(PartialEq)] +pub struct SeatGrid { + rows: Vec, +} + +impl Index for SeatGrid { + type Output = Seat; + + fn index(&self, index: SeatPosition) -> &Self::Output { + &self.rows[index.1][index.0] + } +} + +impl IndexMut for SeatGrid { + fn index_mut(&mut self, index: SeatPosition) -> &mut Self::Output { + &mut self.rows[index.1][index.0] + } +} + +impl Display for SeatGrid { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for row in self.rows.iter() { + let row_string: String = row + .iter() + .map(|&seat| { + let char: char = seat.into(); + char + }) + .collect(); + writeln!(f, "{}", row_string)?; + } + Ok(()) + } +} + +impl From<&[String]> for SeatGrid { + fn from(raw_rows: &[String]) -> Self { + Self { + rows: raw_rows + .iter() + .map(|row| row.chars().map(Seat::from).collect()) + .collect(), + } + } +} + +impl SeatGrid { + fn immediately_adjacent(&self, position: SeatPosition) -> Vec { + let mut adjacent = Vec::new(); + for i in -1..=1 { + for j in -1..=1 { + if i == 0 && j == 0 { + continue; + } + if let Some(seat) = self.attempt_seat_lookup(position, (i, j)) { + adjacent.push(seat) + } + } + } + + adjacent + } + + fn visibly_adjacent(&self, position: SeatPosition) -> Vec { + let mut adjacent = Vec::new(); + + for i in -1..=1 { + for j in -1..=1 { + if i == 0 && j == 0 { + continue; + } + let mut translation = (i, j); + while let Some(seat) = self.attempt_seat_lookup(position, translation) { + if !seat.is_floor() { + adjacent.push(seat); + break; + } else { + translation.0 += i; + translation.1 += j; + } + } + } + } + + adjacent + } + + fn attempt_seat_lookup( + &self, + position: SeatPosition, + translation: (isize, isize), + ) -> Option { + let (x, y) = position; + let (dx, dy) = translation; + let translated = (x as isize + dx, y as isize + dy); + + if translated.0 < 0 + || translated.0 >= self.rows[0].len() as isize + || translated.1 < 0 + || translated.1 >= self.rows.len() as isize + { + None + } else { + // based on previous checks we know we can safely cast it + let new_position = (translated.0 as usize, translated.1 as usize); + + Some(self[new_position]) + } + } + + fn simulate_step(&self, adjacent_seats: F, seat_checker: C) -> Self + where + F: Fn(&SeatGrid, SeatPosition) -> Vec, + C: Fn(&Seat, &[Seat]) -> bool, + { + let mut new_grid = SeatGrid { + rows: self.rows.clone(), + }; + + self.rows.iter().enumerate().for_each(|(y, row)| { + row.iter().enumerate().for_each(|(x, seat)| { + if !seat.is_floor() { + let adjacent = adjacent_seats(self, (x, y)); + + if seat_checker(seat, &adjacent) { + new_grid[(x, y)].swap(); + } + } + }) + }); + + new_grid + } + + fn occupied_count(&self) -> usize { + self.rows + .iter() + .flat_map(|row| row.iter()) + .filter(|seat| seat.is_occupied()) + .count() + } +} + +pub fn part1(input: Vec) -> usize { + let seat_checker = |seat: &Seat, adjacent: &[Seat]| { + // If a seat is empty (L) and there are no occupied seats adjacent to it, the seat becomes occupied. + if seat.is_empty() && !adjacent.iter().any(|adj| adj.is_occupied()) { + return true; + } + // If a seat is occupied (#) and four or more seats adjacent to it are also occupied, the seat becomes empty. + if seat.is_occupied() && adjacent.iter().filter(|seat| seat.is_occupied()).count() >= 4 { + return true; + } + false + }; + + let mut grid = SeatGrid::from(input.as_slice()); + loop { + let next_grid = grid.simulate_step(SeatGrid::immediately_adjacent, seat_checker); + if next_grid == grid { + break; + } + grid = next_grid; + } + grid.occupied_count() +} + +pub fn part2(input: Vec) -> usize { + let seat_checker = |seat: &Seat, adjacent: &[Seat]| { + // If a seat is empty (L) and there are no occupied seats adjacent to it, the seat becomes occupied. + if seat.is_empty() && !adjacent.iter().any(|adj| adj.is_occupied()) { + return true; + } + // it now takes five or more visible occupied seats for an occupied seat to become empty (rather than four or more from the previous rules) + if seat.is_occupied() && adjacent.iter().filter(|seat| seat.is_occupied()).count() >= 5 { + return true; + } + false + }; + + let mut grid = SeatGrid::from(input.as_slice()); + loop { + let next_grid = grid.simulate_step(SeatGrid::visibly_adjacent, seat_checker); + if next_grid == grid { + break; + } + grid = next_grid; + } + grid.occupied_count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "L.LL.LL.LL".to_string(), + "LLLLLLL.LL".to_string(), + "L.L.L..L..".to_string(), + "LLLL.LL.LL".to_string(), + "L.LL.LL.LL".to_string(), + "L.LLLLL.LL".to_string(), + "..L.L.....".to_string(), + "LLLLLLLLLL".to_string(), + "L.LLLLLL.L".to_string(), + "L.LLLLL.LL".to_string(), + ]; + + let expected = 37; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "L.LL.LL.LL".to_string(), + "LLLLLLL.LL".to_string(), + "L.L.L..L..".to_string(), + "LLLL.LL.LL".to_string(), + "L.LL.LL.LL".to_string(), + "L.LLLLL.LL".to_string(), + "..L.L.....".to_string(), + "LLLLLLLLLL".to_string(), + "L.LLLLLL.L".to_string(), + "L.LLLLL.LL".to_string(), + ]; + + let expected = 26; + + assert_eq!(expected, part2(input)) + } + + #[test] + fn display_works_as_expected() { + let input = vec![ + "L.LL.LL.LL".to_string(), + "LLLLLLL.LL".to_string(), + "L.L.L..L..".to_string(), + "LLLL.LL.LL".to_string(), + "L.LL.LL.LL".to_string(), + "L.LLLLL.LL".to_string(), + "..L.L.....".to_string(), + "LLLLLLLLLL".to_string(), + "L.LLLLLL.L".to_string(), + "L.LLLLL.LL".to_string(), + ]; + let grid = SeatGrid::from(&*input); + + let mut expected = input.join("\n"); + expected.push('\n'); + + assert_eq!(expected, format!("{}", grid)); + } +} diff --git a/2020/day11/src/main.rs b/2020/day11/src/main.rs new file mode 100644 index 0000000..37c3850 --- /dev/null +++ b/2020/day11/src/main.rs @@ -0,0 +1,35 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Note: I've started using 'from' rather than 'try_from' as I'm making assumption that +// provided inputs must not be malformed. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day11_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day11").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day12/Cargo.toml b/2020/day12/Cargo.toml new file mode 100644 index 0000000..6e3612c --- /dev/null +++ b/2020/day12/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day12_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day12_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day12/src/lib.rs b/2020/day12/src/lib.rs new file mode 100644 index 0000000..bf25830 --- /dev/null +++ b/2020/day12/src/lib.rs @@ -0,0 +1,254 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::fmt::{self, Display, Formatter}; + +#[derive(Aoc)] +pub struct Day12; + +const NORTH_DIRECTION: char = 'N'; +const SOUTH_DIRECTION: char = 'S'; +const EAST_DIRECTION: char = 'E'; +const WEST_DIRECTION: char = 'W'; +const LEFT_DIRECTION: char = 'L'; +const RIGHT_DIRECTION: char = 'R'; +const FORWARD_DIRECTION: char = 'F'; + +type Position = (isize, isize); + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Direction { + North, + South, + East, + West, + Left, + Right, + Forward, +} + +// as per specs, actions are "single-character" +impl From for Direction { + fn from(c: char) -> Self { + match c { + NORTH_DIRECTION => Direction::North, + SOUTH_DIRECTION => Direction::South, + EAST_DIRECTION => Direction::East, + WEST_DIRECTION => Direction::West, + LEFT_DIRECTION => Direction::Left, + RIGHT_DIRECTION => Direction::Right, + FORWARD_DIRECTION => Direction::Forward, + v => panic!("unknown direction - {}", v), + } + } +} + +#[derive(Copy, Clone)] +pub struct Action { + direction: Direction, + magnitude: isize, +} + +impl From<&String> for Action { + fn from(raw: &String) -> Self { + if !raw.is_ascii() { + panic!("received non-ascii input") + } + let (raw_direction, raw_magnitude) = raw.split_at(1); + + let direction = Direction::from( + raw_direction + .chars() + .next() + .expect("failed to recover direction"), + ); + let magnitude = raw_magnitude.parse().expect("failed to parse magnitude"); + Action { + direction, + magnitude, + } + } +} + +impl Display for Action { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?} {}", self.direction, self.magnitude) + } +} + +impl Action { + fn is_rotation(&self) -> bool { + matches!(self.direction, Direction::Left | Direction::Right) + } + + fn is_translation(&self) -> bool { + matches!( + self.direction, + Direction::North | Direction::East | Direction::South | Direction::West + ) + } +} + +#[derive(Eq, PartialEq)] +pub enum NavigationMode { + Absolute, + Waypoint, +} + +struct Ship { + position: Position, + waypoint: Waypoint, + mode: NavigationMode, +} + +impl Ship { + fn new(waypoint_position: Position, mode: NavigationMode) -> Self { + Ship { + position: (0, 0), + waypoint: Waypoint { + relative_position: waypoint_position, + }, + mode, + } + } + + fn apply_action(&mut self, action: Action) { + if action.is_rotation() { + self.waypoint.apply_rotation(action) + } else if action.is_translation() { + if self.mode == NavigationMode::Waypoint { + self.waypoint.apply_translation(action) + } else { + self.apply_self_translation(action) + } + } else { + // it must be forward + self.move_towards_waypoint(action); + } + } + + fn apply_self_translation(&mut self, action: Action) { + match action.direction { + Direction::North => self.position.1 += action.magnitude, + Direction::South => self.position.1 -= action.magnitude, + Direction::East => self.position.0 += action.magnitude, + Direction::West => self.position.0 -= action.magnitude, + _ => unreachable!(), + } + } + + fn move_towards_waypoint(&mut self, action: Action) { + debug_assert_eq!(action.direction, Direction::Forward); + let (x, y) = self.position; + let dx = self.waypoint.relative_position.0 * action.magnitude; + let dy = self.waypoint.relative_position.1 * action.magnitude; + self.position = (x + dx, y + dy) + } +} + +struct Waypoint { + relative_position: Position, +} + +impl Waypoint { + fn apply_rotation(&mut self, action: Action) { + let magnitude = if action.direction == Direction::Right { + action.magnitude + } else { + 360 - action.magnitude + }; + + let (x, y) = self.relative_position; + match magnitude { + 90 => self.relative_position = (y, -x), + 180 => self.relative_position = (-x, -y), + 270 => self.relative_position = (-y, x), + 360 => (), + v => panic!("invalid rotation - {}", v), + } + } + + fn apply_translation(&mut self, action: Action) { + debug_assert!(action.is_translation()); + match action.direction { + Direction::North => self.relative_position.1 += action.magnitude, + Direction::South => self.relative_position.1 -= action.magnitude, + Direction::East => self.relative_position.0 += action.magnitude, + Direction::West => self.relative_position.0 -= action.magnitude, + _ => unreachable!(), + } + } +} + +pub fn part1(input: Vec) -> usize { + let mut ship = Ship::new((1, 0), NavigationMode::Absolute); + + input + .iter() + .map(Action::from) + .for_each(|action| ship.apply_action(action)); + + (ship.position.0.abs() + ship.position.1.abs()) as usize +} + +pub fn part2(input: Vec) -> usize { + let mut ship = Ship::new((10, 1), NavigationMode::Waypoint); + + input + .iter() + .map(Action::from) + .for_each(|action| ship.apply_action(action)); + + (ship.position.0.abs() + ship.position.1.abs()) as usize +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "F10".to_string(), + "N3".to_string(), + "F7".to_string(), + "R90".to_string(), + "F11".to_string(), + ]; + + let expected = 25; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "F10".to_string(), + "N3".to_string(), + "F7".to_string(), + "R90".to_string(), + "F11".to_string(), + ]; + + let expected = 286; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day12/src/main.rs b/2020/day12/src/main.rs new file mode 100644 index 0000000..b374104 --- /dev/null +++ b/2020/day12/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day12_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day12").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day13/Cargo.toml b/2020/day13/Cargo.toml new file mode 100644 index 0000000..c88c4c9 --- /dev/null +++ b/2020/day13/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day13_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day13_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day13/src/lib.rs b/2020/day13/src/lib.rs new file mode 100644 index 0000000..3b0db32 --- /dev/null +++ b/2020/day13/src/lib.rs @@ -0,0 +1,198 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +#[derive(Aoc)] +pub struct Day13; + +struct Bus { + id: usize, +} + +impl Bus { + fn new(raw_id: &str) -> Option { + match raw_id.parse() { + Ok(id) => Some(Bus { id }), + _ => None, + } + } + + // used for part1 + fn earliest_departure_from(&self, timestamp: usize) -> usize { + // assume id < timestamp + let quo = timestamp / self.id; + let rem = timestamp % self.id; + + let mut n = quo; + if rem != 0 { + n += 1; + } + + self.id * n + } +} + +fn split_into_timestamp_and_buses(input: &str) -> (usize, Vec>) { + let split: Vec<_> = input.split_ascii_whitespace().collect(); + assert_eq!(2, split.len(), "invalid input"); + + let timestamp = split[0].parse().expect("failed to parse timestamp"); + let buses = split[1].split(',').map(Bus::new).collect(); + + (timestamp, buses) +} + +pub fn part1(input: &str) -> usize { + let (timestamp, buses) = split_into_timestamp_and_buses(input); + let (id, departure) = buses + .into_iter() + .flatten() + .map(|bus| (bus.id, bus.earliest_departure_from(timestamp))) + .min_by(|(_, timestamp1), (_, timestamp2)| timestamp1.cmp(timestamp2)) + .unwrap(); + id * (departure - timestamp) +} + +#[allow(clippy::many_single_char_names)] +// code was originally adapted from https://rosettacode.org/wiki/Chinese_remainder_theorem#Rust +fn egcd(a: isize, b: isize) -> (isize, isize, isize) { + if a == 0 { + (b, 0, 1) + } else { + let (g, x, y) = egcd(b % a, a); + (g, y - (b / a) * x, x) + } +} + +// code was originally adapted from https://rosettacode.org/wiki/Chinese_remainder_theorem#Rust +fn mod_inv(x: isize, n: isize) -> Option { + let (g, x, _) = egcd(x, n); + if g == 1 { + Some((x % n + n) % n) + } else { + None + } +} + +// code was originally adapted from https://rosettacode.org/wiki/Chinese_remainder_theorem#Rust +fn crt(residues: &[isize], modulii: &[isize]) -> Option { + let prod = modulii.iter().product::(); + + let mut sum = 0; + + for (&residue, &modulus) in residues.iter().zip(modulii) { + let p = prod / modulus; + sum += residue * mod_inv(p, modulus)? * p + } + + Some(sum % prod) +} + +pub fn part2(input: &str) -> usize { + let (_, buses) = split_into_timestamp_and_buses(input); + let (modulii, residues): (Vec<_>, Vec<_>) = buses + .into_iter() + .enumerate() + .filter_map(|(i, bus)| { + bus.map(|bus| { + ( + bus.id as isize, + (bus.id as isize - i as isize) % bus.id as isize, + ) + }) + }) + .unzip(); + + crt(&residues, &modulii).expect("failed to apply CRT") as usize +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = r#"939 +7,13,x,x,59,x,31,19"#; + + let expected = 295; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input1() { + let input = r#"939 + 7,13,x,x,59,x,31,19"#; + + let expected = 1068781; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn part2_sample_input2() { + let input = r#"42 + 17,x,13,19"#; + + let expected = 3417; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn part2_sample_input3() { + let input = r#"42 +67,7,59,61"#; + + let expected = 754018; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn part2_sample_input4() { + let input = r#"42 + 67,x,7,59,61"#; + + let expected = 779210; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn part2_sample_input5() { + let input = r#"42 + 67,7,x,59,61"#; + + let expected = 1261476; + + assert_eq!(expected, part2(input)); + } + + #[test] + fn part2_sample_input6() { + let input = r#"42 + 1789,37,47,1889"#; + + let expected = 1202161486; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day13/src/main.rs b/2020/day13/src/main.rs new file mode 100644 index 0000000..875b38e --- /dev/null +++ b/2020/day13/src/main.rs @@ -0,0 +1,31 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day13_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_to_string("inputs/2020/day13").expect("failed to read input file"); + + let part1_result = part1(&input); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(&input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day14/Cargo.toml b/2020/day14/Cargo.toml new file mode 100644 index 0000000..86cdd1e --- /dev/null +++ b/2020/day14/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day14_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day14_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day14/src/lib.rs b/2020/day14/src/lib.rs new file mode 100644 index 0000000..2e10891 --- /dev/null +++ b/2020/day14/src/lib.rs @@ -0,0 +1,212 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; + +#[derive(Aoc)] +pub struct Day14; + +const ONE_BIT: char = '1'; +const ZERO_BIT: char = '0'; +const FLOATING_BIT: char = 'X'; + +#[derive(Debug, Copy, Clone)] +pub enum MaskBit { + One, + Zero, + Floating, +} + +impl MaskBit { + fn is_floating(&self) -> bool { + matches!(self, MaskBit::Floating) + } +} + +impl From for usize { + fn from(mask: MaskBit) -> Self { + match mask { + MaskBit::One => 1, + MaskBit::Zero => 0, + MaskBit::Floating => panic!("tried to convert 'floating' mask into usize!"), + } + } +} + +struct Mask([MaskBit; 36]); + +impl From<&String> for Mask { + fn from(raw_mask: &String) -> Self { + let mask = raw_mask.trim_start_matches("mask = ").chars().rev(); + + // since TryFrom> for [T; N] was stabilised, we can just use that to ensure correct length + let mut mask_bits = Vec::with_capacity(36); + + for c in mask { + match c { + ONE_BIT => mask_bits.push(MaskBit::One), + ZERO_BIT => mask_bits.push(MaskBit::Zero), + FLOATING_BIT => mask_bits.push(MaskBit::Floating), + _ => panic!("unexpected mask character - {}", c), + }; + } + + Mask(mask_bits.try_into().expect("failed to parse mask")) + } +} + +type MemoryAddress = usize; +type MemoryValue = usize; + +#[derive(Debug)] +pub struct Memory(HashMap); + +impl Memory { + fn new() -> Self { + Memory(Default::default()) + } + + fn write_with_value_mask(&mut self, address: MemoryAddress, value: MemoryValue, mask: &Mask) { + let mut init = value; + for (i, &bit) in mask.0.iter().enumerate() { + if !bit.is_floating() { + init = (init & !(1 << i)) | (Into::::into(bit) << i); + } + } + self.0.insert(address, init); + } + + fn write_with_address_mask(&mut self, address: MemoryAddress, value: MemoryValue, mask: &Mask) { + let mut target_addresses = vec![address]; + + for (i, &bit) in mask.0.iter().enumerate() { + match bit { + // If the bitmask bit is 0, the corresponding memory address bit is unchanged. + MaskBit::Zero => (), + // If the bitmask bit is 1, the corresponding memory address bit is overwritten with 1. + MaskBit::One => { + for address in target_addresses.iter_mut() { + *address |= 1 << i; + } + } + // If the bitmask bit is X, the corresponding memory address bit is floating. + // Floating bits will take on all possible values. + MaskBit::Floating => { + // set existing ones to 1, and push the 0 variants + let mut new = Vec::with_capacity(target_addresses.len()); + for address in target_addresses.iter_mut() { + new.push(*address & !(1 << i)); + *address |= 1 << i; + } + target_addresses.append(&mut new); + } + } + } + + for address in target_addresses { + self.0.insert(address, value); + } + } +} + +fn parse_into_address_and_value(raw: &str) -> (MemoryAddress, MemoryValue) { + let without_prefix = raw.trim_start_matches("mem["); + let address_value: Vec<_> = without_prefix.split("] = ").collect(); + ( + address_value[0] + .parse() + .expect("failed to parse memory address"), + address_value[1] + .parse() + .expect("failed to parse memory value"), + ) +} + +pub fn part1(input: Vec) -> usize { + let mut current_mask = None; + let mut memory = Memory::new(); + // first entry MUST BE a mask + for raw in input { + if raw.starts_with("mask") { + current_mask = Some(Mask::from(&raw)) + } else { + let (address, value) = parse_into_address_and_value(&raw); + memory.write_with_value_mask( + address, + value, + current_mask.as_ref().expect("no mask was set!"), + ); + } + } + + memory.0.values().sum() +} + +pub fn part2(input: Vec) -> usize { + let mut current_mask = None; + let mut memory = Memory::new(); + // first entry MUST BE a mask + for raw in input { + if raw.starts_with("mask") { + current_mask = Some(Mask::from(&raw)) + } else { + let (address, value) = parse_into_address_and_value(&raw); + memory.write_with_address_mask( + address, + value, + current_mask.as_ref().expect("no mask was set!"), + ); + } + } + + memory.0.values().sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X".to_string(), + "mem[8] = 11".to_string(), + "mem[7] = 101".to_string(), + "mem[8] = 0".to_string(), + ]; + + let expected = 165; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "mask = 000000000000000000000000000000X1001X".to_string(), + "mem[42] = 100".to_string(), + "mask = 00000000000000000000000000000000X0XX".to_string(), + "mem[26] = 1".to_string(), + ]; + + let expected = 208; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day14/src/main.rs b/2020/day14/src/main.rs new file mode 100644 index 0000000..303d87d --- /dev/null +++ b/2020/day14/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day14_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day14").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day15/Cargo.toml b/2020/day15/Cargo.toml new file mode 100644 index 0000000..3650b55 --- /dev/null +++ b/2020/day15/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day15_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day15_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day15/src/lib.rs b/2020/day15/src/lib.rs new file mode 100644 index 0000000..f3a00f5 --- /dev/null +++ b/2020/day15/src/lib.rs @@ -0,0 +1,247 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; +use std::vec; + +#[derive(Aoc)] +pub struct Day15; + +struct VanEckSequence { + initial_sequence: Vec, +} + +impl VanEckSequence { + fn new(initial_sequence: Vec) -> Self { + VanEckSequence { initial_sequence } + } +} + +impl IntoIterator for VanEckSequence { + type Item = usize; + type IntoIter = VanEckSequenceIterator; + + fn into_iter(self) -> Self::IntoIter { + let mut initial_sequence = self.initial_sequence.into_iter(); + + let first = initial_sequence + .next() + .expect("initial sequence was empty!"); + + VanEckSequenceIterator { + initial_sequence, + last_seen: Default::default(), + current_epoch: 0, + current_value: first, + } + } +} + +struct VanEckSequenceIterator { + initial_sequence: vec::IntoIter, + + // map between number and the epoch when it was last seen + last_seen: HashMap, + current_value: usize, + current_epoch: usize, +} + +impl Iterator for VanEckSequenceIterator { + type Item = usize; + + fn next(&mut self) -> Option { + let next = self.current_value; + + // firstly, consume initial sequence + if let Some(initial) = self.initial_sequence.next() { + self.current_value = initial + } else { + // If that was the first time the number has been spoken, the current player says 0. + // Otherwise, the number had been spoken before; + // the current player announces how many turns apart the number is from when it was previously spoken. + if let Some(last_seen) = self.last_seen.get(&self.current_value) { + self.current_value = self.current_epoch - last_seen; + } else { + self.current_value = 0; + } + } + + self.last_seen.insert(next, self.current_epoch); + self.current_epoch += 1; + + Some(next) + } +} + +pub fn part1(input: Vec) -> usize { + VanEckSequence::new(input.to_vec()) + .into_iter() + .nth(2020 - 1) // we subtract one as we count from 0 like a sane person + .unwrap() +} + +// this is not included in coverage for the same reason as the part2 tests +#[cfg(not(tarpaulin_include))] +pub fn part2(input: Vec) -> usize { + VanEckSequence::new(input.to_vec()) + .into_iter() + .nth(30000000 - 1) // we subtract one as we count from 0 like a sane person + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn short_sequence() { + let init = vec![0, 3, 6]; + + let expected_10 = vec![0, 3, 6, 0, 3, 3, 1, 0, 4, 0]; + + assert_eq!( + expected_10, + VanEckSequence::new(init) + .into_iter() + .take(10) + .collect::>() + ); + } + + #[test] + fn part1_sample_input1() { + let input = vec![1, 3, 2]; + + let expected = 1; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input2() { + let input = vec![2, 1, 3]; + + let expected = 10; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input3() { + let input = vec![1, 2, 3]; + + let expected = 27; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input4() { + let input = vec![2, 3, 1]; + + let expected = 78; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input5() { + let input = vec![3, 2, 1]; + + let expected = 438; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input6() { + let input = vec![3, 1, 2]; + + let expected = 1836; + + assert_eq!(expected, part1(input)); + } + + // all of the below tests pass, however, they are not committed as because they are run under + // `debug` release profile (and I can't be bothered to change that) and take too long to + // complete + + // #[test] + // fn part2_sample_input1() { + // let input = vec![0, 3, 6]; + // + // let expected = 175594; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input2() { + // let input = vec![1, 3, 2]; + // + // let expected = 2578; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input3() { + // let input = vec![2, 1, 3]; + // + // let expected = 3544142; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input4() { + // let input = vec![1, 2, 3]; + // + // let expected = 261214; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input5() { + // let input = vec![2, 3, 1]; + // + // let expected = 6895259; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input6() { + // let input = vec![3, 2, 1]; + // + // let expected = 18; + // + // assert_eq!(expected, part2(input)); + // } + // + // #[test] + // fn part2_sample_input7() { + // let input = vec![3, 1, 2]; + // + // let expected = 362; + // + // assert_eq!(expected, part2(input)); + // } +} diff --git a/2020/day15/src/main.rs b/2020/day15/src/main.rs new file mode 100644 index 0000000..fc17db5 --- /dev/null +++ b/2020/day15/src/main.rs @@ -0,0 +1,30 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use day15_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = vec![18, 8, 0, 5, 4, 1, 20]; + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day16/Cargo.toml b/2020/day16/Cargo.toml new file mode 100644 index 0000000..3d1e124 --- /dev/null +++ b/2020/day16/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day16_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day16_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day16/src/lib.rs b/2020/day16/src/lib.rs new file mode 100644 index 0000000..5e4f8f5 --- /dev/null +++ b/2020/day16/src/lib.rs @@ -0,0 +1,266 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; +use std::ops::RangeInclusive; + +#[derive(Aoc)] +pub struct Day16; + +#[derive(Debug, Clone)] +pub struct Category { + name: String, + range1: RangeInclusive, + range2: RangeInclusive, +} + +fn range_from_raw(raw: &str) -> RangeInclusive { + let bounds: Vec<_> = raw + .split('-') + .map(|bound| bound.parse().expect("failed to parse range bound!")) + .collect(); + debug_assert_eq!(2, bounds.len()); + + RangeInclusive::new(bounds[0], bounds[1]) +} + +impl From<&str> for Category { + fn from(raw: &str) -> Self { + let name_ranges: Vec<_> = raw.split(": ").collect(); + debug_assert_eq!(2, name_ranges.len()); + + let ranges: Vec<_> = name_ranges[1].split(" or ").collect(); + debug_assert_eq!(2, ranges.len()); + + Category { + name: name_ranges[0].to_string(), + range1: range_from_raw(ranges[0]), + range2: range_from_raw(ranges[1]), + } + } +} + +impl Category { + fn is_valid_value(&self, value: usize) -> bool { + self.range1.contains(&value) || self.range2.contains(&value) + } +} + +#[derive(Debug, Clone)] +pub struct Ticket { + values: Vec, +} + +impl From<&str> for Ticket { + fn from(raw: &str) -> Self { + Ticket { + values: raw + .split(',') + .map(|raw| raw.parse().expect("failed to parse ticket value")) + .collect(), + } + } +} + +impl Ticket { + // get list of values that do not fit into any category + fn get_invalid_values(&self, categories: &[Category]) -> Vec { + let mut invalid = Vec::new(); + for value in &self.values { + if !categories.iter().any(|cat| cat.is_valid_value(*value)) { + invalid.push(*value) + } + } + + invalid + } + + fn is_valid(&self, categories: &[Category]) -> bool { + for value in &self.values { + if !categories.iter().any(|cat| cat.is_valid_value(*value)) { + return false; + } + } + true + } +} + +fn parse_into_categories(raw: &str) -> Vec { + raw.split('\n').map(Category::from).collect() +} + +fn parse_into_tickets(raw: &str) -> Vec { + // we skip "your ticket:" and "nearby tickets" strings + raw.split('\n').skip(1).map(Ticket::from).collect() +} + +pub fn part1(input: Vec) -> usize { + let categories = parse_into_categories(&input[0]); + // for part1 we ignore our ticket + // let our_ticket = parse_into_tickets(&input[1]).pop().unwrap(); + let nearby_tickets = parse_into_tickets(&input[2]); + + let mut sum = 0; + for ticket in nearby_tickets { + let invalid_ticket_sum: usize = ticket.get_invalid_values(&categories).into_iter().sum(); + sum += invalid_ticket_sum; + } + + sum +} + +// see if there are any categories such that they can only accept a single set of values +// it takes categories by value and returns a new vec rather than do everything by reference +// to decrease the search space and increase performance by that sweet 10% bringing it all +// [on my machine] below 1ms +fn try_get_fixed_categories( + category_values: &mut HashMap>, + categories: Vec, +) -> (HashMap, Vec) { + let mut matched = HashMap::new(); + let mut remaining = Vec::new(); + for category in categories.into_iter() { + let mut is_unique = true; + let mut good_index = usize::MAX; + for (category_idx, values) in category_values.iter() { + if values.iter().all(|val| category.is_valid_value(*val)) { + if good_index != usize::MAX { + is_unique = false; + break; + } else { + good_index = *category_idx + } + } + } + if is_unique && good_index != usize::MAX { + matched.insert(good_index, category); + } else { + remaining.push(category); + } + } + + for idx in matched.keys() { + category_values.remove(idx); + } + + (matched, remaining) +} + +pub fn part2(input: Vec) -> usize { + let mut categories = parse_into_categories(&input[0]); + let our_ticket = parse_into_tickets(&input[1]).pop().unwrap(); + let nearby_valid_tickets: Vec<_> = parse_into_tickets(&input[2]) + .into_iter() + .filter(|ticket| ticket.is_valid(&categories)) + .collect(); + + let num_tickets = nearby_valid_tickets.len() + 1; + + let mut category_values = HashMap::with_capacity(categories.len()); + + for ticket in nearby_valid_tickets + .into_iter() + .chain(std::iter::once(our_ticket.clone())) + { + for (i, value) in ticket.values.iter().enumerate() { + let valid_values = category_values + .entry(i) + .or_insert_with(|| Vec::with_capacity(num_tickets)); + valid_values.push(*value); + } + } + + let mut category_map = HashMap::new(); + loop { + let (matched, remaining_categories) = + try_get_fixed_categories(&mut category_values, categories); + categories = remaining_categories; + + if matched.is_empty() { + break; + } else { + category_map.extend(matched); + } + } + + if !categories.is_empty() { + // according to some smarter person, it is provable that the input must be cheese-able + // since there's a unique solution + panic!("our input was not cheese-able : (") + } + + let mut final_product = 1; + for (idx, category) in category_map { + if category.name.starts_with("departure") { + final_product *= our_ticket.values[idx] + } + } + + final_product +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50" + .to_string(), + "your ticket: +7,1,14" + .to_string(), + "nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12" + .to_string(), + ]; + + let expected = 71; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "departure time: 0-1 or 4-19 +departure station: 0-5 or 8-19 +seat: 0-13 or 16-19" + .to_string(), + "your ticket: +11,12,13" + .to_string(), + "nearby tickets: +3,9,18 +15,1,5 +5,14,9" + .to_string(), + ]; + + let expected = 132; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day16/src/main.rs b/2020/day16/src/main.rs new file mode 100644 index 0000000..13d64ab --- /dev/null +++ b/2020/day16/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day16_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day16") + .expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day17/Cargo.toml b/2020/day17/Cargo.toml new file mode 100644 index 0000000..2dfb051 --- /dev/null +++ b/2020/day17/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day17_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[lib] +name = "day17_2020" +path = "src/lib.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } +rayon = { workspace = true } + +[lints] +workspace = true diff --git a/2020/day17/src/lib.rs b/2020/day17/src/lib.rs new file mode 100644 index 0000000..08ef87f --- /dev/null +++ b/2020/day17/src/lib.rs @@ -0,0 +1,218 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use rayon::prelude::*; +use std::collections::{HashMap, HashSet}; +use std::ops::{Add, AddAssign}; + +#[derive(Aoc)] +pub struct Day17; + +// Point contains list of values for each dimension +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Point(pub Vec); + +impl Point { + pub fn base(dims: usize) -> Point { + Point(vec![0; dims]) + } + + pub fn dimension_adjacent(&self, dim: usize) -> Vec { + let mut positively_adjacent = self.clone(); + positively_adjacent.0[dim - 1] += 1; + + let mut negatively_adjacent = self.clone(); + negatively_adjacent.0[dim - 1] -= 1; + + vec![positively_adjacent, negatively_adjacent] + } + + pub fn adjacent_points(&self) -> Vec { + let mut adjacent = Vec::with_capacity(3usize.pow(self.0.len() as u32 - 1)); + + for dim in 1..=self.0.len() { + let mut dim_adjacent = Vec::new(); + for adj in adjacent.iter().chain(std::iter::once(self)) { + dim_adjacent.append(&mut adj.dimension_adjacent(dim)) + } + adjacent.extend(dim_adjacent.into_iter().filter(|p| p != self)) + } + + adjacent + } +} + +impl Add<(isize, isize, isize)> for &Point { + type Output = Point; + + fn add(self, rhs: (isize, isize, isize)) -> Self::Output { + assert_eq!(3, self.0.len()); + + Point(vec![ + self.0[0] + rhs.0, + self.0[1] + rhs.1, + self.0[2] + rhs.2, + ]) + } +} + +impl AddAssign<(isize, isize, isize)> for Point { + fn add_assign(&mut self, rhs: (isize, isize, isize)) { + assert_eq!(3, self.0.len()); + + self.0[0] += rhs.0; + self.0[1] += rhs.1; + self.0[2] += rhs.2; + } +} + +const ACTIVE_CUBE: char = '#'; +const NUM_CYCLES: usize = 6; + +fn parse_initial_data(input: &[String], dims: usize) -> HashSet { + let mut active_cubes = HashSet::new(); + for (y, raw_row) in input.iter().enumerate() { + raw_row.chars().enumerate().for_each(|(x, char)| { + if char == ACTIVE_CUBE { + let mut coords = vec![0; dims]; + coords[0] = x as isize; + coords[1] = y as isize; + active_cubes.insert(Point(coords)); + } + }) + } + + active_cubes +} + +struct SimulatedPoint { + point: Point, + neighbours: Vec, + should_deactivate: bool, +} + +fn simulate_step_par(active_points: &mut HashSet) { + let simulated_points: Vec<_> = active_points + .par_iter() + .map(|active_point| { + let neighbours = active_point.adjacent_points(); + let active_neighbours = neighbours + .par_iter() + .map(|neighbour| active_points.contains(neighbour)) + .filter(|is_active| *is_active) + .count(); + + SimulatedPoint { + point: active_point.clone(), + neighbours, + should_deactivate: active_neighbours != 2 && active_neighbours != 3, + } + }) + .collect(); + + let mut all_adjacents = HashMap::new(); + for simulated_point in simulated_points { + if simulated_point.should_deactivate { + active_points.remove(&simulated_point.point); + } + for neighbour in simulated_point.neighbours { + *all_adjacents.entry(neighbour).or_insert(0) += 1; + } + } + + for (adjacent, count) in all_adjacents.into_iter() { + if count == 3 { + active_points.insert(adjacent); + } + } +} + +fn simulate_step(active_points: &mut HashSet) { + let mut adjacent_points = HashMap::new(); + + let mut points_to_deactivate = Vec::new(); + + for active_point in active_points.iter() { + let adjacents = active_point.adjacent_points(); + let mut active = 0; + for adjacent in adjacents { + if active_points.contains(&adjacent) { + active += 1; + } + let entry = adjacent_points.entry(adjacent).or_insert(0); + *entry += 1; + } + if active != 2 && active != 3 { + points_to_deactivate.push(active_point.clone()); + } + } + + for (adjacent, count) in adjacent_points.into_iter() { + if count == 3 { + active_points.insert(adjacent); + } + } + + for deactivate in points_to_deactivate.into_iter() { + active_points.remove(&deactivate); + } +} + +pub fn part1(input: Vec) -> usize { + let mut active_points = parse_initial_data(&input, 3); + + for _ in 0..NUM_CYCLES { + simulate_step(&mut active_points); + } + + active_points.len() +} + +pub fn part2(input: Vec) -> usize { + let mut active_points = parse_initial_data(&input, 4); + + for _ in 0..NUM_CYCLES { + simulate_step_par(&mut active_points); + } + + active_points.len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![".#.".to_string(), "..#".to_string(), "###".to_string()]; + + let expected = 112; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![".#.".to_string(), "..#".to_string(), "###".to_string()]; + + let expected = 848; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day17/src/main.rs b/2020/day17/src/main.rs new file mode 100644 index 0000000..b8acd3d --- /dev/null +++ b/2020/day17/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day17_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day17").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day18/Cargo.toml b/2020/day18/Cargo.toml new file mode 100644 index 0000000..e6fc643 --- /dev/null +++ b/2020/day18/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day18_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day18_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day18/src/lib.rs b/2020/day18/src/lib.rs new file mode 100644 index 0000000..d736dd6 --- /dev/null +++ b/2020/day18/src/lib.rs @@ -0,0 +1,272 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::{HashMap, VecDeque}; +use std::fmt::{self, Display, Formatter}; + +#[derive(Aoc)] +pub struct Day18; + +struct Stack { + inner: Vec, +} + +impl Stack { + fn new() -> Self { + Stack { inner: Vec::new() } + } + + fn push(&mut self, val: T) { + self.inner.push(val) + } + + fn pop(&mut self) -> Option { + self.inner.pop() + } + + fn peek(&self) -> Option<&T> { + if self.inner.is_empty() { + None + } else { + Some(&self.inner[self.inner.len() - 1]) + } + } +} + +fn digits_to_number(digits: &[usize]) -> usize { + digits + .iter() + .rev() + .enumerate() + .fold(0, |acc, (idx, digit)| acc + 10usize.pow(idx as u32) * digit) +} + +fn tokenize(input: &str) -> Vec { + let mut tokens = Vec::new(); + let mut iter = input.chars().filter(|c| !c.is_whitespace()).peekable(); + + let mut current_number_digits = Vec::new(); + while let Some(current) = iter.next() { + let next = iter.peek(); + if current.is_numeric() { + current_number_digits + .push(current.to_digit(10).expect("failed to parse numeric!") as usize); + if !match next { + Some(next) => next.is_numeric(), + None => false, + } { + let number = digits_to_number(¤t_number_digits); + current_number_digits = Vec::new(); + tokens.push(Token::Operand(number)) + } + } else { + tokens.push(Token::Operator(Operator::from(current))) + }; + } + + tokens +} + +fn shunting_yard(input: &str, custom_precedence: &HashMap) -> VecDeque { + let mut queue = VecDeque::new(); + let mut stack: Stack = Stack::new(); + + let tokens = tokenize(input); + for token in tokens { + match token { + Token::Operand(_) => queue.push_back(token), + Token::Operator(operator) => match operator { + Operator::LeftParen => stack.push(Operator::LeftParen), + Operator::RightParen => { + while let Some(top) = stack.pop() { + if top == Operator::LeftParen { + break; + } else { + queue.push_back(Token::Operator(top)) + } + } + } + _ => { + let precedence = custom_precedence + .get(&operator) + .copied() + .unwrap_or_else(|| operator.default_precedence()); + while let Some(top) = stack.peek() { + let top_precedence = custom_precedence + .get(top) + .copied() + .unwrap_or_else(|| top.default_precedence()); + + if top_precedence >= precedence { + let popped = stack.pop().unwrap(); + queue.push_back(Token::Operator(popped)) + } else { + break; + } + } + stack.push(operator) + } + }, + } + } + + while let Some(operator) = stack.pop() { + queue.push_back(Token::Operator(operator)) + } + + queue +} + +fn calculate(mut rpn_queue: VecDeque) -> usize { + // in our case we only have binary operators + let mut stack = Vec::with_capacity(2); + + while let Some(token) = rpn_queue.pop_front() { + match token { + Token::Operand(num) => stack.push(num), + Token::Operator(operator) => { + if let Some(y) = stack.pop() { + if let Some(x) = stack.pop() { + stack.push(operator.apply(x, y)); + } + } + } + } + } + + stack.pop().unwrap() +} + +#[derive(Debug)] +pub enum Token { + Operator(Operator), + Operand(usize), +} + +// no other operators are required by the specs +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum Operator { + Addition, + Multiplication, + LeftParen, + RightParen, +} + +impl Display for Operator { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Operator::Addition => write!(f, "+"), + Operator::Multiplication => write!(f, "*"), + Operator::LeftParen => write!(f, "("), + Operator::RightParen => write!(f, ")"), + } + } +} + +impl Operator { + fn default_precedence(&self) -> usize { + match self { + Operator::LeftParen | Operator::RightParen => 0, + Operator::Addition => 1, + Operator::Multiplication => 2, + } + } + + fn apply(&self, x: usize, y: usize) -> usize { + match self { + Operator::Addition => x + y, + Operator::Multiplication => x * y, + _ => panic!("tried to apply parentheses operator!"), + } + } +} + +impl From for Operator { + fn from(c: char) -> Self { + match c { + '+' => Operator::Addition, + '*' => Operator::Multiplication, + '(' => Operator::LeftParen, + ')' => Operator::RightParen, + v => panic!("invalid operator {}", v), + } + } +} + +pub fn part1(input: Vec) -> usize { + let mut precedence = HashMap::new(); + precedence.insert(Operator::Multiplication, 1); + + input + .iter() + .map(|raw| shunting_yard(raw, &precedence)) + .map(calculate) + .sum() +} + +pub fn part2(input: Vec) -> usize { + let mut precedence = HashMap::new(); + precedence.insert(Operator::Multiplication, 1); + precedence.insert(Operator::Addition, 2); + + input + .iter() + .map(|raw| shunting_yard(raw, &precedence)) + .map(calculate) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn digits_to_number() { + assert_eq!(super::digits_to_number(&[1, 2, 3]), 123); + assert_eq!(super::digits_to_number(&[1]), 1); + } + + #[test] + fn part1_sample_input() { + let input = vec![ + "2 * 3 + (4 * 5)".to_string(), + "5 + (8 * 3 + 9 + 3 * 4 * 3)".to_string(), + "5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))".to_string(), + "((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2".to_string(), + ]; + + let expected = 26335; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "2 * 3 + (4 * 5)".to_string(), + "5 + (8 * 3 + 9 + 3 * 4 * 3)".to_string(), + "5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))".to_string(), + "((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2".to_string(), + ]; + + let expected = 693891; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day18/src/main.rs b/2020/day18/src/main.rs new file mode 100644 index 0000000..b3c3d25 --- /dev/null +++ b/2020/day18/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day18_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day18").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day19/Cargo.toml b/2020/day19/Cargo.toml new file mode 100644 index 0000000..832a81d --- /dev/null +++ b/2020/day19/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day19_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day19_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day19/src/lib.rs b/2020/day19/src/lib.rs new file mode 100644 index 0000000..1b34013 --- /dev/null +++ b/2020/day19/src/lib.rs @@ -0,0 +1,281 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +#[derive(Aoc)] +pub struct Day19; + +#[derive(Debug)] +pub struct Grammar { + rules: Vec>, +} + +impl From<&str> for Grammar { + fn from(rules: &str) -> Self { + let unsorted_rules: Vec<_> = rules + .split('\n') + .map(|raw_rule| { + let id_rule: Vec<_> = raw_rule.split(": ").collect(); + let rule_id: usize = id_rule[0].parse().expect("failed to parse rule id"); + let rule = Rule::from(id_rule[1]); + (rule_id, rule) + }) + .collect(); + + let mut rules = vec![None; unsorted_rules.iter().map(|(id, _)| *id).max().unwrap() + 1]; + + for (id, rule) in unsorted_rules.into_iter() { + rules[id] = Some(rule) + } + + Grammar { rules } + } +} + +impl Grammar { + // perform recursive descent parsing + // returns number of characters consumed by the rule on the word + fn check_word_rule(&self, chars: &[char], input_rule: usize) -> Vec { + if chars.is_empty() { + return Vec::new(); + } + + match &self.rules[input_rule] + .as_ref() + .expect("the rule does not exist!") + { + Rule::Terminal(c) => { + if &chars[0] == c { + vec![1] + } else { + Vec::new() + } + } + Rule::Nonterminal(rule) => { + let mut used_by_subrules = Vec::new(); + + for subrule in rule.subrules.iter() { + if subrule.len() > chars.len() { + // due to how substitutions work here, we must have at least n characters + // for n rules left + continue; + } + let mut subrule_consumed = vec![0]; + for rule in subrule.iter() { + let mut new_thing = Vec::new(); + for consumed in subrule_consumed { + let consumed_possibilities = + self.check_word_rule(&chars[consumed..], *rule); + if consumed_possibilities.is_empty() { + continue; + } + for consumed_pos in consumed_possibilities { + new_thing.push(consumed + consumed_pos) + } + } + subrule_consumed = new_thing; + } + + // if we didn't consume anything, it means the subrule was invalid + if !subrule_consumed.is_empty() { + used_by_subrules.append(&mut subrule_consumed) + } + } + used_by_subrules + } + } + } + + fn check_word(&self, word: &str) -> bool { + let chars: Vec<_> = word.chars().collect(); + let num_consumed = self.check_word_rule(&chars, 0); + if num_consumed.is_empty() { + false + } else { + num_consumed[0] == chars.len() + } + } +} + +#[derive(Debug, Clone)] +pub enum Rule { + Terminal(char), + Nonterminal(NonterminalRule), +} + +type Subrule = Vec; + +#[derive(Debug, Clone)] +pub struct NonterminalRule { + subrules: Vec, +} + +impl From<&str> for Rule { + fn from(raw: &str) -> Self { + // at most there are 2 subrules + let mut subrules = Vec::with_capacity(2); + for subrule in raw.split(" | ") { + let mut rules = Vec::new(); + for rule in subrule.split_ascii_whitespace() { + if let Ok(rule_id) = rule.parse() { + rules.push(rule_id) + } else { + return Rule::Terminal(rule.chars().nth(1).unwrap()); + } + } + subrules.push(rules) + } + + Rule::Nonterminal(NonterminalRule { subrules }) + } +} + +pub fn part1(input: Vec) -> usize { + let grammar = Grammar::from(&*input[0]); + + input[1] + .split('\n') + .filter(|word| grammar.check_word(word)) + .count() +} + +fn do_part2_grammar_change(grammar: &mut Grammar) { + grammar.rules[8] = Some(Rule::Nonterminal(NonterminalRule { + subrules: vec![vec![42], vec![42, 8]], + })); + grammar.rules[11] = Some(Rule::Nonterminal(NonterminalRule { + subrules: vec![vec![42, 31], vec![42, 11, 31]], + })); +} + +pub fn part2(input: Vec) -> usize { + let mut grammar = Grammar::from(&*input[0]); + do_part2_grammar_change(&mut grammar); + + input[1] + .split('\n') + .filter(|word| grammar.check_word(word)) + .count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_small_sample() { + let input = vec![ + r#"0: 1 2 +1: "a" +2: 1 3 | 3 1 +3: "b""# + .to_string(), + r#"aab +aba"# + .to_string(), + ]; + + let expected = 2; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part1_sample_input() { + let input = vec![ + r#"0: 4 1 5 +1: 2 3 | 3 2 +2: 4 4 | 5 5 +3: 4 5 | 5 4 +4: "a" +5: "b""# + .to_string(), + r#"ababbb +bababa +abbbab +aaabbb +aaaabbb"# + .to_string(), + ]; + + let expected = 2; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + r#"42: 9 14 | 10 1 +9: 14 27 | 1 26 +10: 23 14 | 28 1 +1: "a" +11: 42 31 +5: 1 14 | 15 1 +19: 14 1 | 14 14 +12: 24 14 | 19 1 +16: 15 1 | 14 14 +31: 14 17 | 1 13 +6: 14 14 | 1 14 +2: 1 24 | 14 4 +0: 8 11 +13: 14 3 | 1 12 +15: 1 | 14 +17: 14 2 | 1 7 +23: 25 1 | 22 14 +28: 16 1 +4: 1 1 +20: 14 14 | 1 15 +3: 5 14 | 16 1 +27: 1 6 | 14 18 +14: "b" +21: 14 1 | 1 14 +25: 1 1 | 1 14 +22: 14 14 +8: 42 +26: 14 22 | 1 20 +18: 15 15 +7: 14 5 | 1 21 +24: 14 1"# + .to_string(), + r#"abbbbbabbbaaaababbaabbbbabababbbabbbbbbabaaaa +bbabbbbaabaabba +babbbbaabbbbbabbbbbbaabaaabaaa +aaabbbbbbaaaabaababaabababbabaaabbababababaaa +bbbbbbbaaaabbbbaaabbabaaa +bbbababbbbaaaaaaaabbababaaababaabab +ababaaaaaabaaab +ababaaaaabbbaba +baabbaaaabbaaaababbaababb +abbbbabbbbaaaababbbbbbaaaababb +aaaaabbaabaaaaababaa +aaaabbaaaabbaaa +aaaabbaabbaaaaaaabbbabbbaaabbaabaaa +babaaabbbaaabaababbaabababaaab +aabbbbbaabbbaaaaaabbbbbababaaaaabbaaabba"# + .to_string(), + ]; + + let expected_p1 = 3; + let expected_p2 = 12; + + assert_eq!(expected_p1, part1(input.clone())); + assert_eq!(expected_p2, part2(input)); + } +} diff --git a/2020/day19/src/main.rs b/2020/day19/src/main.rs new file mode 100644 index 0000000..f065fac --- /dev/null +++ b/2020/day19/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day19_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day19") + .expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day20/Cargo.toml b/2020/day20/Cargo.toml new file mode 100644 index 0000000..a7cdcf8 --- /dev/null +++ b/2020/day20/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day20_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day20_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day20/src/lib.rs b/2020/day20/src/lib.rs new file mode 100644 index 0000000..785059e --- /dev/null +++ b/2020/day20/src/lib.rs @@ -0,0 +1,1036 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::VecDeque; +use std::fmt::{self, Debug, Display, Formatter}; +use std::mem; + +#[derive(Aoc)] +pub struct Day20; + +const ACTIVE_PIXEL: char = '#'; +const INACTIVE_PIXEL: char = '.'; + +#[derive(Debug)] +pub enum MatchedSide { + Top, + Right, + Bottom, + Left, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Pixel { + Active, + Inactive, +} + +impl From for Pixel { + fn from(c: char) -> Self { + match c { + ACTIVE_PIXEL => Pixel::Active, + INACTIVE_PIXEL => Pixel::Inactive, + _ => panic!("invalid pixel {}", c), + } + } +} + +impl From for char { + fn from(pixel: Pixel) -> Self { + match pixel { + // use those unicode characters instead of the original ones + // for way better readability + Pixel::Active => 'â– ', //'ACTIVE_PIXEL, + Pixel::Inactive => 'â–¡', //INACTIVE_PIXEL, + } + } +} + +impl Pixel { + fn is_active(&self) -> bool { + matches!(self, Pixel::Active) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Tile { + id: usize, + rows: Vec>, +} + +impl From<&String> for Tile { + fn from(raw: &String) -> Self { + let lines: Vec<_> = raw.lines().collect(); + // first line contains id + let id = lines[0] + .strip_suffix(':') + .unwrap() + .strip_prefix("Tile ") + .unwrap() + .parse() + .expect("failed to parse tile id"); + + let rows = lines[1..] + .iter() + .map(|row| row.chars().map(Pixel::from).collect()) + .collect(); + + Tile { id, rows } + } +} + +impl Display for Tile { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!(f, "Tile {}:", self.id)?; + for row in self.rows.iter() { + writeln!( + f, + "{}", + row.iter() + .map(|&pixel| Into::::into(pixel)) + .collect::() + )?; + } + Ok(()) + } +} + +impl Tile { + fn unset() -> Self { + Tile { + id: 0, + rows: std::iter::repeat(std::iter::repeat(Pixel::Inactive).take(10).collect()) + .take(10) + .collect(), + } + } + + fn rotate_clockwise(&mut self) { + let columns: Vec<_> = (0..self.rows.len()) + .map(|i| { + let mut column = self.column(i); + column.reverse(); + column + }) + .collect(); + self.rows = columns + } + + fn flip_horizontally(&mut self) { + self.rows.reverse(); + } + + fn column(&self, idx: usize) -> Vec { + self.rows.iter().map(|row| row[idx]).collect() + } + + fn row(&self, idx: usize) -> &[Pixel] { + &self.rows[idx] + } + + fn top_row(&self) -> &[Pixel] { + self.row(0) + } + + fn bottom_row(&self) -> &[Pixel] { + self.row(self.rows.len() - 1) + } + + fn left_column(&self) -> Vec { + self.column(0) + } + + fn right_column(&self) -> Vec { + self.column(self.rows[0].len() - 1) + } + + fn edges(&self) -> Edges { + Edges { + id: self.id, + + top: self.top_row().to_vec(), + right: self.right_column(), + bottom: self.bottom_row().to_vec(), + left: self.left_column(), + } + } + + fn rotate_n_clockwise(&mut self, n: usize) { + if n == 0 { + return; + } + for _ in 0..n { + self.rotate_clockwise(); + } + } + + fn try_match(&self, other: &mut Self) -> Option { + let self_edges = self.edges(); + let mut other_edges = other.edges(); + if !self_edges.matches(&other_edges) { + // see if the match is even possible + return None; + } + + // and only then determine correct orientation of the other tile + + if let Some(side) = self_edges.direct_match(&other_edges) { + // println!("no rotation on {:?}", side); + return Some(side); + } + + // for 90, 180, 270 rotation... + for i in 1..=3 { + other_edges.rotate_clockwise(); + if let Some(side) = self_edges.direct_match(&other_edges) { + other.rotate_n_clockwise(i); + return Some(side); + } + } + // reset + other_edges.rotate_clockwise(); + + other_edges.flip_horizontally(); + if let Some(side) = self_edges.direct_match(&other_edges) { + other.flip_horizontally(); + return Some(side); + } + + for i in 1..=3 { + other_edges.rotate_clockwise(); + if let Some(side) = self_edges.direct_match(&other_edges) { + other.flip_horizontally(); + other.rotate_n_clockwise(i); + return Some(side); + } + } + + unreachable!("we determined a match was possible!") + } + + fn remove_border(self) -> Vec> { + let num_rows = self.rows.len(); + self.rows + .into_iter() + .skip(1) + .take(num_rows - 2) + .map(|row| row.into_iter().skip(1).take(num_rows - 2).collect()) + .collect() + } +} + +#[derive(Debug, PartialEq)] +pub struct Edges { + id: usize, + + top: Vec, + right: Vec, + bottom: Vec, + left: Vec, +} + +impl Edges { + fn flip_horizontally(&mut self) { + mem::swap(&mut self.top, &mut self.bottom); + self.right.reverse(); + self.left.reverse(); + } + + fn rotate_clockwise(&mut self) { + // start: {top, right, bottom, left} + + // {right, top, bottom, left} + mem::swap(&mut self.top, &mut self.right); + + // {bottom, top, right, left} + mem::swap(&mut self.top, &mut self.bottom); + + // {left, top, right, bottom} + mem::swap(&mut self.top, &mut self.left); + + self.top.reverse(); + self.bottom.reverse(); + } + + fn direct_match(&self, other: &Self) -> Option { + if self.top == other.bottom { + Some(MatchedSide::Top) + } else if self.right == other.left { + Some(MatchedSide::Right) + } else if self.bottom == other.top { + Some(MatchedSide::Bottom) + } else if self.left == other.right { + Some(MatchedSide::Left) + } else { + None + } + } + + fn matches(&self, other: &Self) -> bool { + // TODO: just keep it on hand + let self_reversed = self.reversed(); + let all_self = std::iter::once(&self.top) + .chain(std::iter::once(&self.right)) + .chain(std::iter::once(&self.bottom)) + .chain(std::iter::once(&self.left)) + .chain(std::iter::once(&self_reversed.top)) + .chain(std::iter::once(&self_reversed.right)) + .chain(std::iter::once(&self_reversed.bottom)) + .chain(std::iter::once(&self_reversed.left)); + + for self_edge in all_self { + for other_edge in std::iter::once(&other.top) + .chain(std::iter::once(&other.right)) + .chain(std::iter::once(&other.bottom)) + .chain(std::iter::once(&other.left)) + { + if self_edge == other_edge { + return true; + } + } + } + + false + } + + fn reversed(&self) -> Self { + Edges { + id: self.id, + top: self.top.iter().cloned().rev().collect(), + right: self.right.iter().cloned().rev().collect(), + bottom: self.bottom.iter().cloned().rev().collect(), + left: self.left.iter().cloned().rev().collect(), + } + } +} + +struct TileGrid { + tiles: VecDeque>>, +} + +impl TileGrid { + fn insert_tile( + &mut self, + relative_to: (usize, usize), + side: MatchedSide, + tile: Tile, + ) -> Option<(usize, usize)> { + let (x, y) = relative_to; + match side { + MatchedSide::Top => { + if y == 0 { + self.shift_all_down(); + debug_assert!(self.tiles[y][x].is_none()); + self.tiles[y][x] = Some(tile); + return Some((0, 1)); + } + + debug_assert!(self.tiles[y - 1][x].is_none()); + self.tiles[y - 1][x] = Some(tile); + None + } + MatchedSide::Right => { + debug_assert!(self.tiles[y][x + 1].is_none()); + self.tiles[y][x + 1] = Some(tile); + None + } + MatchedSide::Bottom => { + debug_assert!(self.tiles[y + 1][x].is_none()); + self.tiles[y + 1][x] = Some(tile); + None + } + MatchedSide::Left => { + if x == 0 { + self.shift_all_right(); + debug_assert!(self.tiles[y][x].is_none()); + self.tiles[y][x] = Some(tile); + return Some((1, 0)); + } + + debug_assert!(self.tiles[y][x - 1].is_none()); + self.tiles[y][x - 1] = Some(tile); + None + } + } + } + + fn insert_tiles(&mut self, tiles: Vec) { + // firstly, allow us to take them arbitrarily + let mut tiles: Vec> = tiles.into_iter().map(Some).collect(); + + // find two tiles and their orientations such that they don't fit to any other tile + + // start with any tile we have + self.tiles[0][0] = tiles.pop().unwrap(); + + loop { + if tiles.iter().all(|tile| tile.is_none()) { + break; + } + + let mut tiles_to_place = Vec::new(); + + for (y, row) in self.tiles.iter().enumerate() { + for (x, existing_tile) in row.iter().enumerate().filter(|(_, tile)| tile.is_some()) + { + let tile = existing_tile.as_ref().unwrap(); + for unmatched_tile in tiles.iter_mut().filter(|tile| tile.is_some()) { + if let Some(matched_side) = tile.try_match(unmatched_tile.as_mut().unwrap()) + { + tiles_to_place.push(( + (x, y), + matched_side, + unmatched_tile.take().unwrap(), + )); + } + } + } + } + + let mut relative_delta = (0, 0); + for (relative_position, matched_side, tile) in tiles_to_place { + let true_relative = ( + relative_position.0 + relative_delta.0, + relative_position.1 + relative_delta.1, + ); + + if let Some(delta) = self.insert_tile(true_relative, matched_side, tile) { + relative_delta.0 = delta.0; + relative_delta.1 = delta.1; + } + } + } + } +} + +impl Display for TileGrid { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for row in &self.tiles { + let mut grid_rows = vec![" ".to_string(); 10]; + for tile in row { + let tile = tile.as_ref().cloned().unwrap_or_else(Tile::unset); + let tile_rows = tile.rows; + for (i, actual_tile_row) in tile_rows.iter().enumerate() { + grid_rows[i] += &*(" ".to_owned() + + &*actual_tile_row + .iter() + .map(|&pixel| Into::::into(pixel)) + .collect::()) + } + } + + for grid_row in grid_rows { + writeln!(f, "{}", grid_row)?; + } + writeln!(f)?; + } + Ok(()) + } +} + +impl Debug for TileGrid { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for row in &self.tiles { + for tile in row { + if let Some(tile) = tile { + write!(f, "[{}]", tile.id)?; + } else { + write!(f, "[????]")?; + } + } + writeln!(f)?; + } + Ok(()) + } +} + +impl TileGrid { + fn new(size: usize) -> Self { + let tiles = std::iter::repeat(std::iter::repeat(None).take(size).collect()) + .take(size) + .collect(); + + TileGrid { tiles } + } + + fn shift_all_down(&mut self) { + self.tiles.rotate_right(1); + // make sure first row is None now! + assert!(self.tiles[0].iter().all(|tile| tile.is_none())); + } + + fn shift_all_right(&mut self) { + for row in self.tiles.iter_mut() { + row.rotate_right(1); + // make sure first element is a None! + assert!(row[0].is_none()); + } + } +} + +struct Image { + rows: Vec>, +} + +impl From for Image { + fn from(grid: TileGrid) -> Self { + let per_tile_rows = grid.tiles[0][0].as_ref().unwrap().rows.len() - 2; + let num_rows = grid.tiles.len() * (per_tile_rows); + let mut rows = Vec::with_capacity(num_rows); + for row in grid.tiles.into_iter() { + let mut grid_rows = vec![Vec::with_capacity(num_rows); per_tile_rows]; + for tile in row.into_iter().map(|tile| tile.unwrap()) { + let borderless = tile.remove_border(); + for (i, mut tile_row) in borderless.into_iter().enumerate() { + grid_rows[i].append(&mut tile_row) + } + } + rows.append(&mut grid_rows); + } + + Image { rows } + } +} + +impl Display for Image { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for row in self.rows.iter() { + writeln!( + f, + "{}", + row.iter() + .map(|&pixel| Into::::into(pixel)) + .collect::() + )?; + } + Ok(()) + } +} + +impl Image { + fn rotate_clockwise(&mut self) { + let columns: Vec<_> = (0..self.rows.len()) + .map(|i| { + let mut column = self.column(i); + column.reverse(); + column + }) + .collect(); + self.rows = columns + } + + fn flip_horizontally(&mut self) { + self.rows.reverse(); + } + + fn column(&self, idx: usize) -> Vec { + self.rows.iter().map(|row| row[idx]).collect() + } + + fn check_for_monster(&self, base: (usize, usize)) -> Option { + let monster = monster(); + + for (y, row) in monster.iter().enumerate() { + for x in row.iter() { + if base.1 + y >= self.rows.len() || base.0 + *x >= self.rows.len() { + return None; + } + if !self.rows[base.1 + y][base.0 + *x].is_active() { + return Some(false); + } + } + } + + Some(true) + } + + fn find_monsters_in_orientation(&self) -> usize { + let mut monsters = 0; + // right now just completely ignore Nones + + for (y, row) in self.rows.iter().enumerate() { + for (x, _) in row.iter().enumerate() { + if let Some(res) = self.check_for_monster((x, y)) { + if res { + monsters += 1; + } + } else { + break; + } + } + } + + monsters + } + + fn find_monsters(&mut self) -> usize { + let monsters = self.find_monsters_in_orientation(); + if monsters != 0 { + return monsters; + } + + // for 90, 180, 270 rotation... + for _ in 1..=3 { + self.rotate_clockwise(); + let monsters = self.find_monsters_in_orientation(); + if monsters != 0 { + return monsters; + } + } + // reset + self.rotate_clockwise(); + + self.flip_horizontally(); + + let monsters = self.find_monsters_in_orientation(); + if monsters != 0 { + return monsters; + } + for _ in 1..=3 { + self.rotate_clockwise(); + let monsters = self.find_monsters_in_orientation(); + if monsters != 0 { + return monsters; + } + } + + unreachable!("there are no monsters in the image!!") + } + + fn active_count(&self) -> usize { + self.rows + .iter() + .map(|row| row.iter().filter(|pixel| pixel.is_active()).count()) + .sum() + } +} + +#[inline] +fn monster() -> [Vec; 3] { + /* + # + # ## ## ### + # # # # # # + */ + + // row1: 18 + // row2: 0,5 ,6, 11, 12, 17, 18, 19 + // row3: 1,4 7, 10, 13, 16 + [ + vec![18], + vec![0, 5, 6, 11, 12, 17, 18, 19], + vec![1, 4, 7, 10, 13, 16], + ] +} + +pub fn part1(input: Vec) -> usize { + let tiles: Vec<_> = input.iter().map(Tile::from).collect(); + + let side_len = (tiles.len() as f64).sqrt(); + // make sure it's a perfect square + assert_eq!((side_len as usize).pow(2), tiles.len()); + + let mut tile_grid = TileGrid::new(side_len as usize); + tile_grid.insert_tiles(tiles); + + tile_grid.tiles[0][0].as_ref().unwrap().id + * tile_grid.tiles[tile_grid.tiles.len() - 1][0] + .as_ref() + .unwrap() + .id + * tile_grid.tiles[tile_grid.tiles.len() - 1][tile_grid.tiles[0].len() - 1] + .as_ref() + .unwrap() + .id + * tile_grid.tiles[0][tile_grid.tiles[0].len() - 1] + .as_ref() + .unwrap() + .id +} + +pub fn part2(input: Vec) -> usize { + let tiles: Vec<_> = input.iter().map(Tile::from).collect(); + + let side_len = (tiles.len() as f64).sqrt(); + // make sure it's a perfect square + assert_eq!((side_len as usize).pow(2), tiles.len()); + + let mut tile_grid = TileGrid::new(side_len as usize); + tile_grid.insert_tiles(tiles); + + let mut image = Image::from(tile_grid); + let monsters = image.find_monsters(); + + // 15 is the number of tiles used by a single monster + image.active_count() - 15 * monsters +} + +#[cfg(test)] +mod tests { + use super::*; + + fn compare_tile_edges(edges: &Edges, tile: &Tile) { + assert_eq!(edges.top, tile.row(0)); + assert_eq!(edges.bottom, tile.row(tile.rows.len() - 1)); + assert_eq!(edges.right, tile.column(tile.rows[0].len() - 1)); + assert_eq!(edges.left, tile.column(0)); + } + + #[test] + fn edge_rotation_is_tile_consistent() { + let raw_tile = r#"Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..###"# + .to_string(); + let mut tile = Tile::from(&raw_tile); + let mut edges = tile.edges(); + compare_tile_edges(&edges, &tile); + + // for 90, 180, 270 rotation... + for _ in 0..3 { + edges.rotate_clockwise(); + tile.rotate_clockwise(); + compare_tile_edges(&edges, &tile); + + edges.flip_horizontally(); + tile.flip_horizontally(); + compare_tile_edges(&edges, &tile); + + edges.flip_horizontally(); + tile.flip_horizontally(); + compare_tile_edges(&edges, &tile); + + edges.flip_horizontally(); + tile.flip_horizontally(); + compare_tile_edges(&edges, &tile); + } + } + + #[test] + fn tile_rotation() { + let raw_tile = r#"Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..###"# + .to_string(); + let mut tile = Tile::from(&raw_tile); + + let expected_rotated = r#"Tile 2311: +.#..#####. +.#.####.#. +###...#..# +#..#.##..# +#....#.##. +...##.##.# +.#...#.... +#.#.##.... +##.###.#.# +#..##.#..."# + .to_string(); + let rotated = Tile::from(&expected_rotated); + + tile.rotate_clockwise(); + assert_eq!(tile, rotated); + } + + #[test] + fn tile_horizontal_flip() { + let raw_tile = r#"Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..###"# + .to_string(); + let mut tile = Tile::from(&raw_tile); + + let expected_flipped = r#"Tile 2311: +..###..### +###...#.#. +..#....#.. +.#.#.#..## +##...#.### +##.##.###. +####.#...# +#...##..#. +##..#..... +..##.#..#."# + .to_string(); + let flipped = Tile::from(&expected_flipped); + + tile.flip_horizontally(); + assert_eq!(tile, flipped); + } + + #[test] + fn part1_sample_input() { + let input = vec![ + r#"Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..###"# + .to_string(), + r#"Tile 1951: +#.##...##. +#.####...# +.....#..## +#...###### +.##.#....# +.###.##### +###.##.##. +.###....#. +..#.#..#.# +#...##.#.."# + .to_string(), + r#"Tile 1171: +####...##. +#..##.#..# +##.#..#.#. +.###.####. +..###.#### +.##....##. +.#...####. +#.##.####. +####..#... +.....##..."# + .to_string(), + r#"Tile 1427: +###.##.#.. +.#..#.##.. +.#.##.#..# +#.#.#.##.# +....#...## +...##..##. +...#.##### +.#.####.#. +..#..###.# +..##.#..#."# + .to_string(), + r#"Tile 1489: +##.#.#.... +..##...#.. +.##..##... +..#...#... +#####...#. +#..#.#.#.# +...#.#.#.. +##.#...##. +..##.##.## +###.##.#.."# + .to_string(), + r#"Tile 2473: +#....####. +#..#.##... +#.##..#... +######.#.# +.#...#.#.# +.######### +.###.#..#. +########.# +##...##.#. +..###.#.#."# + .to_string(), + r#"Tile 2971: +..#.#....# +#...###... +#.#.###... +##.##..#.. +.#####..## +.#..####.# +#..#.#..#. +..####.### +..#.#.###. +...#.#.#.#"# + .to_string(), + r#"Tile 2729: +...#.#.#.# +####.#.... +..#.#..... +....#..#.# +.##..##.#. +.#.####... +####.#.#.. +##.####... +##..#.##.. +#.##...##."# + .to_string(), + r#"Tile 3079: +#.#.#####. +.#..###### +..#....... +######.... +####.#..#. +.#...#.##. +#.#####.## +..#.###... +..#....... +..#.###..."# + .to_string(), + ]; + + let expected = 20899048083289; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + r#"Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..###"# + .to_string(), + r#"Tile 1951: +#.##...##. +#.####...# +.....#..## +#...###### +.##.#....# +.###.##### +###.##.##. +.###....#. +..#.#..#.# +#...##.#.."# + .to_string(), + r#"Tile 1171: +####...##. +#..##.#..# +##.#..#.#. +.###.####. +..###.#### +.##....##. +.#...####. +#.##.####. +####..#... +.....##..."# + .to_string(), + r#"Tile 1427: +###.##.#.. +.#..#.##.. +.#.##.#..# +#.#.#.##.# +....#...## +...##..##. +...#.##### +.#.####.#. +..#..###.# +..##.#..#."# + .to_string(), + r#"Tile 1489: +##.#.#.... +..##...#.. +.##..##... +..#...#... +#####...#. +#..#.#.#.# +...#.#.#.. +##.#...##. +..##.##.## +###.##.#.."# + .to_string(), + r#"Tile 2473: +#....####. +#..#.##... +#.##..#... +######.#.# +.#...#.#.# +.######### +.###.#..#. +########.# +##...##.#. +..###.#.#."# + .to_string(), + r#"Tile 2971: +..#.#....# +#...###... +#.#.###... +##.##..#.. +.#####..## +.#..####.# +#..#.#..#. +..####.### +..#.#.###. +...#.#.#.#"# + .to_string(), + r#"Tile 2729: +...#.#.#.# +####.#.... +..#.#..... +....#..#.# +.##..##.#. +.#.####... +####.#.#.. +##.####... +##..#.##.. +#.##...##."# + .to_string(), + r#"Tile 3079: +#.#.#####. +.#..###### +..#....... +######.... +####.#..#. +.#...#.##. +#.#####.## +..#.###... +..#....... +..#.###..."# + .to_string(), + ]; + + let expected = 273; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day20/src/main.rs b/2020/day20/src/main.rs new file mode 100644 index 0000000..86e9145 --- /dev/null +++ b/2020/day20/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day20_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day20") + .expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day21/Cargo.toml b/2020/day21/Cargo.toml new file mode 100644 index 0000000..0825aa3 --- /dev/null +++ b/2020/day21/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day21_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day21_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day21/src/lib.rs b/2020/day21/src/lib.rs new file mode 100644 index 0000000..a353b5c --- /dev/null +++ b/2020/day21/src/lib.rs @@ -0,0 +1,166 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::HashMap; + +#[derive(Aoc)] +pub struct Day21; + +type Ingredient = String; +type Allergen = String; + +struct Allergens(HashMap); + +fn split_into_ingredients_and_allergens(raw: &str) -> (Vec<&str>, Vec<&str>) { + let split: Vec<_> = raw.split(" (contains ").collect(); + assert_eq!(2, split.len()); + let ingredients = split[0].split_ascii_whitespace().collect(); + let allergens = split[1].strip_suffix(')').unwrap().split(", ").collect(); + + (ingredients, allergens) +} + +fn find_allergen( + possible_allergen_sources: &mut HashMap<&str, HashMap<&str, usize>>, + known_allergens: &mut HashMap, +) { + let mut discovered = Vec::new(); + for (allergen, possible_sources) in possible_allergen_sources.iter() { + let mut most_common = vec![("", 0)]; + for (possible_source, count) in possible_sources.iter() { + if known_allergens.contains_key(*possible_source) { + // we already know it has different thing + continue; + } + match *count { + n if n > most_common[0].1 => most_common = vec![(possible_source, *count)], + n if n == most_common[0].1 => most_common.push((possible_source, *count)), + _ => (), + } + } + + if most_common.len() == 1 { + discovered.push(allergen.to_string()); + known_allergens.insert(most_common[0].0.to_string(), allergen.to_string()); + } + } + if discovered.is_empty() { + panic!("o-oh, we didn't find anything!"); + } + for discovered_allergen in discovered { + possible_allergen_sources.remove(&*discovered_allergen); + } +} + +impl From<&[(Vec<&str>, Vec<&str>)]> for Allergens { + fn from(ingredient_allergens: &[(Vec<&str>, Vec<&str>)]) -> Self { + let mut possible_allergen_sources = HashMap::new(); + for (ingredients, allergens) in ingredient_allergens { + for allergen in allergens.iter() { + for ingredient in ingredients.iter() { + let sources = possible_allergen_sources + .entry(*allergen) + .or_insert_with(HashMap::new); + *sources.entry(*ingredient).or_insert(0) += 1; + } + } + } + + let mut known_allergens = HashMap::new(); + + // let's pray to the old gods and the new that this doesn't get stuck in infinite loop + loop { + find_allergen(&mut possible_allergen_sources, &mut known_allergens); + if possible_allergen_sources.is_empty() { + break; + } + } + + Allergens(known_allergens) + } +} + +pub fn part1(input: Vec) -> usize { + let ingredient_allergens: Vec<_> = input + .iter() + .map(|raw| split_into_ingredients_and_allergens(raw)) + .collect(); + let allergens = Allergens::from(&*ingredient_allergens); + + let mut safe_ingredients = 0; + for (ingredients, _) in ingredient_allergens { + for ingredient in ingredients { + if !allergens.0.contains_key(ingredient) { + safe_ingredients += 1 + } + } + } + + safe_ingredients +} + +pub fn part2(input: Vec) -> String { + let ingredient_allergens: Vec<_> = input + .iter() + .map(|raw| split_into_ingredients_and_allergens(raw)) + .collect(); + let allergens = Allergens::from(&*ingredient_allergens); + + let mut ingredients: Vec<_> = allergens.0.into_iter().collect(); + + ingredients.sort_by(|(_, a1), (_, a2)| a1.cmp(a2)); + ingredients + .into_iter() + .map(|(k, _)| k) + .collect::>() + .join(",") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "mxmxvkd kfcds sqjhc nhms (contains dairy, fish)".to_string(), + "trh fvjkl sbzzf mxmxvkd (contains dairy)".to_string(), + "sqjhc fvjkl (contains soy)".to_string(), + "sqjhc mxmxvkd sbzzf (contains fish)".to_string(), + ]; + + let expected = 5; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "mxmxvkd kfcds sqjhc nhms (contains dairy, fish)".to_string(), + "trh fvjkl sbzzf mxmxvkd (contains dairy)".to_string(), + "sqjhc fvjkl (contains soy)".to_string(), + "sqjhc mxmxvkd sbzzf (contains fish)".to_string(), + ]; + + let expected = "mxmxvkd,sqjhc,fvjkl"; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day21/src/main.rs b/2020/day21/src/main.rs new file mode 100644 index 0000000..b25d91c --- /dev/null +++ b/2020/day21/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day21_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day21").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day22/Cargo.toml b/2020/day22/Cargo.toml new file mode 100644 index 0000000..e64d566 --- /dev/null +++ b/2020/day22/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day22_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day22_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day22/src/lib.rs b/2020/day22/src/lib.rs new file mode 100644 index 0000000..b2049d0 --- /dev/null +++ b/2020/day22/src/lib.rs @@ -0,0 +1,261 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::collections::{HashSet, VecDeque}; + +#[derive(Aoc)] +pub struct Day22; + +#[derive(Debug)] +pub struct Player { + deck: VecDeque, +} + +impl From<&String> for Player { + fn from(raw: &String) -> Self { + // skip the "Player 1:" line + let deck = raw + .lines() + .skip(1) + .map(|card| card.parse().expect("failed to parse the card")) + .collect(); + + Player { deck } + } +} + +impl Player { + fn clone_with_n_first(&self, n: usize) -> Self { + Player { + deck: self.deck.iter().take(n).cloned().collect(), + } + } + + fn play_card(&mut self) -> usize { + self.deck + .pop_front() + .expect("tried to play a card from an empty deck!") + } + + fn peek_next(&self) -> Option<&usize> { + self.deck.front() + } + + fn cards_left(&self) -> usize { + self.deck.len() + } + + fn play_round(&mut self, other: &mut Self) -> Option { + if self.peek_next().is_none() { + // player self lost + return Some(false); + } + if other.peek_next().is_none() { + // player self won + return Some(true); + } + + let played1 = self.play_card(); + let played2 = other.play_card(); + + if played1 > played2 { + self.insert_won((played1, played2)); + } else { + other.insert_won((played2, played1)); + } + + None + } + + fn insert_won(&mut self, cards: (usize, usize)) { + self.deck.push_back(cards.0); + self.deck.push_back(cards.1); + } + + fn calculate_final_score(&self) -> usize { + self.deck + .iter() + .rev() + .enumerate() + .map(|(i, card)| (i + 1) * *card) + .sum() + } +} + +#[derive(Debug)] +pub struct RecursiveGame { + player1: Player, + player2: Player, + previously_played: HashSet<(VecDeque, VecDeque)>, +} + +impl RecursiveGame { + fn new(player1: Player, player2: Player) -> Self { + RecursiveGame { + player1, + player2, + previously_played: HashSet::new(), + } + } + + // returns whether player1 won or lost + fn play(&mut self) -> bool { + loop { + if let Some(result) = self.play_round() { + return result; + } + } + } + + fn play_round(&mut self) -> Option { + let p1_peek = self.player1.peek_next(); + let p2_peek = self.player2.peek_next(); + + if p1_peek.is_none() { + // player1 lost + return Some(false); + } + if p2_peek.is_none() { + // player1 won + return Some(true); + } + + // Before either player deals a card, + // if there was a previous round in this game that had exactly + // the same cards in the same order in the same players' decks, + // the game instantly ends in a win for player 1. + let cloned_decks = (self.player1.deck.clone(), self.player2.deck.clone()); + if self.previously_played.contains(&cloned_decks) { + // player 1 won + return Some(true); + } + + // Otherwise, this round's cards must be in a new configuration; + // the players begin the round by each drawing the top card of their deck as normal. + let played1 = self.player1.play_card(); + let played2 = self.player2.play_card(); + + self.previously_played.insert(cloned_decks); + + // If both players have at least as many cards remaining in their deck as the value of the card they just drew, + // the winner of the round is determined by playing a new game of Recursive Combat. + if played1 <= self.player1.cards_left() && played2 <= self.player2.cards_left() { + let mut new_game = RecursiveGame { + player1: self.player1.clone_with_n_first(played1), + player2: self.player2.clone_with_n_first(played2), + previously_played: HashSet::new(), + }; + if new_game.play() { + self.player1.insert_won((played1, played2)); + } else { + self.player2.insert_won((played2, played1)); + } + } else { + // Otherwise, at least one player must not have enough cards left in their deck to recurse; + // the winner of the round is the player with the higher-value card. + if played1 > played2 { + self.player1.insert_won((played1, played2)); + } else { + self.player2.insert_won((played2, played1)); + } + } + + None + } +} + +pub fn part1(input: Vec) -> usize { + let mut player1 = Player::from(&input[0]); + let mut player2 = Player::from(&input[1]); + + loop { + if let Some(result) = player1.play_round(&mut player2) { + return if result { + player1.calculate_final_score() + } else { + player2.calculate_final_score() + }; + } + } +} + +pub fn part2(input: Vec) -> usize { + let player1 = Player::from(&input[0]); + let player2 = Player::from(&input[1]); + + let mut recursive_game = RecursiveGame::new(player1, player2); + if recursive_game.play() { + recursive_game.player1.calculate_final_score() + } else { + recursive_game.player2.calculate_final_score() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + r#"Player 1: +9 +2 +6 +3 +1"# + .to_string(), + r#"Player 2: +5 +8 +4 +7 +10"# + .to_string(), + ]; + + let expected = 306; + + assert_eq!(expected, part1(input)); + } + + #[test] + fn part2_sample_input() { + let input = vec![ + r#"Player 1: +9 +2 +6 +3 +1"# + .to_string(), + r#"Player 2: +5 +8 +4 +7 +10"# + .to_string(), + ]; + + let expected = 291; + + assert_eq!(expected, part2(input)); + } +} diff --git a/2020/day22/src/main.rs b/2020/day22/src/main.rs new file mode 100644 index 0000000..4428119 --- /dev/null +++ b/2020/day22/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day22_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = input_read::read_into_string_groups("inputs/2020/day22") + .expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day23/Cargo.toml b/2020/day23/Cargo.toml new file mode 100644 index 0000000..c6a76ce --- /dev/null +++ b/2020/day23/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day23_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day23_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day23/src/lib.rs b/2020/day23/src/lib.rs new file mode 100644 index 0000000..c3c22a7 --- /dev/null +++ b/2020/day23/src/lib.rs @@ -0,0 +1,366 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::{self, Debug, Formatter}; +use std::rc::Rc; + +#[derive(Aoc)] +pub struct Day23; + +#[derive(Default)] +pub struct Node { + data: usize, + // it can only be a `None` if it got detached while moving it to new location + next: Option>>, +} + +impl Node { + // assumes next MUST exist + fn next_node(&self) -> Rc> { + Rc::clone(self.next.as_ref().unwrap()) + } +} + +struct NumRingBuffer { + head: Rc>, + lookup_cache: HashMap>>, + size: usize, +} + +impl NumRingBuffer { + fn new(mut data: Vec) -> Self { + let size = data.len(); + let mut lookup_cache = HashMap::new(); + + let mut current = Rc::new(RefCell::new(Node { + data: data.remove(0), + next: None, + })); + + lookup_cache.insert(current.borrow().data, Rc::clone(¤t)); + + let head = Rc::clone(¤t); + + for node in data.into_iter().map(|num| { + Rc::new(RefCell::new(Node { + data: num, + next: None, + })) + }) { + current.borrow_mut().next = Some(Rc::clone(&node)); + lookup_cache.insert(node.borrow().data, Rc::clone(&node)); + current = node; + } + + // last node: + current.borrow_mut().next = Some(Rc::clone(&head)); + + NumRingBuffer { + head, + size, + lookup_cache, + } + } + + fn new_million(mut init_data: Vec) -> Self { + let size = 1_000_000; + let mut lookup_cache = HashMap::new(); + + let mut current = Rc::new(RefCell::new(Node { + data: init_data.remove(0), + next: None, + })); + + lookup_cache.insert(current.borrow().data, Rc::clone(¤t)); + + let head = Rc::clone(¤t); + + // note: we're taking 999_999 elements as we've already consumed one + for node in init_data + .into_iter() + .chain(std::iter::successors(Some(10), |first| Some(*first + 1))) + .take(999999) + .map(|num| { + Rc::new(RefCell::new(Node { + data: num, + next: None, + })) + }) + { + current.borrow_mut().next = Some(Rc::clone(&node)); + lookup_cache.insert(node.borrow().data, Rc::clone(&node)); + current = node; + } + + // last node: + current.borrow_mut().next = Some(Rc::clone(&head)); + + NumRingBuffer { + head, + size, + lookup_cache, + } + } + + fn move_head(&mut self) { + let next = self.head.borrow().next_node(); + self.head = next; + } + + fn read_head(&self) -> usize { + self.head.borrow().data + } + + fn find_one_node(&self) -> Rc> { + Rc::clone(self.lookup_cache.get(&1).unwrap()) + } + + fn part1_result(&self) -> usize { + let mut digits = Vec::with_capacity(self.size); + // look for '1' + let one_node = self.find_one_node(); + + // we have a '1' node. progress once + let mut not_one = one_node.borrow().next_node(); + while not_one.borrow().data != 1 { + digits.push(not_one.borrow().data); + let next = not_one.borrow().next_node(); + not_one = next; + } + + digits + .iter() + .rev() + .enumerate() + .fold(0, |acc, (idx, digit)| acc + 10usize.pow(idx as u32) * digit) + } + + fn take_next_three(&mut self) -> (Rc>, [usize; 3]) { + let next = self.head.borrow_mut().next.take().unwrap(); + let mut picked_values = [0; 3]; + + let mut current = Rc::clone(&next); + + // determine the fourth element + // ignore clippy as I can't be bothered to implement iterator for the nodes + #[allow(clippy::needless_range_loop)] + for i in 0..3 { + picked_values[i] = current.borrow().data; + let next = current.borrow().next_node(); + if i == 2 { + // clear the 'next' of the third element we're removing + current.borrow_mut().next = None; + } + current = next; + } + + // point to the fourth element + self.head.borrow_mut().next = Some(current); + + (next, picked_values) + } + + fn insert_after(&mut self, nodes: Rc>, val: usize) { + let insertion_target = self.lookup_cache.get(&val).unwrap(); + + // get the node to which our last three should point to + let tail = insertion_target.borrow().next_node(); + + let mut nodes_ptr = Rc::clone(&nodes); + // make the target node point to our subchain + insertion_target.borrow_mut().next = Some(nodes); + + // and finally redirect the subchain to the correct tail + while nodes_ptr.borrow().next.is_some() { + let next = nodes_ptr.borrow().next_node(); + nodes_ptr = next; + } + nodes_ptr.borrow_mut().next = Some(tail); + } +} + +impl Debug for NumRingBuffer { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let head = self.head.borrow().data; + let mut current = Rc::clone(&self.head); + let mut values = vec![current.borrow().data]; + + let next = current.borrow().next_node(); + current = next; + + while current.borrow().data != head { + values.push(current.borrow().data); + let next = current.borrow().next_node(); + current = next; + } + + write!(f, "head: {}, {:?}", self.head.borrow().data, values) + } +} + +struct CrabGame { + buf: NumRingBuffer, + picked: Option<(Rc>, [usize; 3])>, + destination: usize, +} + +impl CrabGame { + fn new(input: usize) -> CrabGame { + let split_input = split_into_digits(input); + let buf = NumRingBuffer::new(split_input); + + CrabGame { + buf, + picked: None, + destination: 0, + } + } + + fn new_million(input: usize) -> CrabGame { + let split_input = split_into_digits(input); + let buf = NumRingBuffer::new_million(split_input); + + CrabGame { + buf, + picked: None, + destination: 0, + } + } + + fn part1_result(&self) -> usize { + self.buf.part1_result() + } + + fn part2_result(&self) -> usize { + let one_node = self.buf.find_one_node(); + let next = one_node.borrow().next_node(); + let label1 = next.borrow().data; + let label2 = next.borrow().next_node().borrow().data; + label1 * label2 + } + + fn make_n_moves(&mut self, n: usize) { + for _ in 0..n { + self.make_move(); + } + } + + fn sub_one(&self, val: usize) -> usize { + let res = val - 1; + if res == 0 { + self.buf.size + } else { + res + } + } + + fn select_destination_cup(&self) -> usize { + let mut potential = self.sub_one(self.buf.read_head()); + while potential == self.picked.as_ref().unwrap().1[0] + || potential == self.picked.as_ref().unwrap().1[1] + || potential == self.picked.as_ref().unwrap().1[2] + { + potential = self.sub_one(potential) + } + + potential + } + + fn make_move(&mut self) { + self.picked = Some(self.buf.take_next_three()); + self.destination = self.select_destination_cup(); + + let (next_nodes, _) = self.picked.take().unwrap(); + self.buf.insert_after(next_nodes, self.destination); + self.buf.move_head(); + } +} + +pub fn part1(input: usize) -> usize { + let mut game = CrabGame::new(input); + game.make_n_moves(100); + game.part1_result() +} + +// this is not included in coverage for the same reason as the part2 test +#[cfg(not(tarpaulin_include))] +pub fn part2(input: usize) -> usize { + let mut game = CrabGame::new_million(input); + game.make_n_moves(10_000_000); + game.part2_result() +} + +fn split_into_digits(number: usize) -> Vec { + let mut digits = Vec::new(); + let mut n = number; + while n > 9 { + digits.push(n % 10); + n /= 10; + } + digits.push(n); + digits.reverse(); + digits +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_small_sample() { + let input = 389125467; + let mut game = CrabGame::new(input); + game.make_move(); + let res = game.part1_result(); + + assert_eq!(54673289, res) + } + + #[test] + fn part1_small_sample2() { + let input = 389125467; + let mut game = CrabGame::new(input); + game.make_n_moves(10); + let res = game.part1_result(); + + assert_eq!(92658374, res) + } + + #[test] + fn part1_sample_input() { + let input = 389125467; + let expected = 67384529; + + assert_eq!(expected, part1(input)) + } + + // the below test passes, however, it is not committed as because is it run under + // `debug` release profile (and I can't be bothered to change that) it take too long to + // complete + + // #[test] + // fn part2_sample_input() { + // let input = 389125467; + // let expected = 149245887792; + // + // assert_eq!(expected, part2(input)) + // } +} diff --git a/2020/day23/src/main.rs b/2020/day23/src/main.rs new file mode 100644 index 0000000..edf34e3 --- /dev/null +++ b/2020/day23/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use day23_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = 364289715; + + let part1_result = part1(input); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day24/Cargo.toml b/2020/day24/Cargo.toml new file mode 100644 index 0000000..27ceceb --- /dev/null +++ b/2020/day24/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "day24_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day24_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } +rayon = { workspace = true } + +day17_2020 = { path = "../day17" } + +[lints] +workspace = true diff --git a/2020/day24/src/lib.rs b/2020/day24/src/lib.rs new file mode 100644 index 0000000..b464654 --- /dev/null +++ b/2020/day24/src/lib.rs @@ -0,0 +1,236 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; +use day17_2020::Point; +use rayon::prelude::*; +use std::collections::{HashMap, HashSet}; + +#[derive(Aoc)] +pub struct Day24; + +const EAST: char = 'e'; +const SOUTH: char = 's'; +const WEST: char = 'w'; +const NORTH: char = 'n'; + +const DAYS_TO_SIMULATE: usize = 100; + +const EAST_DIR: (isize, isize, isize) = (1, -1, 0); +const SOUTH_EAST_DIR: (isize, isize, isize) = (0, -1, 1); +const SOUTH_WEST_DIR: (isize, isize, isize) = (-1, 0, 1); +const WEST_DIR: (isize, isize, isize) = (-1, 1, 0); +const NORTH_WEST_DIR: (isize, isize, isize) = (0, 1, -1); +const NORTH_EAST_DIR: (isize, isize, isize) = (1, 0, -1); + +// represent Hexagon as a 3D point +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Hexagon { + location: Point, +} + +impl From<&String> for Hexagon { + fn from(raw: &String) -> Self { + let mut reference = Point::base(3); + + let mut iter = raw.chars(); + while let Some(current) = iter.next() { + match current { + EAST => reference += EAST_DIR, + SOUTH => match iter.next().expect("invalid hex direction") { + EAST => reference += SOUTH_EAST_DIR, + WEST => reference += SOUTH_WEST_DIR, + _ => panic!("invalid hex direction"), + }, + WEST => reference += WEST_DIR, + NORTH => match iter.next().expect("invalid hex direction") { + WEST => reference += NORTH_WEST_DIR, + EAST => reference += NORTH_EAST_DIR, + _ => panic!("invalid hex direction"), + }, + _ => panic!("invalid hex direction"), + } + } + + Hexagon { + location: reference, + } + } +} + +impl Hexagon { + fn adjacent_hexes(&self) -> Vec { + vec![ + Hexagon { + location: &self.location + EAST_DIR, + }, + Hexagon { + location: &self.location + SOUTH_EAST_DIR, + }, + Hexagon { + location: &self.location + SOUTH_WEST_DIR, + }, + Hexagon { + location: &self.location + WEST_DIR, + }, + Hexagon { + location: &self.location + NORTH_WEST_DIR, + }, + Hexagon { + location: &self.location + NORTH_EAST_DIR, + }, + ] + } +} + +pub fn part1(input: Vec) -> usize { + let mut active = HashSet::new(); + input.iter().map(Hexagon::from).for_each(|hex| { + if active.contains(&hex) { + active.remove(&hex); + } else { + active.insert(hex); + } + }); + + active.len() +} + +struct SimulatedHexagon { + hexagon: Hexagon, + neighbours: Vec, + should_deactivate: bool, +} + +fn simulate_step_par(active_hexes: &mut HashSet) { + let simulated_hexes: Vec<_> = active_hexes + .par_iter() + .map(|active_hexagon| { + let neighbours = active_hexagon.adjacent_hexes(); + let active_neighbours = neighbours + .par_iter() + .map(|neighbour| active_hexes.contains(neighbour)) + .filter(|is_active| *is_active) + .count(); + + SimulatedHexagon { + hexagon: active_hexagon.clone(), + neighbours, + should_deactivate: active_neighbours == 0 || active_neighbours > 2, + } + }) + .collect(); + + let mut all_adjacents = HashMap::new(); + for simulated_hex in simulated_hexes { + if simulated_hex.should_deactivate { + active_hexes.remove(&simulated_hex.hexagon); + } + for neighbour in simulated_hex.neighbours { + *all_adjacents.entry(neighbour).or_insert(0) += 1; + } + } + + for (adjacent, count) in all_adjacents.into_iter() { + if count == 2 { + active_hexes.insert(adjacent); + } + } +} + +pub fn part2(input: Vec) -> usize { + let mut active = HashSet::new(); + input.iter().map(Hexagon::from).for_each(|hex| { + if active.contains(&hex) { + active.remove(&hex); + } else { + active.insert(hex); + } + }); + + for _ in 0..DAYS_TO_SIMULATE { + simulate_step_par(&mut active); + } + + active.len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "sesenwnenenewseeswwswswwnenewsewsw".to_string(), + "neeenesenwnwwswnenewnwwsewnenwseswesw".to_string(), + "seswneswswsenwwnwse".to_string(), + "nwnwneseeswswnenewneswwnewseswneseene".to_string(), + "swweswneswnenwsewnwneneseenw".to_string(), + "eesenwseswswnenwswnwnwsewwnwsene".to_string(), + "sewnenenenesenwsewnenwwwse".to_string(), + "wenwwweseeeweswwwnwwe".to_string(), + "wsweesenenewnwwnwsenewsenwwsesesenwne".to_string(), + "neeswseenwwswnwswswnw".to_string(), + "nenwswwsewswnenenewsenwsenwnesesenew".to_string(), + "enewnwewneswsewnwswenweswnenwsenwsw".to_string(), + "sweneswneswneneenwnewenewwneswswnese".to_string(), + "swwesenesewenwneswnwwneseswwne".to_string(), + "enesenwswwswneneswsenwnewswseenwsese".to_string(), + "wnwnesenesenenwwnenwsewesewsesesew".to_string(), + "nenewswnwewswnenesenwnesewesw".to_string(), + "eneswnwswnwsenenwnwnwwseeswneewsenese".to_string(), + "neswnwewnwnwseenwseesewsenwsweewe".to_string(), + "wseweeenwnesenwwwswnew".to_string(), + ]; + + let expected = 10; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "sesenwnenenewseeswwswswwnenewsewsw".to_string(), + "neeenesenwnwwswnenewnwwsewnenwseswesw".to_string(), + "seswneswswsenwwnwse".to_string(), + "nwnwneseeswswnenewneswwnewseswneseene".to_string(), + "swweswneswnenwsewnwneneseenw".to_string(), + "eesenwseswswnenwswnwnwsewwnwsene".to_string(), + "sewnenenenesenwsewnenwwwse".to_string(), + "wenwwweseeeweswwwnwwe".to_string(), + "wsweesenenewnwwnwsenewsenwwsesesenwne".to_string(), + "neeswseenwwswnwswswnw".to_string(), + "nenwswwsewswnenenewsenwsenwnesesenew".to_string(), + "enewnwewneswsewnwswenweswnenwsenwsw".to_string(), + "sweneswneswneneenwnewenewwneswswnese".to_string(), + "swwesenesewenwneswnwwneseswwne".to_string(), + "enesenwswwswneneswsenwnewswseenwsese".to_string(), + "wnwnesenesenenwwnenwsewesewsesesew".to_string(), + "nenewswnwewswnenesenwnesewesw".to_string(), + "eneswnwswnwsenenwnwnwwseeswneewsenese".to_string(), + "neswnwewnwnwseenwseesewsenwsweewe".to_string(), + "wseweeenwnesenwwwswnew".to_string(), + ]; + + let expected = 2208; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2020/day24/src/main.rs b/2020/day24/src/main.rs new file mode 100644 index 0000000..99a4b5a --- /dev/null +++ b/2020/day24/src/main.rs @@ -0,0 +1,32 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::legacy::input_read; +use day24_2020::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = + input_read::read_line_input("inputs/2020/day24").expect("failed to read input file"); + + let part1_result = part1(input.clone()); + println!("Part 1 result is {}", part1_result); + + let part2_result = part2(input); + println!("Part 2 result is {}", part2_result); +} diff --git a/2020/day25/Cargo.toml b/2020/day25/Cargo.toml new file mode 100644 index 0000000..d356b6f --- /dev/null +++ b/2020/day25/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day25_2020" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day25_2020" +path = "src/lib.rs" + +[dependencies] +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +anyhow = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2020/day25/src/lib.rs b/2020/day25/src/lib.rs new file mode 100644 index 0000000..2e71b0b --- /dev/null +++ b/2020/day25/src/lib.rs @@ -0,0 +1,112 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_solution::Aoc; + +#[derive(Aoc)] +pub struct Day25; + +const ORDER: usize = 20201227; +const GENERATOR: usize = 7; + +type PrivateKey = usize; +type PublicKey = usize; + +// perform (base ** exp) % modulus +// basically https://en.wikipedia.org/wiki/Modular_exponentiation#Right-to-left_binary_method +#[inline] +fn mod_pow(mut base: usize, mut exp: usize, modulus: usize) -> usize { + if modulus == 1 { + return 0; + } + let mut result = 1; + base %= modulus; + while exp > 0 { + if exp % 2 == 1 { + result = result * base % modulus; + } + exp >>= 1; + base = base * base % modulus + } + result +} + +// sanity check to see if I understood the task +#[inline] +fn derive_public_key(secret: PrivateKey) -> PublicKey { + mod_pow(GENERATOR, secret, ORDER) +} + +// it's the last day of the advent of code so +// let's be lazy about it and since the values are small, just brute-force it. +// could I have implemented something fancier like baby-step giant-step? +// yes. did the task require it? no. +#[inline] +fn reverse_private_key(public: PublicKey) -> PrivateKey { + let mut candidate = 1; + + loop { + if derive_public_key(candidate) == public { + return candidate; + } + candidate += 1; + } +} + +#[inline] +fn diffie_hellman_ish_thing(local_secret: PrivateKey, remote_public: PublicKey) -> PublicKey { + mod_pow(remote_public, local_secret, ORDER) +} + +pub fn part1(pub_keys: (PublicKey, PublicKey)) -> usize { + // just reverse a single key + let private = reverse_private_key(pub_keys.0); + diffie_hellman_ish_thing(private, pub_keys.1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn public_key_derivation() { + assert_eq!(5764801, derive_public_key(8)); + assert_eq!(17807724, derive_public_key(11)); + } + + #[test] + fn reversing_private_key() { + assert_eq!(8, reverse_private_key(5764801)); + assert_eq!(11, reverse_private_key(17807724)); + } + + #[test] + fn diffie_hellman_ish() { + assert_eq!(14897079, diffie_hellman_ish_thing(8, 17807724)); + assert_eq!(14897079, diffie_hellman_ish_thing(11, 5764801)); + } + + #[test] + fn part1_sample_input() { + let input = (5764801, 17807724); + + let expected = 14897079; + + assert_eq!(expected, part1(input)) + } +} diff --git a/2020/day25/src/main.rs b/2020/day25/src/main.rs new file mode 100644 index 0000000..d27f653 --- /dev/null +++ b/2020/day25/src/main.rs @@ -0,0 +1,23 @@ +// Copyright 2020 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use day25_2020::part1; + +#[cfg(not(tarpaulin_include))] +fn main() { + let input = (14788856, 19316454); + + let part1_result = part1(input); + println!("Part 1 result is {}", part1_result); +} diff --git a/2021/day01/Cargo.toml b/2021/day01/Cargo.toml new file mode 100644 index 0000000..d957c3a --- /dev/null +++ b/2021/day01/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day01_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day01_2021" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day01/src/lib.rs b/2021/day01/src/lib.rs new file mode 100644 index 0000000..e268ef3 --- /dev/null +++ b/2021/day01/src/lib.rs @@ -0,0 +1,62 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use itertools::Itertools; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day01; + +pub fn part1(input: Vec) -> usize { + input.iter().tuple_windows().filter(|(a, b)| a < b).count() +} + +pub fn part2(input: Vec) -> usize { + input + .iter() + .tuple_windows() + .map(|(a, b, c)| a + b + c) + .tuple_windows() + .filter(|(a, b)| a < b) + .count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![199, 200, 208, 210, 200, 207, 240, 269, 260, 263]; + let expected = 7; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![199, 200, 208, 210, 200, 207, 240, 269, 260, 263]; + let expected = 5; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day01/src/main.rs b/2021/day01/src/main.rs new file mode 100644 index 0000000..6923357 --- /dev/null +++ b/2021/day01/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::{execute_vec, input_read}; +use day01_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec( + "inputs/2021/day01", + input_read::read_parsed_line_input, + part1, + part2, + ) +} diff --git a/2021/day02/Cargo.toml b/2021/day02/Cargo.toml new file mode 100644 index 0000000..609f1d8 --- /dev/null +++ b/2021/day02/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day02_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day02_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day02/src/lib.rs b/2021/day02/src/lib.rs new file mode 100644 index 0000000..e73c2d1 --- /dev/null +++ b/2021/day02/src/lib.rs @@ -0,0 +1,148 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::{bail, Context}; +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = i64, runner = part1))] +#[aoc(part2(output = i64, runner = part2))] +pub struct Day02; + +const FORWARD_CMD: &str = "forward"; +const DOWN_CMD: &str = "down"; +const UP_CMD: &str = "up"; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Command { + Forward(i64), + Down(i64), + Up(i64), +} + +impl FromStr for Command { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut cmd_magnitude = s.split_ascii_whitespace(); + let raw_cmd = cmd_magnitude.next().context("invalid command")?; + let magnitude = cmd_magnitude.next().context("invalid command")?.parse()?; + + match raw_cmd { + FORWARD_CMD => Ok(Command::Forward(magnitude)), + DOWN_CMD => Ok(Command::Down(magnitude)), + UP_CMD => Ok(Command::Up(magnitude)), + _ => bail!("invalid command"), + } + } +} + +struct Submarine { + x_pos: i64, + y_pos: i64, + aim: i64, +} + +impl Submarine { + fn new() -> Submarine { + Submarine { + x_pos: 0, + y_pos: 0, + aim: 0, + } + } + + fn move_in_direction(&mut self, cmd: Command) { + match cmd { + Command::Forward(magnitude) => self.x_pos += magnitude, + Command::Down(magnitude) => self.y_pos += magnitude, + Command::Up(magnitude) => self.y_pos -= magnitude, + } + } + + fn steer_in_direction(&mut self, cmd: Command) { + match cmd { + Command::Forward(magnitude) => { + self.x_pos += magnitude; + self.y_pos += magnitude * self.aim + } + Command::Down(magnitude) => self.aim += magnitude, + Command::Up(magnitude) => self.aim -= magnitude, + } + } +} + +pub fn part1(input: Vec) -> i64 { + let mut sub = Submarine::new(); + for cmd in input { + sub.move_in_direction(cmd) + } + sub.x_pos * sub.y_pos +} + +pub fn part2(input: Vec) -> i64 { + let mut sub = Submarine::new(); + for cmd in input { + sub.steer_in_direction(cmd) + } + sub.x_pos * sub.y_pos +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + Command::Forward(5), + Command::Down(5), + Command::Forward(8), + Command::Up(3), + Command::Down(8), + Command::Forward(2), + ]; + let expected = 150; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + Command::Forward(5), + Command::Down(5), + Command::Forward(8), + Command::Up(3), + Command::Down(8), + Command::Forward(2), + ]; + let expected = 900; + assert_eq!(expected, part2(input)) + } + + #[test] + fn command_parsing() { + assert_eq!(Command::Up(42), "up 42".parse().unwrap()); + assert_eq!(Command::Down(123), "down 123".parse().unwrap()); + assert_eq!(Command::Forward(1), "forward 1".parse().unwrap()); + } +} diff --git a/2021/day02/src/main.rs b/2021/day02/src/main.rs new file mode 100644 index 0000000..31577de --- /dev/null +++ b/2021/day02/src/main.rs @@ -0,0 +1,26 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::{execute_vec, input_read}; +use day02_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec( + "inputs/2021/day02", + input_read::read_parsed_line_input, + part1, + part2, + ) +} diff --git a/2021/day03/Cargo.toml b/2021/day03/Cargo.toml new file mode 100644 index 0000000..0f06f1b --- /dev/null +++ b/2021/day03/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day03_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day03_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day03/src/lib.rs b/2021/day03/src/lib.rs new file mode 100644 index 0000000..458b301 --- /dev/null +++ b/2021/day03/src/lib.rs @@ -0,0 +1,147 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = u32, runner = part1))] +#[aoc(part2(output = u32, runner = part2))] +pub struct Day03; + +fn most_common_bit(input: &[u16], position: u8) -> u8 { + let mut set_count = 0; + for num in input { + set_count += num >> position & 1; + } + + let unset = input.len() as u16 - set_count; + match set_count { + set if set >= unset => 1, + _ => 0, + } +} + +pub fn part1(input: Vec) -> u32 { + let num_bits = input[0].len() as u8; + + let input: Vec<_> = input + .iter() + .map(|s| u16::from_str_radix(s, 2).unwrap()) + .collect(); + + let mut gamma_rate = 0; + + for bit in 0..num_bits { + gamma_rate |= (most_common_bit(&input, bit) as u16) << bit; + } + + let mask = (1 << num_bits) - 1; + let epsilon = !gamma_rate & mask; + + gamma_rate as u32 * epsilon as u32 +} + +fn sieve(mut input: Vec, num_bits: u8, most_common: bool) -> u16 { + // we need to work from the most significant bit + for bit in (0..num_bits).rev() { + if input.len() == 1 { + return input[0]; + } + + let mut target_bit = most_common_bit(&input, bit); + + // least common is just reverse of most common + if !most_common { + target_bit = !target_bit & 1; + } + + input.retain(|x| (x >> bit & 1) as u8 == target_bit) + } + + if input.len() > 1 { + panic!("we run out of numbers to sift through"); + } else { + input[0] + } +} + +pub fn part2(input: Vec) -> u32 { + let num_bits = input[0].len() as u8; + + let input: Vec<_> = input + .iter() + .map(|s| u16::from_str_radix(s, 2).unwrap()) + .collect(); + + let o2 = sieve(input.clone(), num_bits, true) as u32; + let co2 = sieve(input, num_bits, false) as u32; + + o2 * co2 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "00100".to_string(), + "11110".to_string(), + "10110".to_string(), + "10111".to_string(), + "10101".to_string(), + "01111".to_string(), + "00111".to_string(), + "11100".to_string(), + "10000".to_string(), + "11001".to_string(), + "00010".to_string(), + "01010".to_string(), + ]; + + let expected = 198; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "00100".to_string(), + "11110".to_string(), + "10110".to_string(), + "10111".to_string(), + "10101".to_string(), + "01111".to_string(), + "00111".to_string(), + "11100".to_string(), + "10000".to_string(), + "11001".to_string(), + "00010".to_string(), + "01010".to_string(), + ]; + + let expected = 230; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day03/src/main.rs b/2021/day03/src/main.rs new file mode 100644 index 0000000..8d8a45d --- /dev/null +++ b/2021/day03/src/main.rs @@ -0,0 +1,21 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::{execute_vec, read_input_lines}; +use day03_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day03", read_input_lines, part1, part2) +} diff --git a/2021/day04/Cargo.toml b/2021/day04/Cargo.toml new file mode 100644 index 0000000..7aa6bfb --- /dev/null +++ b/2021/day04/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day04_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day04_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day04/src/lib.rs b/2021/day04/src/lib.rs new file mode 100644 index 0000000..464053b --- /dev/null +++ b/2021/day04/src/lib.rs @@ -0,0 +1,309 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::StringGroupsParser; +use aoc_solution::Aoc; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = StringGroupsParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day04; + +const GRID_SIZE: usize = 5; + +#[derive(Debug, Default)] +pub struct BingoField { + value: u8, + marked: bool, +} + +impl Display for BingoField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.marked { + write!(f, "[{:>2}]", self.value) + } else { + write!(f, " {:>2} ", self.value) + } + } +} + +impl BingoField { + fn new(value: u8) -> Self { + BingoField { + value, + marked: false, + } + } + + fn mark(&mut self) { + self.marked = true + } + + fn is_marked(&self) -> bool { + self.marked + } +} + +// card is defined to be a 5x5 grid +#[derive(Debug)] +pub struct BingoBoard { + rows: [[BingoField; GRID_SIZE]; GRID_SIZE], +} + +impl FromStr for BingoBoard { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut rows: [[BingoField; GRID_SIZE]; GRID_SIZE] = Default::default(); + for (i, row) in s.lines().enumerate() { + for (j, val) in row.split_ascii_whitespace().enumerate() { + let val = val.parse()?; + rows[i][j] = BingoField::new(val); + } + } + + Ok(BingoBoard { rows }) + } +} + +impl Display for BingoBoard { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for row in &self.rows { + for value in row { + write!(f, "{value}")? + } + writeln!(f)? + } + Ok(()) + } +} + +impl BingoBoard { + fn check_win_condition(&self) -> bool { + for i in 0..GRID_SIZE { + if self.check_row(i) { + return true; + } + if self.check_column(i) { + return true; + } + } + + false + } + + fn check_row(&self, row: usize) -> bool { + self.rows[row].iter().all(|field| field.is_marked()) + } + + fn check_column(&self, column: usize) -> bool { + for row in &self.rows { + if !row[column].is_marked() { + return false; + } + } + true + } + + fn mark_value(&mut self, value: u8) { + for row in self.rows.iter_mut() { + for field in row.iter_mut() { + if field.value == value { + field.mark(); + return; + } + } + } + } + + fn calculate_score(&self) -> usize { + let mut score = 0; + for row in self.rows.iter() { + for field in row.iter() { + if !field.is_marked() { + score += field.value as usize + } + } + } + score + } +} + +#[derive(Debug)] +pub struct BingoGame { + currently_played: usize, + drawn_numbers: Vec, + boards: Vec, +} + +impl BingoGame { + fn from_raw(input: Vec) -> Self { + assert!(input.len() > 2); + let drawn_numbers = input[0] + .split(',') + .map(|val| val.parse().unwrap()) + .collect(); + let boards = input + .iter() + .skip(1) + .map(|val| val.parse().unwrap()) + .collect(); + + BingoGame { + currently_played: 0, + drawn_numbers, + boards, + } + } + + fn play_round(&mut self, drawn: u8) -> Option { + for board in self.boards.iter_mut() { + board.mark_value(drawn); + if board.check_win_condition() { + return Some(board.calculate_score() * drawn as usize); + } + } + + None + } + + fn play_round_with_removal(&mut self, drawn: u8) -> Option { + let mut to_remove = Vec::new(); + let boards = self.boards.len(); + for (i, board) in self.boards.iter_mut().enumerate().rev() { + board.mark_value(drawn); + if board.check_win_condition() { + if boards == 1 { + return Some(board.calculate_score() * drawn as usize); + } else { + to_remove.push(i) + } + } + } + + for remove in to_remove { + self.boards.remove(remove); + } + + None + } + + fn draw_number(&mut self) -> u8 { + let value = self + .drawn_numbers + .get(self.currently_played) + .expect("run out of values to draw"); + self.currently_played += 1; + *value + } + + fn play(&mut self) -> usize { + loop { + let drawn = self.draw_number(); + if let Some(winning_score) = self.play_round(drawn) { + return winning_score; + } + } + } + + fn play_until_final_board(&mut self) -> usize { + loop { + let drawn = self.draw_number(); + if let Some(winning_score) = self.play_round_with_removal(drawn) { + return winning_score; + } + } + } +} + +pub fn part1(input: Vec) -> usize { + let mut game = BingoGame::from_raw(input); + game.play() +} + +pub fn part2(input: Vec) -> usize { + let mut game = BingoGame::from_raw(input); + game.play_until_final_board() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1".to_string(), + r#"22 13 17 11 0 +8 2 23 4 24 +21 9 14 16 7 +6 10 3 18 5 +1 12 20 15 19"# + .to_string(), + r#"3 15 0 2 22 +9 18 13 17 5 +19 8 7 25 23 +20 11 10 24 4 +14 21 16 12 6"# + .to_string(), + r#"14 21 17 24 4 +10 16 15 9 19 +18 8 23 26 20 +22 11 13 6 5 +2 0 12 3 7"# + .to_string(), + ]; + + let expected = 4512; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1".to_string(), + r#"22 13 17 11 0 +8 2 23 4 24 +21 9 14 16 7 +6 10 3 18 5 +1 12 20 15 19"# + .to_string(), + r#"3 15 0 2 22 +9 18 13 17 5 +19 8 7 25 23 +20 11 10 24 4 +14 21 16 12 6"# + .to_string(), + r#"14 21 17 24 4 +10 16 15 9 19 +18 8 23 26 20 +22 11 13 6 5 +2 0 12 3 7"# + .to_string(), + ]; + + let expected = 1924; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day04/src/main.rs b/2021/day04/src/main.rs new file mode 100644 index 0000000..8b1d970 --- /dev/null +++ b/2021/day04/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_into_string_groups; +use day04_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day04", read_into_string_groups, part1, part2) +} diff --git a/2021/day05/Cargo.toml b/2021/day05/Cargo.toml new file mode 100644 index 0000000..00e0434 --- /dev/null +++ b/2021/day05/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day05_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day05_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day05/src/lib.rs b/2021/day05/src/lib.rs new file mode 100644 index 0000000..da00118 --- /dev/null +++ b/2021/day05/src/lib.rs @@ -0,0 +1,213 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::Context; +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day05; + +#[derive(Debug)] +pub struct MalformedVentLine; + +#[derive(Debug, Copy, Clone)] +pub struct VentLine { + start: (i32, i32), + end: (i32, i32), +} + +impl Display for VentLine { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{},{} -> {},{}", + self.start.0, self.start.1, self.end.0, self.end.1 + ) + } +} + +impl FromStr for VentLine { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut coords = s.split(" -> "); + let start = coords.next().context("malformed vent line")?; + let mut x_y1 = start.split(','); + let x1 = x_y1.next().context("malformed vent line")?.parse()?; + let y1 = x_y1.next().context("malformed vent line")?.parse()?; + + let end = coords.next().context("malformed vent line")?; + let mut x_y2 = end.split(','); + let x2 = x_y2.next().context("malformed vent line")?.parse()?; + let y2 = x_y2.next().context("malformed vent line")?.parse()?; + + Ok(VentLine { + start: (x1, y1), + end: (x2, y2), + }) + } +} + +impl VentLine { + fn is_vertical(&self) -> bool { + self.start.0 == self.end.0 + } + + fn is_horizontal(&self) -> bool { + self.start.1 == self.end.1 + } + + // in the case of this task and our input, all slopes are guaranteed to be integers + fn slope(&self) -> Option { + let dx = self.end.0 - self.start.0; + if dx == 0 { + return None; + } + let dy = self.end.1 - self.start.1; + Some(dy / dx) + } + + fn interception(&self, slope: i32) -> i32 { + self.start.1 - slope * self.start.0 + } + + fn covered_points(&self) -> Vec<(i32, i32)> { + match self.slope() { + Some(m) => { + let b = self.interception(m); + if self.start.0 > self.end.0 { + (self.end.0..=self.start.0) + .map(|x| (x, m * x + b)) + .rev() + .collect() + } else { + (self.start.0..=self.end.0) + .map(|x| (x, m * x + b)) + .collect() + } + } + None => { + if self.start.1 > self.end.1 { + (self.end.1..=self.start.1) + .map(|y| (self.start.0, y)) + .rev() + .collect() + } else { + (self.start.1..=self.end.1) + .map(|y| (self.start.0, y)) + .collect() + } + } + } + } +} + +pub fn part1(input: Vec) -> usize { + let mut coverage: HashMap<_, i32> = HashMap::new(); + + input + .iter() + .filter(|line| line.is_vertical() || line.is_horizontal()) + .for_each(|line| { + for covered_point in line.covered_points() { + *coverage.entry(covered_point).or_default() += 1i32; + } + }); + + coverage.values().filter(|&&count| count >= 2).count() +} + +pub fn part2(input: Vec) -> usize { + let mut coverage: HashMap<_, i32> = HashMap::new(); + + input.iter().for_each(|line| { + for covered_point in line.covered_points() { + *coverage.entry(covered_point).or_default() += 1i32; + } + }); + + coverage.values().filter(|&&count| count >= 2).count() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn point_cover() { + let line1 = VentLine { + start: (1, 1), + end: (1, 3), + }; + assert_eq!(vec![(1, 1), (1, 2), (1, 3)], line1.covered_points()); + + let line2 = VentLine { + start: (9, 7), + end: (7, 7), + }; + assert_eq!(vec![(9, 7), (8, 7), (7, 7)], line2.covered_points()); + } + + #[test] + fn part1_sample_input() { + let input = vec![ + "0,9 -> 5,9".parse().unwrap(), + "8,0 -> 0,8".parse().unwrap(), + "9,4 -> 3,4".parse().unwrap(), + "2,2 -> 2,1".parse().unwrap(), + "7,0 -> 7,4".parse().unwrap(), + "6,4 -> 2,0".parse().unwrap(), + "0,9 -> 2,9".parse().unwrap(), + "3,4 -> 1,4".parse().unwrap(), + "0,0 -> 8,8".parse().unwrap(), + "5,5 -> 8,2".parse().unwrap(), + ]; + + let expected = 5; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "0,9 -> 5,9".parse().unwrap(), + "8,0 -> 0,8".parse().unwrap(), + "9,4 -> 3,4".parse().unwrap(), + "2,2 -> 2,1".parse().unwrap(), + "7,0 -> 7,4".parse().unwrap(), + "6,4 -> 2,0".parse().unwrap(), + "0,9 -> 2,9".parse().unwrap(), + "3,4 -> 1,4".parse().unwrap(), + "0,0 -> 8,8".parse().unwrap(), + "5,5 -> 8,2".parse().unwrap(), + ]; + + let expected = 12; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day05/src/main.rs b/2021/day05/src/main.rs new file mode 100644 index 0000000..ca2129e --- /dev/null +++ b/2021/day05/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_line_input; +use day05_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day05", read_parsed_line_input, part1, part2) +} diff --git a/2021/day06/Cargo.toml b/2021/day06/Cargo.toml new file mode 100644 index 0000000..9c42e85 --- /dev/null +++ b/2021/day06/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day06_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day06_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day06/src/lib.rs b/2021/day06/src/lib.rs new file mode 100644 index 0000000..c58c947 --- /dev/null +++ b/2021/day06/src/lib.rs @@ -0,0 +1,80 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::CommaSeparatedParser; +use aoc_solution::Aoc; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = CommaSeparatedParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day06; + +fn naive_simulation(cycle_timers: &[usize], days: usize) -> usize { + let mut timers: [usize; 9] = Default::default(); + for timer in cycle_timers { + timers[*timer] += 1; + } + + for _ in 0..days { + let t_0 = timers[0]; + timers[0] = timers[1]; + timers[1] = timers[2]; + timers[2] = timers[3]; + timers[3] = timers[4]; + timers[4] = timers[5]; + timers[5] = timers[6]; + timers[6] = timers[7] + t_0; + timers[7] = timers[8]; + timers[8] = t_0; + } + + timers.iter().sum() +} + +pub fn part1(input: Vec) -> usize { + naive_simulation(&input, 80) +} + +pub fn part2(input: Vec) -> usize { + naive_simulation(&input, 256) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![3, 4, 3, 1, 2]; + + let expected = 5934; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![3, 4, 3, 1, 2]; + + let expected = 26984457539; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day06/src/main.rs b/2021/day06/src/main.rs new file mode 100644 index 0000000..c066eae --- /dev/null +++ b/2021/day06/src/main.rs @@ -0,0 +1,27 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_comma_separated_values; +use day06_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec( + "inputs/2021/day06", + read_parsed_comma_separated_values, + part1, + part2, + ) +} diff --git a/2021/day07/Cargo.toml b/2021/day07/Cargo.toml new file mode 100644 index 0000000..dc13a87 --- /dev/null +++ b/2021/day07/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day07_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day07_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day07/src/lib.rs b/2021/day07/src/lib.rs new file mode 100644 index 0000000..e6f49af --- /dev/null +++ b/2021/day07/src/lib.rs @@ -0,0 +1,82 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::CommaSeparatedParser; +use aoc_solution::Aoc; +use std::cmp::min; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = CommaSeparatedParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day07; + +fn abs_diff(a: usize, b: usize) -> usize { + (a as isize - b as isize).unsigned_abs() +} + +pub fn part1(input: Vec) -> usize { + let mut owned_input = input.to_vec(); + let idx = input.len() / 2; + let (_, median, _) = owned_input.select_nth_unstable(idx); + + input.iter().map(|&x| abs_diff(x, *median)).sum() +} + +pub fn part2(input: Vec) -> usize { + fn fuel_cost(from: usize, to: usize) -> usize { + (1..=abs_diff(from, to)).sum() + } + + // so apparently we can't use just mean since its minimises distance^2 + // and we need to minimise (distance * (distance + 1)) / 2. + // so rather than just doing a big binary search, just try 2 values closest + // to minimised d^2 and choose the smaller one + let sum: usize = input.iter().sum(); + let mean_f = (sum as f32 / input.len() as f32).floor() as usize; + let mean_c = (sum as f32 / input.len() as f32).ceil() as usize; + + let min_f = input.iter().map(|&x| fuel_cost(x, mean_f)).sum(); + let min_c = input.iter().map(|&x| fuel_cost(x, mean_c)).sum(); + + min(min_f, min_c) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![16, 1, 2, 0, 4, 2, 7, 1, 2, 14]; + + let expected = 37; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![16, 1, 2, 0, 4, 2, 7, 1, 2, 14]; + + let expected = 168; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day07/src/main.rs b/2021/day07/src/main.rs new file mode 100644 index 0000000..5aacc5e --- /dev/null +++ b/2021/day07/src/main.rs @@ -0,0 +1,27 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_comma_separated_values; +use day07_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec( + "inputs/2021/day07", + read_parsed_comma_separated_values, + part1, + part2, + ) +} diff --git a/2021/day08/Cargo.toml b/2021/day08/Cargo.toml new file mode 100644 index 0000000..8d4bdf4 --- /dev/null +++ b/2021/day08/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day08_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day08_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day08/src/lib.rs b/2021/day08/src/lib.rs new file mode 100644 index 0000000..e07c033 --- /dev/null +++ b/2021/day08/src/lib.rs @@ -0,0 +1,218 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::collections::{HashMap, HashSet}; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day08; + +fn split_into_pattern_and_display(raw: &str) -> (Vec, Vec) { + let mut split = raw.split(" | "); + ( + split + .next() + .unwrap() + .split_ascii_whitespace() + .map(|s| s.to_owned()) + .collect(), + split + .next() + .unwrap() + .split_ascii_whitespace() + .map(|s| s.to_owned()) + .collect(), + ) +} + +fn count_uniques(source: &[String]) -> usize { + source + .iter() + .filter(|digit| { + digit.len() == 2 || digit.len() == 4 || digit.len() == 3 || digit.len() == 7 + }) + .count() +} + +fn contains_digit(checked: &str, against: &str) -> bool { + for char in against.chars() { + if !checked.contains(char) { + return false; + } + } + true +} + +// basically just sort it +fn normalise_digit(raw: &str) -> String { + let mut chars = raw.chars().collect::>(); + chars.sort_unstable(); + chars.into_iter().collect() +} + +fn determine_substitutions(signal: &[String]) -> HashMap { + let mut identified: [Option; 10] = Default::default(); + let mut substitutions = HashMap::new(); + + let mut normalised_signal = signal + .iter() + .map(|raw| normalise_digit(raw)) + .collect::>(); + + // identify 1, 7, 4, 8 + for digit in normalised_signal.iter() { + if digit.len() == 2 { + identified[1] = Some(digit.clone()); + substitutions.insert(digit.clone(), 1); + } else if digit.len() == 3 { + identified[7] = Some(digit.clone()); + substitutions.insert(digit.clone(), 7); + } else if digit.len() == 4 { + identified[4] = Some(digit.clone()); + substitutions.insert(digit.clone(), 4); + } else if digit.len() == 7 { + identified[8] = Some(digit.clone()); + substitutions.insert(digit.clone(), 8); + } + } + + normalised_signal.remove(identified[1].as_ref().unwrap()); + normalised_signal.remove(identified[7].as_ref().unwrap()); + normalised_signal.remove(identified[4].as_ref().unwrap()); + normalised_signal.remove(identified[8].as_ref().unwrap()); + + // identify 3, 9, 6, 0 + for digit in normalised_signal.iter() { + if digit.len() == 5 { + if contains_digit(digit, identified[1].as_ref().unwrap()) { + identified[3] = Some(digit.clone()); + substitutions.insert(digit.clone(), 3); + } + } else if digit.len() == 6 { + if contains_digit(digit, identified[4].as_ref().unwrap()) { + identified[9] = Some(digit.clone()); + substitutions.insert(digit.clone(), 9); + } else if !contains_digit(digit, identified[1].as_ref().unwrap()) { + identified[6] = Some(digit.clone()); + substitutions.insert(digit.clone(), 6); + } else { + identified[0] = Some(digit.clone()); + substitutions.insert(digit.clone(), 0); + } + } else { + panic!("invalid length") + } + } + + normalised_signal.remove(identified[3].as_ref().unwrap()); + normalised_signal.remove(identified[9].as_ref().unwrap()); + normalised_signal.remove(identified[6].as_ref().unwrap()); + normalised_signal.remove(identified[0].as_ref().unwrap()); + + for digit in normalised_signal { + // only 2 and 5 are left; 5 is subset of 9, while 2 is not. + if contains_digit(identified[9].as_ref().unwrap(), &digit) { + identified[5] = Some(digit.clone()); + substitutions.insert(digit.clone(), 5); + } else { + identified[2] = Some(digit.clone()); + substitutions.insert(digit.clone(), 2); + } + } + + substitutions +} + +pub fn part1(input: Vec) -> usize { + input + .iter() + .map(|signal_display| { + let (_, display) = split_into_pattern_and_display(signal_display); + count_uniques(&display) + }) + .sum() +} + +pub fn part2(input: Vec) -> usize { + input + .iter() + .map(|signal_display| { + let (signal, display) = split_into_pattern_and_display(signal_display); + let substitutions = determine_substitutions(&signal); + let display_values = display + .iter() + .map(|digit| normalise_digit(digit)) + .map(|normalised| substitutions.get(&normalised).unwrap()) + .collect::>(); + display_values[0] * 1000 + + display_values[1] * 100 + + display_values[2] * 10 + + display_values[3] + }) + .sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe".to_string(), + "edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc".to_string(), + "fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg".to_string(), + "fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb".to_string(), + "aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea".to_string(), + "fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb".to_string(), + "dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe".to_string(), + "bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef".to_string(), + "egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb".to_string(), + "gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce".to_string(), + ]; + + let expected = 26; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe".to_string(), + "edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc".to_string(), + "fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg".to_string(), + "fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb".to_string(), + "aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea".to_string(), + "fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb".to_string(), + "dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe".to_string(), + "bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef".to_string(), + "egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb".to_string(), + "gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce".to_string(), + ]; + + let expected = 61229; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day08/src/main.rs b/2021/day08/src/main.rs new file mode 100644 index 0000000..d47c8bd --- /dev/null +++ b/2021/day08/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_input_lines; +use day08_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day08", read_input_lines, part1, part2) +} diff --git a/2021/day09/Cargo.toml b/2021/day09/Cargo.toml new file mode 100644 index 0000000..9135457 --- /dev/null +++ b/2021/day09/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day09_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day09_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day09/src/lib.rs b/2021/day09/src/lib.rs new file mode 100644 index 0000000..f780b55 --- /dev/null +++ b/2021/day09/src/lib.rs @@ -0,0 +1,237 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::cmp::Reverse; +use std::collections::HashSet; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day09; + +#[derive(Debug)] +pub struct Basin { + points: HashSet, +} + +impl Basin { + fn size(&self) -> usize { + self.points.len() + } +} + +#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Point { + x: usize, + y: usize, + height: usize, +} + +impl Point { + fn new(x: usize, y: usize, height: usize) -> Self { + Point { x, y, height } + } + + fn risk_level(&self) -> usize { + self.height + 1 + } +} + +#[derive(Debug)] +pub struct HeightMap { + rows: Vec>, +} + +impl HeightMap { + fn from_raw_rows(raw: &[String]) -> Self { + let rows = raw + .iter() + .map(|raw_row| { + raw_row + .chars() + .map(|c| c.to_digit(10).unwrap() as usize) + .collect() + }) + .collect(); + HeightMap { rows } + } + + fn check_low_point(&self, x: usize, y: usize, value: usize) -> bool { + // left + if x > 0 && self.rows[y][x - 1] <= value { + return false; + } + + // top + if y > 0 && self.rows[y - 1][x] <= value { + return false; + } + + // right + if let Some(&right) = self.rows[y].get(x + 1) { + if right <= value { + return false; + } + } + + // down + if let Some(down_row) = self.rows.get(y + 1) { + if down_row[x] <= value { + return false; + } + } + + true + } + + fn low_points(&self) -> Vec { + let mut low_points = Vec::new(); + for (y, row) in self.rows.iter().enumerate() { + for (x, value) in row.iter().enumerate() { + if self.check_low_point(x, y, *value) { + low_points.push(Point::new(x, y, *value)) + } + } + } + low_points + } + + fn check_surrounding_points_for_common_basin(&self, point: Point) -> Vec { + let mut new_basin_members = Vec::with_capacity(4); + + // left + if point.x > 0 { + let left_value = self.rows[point.y][point.x - 1]; + if left_value != 9 { + new_basin_members.push(Point::new(point.x - 1, point.y, left_value)) + } + } + + // top + if point.y > 0 { + let top_value = self.rows[point.y - 1][point.x]; + if top_value != 9 { + new_basin_members.push(Point::new(point.x, point.y - 1, top_value)) + } + } + + // right + if let Some(&right_value) = self.rows[point.y].get(point.x + 1) { + if right_value != 9 { + new_basin_members.push(Point::new(point.x + 1, point.y, right_value)) + } + } + + // down + if let Some(down_row) = self.rows.get(point.y + 1) { + let down_value = down_row[point.x]; + if down_value != 9 { + new_basin_members.push(Point::new(point.x, point.y + 1, down_value)) + } + } + + new_basin_members + } + + fn basin_around(&self, point: Point) -> Basin { + let mut basin_points = HashSet::new(); + basin_points.insert(point); + let mut unchecked_points = vec![point]; + + loop { + let mut new_unchecked = Vec::new(); + for unchecked in &unchecked_points { + for new_point in self.check_surrounding_points_for_common_basin(*unchecked) { + if !basin_points.contains(&new_point) { + basin_points.insert(new_point); + new_unchecked.push(new_point); + } + } + } + + unchecked_points = new_unchecked; + if unchecked_points.is_empty() { + break; + } + } + + Basin { + points: basin_points, + } + } +} + +pub fn part1(input: Vec) -> usize { + HeightMap::from_raw_rows(&input) + .low_points() + .into_iter() + .map(|point| point.risk_level()) + .sum() +} + +pub fn part2(input: Vec) -> usize { + let height_map = HeightMap::from_raw_rows(&input); + let low_points = height_map.low_points(); + + let mut basins = low_points + .into_iter() + .map(|point| height_map.basin_around(point)) + .collect::>(); + basins.sort_by_key(|b| Reverse(b.size())); + + basins.iter().take(3).map(|basin| basin.size()).product() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "2199943210".to_string(), + "3987894921".to_string(), + "9856789892".to_string(), + "8767896789".to_string(), + "9899965678".to_string(), + ]; + + let expected = 15; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "2199943210".to_string(), + "3987894921".to_string(), + "9856789892".to_string(), + "8767896789".to_string(), + "9899965678".to_string(), + ]; + + let expected = 1134; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day09/src/main.rs b/2021/day09/src/main.rs new file mode 100644 index 0000000..c3e217d --- /dev/null +++ b/2021/day09/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_input_lines; +use day09_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day09", read_input_lines, part1, part2) +} diff --git a/2021/day10/Cargo.toml b/2021/day10/Cargo.toml new file mode 100644 index 0000000..cea3254 --- /dev/null +++ b/2021/day10/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day10_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day10_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day10/src/lib.rs b/2021/day10/src/lib.rs new file mode 100644 index 0000000..10e0801 --- /dev/null +++ b/2021/day10/src/lib.rs @@ -0,0 +1,274 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day10; + +struct Stack { + inner: Vec, + size: usize, +} + +impl Stack { + fn new() -> Self { + Stack { + inner: Vec::new(), + size: 0, + } + } + + fn push(&mut self, value: T) { + self.inner.push(value) + } + + fn pop(&mut self) -> Option { + self.inner.pop() + } + + fn is_empty(&self) -> bool { + self.inner.is_empty() + } +} + +impl Clone for Stack { + fn clone(&self) -> Self { + Stack { + inner: self.inner.clone(), + size: self.size, + } + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub struct Bracket { + typ: BracketType, + opening: bool, +} + +impl From for Bracket { + fn from(c: char) -> Self { + match c { + '(' => Bracket::new(BracketType::Parentheses, true), + ')' => Bracket::new(BracketType::Parentheses, false), + '[' => Bracket::new(BracketType::Square, true), + ']' => Bracket::new(BracketType::Square, false), + '{' => Bracket::new(BracketType::Curly, true), + '}' => Bracket::new(BracketType::Curly, false), + '<' => Bracket::new(BracketType::Angle, true), + '>' => Bracket::new(BracketType::Angle, false), + n => panic!("invalid bracket type found - {n}"), + } + } +} + +impl Bracket { + fn new(typ: BracketType, opening: bool) -> Self { + Bracket { typ, opening } + } + + fn is_opening(&self) -> bool { + self.opening + } + + fn inverse(&self) -> Bracket { + Bracket { + typ: self.typ, + opening: !self.opening, + } + } + + fn error_score(&self) -> usize { + self.typ.error_score() + } + + fn completion_score(&self) -> usize { + self.typ.completion_score() + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum BracketType { + Square, + Curly, + Angle, + Parentheses, +} + +impl BracketType { + fn error_score(&self) -> usize { + match self { + BracketType::Parentheses => 3, + BracketType::Square => 57, + BracketType::Curly => 1197, + BracketType::Angle => 25137, + } + } + + fn completion_score(&self) -> usize { + match self { + BracketType::Parentheses => 1, + BracketType::Square => 2, + BracketType::Curly => 3, + BracketType::Angle => 4, + } + } +} + +#[derive(Debug)] +pub enum LineError { + Incomplete, + Corrupted(Bracket), +} + +impl LineError { + fn is_incomplete(&self) -> bool { + matches!(self, LineError::Incomplete) + } +} + +fn validate_line(line: &str) -> Result<(), LineError> { + let mut stack = Stack::new(); + + for bracket in line.chars().map(Bracket::from) { + if bracket.is_opening() { + stack.push(bracket) + } else { + let popped = match stack.pop() { + None => return Err(LineError::Corrupted(bracket)), + Some(bracket) => bracket, + }; + if popped.inverse() != bracket { + return Err(LineError::Corrupted(bracket)); + } + } + } + + if !stack.is_empty() { + Err(LineError::Incomplete) + } else { + Ok(()) + } +} + +fn complete_line(incomplete_line: &str) -> Vec { + let mut stack = Stack::new(); + + // first, fill up the stack with available characters + for bracket in incomplete_line.chars().map(Bracket::from) { + if bracket.is_opening() { + stack.push(bracket) + } else { + stack.pop(); + } + } + + let mut completion_brackets = Vec::new(); + + while let Some(popped) = stack.pop() { + completion_brackets.push(popped.inverse()) + } + + completion_brackets +} + +fn calculate_completion_score(completion_brackets: Vec) -> usize { + let mut score = 0; + + for bracket in completion_brackets { + score *= 5; + score += bracket.completion_score() + } + + score +} + +pub fn part1(input: Vec) -> usize { + input + .iter() + .map(|line| match validate_line(line) { + Err(LineError::Corrupted(bracket)) => bracket.error_score(), + _ => 0, + }) + .sum() +} + +pub fn part2(input: Vec) -> usize { + let mut scores = input + .iter() + .filter(|line| match validate_line(line) { + Err(err) => err.is_incomplete(), + _ => false, + }) + .map(|incomplete_line| calculate_completion_score(complete_line(incomplete_line))) + .collect::>(); + + scores.sort_unstable(); + scores[scores.len() / 2] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "[({(<(())[]>[[{[]{<()<>>".to_string(), + "[(()[<>])]({[<{<<[]>>(".to_string(), + "{([(<{}[<>[]}>{[]{[(<()>".to_string(), + "(((({<>}<{<{<>}{[]{[]{}".to_string(), + "[[<[([]))<([[{}[[()]]]".to_string(), + "[{[{({}]{}}([{[{{{}}([]".to_string(), + "{<[[]]>}<{[{[{[]{()[[[]".to_string(), + "[<(<(<(<{}))><([]([]()".to_string(), + "<{([([[(<>()){}]>(<<{{".to_string(), + "<{([{{}}[<[[[<>{}]]]>[]]".to_string(), + ]; + + let expected = 26397; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "[({(<(())[]>[[{[]{<()<>>".to_string(), + "[(()[<>])]({[<{<<[]>>(".to_string(), + "{([(<{}[<>[]}>{[]{[(<()>".to_string(), + "(((({<>}<{<{<>}{[]{[]{}".to_string(), + "[[<[([]))<([[{}[[()]]]".to_string(), + "[{[{({}]{}}([{[{{{}}([]".to_string(), + "{<[[]]>}<{[{[{[]{()[[[]".to_string(), + "[<(<(<(<{}))><([]([]()".to_string(), + "<{([([[(<>()){}]>(<<{{".to_string(), + "<{([{{}}[<[[[<>{}]]]>[]]".to_string(), + ]; + + let expected = 288957; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day10/src/main.rs b/2021/day10/src/main.rs new file mode 100644 index 0000000..df6b4fc --- /dev/null +++ b/2021/day10/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_input_lines; +use day10_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day10", read_input_lines, part1, part2) +} diff --git a/2021/day11/Cargo.toml b/2021/day11/Cargo.toml new file mode 100644 index 0000000..6a8c4ac --- /dev/null +++ b/2021/day11/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day11_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day11_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day11/src/lib.rs b/2021/day11/src/lib.rs new file mode 100644 index 0000000..3b6fc3a --- /dev/null +++ b/2021/day11/src/lib.rs @@ -0,0 +1,219 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::collections::HashSet; +use std::ops::{Index, IndexMut}; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day11; + +#[derive(Debug)] +pub struct SquidGrid { + inner: [[u8; 10]; 10], +} + +impl Index<(usize, usize)> for SquidGrid { + type Output = u8; + + fn index(&self, index: (usize, usize)) -> &Self::Output { + let (x, y) = index; + &self.inner[y][x] + } +} + +impl IndexMut<(usize, usize)> for SquidGrid { + fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output { + let (x, y) = index; + &mut self.inner[y][x] + } +} + +impl SquidGrid { + fn parse(raw: &[String]) -> Self { + let mut rows: [[u8; 10]; 10] = Default::default(); + for (i, line) in raw.iter().enumerate() { + let mut row: [u8; 10] = Default::default(); + for (j, digit) in line.chars().enumerate() { + row[j] = digit.to_digit(10).unwrap() as u8; + } + rows[i] = row; + } + + SquidGrid { inner: rows } + } + + fn flash(&mut self, octopus: (usize, usize), flashed: &mut HashSet<(usize, usize)>) { + flashed.insert(octopus); + + // (x - 1), (y - 1) + // (x - 1), (y) + // (x - 1), (y + 1) + // (x), (y + 1) + // (x), (y - 1) + // (x + 1), (y - 1) + // (x + 1), (y) + // (x + 1), (y + 1) + + let x = octopus.0; + let y = octopus.1; + + let x_minus_1 = if x > 0 { Some(x - 1) } else { None }; + let x_plus_1 = if x < 9 { Some(x + 1) } else { None }; + let y_minus_1 = if y > 0 { Some(y - 1) } else { None }; + let y_plus_1 = if y < 9 { Some(y + 1) } else { None }; + + let adjacent = &[ + (x_minus_1, y_minus_1), + (x_minus_1, Some(y)), + (x_minus_1, y_plus_1), + (Some(x), y_plus_1), + (Some(x), y_minus_1), + (x_plus_1, y_minus_1), + (x_plus_1, Some(y)), + (x_plus_1, y_plus_1), + ]; + + for (x, y) in adjacent { + if let Some(x) = *x { + if let Some(y) = *y { + self[(x, y)] += 1; + + // if adjacent's energy went above 9 and it hasn't flashed during this step, + // it should flash + if self[(x, y)] > 9 && !flashed.contains(&(x, y)) { + self.flash((x, y), flashed); + } + } + } + } + } + + fn flash_all(&mut self, to_flash: Vec<(usize, usize)>) -> HashSet<(usize, usize)> { + let mut flashed = HashSet::new(); + + for octopus in to_flash { + if !flashed.contains(&octopus) { + self.flash(octopus, &mut flashed); + } + } + + flashed + } + + fn simulate_step(&mut self) -> usize { + let mut to_flash = Vec::new(); + // First, the energy level of each octopus increases by 1. + for (y, row) in self.inner.iter_mut().enumerate() { + for (x, squid) in row.iter_mut().enumerate() { + *squid += 1; + + if *squid > 9 { + to_flash.push((x, y)); + } + } + } + + // Then, any octopus with an energy level greater than 9 flashes. + let flashed = self.flash_all(to_flash); + let flashed_count = flashed.len(); + + for (x, y) in flashed { + // Finally, any octopus that flashed during this step has its energy level set to 0, as it used all of its energy to flash. + self[(x, y)] = 0; + } + flashed_count + } + + fn naive_simulation(&mut self, steps: usize) -> usize { + let mut flashed = 0; + + for _ in 0..steps { + flashed += self.simulate_step(); + } + flashed + } + + fn wait_for_sync(&mut self) -> usize { + let mut step = 0; + loop { + step += 1; + if self.simulate_step() == 100 { + return step; + } + } + } +} + +pub fn part1(input: Vec) -> usize { + SquidGrid::parse(&input).naive_simulation(100) +} + +pub fn part2(input: Vec) -> usize { + SquidGrid::parse(&input).wait_for_sync() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "5483143223".to_string(), + "2745854711".to_string(), + "5264556173".to_string(), + "6141336146".to_string(), + "6357385478".to_string(), + "4167524645".to_string(), + "2176841721".to_string(), + "6882881134".to_string(), + "4846848554".to_string(), + "5283751526".to_string(), + ]; + + let expected = 1656; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "5483143223".to_string(), + "2745854711".to_string(), + "5264556173".to_string(), + "6141336146".to_string(), + "6357385478".to_string(), + "4167524645".to_string(), + "2176841721".to_string(), + "6882881134".to_string(), + "4846848554".to_string(), + "5283751526".to_string(), + ]; + + let expected = 195; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day11/src/main.rs b/2021/day11/src/main.rs new file mode 100644 index 0000000..f0cab43 --- /dev/null +++ b/2021/day11/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_input_lines; +use day11_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day11", read_input_lines, part1, part2) +} diff --git a/2021/day12/Cargo.toml b/2021/day12/Cargo.toml new file mode 100644 index 0000000..6368a8c --- /dev/null +++ b/2021/day12/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day12_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day12_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day12/src/lib.rs b/2021/day12/src/lib.rs new file mode 100644 index 0000000..40c074e --- /dev/null +++ b/2021/day12/src/lib.rs @@ -0,0 +1,267 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::Context; +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Display, Formatter}; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day12; + +#[derive(Debug)] +pub struct Graph { + edges: HashMap>, +} + +impl Graph { + fn construct(raw_edges: &[Edge]) -> Self { + let mut edges: HashMap<_, Vec<_>> = HashMap::new(); + for edge in raw_edges.iter().cloned() { + edges + .entry(edge.from.clone()) + .or_default() + .push(edge.to.clone()); + edges.entry(edge.to).or_default().push(edge.from); + } + + Graph { edges } + } +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct Node { + name: String, + is_big: bool, +} + +impl Display for Node { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.name, f) + } +} + +impl Node { + fn new(name: &str) -> Self { + Node { + name: name.to_owned(), + is_big: name.to_ascii_uppercase() == name, + } + } + + fn is_end(&self) -> bool { + self.name == "end" + } + + fn is_start(&self) -> bool { + self.name == "start" + } + + fn count_paths(&self, graph: &Graph, mut visited: HashSet, double_visit: bool) -> usize { + if self.is_end() { + return 1; + } + visited.insert(self.clone()); + + let mut paths = 0; + for node in graph.edges.get(self).unwrap() { + if node.is_big || !visited.contains(node) { + paths += node.count_paths(graph, visited.clone(), double_visit) + } else if double_visit && !node.is_end() && !node.is_start() { + paths += node.count_paths(graph, visited.clone(), false) + } + } + paths + } +} + +#[derive(Debug, Clone)] +pub struct Edge { + from: Node, + to: Node, +} + +impl FromStr for Edge { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut nodes = s.split('-'); + let from = Node::new(nodes.next().context("malformed edge")?); + let to = Node::new(nodes.next().context("malformed edge")?); + Ok(Edge { from, to }) + } +} + +pub fn part1(input: Vec) -> usize { + let graph = Graph::construct(&input); + let start = Node { + name: "start".to_owned(), + is_big: false, + }; + start.count_paths(&graph, HashSet::new(), false) +} + +pub fn part2(input: Vec) -> usize { + let graph = Graph::construct(&input); + let start = Node { + name: "start".to_owned(), + is_big: false, + }; + start.count_paths(&graph, HashSet::new(), true) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input1() { + let input = vec![ + "start-A".parse().unwrap(), + "start-b".parse().unwrap(), + "A-c".parse().unwrap(), + "A-b".parse().unwrap(), + "b-d".parse().unwrap(), + "A-end".parse().unwrap(), + "b-end".parse().unwrap(), + ]; + + let expected = 10; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part1_sample_input2() { + let input = vec![ + "dc-end".parse().unwrap(), + "HN-start".parse().unwrap(), + "start-kj".parse().unwrap(), + "dc-start".parse().unwrap(), + "dc-HN".parse().unwrap(), + "LN-dc".parse().unwrap(), + "HN-end".parse().unwrap(), + "kj-sa".parse().unwrap(), + "kj-HN".parse().unwrap(), + "kj-dc".parse().unwrap(), + ]; + + let expected = 19; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part1_sample_input3() { + let input = vec![ + "fs-end".parse().unwrap(), + "he-DX".parse().unwrap(), + "fs-he".parse().unwrap(), + "start-DX".parse().unwrap(), + "pj-DX".parse().unwrap(), + "end-zg".parse().unwrap(), + "zg-sl".parse().unwrap(), + "zg-pj".parse().unwrap(), + "pj-he".parse().unwrap(), + "RW-he".parse().unwrap(), + "fs-DX".parse().unwrap(), + "pj-RW".parse().unwrap(), + "zg-RW".parse().unwrap(), + "start-pj".parse().unwrap(), + "he-WI".parse().unwrap(), + "zg-he".parse().unwrap(), + "pj-fs".parse().unwrap(), + "start-RW".parse().unwrap(), + ]; + + let expected = 226; + + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input1() { + let input = vec![ + "start-A".parse().unwrap(), + "start-b".parse().unwrap(), + "A-c".parse().unwrap(), + "A-b".parse().unwrap(), + "b-d".parse().unwrap(), + "A-end".parse().unwrap(), + "b-end".parse().unwrap(), + ]; + + let expected = 36; + + assert_eq!(expected, part2(input)) + } + + #[test] + fn part2_sample_input2() { + let input = vec![ + "dc-end".parse().unwrap(), + "HN-start".parse().unwrap(), + "start-kj".parse().unwrap(), + "dc-start".parse().unwrap(), + "dc-HN".parse().unwrap(), + "LN-dc".parse().unwrap(), + "HN-end".parse().unwrap(), + "kj-sa".parse().unwrap(), + "kj-HN".parse().unwrap(), + "kj-dc".parse().unwrap(), + ]; + + let expected = 103; + + assert_eq!(expected, part2(input)) + } + + #[test] + fn part2_sample_input3() { + let input = vec![ + "fs-end".parse().unwrap(), + "he-DX".parse().unwrap(), + "fs-he".parse().unwrap(), + "start-DX".parse().unwrap(), + "pj-DX".parse().unwrap(), + "end-zg".parse().unwrap(), + "zg-sl".parse().unwrap(), + "zg-pj".parse().unwrap(), + "pj-he".parse().unwrap(), + "RW-he".parse().unwrap(), + "fs-DX".parse().unwrap(), + "pj-RW".parse().unwrap(), + "zg-RW".parse().unwrap(), + "start-pj".parse().unwrap(), + "he-WI".parse().unwrap(), + "zg-he".parse().unwrap(), + "pj-fs".parse().unwrap(), + "start-RW".parse().unwrap(), + ]; + + let expected = 3509; + + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day12/src/main.rs b/2021/day12/src/main.rs new file mode 100644 index 0000000..672193c --- /dev/null +++ b/2021/day12/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_line_input; +use day12_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day12", read_parsed_line_input, part1, part2) +} diff --git a/2021/day13/Cargo.toml b/2021/day13/Cargo.toml new file mode 100644 index 0000000..2f64d60 --- /dev/null +++ b/2021/day13/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day13_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day13_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day13/src/lib.rs b/2021/day13/src/lib.rs new file mode 100644 index 0000000..448ed64 --- /dev/null +++ b/2021/day13/src/lib.rs @@ -0,0 +1,264 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::{bail, Context}; +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use std::collections::{BTreeSet, VecDeque}; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Manual)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = String, runner = part2))] +pub struct Day13; + +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, Ord, PartialOrd)] +pub struct Point { + x: usize, + y: usize, +} + +impl FromStr for Point { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut split = s.split(','); + let x = split.next().context("malformed point")?.parse()?; + let y = split.next().context("malformed point")?.parse()?; + Ok(Point { x, y }) + } +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum Axis { + X, + Y, +} + +#[derive(Debug, Copy, Clone)] +pub struct Fold { + axis: Axis, + at: usize, +} + +impl FromStr for Fold { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let stripped = s.strip_prefix("fold along ").context("malformed fold")?; + let mut split = stripped.split('='); + let axis = match split.next().context("malformed fold")? { + "x" => Axis::X, + "y" => Axis::Y, + _ => bail!("malformed fold"), + }; + let at = split.next().context("malformed fold")?.parse()?; + + Ok(Fold { axis, at }) + } +} + +#[derive(Debug, Clone)] +pub struct Manual { + points: BTreeSet, + folds: VecDeque, +} + +impl FromStr for Manual { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let lines = s + .replace("\r\n", "\n") // Windows fix + .split("\n\n") + .map(|split| split.to_owned()) + .collect::>(); + + let points = lines[0].lines().map(|s| s.parse().unwrap()).collect(); + let folds = lines[1].lines().map(|s| s.parse().unwrap()).collect(); + + Ok(Manual { points, folds }) + } +} + +impl Manual { + #[cfg(test)] + fn from_raw(raw: &[String]) -> Manual { + let points = raw[0].lines().map(|s| s.parse().unwrap()).collect(); + let folds = raw[1].lines().map(|s| s.parse().unwrap()).collect(); + + Manual { points, folds } + } + + fn fold_at_y_axis(&mut self, at: usize) { + let mut new_points: BTreeSet = self + .points + .iter() + .filter(|point| point.y < at) + .copied() + .collect(); + for point in &self.points { + if point.y > at { + new_points.insert(Point { + x: point.x, + y: 2 * at - point.y, + }); + } + } + + self.points = new_points + } + + fn fold_at_x_axis(&mut self, at: usize) { + let mut new_points: BTreeSet = self + .points + .iter() + .filter(|point| point.x < at) + .copied() + .collect(); + for point in &self.points { + if point.x > at { + new_points.insert(Point { + x: 2 * at - point.x, + y: point.y, + }); + } + } + + self.points = new_points + } + + fn fold(&mut self) -> bool { + if let Some(fold) = self.folds.pop_front() { + if fold.axis == Axis::Y { + self.fold_at_y_axis(fold.at) + } else { + self.fold_at_x_axis(fold.at) + } + true + } else { + false + } + } + + fn final_manual(&self) -> String { + let max_x = self.points.iter().max_by_key(|point| point.x).unwrap().x; + let max_y = self.points.iter().max_by_key(|point| point.y).unwrap().y; + let mut out = vec![String::new()]; + for y in 0..=max_y { + let mut row = Vec::with_capacity(max_x); + for x in 0..=max_x { + if self.points.contains(&Point { x, y }) { + row.push('â–ˆ'); + } else { + row.push('â €') + } + } + out.push(row.into_iter().collect::()) + } + out.join("\n") + } +} + +pub fn part1(mut manual: Manual) -> usize { + manual.fold(); + manual.points.len() +} + +pub fn part2(mut manual: Manual) -> String { + while manual.fold() {} + manual.final_manual() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = vec![ + "6,10 +0,14 +9,10 +0,3 +10,4 +4,11 +6,0 +6,12 +4,1 +0,13 +10,12 +3,4 +3,0 +8,4 +1,10 +2,14 +8,10 +9,0" + .to_string(), + "fold along y=7 +fold along x=5" + .to_string(), + ]; + + let manual = Manual::from_raw(&input); + let expected = 17; + + assert_eq!(expected, part1(manual)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "6,10 +0,14 +9,10 +0,3 +10,4 +4,11 +6,0 +6,12 +4,1 +0,13 +10,12 +3,4 +3,0 +8,4 +1,10 +2,14 +8,10 +9,0" + .to_string(), + "fold along y=7 +fold along x=5" + .to_string(), + ]; + + let manual = Manual::from_raw(&input); + let expected = r#" +█████ +█⠀⠀⠀█ +█⠀⠀⠀█ +█⠀⠀⠀█ +█████"#; + + assert_eq!(expected, part2(manual)) + } +} diff --git a/2021/day13/src/main.rs b/2021/day13/src/main.rs new file mode 100644 index 0000000..203717d --- /dev/null +++ b/2021/day13/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day13_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day13", read_parsed, part1, part2) +} diff --git a/2021/day14/Cargo.toml b/2021/day14/Cargo.toml new file mode 100644 index 0000000..af32c5b --- /dev/null +++ b/2021/day14/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day14_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day14_2021" +path = "src/lib.rs" + +[dependencies] +itertools = { workspace = true } +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day14/src/lib.rs b/2021/day14/src/lib.rs new file mode 100644 index 0000000..c01efd1 --- /dev/null +++ b/2021/day14/src/lib.rs @@ -0,0 +1,222 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::Context; +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use itertools::Itertools; +use std::collections::HashMap; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Manual)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day14; + +type Pair = (char, char); + +#[derive(Debug, Clone)] +pub struct Rule { + pair: Pair, + insertion: char, +} + +impl FromStr for Rule { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut split = s.split(" -> "); + let mut pair_raw = split.next().context("malformed rule")?.chars(); + let pair = ( + pair_raw.next().context("malformed rule")?, + pair_raw.next().context("malformed rule")?, + ); + + let insertion = split + .next() + .context("malformed rule")? + .to_owned() + .chars() + .next() + .context("malformed rule")?; + + Ok(Rule { pair, insertion }) + } +} + +impl Rule { + fn apply(&self) -> (Pair, Pair) { + ((self.pair.0, self.insertion), (self.insertion, self.pair.1)) + } +} + +#[derive(Debug, Clone)] +pub struct Manual { + front: char, + pairs: HashMap, + rules: Vec, +} + +impl FromStr for Manual { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let lines = s + .replace("\r\n", "\n") // Windows fix + .split("\n\n") + .map(|split| split.to_owned()) + .collect::>(); + + let mut pairs: HashMap = HashMap::new(); + + let mut front = 'Z'; + for (i, pair) in lines[0].chars().tuple_windows().enumerate() { + *pairs.entry(pair).or_default() += 1; + if i == 0 { + front = pair.0; + } + } + + // let points = lines[0].lines().map(|s| s.parse().unwrap()).collect(); + let mut rules = Vec::new(); + for rule in lines[1].lines() { + rules.push(rule.parse()?) + } + + Ok(Manual { + front, + pairs, + rules, + }) + } +} + +impl Manual { + fn step(&mut self) { + let mut new_pairs = self.pairs.clone(); + for rule in &self.rules { + if let Some(count) = self.pairs.remove(&rule.pair) { + let inserted = rule.apply(); + + *new_pairs.entry(rule.pair).or_default() -= count; + *new_pairs.entry(inserted.0).or_default() += count; + *new_pairs.entry(inserted.1).or_default() += count; + } + } + + self.pairs = new_pairs + .into_iter() + .filter(|(_, count)| *count != 0) + .collect(); + } + + fn apply_steps(&mut self, count: usize) { + for _ in 0..count { + self.step() + } + } + + fn element_count(&self) -> HashMap { + let mut count = HashMap::new(); + for (pair, occurrences) in self.pairs.iter() { + *count.entry(pair.1).or_default() += occurrences; + } + *count.entry(self.front).or_default() += 1; + count + } + + fn max_frequency_difference(&self) -> usize { + let count = self.element_count(); + + count.iter().max_by_key(|(_, &count)| count).unwrap().1 + - count.iter().min_by_key(|(_, &count)| count).unwrap().1 + } +} + +pub fn part1(mut manual: Manual) -> usize { + manual.apply_steps(10); + manual.max_frequency_difference() +} + +pub fn part2(mut manual: Manual) -> usize { + manual.apply_steps(40); + manual.max_frequency_difference() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = "NNCB + +CH -> B +HH -> N +CB -> H +NH -> C +HB -> C +HC -> B +HN -> C +NN -> C +BH -> H +NC -> B +NB -> B +BN -> B +BB -> N +BC -> B +CC -> N +CN -> C" + .to_string(); + + let manual = input.parse().unwrap(); + let expected = 1588; + + assert_eq!(expected, part1(manual)); + } + + #[test] + fn part2_sample_input() { + let input = "NNCB + +CH -> B +HH -> N +CB -> H +NH -> C +HB -> C +HC -> B +HN -> C +NN -> C +BH -> H +NC -> B +NB -> B +BN -> B +BB -> N +BC -> B +CC -> N +CN -> C" + .to_string(); + + let manual = input.parse().unwrap(); + let expected = 2188189693529; + + assert_eq!(expected, part2(manual)); + } +} diff --git a/2021/day14/src/main.rs b/2021/day14/src/main.rs new file mode 100644 index 0000000..4679a01 --- /dev/null +++ b/2021/day14/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day14_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day14", read_parsed, part1, part2) +} diff --git a/2021/day15/Cargo.toml b/2021/day15/Cargo.toml new file mode 100644 index 0000000..f8652b2 --- /dev/null +++ b/2021/day15/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day15_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day15_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +pathfinding = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day15/src/lib.rs b/2021/day15/src/lib.rs new file mode 100644 index 0000000..a3ccce1 --- /dev/null +++ b/2021/day15/src/lib.rs @@ -0,0 +1,193 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use pathfinding::prelude::dijkstra; +use std::ops::Index; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = RiskLevelMap)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day15; + +#[derive(Debug, Clone)] +pub struct RiskLevelMap { + rows: Vec>, +} + +type Pos = (usize, usize); + +impl FromStr for RiskLevelMap { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let rows: Vec> = s + .lines() + .map(|row| { + row.chars() + .map(|char| char.to_digit(10).unwrap() as usize) + .collect() + }) + .collect(); + + Ok(Self { rows }) + } +} + +impl Index for RiskLevelMap { + type Output = usize; + + fn index(&self, index: Pos) -> &Self::Output { + let (x, y) = index; + &self.rows[y][x] + } +} + +impl RiskLevelMap { + fn lowest_risk_path_cost(&self) -> usize { + let start = (0usize, 0usize); + let end = (self.rows[0].len() - 1, self.rows.len() - 1); + let (_, cost) = dijkstra(&start, |pos| self.node_successors(pos), |&p| p == end).unwrap(); + + cost + } + + fn node_successors(&self, node: &Pos) -> Vec<(Pos, usize)> { + let mut successors = Vec::new(); + if node.0 > 0 { + let left = (node.0 - 1, node.1); + successors.push((left, self[left])) + } + + if node.0 < self.rows[0].len() - 1 { + let right = (node.0 + 1, node.1); + successors.push((right, self[right])) + } + + if node.1 > 0 { + let top = (node.0, node.1 - 1); + successors.push((top, self[top])) + } + + if node.1 < self.rows.len() - 1 { + let bottom = (node.0, node.1 + 1); + successors.push((bottom, self[bottom])) + } + + successors + } + + fn map_value(i: usize, val: usize) -> usize { + if i == 0 { + val + } else { + let res = val + i; + if res > 9 { + res - 9 + } else { + res + } + } + } + + fn expand_row_five_folds(&mut self, row: usize) { + let old = std::mem::take(&mut self.rows[row]); + self.rows[row] = std::iter::repeat(old) + .take(5) + .enumerate() + .flat_map(|(i, vals)| vals.into_iter().map(move |v| Self::map_value(i, v))) + .collect::>(); + } + + fn expand_columns_five_folds(&mut self) { + let rows = self.rows.clone(); + for i in 1..=4 { + for row in rows.clone() { + let new_row = row + .clone() + .into_iter() + .map(|v| Self::map_value(i, v)) + .collect(); + self.rows.push(new_row); + } + } + } + + fn expand_five_folds(&mut self) { + for i in 0..self.rows.len() { + self.expand_row_five_folds(i) + } + self.expand_columns_five_folds() + } +} + +pub fn part1(risk_map: RiskLevelMap) -> usize { + risk_map.lowest_risk_path_cost() +} + +pub fn part2(mut risk_map: RiskLevelMap) -> usize { + risk_map.expand_five_folds(); + risk_map.lowest_risk_path_cost() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let input = "1163751742 +1381373672 +2136511328 +3694931569 +7463417111 +1319128137 +1359912421 +3125421639 +1293138521 +2311944581" + .parse() + .unwrap(); + + let expected = 40; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = "1163751742 +1381373672 +2136511328 +3694931569 +7463417111 +1319128137 +1359912421 +3125421639 +1293138521 +2311944581" + .parse() + .unwrap(); + + let expected = 315; + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day15/src/main.rs b/2021/day15/src/main.rs new file mode 100644 index 0000000..64d3ffd --- /dev/null +++ b/2021/day15/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day15_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day15", read_parsed, part1, part2) +} diff --git a/2021/day16/Cargo.toml b/2021/day16/Cargo.toml new file mode 100644 index 0000000..5ef1a63 --- /dev/null +++ b/2021/day16/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "day16_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day16_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +hex = { workspace = true } +bitvec = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day16/src/lib.rs b/2021/day16/src/lib.rs new file mode 100644 index 0000000..60c538c --- /dev/null +++ b/2021/day16/src/lib.rs @@ -0,0 +1,430 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use bitvec::prelude::*; +use bitvec::view::BitView; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Packet)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day16; + +const SUM_TYPE_ID: u64 = 0; +const PRODUCT_TYPE_ID: u64 = 1; +const MIN_TYPE_ID: u64 = 2; +const MAX_TYPE_ID: u64 = 3; +const LITERAL_VAL_TYPE_ID: u64 = 4; +const GREATER_THAN_TYPE_ID: u64 = 5; +const LESS_THAN_TYPE_ID: u64 = 6; +const EQUAL_TYPE_ID: u64 = 7; + +fn bits_to_u64(bits: &BitSlice) -> u64 { + let mut res = 0u64; + res.view_bits_mut::()[u64::BITS as usize - bits.len()..].clone_from_bitslice(bits); + res +} + +#[derive(Debug, Clone, Eq, PartialEq, Copy)] +pub enum Type { + Sum, + Product, + Min, + Max, + Literal, + GreaterThan, + LessThan, + Equal, +} + +impl From for Type { + fn from(val: u64) -> Self { + match val { + n if n == SUM_TYPE_ID => Type::Sum, + n if n == PRODUCT_TYPE_ID => Type::Product, + n if n == MIN_TYPE_ID => Type::Min, + n if n == MAX_TYPE_ID => Type::Max, + n if n == LITERAL_VAL_TYPE_ID => Type::Literal, + n if n == GREATER_THAN_TYPE_ID => Type::GreaterThan, + n if n == LESS_THAN_TYPE_ID => Type::LessThan, + n if n == EQUAL_TYPE_ID => Type::Equal, + _ => unreachable!(), + } + } +} + +impl Type { + fn is_literal(&self) -> bool { + matches!(self, Type::Literal) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Header { + version: u64, + type_id: Type, +} + +impl Header { + const LEN: usize = 6; + + fn from_bits(bits: &BitSlice) -> Self { + let version = bits_to_u64(&bits[..3]); + let type_id_u64 = bits_to_u64(&bits[3..6]); + let type_id = Type::from(type_id_u64); + + Header { version, type_id } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Content { + Literal(u64), + Operator(Vec), +} + +impl Content { + fn parse_literal_value(bits: &BitSlice) -> (Self, usize) { + let mut i = 0; + let mut literal_bits: BitVec = BitVec::new(); + + loop { + literal_bits.push(bits[i + 1]); + literal_bits.push(bits[i + 2]); + literal_bits.push(bits[i + 3]); + literal_bits.push(bits[i + 4]); + + i += 5; + + if !bits[i - 5] { + break; + } + } + + (Content::Literal(bits_to_u64(&literal_bits)), i) + } + + fn parse_operator_length_type_1(bits: &BitSlice) -> (Self, usize) { + let mut sub_packets = Vec::new(); + // The next 11 bits are a number that represents the number of sub-packets immediately contained by this packet. + let num_packets = bits_to_u64(&bits[..11]); + let mut i = 11; + + for _ in 0..num_packets { + let (inner_packet, used_bytes) = Packet::from_bits(&bits[i..]); + sub_packets.push(inner_packet); + i += used_bytes; + } + + (Content::Operator(sub_packets), i) + } + + fn parse_operator_length_type_0(bits: &BitSlice) -> (Self, usize) { + let mut sub_packets = Vec::new(); + // The next 15 bits are a number that represents the total length in bits of the sub-packets contained by this packet. + let subpackets_len = bits_to_u64(&bits[..15]); + let mut bytes_left = subpackets_len as usize; + let mut i = 15; + while bytes_left > 0 { + let (inner_packet, used_bytes) = Packet::from_bits(&bits[i..]); + sub_packets.push(inner_packet); + + i += used_bytes; + bytes_left -= used_bytes; + } + (Content::Operator(sub_packets), i) + } + + fn from_bits(bits: &BitSlice, typ: Type) -> (Self, usize) { + if typ.is_literal() { + Self::parse_literal_value(bits) + } else { + let length_type_id = bits[0]; + if length_type_id { + let (content, used_bytes) = Self::parse_operator_length_type_1(&bits[1..]); + (content, used_bytes + 1) + } else { + let (content, used_bytes) = Self::parse_operator_length_type_0(&bits[1..]); + (content, used_bytes + 1) + } + } + } + + fn compute(&self, func: F) -> usize + where + F: FnOnce(&[usize]) -> usize, + { + match self { + Content::Literal(val) => *val as usize, + Content::Operator(packets) => { + let sub_results = packets + .iter() + .map(|packet| packet.calculate()) + .collect::>(); + func(&sub_results) + } + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Packet { + header: Header, + content: Content, +} + +impl Packet { + fn version_sum(&self) -> usize { + match &self.content { + Content::Literal(_) => self.header.version as usize, + Content::Operator(operands) => { + self.header.version as usize + + operands + .iter() + .map(|packet| packet.version_sum()) + .sum::() + } + } + } + + fn calculate(&self) -> usize { + match self.header.type_id { + Type::Sum => self.content.compute(|vals| vals.iter().sum()), + Type::Product => self.content.compute(|vals| vals.iter().product()), + Type::Min => self.content.compute(|vals| *vals.iter().min().unwrap()), + Type::Max => self.content.compute(|vals| *vals.iter().max().unwrap()), + Type::Literal => self.content.compute(|_| Default::default()), + Type::GreaterThan => self.content.compute(|vals| usize::from(vals[0] > vals[1])), + Type::LessThan => self.content.compute(|vals| usize::from(vals[0] < vals[1])), + Type::Equal => self.content.compute(|vals| usize::from(vals[0] == vals[1])), + } + } +} + +impl FromStr for Packet { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let decoded = hex::decode(s)?; + let bits = BitVec::::from_slice(&decoded); + let bit_slice = bits.as_bitslice(); + let (packet, _) = Packet::from_bits(bit_slice); + Ok(packet) + } +} + +impl Packet { + fn from_bits(bits: &BitSlice) -> (Self, usize) { + let header = Header::from_bits(&bits[..6]); + let (content, bytes_used) = Content::from_bits(&bits[6..], header.type_id); + let packet = Packet { header, content }; + (packet, bytes_used + Header::LEN) + } +} + +pub fn part1(packet: Packet) -> usize { + packet.version_sum() +} + +pub fn part2(packet: Packet) -> usize { + packet.calculate() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn literal_packet_parsing() { + let packet = "D2FE28".parse().unwrap(); + let expected = Packet { + header: Header { + version: 6, + type_id: Type::Literal, + }, + content: Content::Literal(2021), + }; + + assert_eq!(expected, packet); + } + + #[test] + fn operator_type0_packet_parsing() { + let packet = "38006F45291200".parse().unwrap(); + let expected = Packet { + header: Header { + version: 1, + type_id: Type::LessThan, + }, + content: Content::Operator(vec![ + Packet { + header: Header { + version: 6, + type_id: Type::Literal, + }, + content: Content::Literal(10), + }, + Packet { + header: Header { + version: 2, + type_id: Type::Literal, + }, + content: Content::Literal(20), + }, + ]), + }; + + assert_eq!(expected, packet); + } + + #[test] + fn operator_type1_packet_parsing() { + let packet = "EE00D40C823060".parse().unwrap(); + let expected = Packet { + header: Header { + version: 7, + type_id: Type::Max, + }, + content: Content::Operator(vec![ + Packet { + header: Header { + version: 2, + type_id: Type::Literal, + }, + content: Content::Literal(1), + }, + Packet { + header: Header { + version: 4, + type_id: Type::Literal, + }, + content: Content::Literal(2), + }, + Packet { + header: Header { + version: 1, + type_id: Type::Literal, + }, + content: Content::Literal(3), + }, + ]), + }; + + assert_eq!(expected, packet); + } + + #[test] + fn part1_sample_input_1() { + let packet = "8A004A801A8002F478".parse().unwrap(); + let expected = 16; + + assert_eq!(expected, part1(packet)); + } + + #[test] + fn part1_sample_input_2() { + let packet = "620080001611562C8802118E34".parse().unwrap(); + let expected = 12; + + assert_eq!(expected, part1(packet)); + } + + #[test] + fn part1_sample_input_3() { + let packet = "C0015000016115A2E0802F182340".parse().unwrap(); + let expected = 23; + + assert_eq!(expected, part1(packet)); + } + + #[test] + fn part1_sample_input_4() { + let packet = "A0016C880162017C3686B18A3D4780".parse().unwrap(); + let expected = 31; + + assert_eq!(expected, part1(packet)); + } + + #[test] + fn part2_sample_input_1() { + let packet = "C200B40A82".parse().unwrap(); + let expected = 3; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_2() { + let packet = "04005AC33890".parse().unwrap(); + let expected = 54; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_3() { + let packet = "880086C3E88112".parse().unwrap(); + let expected = 7; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_4() { + let packet = "CE00C43D881120".parse().unwrap(); + let expected = 9; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_5() { + let packet = "D8005AC2A8F0".parse().unwrap(); + let expected = 1; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_6() { + let packet = "F600BC2D8F".parse().unwrap(); + let expected = 0; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_7() { + let packet = "9C005AC2F8F0".parse().unwrap(); + let expected = 0; + + assert_eq!(expected, part2(packet)); + } + + #[test] + fn part2_sample_input_8() { + let packet = "9C0141080250320F1802104A08".parse().unwrap(); + let expected = 1; + + assert_eq!(expected, part2(packet)); + } +} diff --git a/2021/day16/src/main.rs b/2021/day16/src/main.rs new file mode 100644 index 0000000..3efef18 --- /dev/null +++ b/2021/day16/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day16_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day16", read_parsed, part1, part2) +} diff --git a/2021/day17/Cargo.toml b/2021/day17/Cargo.toml new file mode 100644 index 0000000..c29add1 --- /dev/null +++ b/2021/day17/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day17_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day17_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day17/src/lib.rs b/2021/day17/src/lib.rs new file mode 100644 index 0000000..04052e7 --- /dev/null +++ b/2021/day17/src/lib.rs @@ -0,0 +1,162 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::Context; +use aoc_common::legacy::parse_raw_range; +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use std::cmp::max; +use std::ops::RangeInclusive; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Target)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day17; + +#[derive(Debug, Clone)] +pub struct Target { + x_range: RangeInclusive, + y_range: RangeInclusive, +} + +impl FromStr for Target { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let stripped = s + .strip_prefix("target area: ") + .context("malformed target")?; + let mut ranges = stripped.split(", "); + + let x_range = parse_raw_range(ranges.next().context("malformed target")?)?; + let y_range = parse_raw_range(ranges.next().context("malformed target")?)?; + + Ok(Target { x_range, y_range }) + } +} + +impl Target { + fn maximise_altitude(&self) -> usize { + // only consider y acceleration, since probe's y position is independent of the x position + // and we know there must exist *some* x acceleration for which this will work, otherwise + // this task would have no solution + + // also note that since we're launching upwards, we will have to reach y = 0 again + // and we're going to have Vy = -Vy_0 at that point + // now, to maximise the altitude, we must maximise our launch velocity and therefore + // also speed at which we cross y = 0 + // So to maintain the highest possible speed, we must therefore reach the bottom of the target + // in a single step after reaching y = 0 + // so we must cross y = 0 at min y_pos of target + 1 (so that we would not miss it) + + // also: + // y = Vy_0 * t - 1/2 t^2 + 1/2 t + // y' = Vy_0 + 1/2 - t; y' = 0 <=> t = Vy0 + 1/2, so probe will reach its max attitude at t = Vy0 + 1/2 + // therefore we have to consider t = Vy0 and t = Vy0 + 1 + + let vy_0 = (*self.y_range.start() + 1).unsigned_abs(); + let y = |t: usize| vy_0 * t - t * t / 2 + t / 2; + + let t1 = vy_0; + let t2 = vy_0 + 1; + + let y1 = y(t1); + let y2 = y(t2); + + max(y1, y2) + } +} + +struct Velocity { + dx: isize, + dy: isize, +} + +impl Velocity { + #[allow(clippy::comparison_chain)] + fn step(&mut self) { + self.dy -= 1; + + if self.dx > 0 { + self.dx -= 1 + } else if self.dx < 0 { + self.dx += 1 + } + } + + fn move_probe(&self, probe: &mut (isize, isize)) { + probe.0 += self.dx; + probe.1 += self.dy; + } +} + +pub fn part1(target: Target) -> usize { + target.maximise_altitude() +} + +pub fn part2(target: Target) -> usize { + // unfortunately I'm running out of time now, so we're left to bruteforcing here : ( + let mut valid_velocities = 0; + for dx in 0..*target.x_range.end() * 2 { + for dy in *target.y_range.start()..target.y_range.start().abs() { + let mut v = Velocity { dx, dy }; + let mut probe = (0, 0); + loop { + if target.x_range.contains(&probe.0) && target.y_range.contains(&probe.1) { + valid_velocities += 1; + break; + } + if probe.0 > *target.x_range.end() { + break; + } + if probe.1 < *target.y_range.start() { + break; + } + + v.move_probe(&mut probe); + v.step(); + } + } + } + + valid_velocities +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let target = "target area: x=20..30, y=-10..-5".parse().unwrap(); + + let expected = 45; + assert_eq!(expected, part1(target)) + } + + #[test] + fn part2_sample_input() { + let target = "target area: x=20..30, y=-10..-5".parse().unwrap(); + + let expected = 112; + assert_eq!(expected, part2(target)) + } +} diff --git a/2021/day17/src/main.rs b/2021/day17/src/main.rs new file mode 100644 index 0000000..53e73ce --- /dev/null +++ b/2021/day17/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day17_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day17", read_parsed, part1, part2) +} diff --git a/2021/day18/Cargo.toml b/2021/day18/Cargo.toml new file mode 100644 index 0000000..40b9266 --- /dev/null +++ b/2021/day18/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day18_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day18_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +itertools = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day18/src/lib.rs b/2021/day18/src/lib.rs new file mode 100644 index 0000000..397584c --- /dev/null +++ b/2021/day18/src/lib.rs @@ -0,0 +1,576 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use itertools::Itertools; +use std::cmp::max; +use std::ops::Add; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = u32, runner = part1))] +#[aoc(part2(output = u32, runner = part2))] +pub struct Day18; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Number { + Regular(u32), + Pair, +} + +impl Number { + fn must_get_regular(&self) -> u32 { + match self { + Number::Regular(val) => *val, + _ => unreachable!(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Default)] +pub struct NumberTree { + heights: Vec>>, +} + +impl NumberTree { + fn ensure_height(&mut self, height: usize) { + if self.heights.get_mut(height).is_none() { + let height_size = 2usize.pow(height as u32); + let mut height_data = Vec::with_capacity(height_size); + height_data.resize_with(height_size, || None); + + self.heights.insert(height, height_data); + } + } + + fn insert_pair_node(&mut self, height: usize, branch: usize) { + self.ensure_height(height); + debug_assert!(self.heights[height][branch].is_none()); + self.heights[height][branch] = Some(Number::Pair) + } + + fn insert_num_node(&mut self, height: usize, branch: usize, val: u32) { + self.ensure_height(height); + debug_assert!(self.heights[height][branch].is_none()); + self.heights[height][branch] = Some(Number::Regular(val)) + } + + fn explode_pair(&mut self, height: usize, branch: usize) { + debug_assert_eq!(self.heights[height][branch], Some(Number::Pair)); + debug_assert!(matches!( + self.heights[height + 1][branch * 2], + Some(Number::Regular(_)) + )); + debug_assert!(matches!( + self.heights[height + 1][branch * 2 + 1], + Some(Number::Regular(_)) + )); + self.heights[height][branch] = Some(Number::Regular(0)); + + let left_val = self.heights[height + 1][branch * 2] + .take() + .unwrap() + .must_get_regular(); + let right_val = self.heights[height + 1][branch * 2 + 1] + .take() + .unwrap() + .must_get_regular(); + + self.add_left_of(height, branch, left_val); + self.add_right_of(height, branch, right_val); + + // cleanup + if self.heights[5].iter().all(|val| val.is_none()) { + self.heights.remove(5); + } + } + + fn split_value(&mut self, height: usize, branch: usize) { + debug_assert!(matches!( + self.heights[height][branch], + Some(Number::Regular(_)) + )); + let val = self.heights[height][branch] + .as_ref() + .unwrap() + .must_get_regular(); + debug_assert!(val >= 10); + + let x = val / 2; + let y = if val % 2 == 0 { x } else { x + 1 }; + + self.heights[height][branch] = Some(Number::Pair); + self.insert_num_node(height + 1, branch * 2, x); + self.insert_num_node(height + 1, branch * 2 + 1, y); + } + + fn _magnitude(&self, height: usize, branch: usize) -> u32 { + match self.heights[height][branch] { + Some(Number::Regular(val)) => val, + Some(Number::Pair) => { + 3 * self._magnitude(height + 1, branch * 2) + + 2 * self._magnitude(height + 1, branch * 2 + 1) + } + None => unreachable!(), + } + } + + fn magnitude(&self) -> u32 { + self._magnitude(0, 0) + } + + fn add_left_of(&mut self, this_height: usize, this_branch: usize, val: u32) { + let in_order = self.in_order_values(); + if let Some(this_id) = in_order + .iter() + .position(|n| n == &((this_height, this_branch), 0)) + { + if this_id > 0 { + let ((height, branch), current_val) = in_order[this_id - 1]; + self.heights[height][branch] = Some(Number::Regular(current_val + val)) + } + } + } + + fn add_right_of(&mut self, this_height: usize, this_branch: usize, val: u32) { + let in_order = self.in_order_values(); + if let Some(this_id) = in_order + .iter() + .position(|n| n == &((this_height, this_branch), 0)) + { + if this_id < in_order.len() - 1 { + let ((height, branch), current_val) = in_order[this_id + 1]; + self.heights[height][branch] = Some(Number::Regular(current_val + val)) + } + } + } + + fn explode(&mut self) -> bool { + let mut to_explode = None; + // values whose parents have to explode will only ever exist on height 5 + match self.heights.get_mut(5) { + None => return false, + Some(vals) => { + for (branch, val) in vals.iter().enumerate() { + if val.is_some() { + to_explode = Some(branch); + break; + } + } + } + } + + if let Some(exploding_branch) = to_explode { + // we explode the parent + self.explode_pair(4, exploding_branch / 2); + true + } else { + false + } + } + + fn in_order_traversal(&self, node: (usize, usize)) -> Vec<((usize, usize), u32)> { + match &self.heights[node.0][node.1] { + Some(Number::Regular(val)) => vec![((node.0, node.1), *val)], + Some(Number::Pair) => { + let left = self.in_order_traversal((node.0 + 1, node.1 * 2)); + let mut right = self.in_order_traversal((node.0 + 1, node.1 * 2 + 1)); + let mut res = left; + res.append(&mut right); + res + } + None => vec![], + } + } + + fn in_order_values(&self) -> Vec<((usize, usize), u32)> { + self.in_order_traversal((0, 0)) + } + + fn split(&mut self) -> bool { + let in_order = self.in_order_values(); + for ((height, branch), val) in in_order { + if val >= 10 { + self.split_value(height, branch); + return true; + } + } + false + } + + fn reduce(&mut self) { + loop { + if self.explode() { + continue; + } else if !self.split() { + break; + } + } + } +} + +impl Number { + fn parse_into_tree( + chars: &[char], + tree: &mut NumberTree, + height: usize, + branch: usize, + ) -> usize { + // each pair starts with `[`, so we can ignore first character + let mut used_chars = 1; + if chars[1] == '[' { + tree.insert_pair_node(height + 1, branch * 2); + let used = Self::parse_into_tree(&chars[1..], tree, height + 1, branch * 2); + used_chars += used; + } else { + let val = chars[1].to_digit(10).unwrap(); + tree.insert_num_node(height + 1, branch * 2, val); + used_chars += 1; + }; + + // next we have to have a comma + assert_eq!(chars[used_chars], ','); + used_chars += 1; + + if chars[used_chars] == '[' { + tree.insert_pair_node(height + 1, branch * 2 + 1); + let used = + Self::parse_into_tree(&chars[used_chars..], tree, height + 1, branch * 2 + 1); + used_chars += used; + } else { + let val = chars[used_chars].to_digit(10).unwrap(); + tree.insert_num_node(height + 1, branch * 2 + 1, val); + used_chars += 1; + }; + + // next we have to have a closing bracket + assert_eq!(chars[used_chars], ']'); + used_chars += 1; + + used_chars + } +} + +impl<'a> Add<&'a NumberTree> for NumberTree { + type Output = NumberTree; + + fn add(self, rhs: &'a NumberTree) -> Self::Output { + let mut res = self.clone(); + let final_height = max(self.heights.len(), rhs.heights.len()); + for height in 1..final_height { + res.ensure_height(height) + } + + res.heights.insert(0, vec![Some(Number::Pair)]); + + for (height, height_data) in rhs.heights.iter().enumerate() { + for val in height_data.iter() { + res.heights[height + 1].push(val.clone()) + } + } + for height in 0..res.heights.len() { + let height_size = 2usize.pow(height as u32); + res.heights[height].resize_with(height_size, || None); + } + + res.reduce(); + res + } +} + +impl FromStr for NumberTree { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut tree = NumberTree { + heights: Vec::new(), + }; + // we assume that the tree consists of a single pair at the root + tree.heights.push(vec![Some(Number::Pair)]); + + Number::parse_into_tree(&s.chars().collect::>(), &mut tree, 0, 0); + Ok(tree) + } +} + +pub fn part1(numbers: Vec) -> u32 { + let mut acc = numbers[0].clone(); + for num in numbers.iter().skip(1) { + acc = acc + num; + } + acc.magnitude() +} + +pub fn part2(numbers: Vec) -> u32 { + // no point in using short numbers, they won't produce high magnitudes + numbers + .iter() + .filter(|num| num.heights.len() >= 5) + .permutations(2) + .map(|nums| { + max( + (nums[0].clone() + &nums[1].clone()).magnitude(), + (nums[1].clone() + &nums[0].clone()).magnitude(), + ) + }) + .max() + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn number_parsing() { + let num: NumberTree = "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".parse().unwrap(); + let expected = NumberTree { + heights: vec![ + vec![Some(Number::Pair)], + vec![Some(Number::Pair), Some(Number::Pair)], + vec![ + Some(Number::Pair), + Some(Number::Pair), + Some(Number::Regular(8)), + Some(Number::Regular(1)), + ], + vec![ + Some(Number::Pair), + Some(Number::Regular(4)), + Some(Number::Pair), + Some(Number::Pair), + None, + None, + None, + None, + ], + vec![ + Some(Number::Regular(0)), + Some(Number::Regular(7)), + None, + None, + Some(Number::Regular(7)), + Some(Number::Regular(8)), + Some(Number::Regular(6)), + Some(Number::Regular(0)), + None, + None, + None, + None, + None, + None, + None, + None, + ], + ], + }; + assert_eq!(expected, num); + } + + #[test] + fn explosion() { + let mut before: NumberTree = "[[[[[9,8],1],2],3],4]".parse().unwrap(); + assert!(before.explode()); + let after: NumberTree = "[[[[0,9],2],3],4]".parse().unwrap(); + assert_eq!(after, before); + + let mut before: NumberTree = "[7,[6,[5,[4,[3,2]]]]]".parse().unwrap(); + assert!(before.explode()); + let after: NumberTree = "[7,[6,[5,[7,0]]]]".parse().unwrap(); + assert_eq!(after, before); + + let mut before: NumberTree = "[[6,[5,[4,[3,2]]]],1]".parse().unwrap(); + assert!(before.explode()); + let after: NumberTree = "[[6,[5,[7,0]]],3]".parse().unwrap(); + assert_eq!(after, before); + + let mut before: NumberTree = "[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]".parse().unwrap(); + assert!(before.explode()); + let after: NumberTree = "[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]".parse().unwrap(); + assert_eq!(after, before); + + let mut before: NumberTree = "[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]".parse().unwrap(); + assert!(before.explode()); + let after: NumberTree = "[[3,[2,[8,0]]],[9,[5,[7,0]]]]".parse().unwrap(); + assert_eq!(after, before); + } + + #[test] + fn magnitude() { + let tree: NumberTree = "[[1,2],[[3,4],5]]".parse().unwrap(); + let expected = 143; + assert_eq!(tree.magnitude(), expected); + + let tree: NumberTree = "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".parse().unwrap(); + let expected = 1384; + assert_eq!(tree.magnitude(), expected); + + let tree: NumberTree = "[[[[1,1],[2,2]],[3,3]],[4,4]]".parse().unwrap(); + let expected = 445; + assert_eq!(tree.magnitude(), expected); + + let tree: NumberTree = "[[[[3,0],[5,3]],[4,4]],[5,5]]".parse().unwrap(); + let expected = 791; + assert_eq!(tree.magnitude(), expected); + + let tree: NumberTree = "[[[[5,0],[7,4]],[5,5]],[6,6]]".parse().unwrap(); + let expected = 1137; + assert_eq!(tree.magnitude(), expected); + + let tree: NumberTree = "[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]" + .parse() + .unwrap(); + let expected = 3488; + assert_eq!(tree.magnitude(), expected); + } + + #[test] + fn sample_addition() { + let t1: NumberTree = "[[[[4,3],4],4],[7,[[8,4],9]]]".parse().unwrap(); + let t2: NumberTree = "[1,1]".parse().unwrap(); + + let expected: NumberTree = "[[[[0,7],4],[[7,8],[6,0]]],[8,1]]".parse().unwrap(); + assert_eq!(expected, t1 + &t2) + } + + #[test] + fn sample_sum() { + let nums: Vec = vec![ + "[[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]]".parse().unwrap(), + "[7,[[[3,7],[4,3]],[[6,3],[8,8]]]]".parse().unwrap(), + "[[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]]".parse().unwrap(), + "[[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]]" + .parse() + .unwrap(), + "[7,[5,[[3,8],[1,4]]]]".parse().unwrap(), + "[[2,[2,2]],[8,[8,1]]]".parse().unwrap(), + "[2,9]".parse().unwrap(), + "[1,[[[9,3],9],[[9,0],[0,7]]]]".parse().unwrap(), + "[[[5,[7,4]],7],1]".parse().unwrap(), + "[[[[4,2],2],6],[8,7]]".parse().unwrap(), + ]; + + let s1: NumberTree = "[[[[4,0],[5,4]],[[7,7],[6,0]]],[[8,[7,7]],[[7,9],[5,0]]]]" + .parse() + .unwrap(); + + let s2: NumberTree = "[[[[6,7],[6,7]],[[7,7],[0,7]]],[[[8,7],[7,7]],[[8,8],[8,0]]]]" + .parse() + .unwrap(); + + let s3: NumberTree = "[[[[7,0],[7,7]],[[7,7],[7,8]]],[[[7,7],[8,8]],[[7,7],[8,7]]]]" + .parse() + .unwrap(); + + let s4: NumberTree = "[[[[7,7],[7,8]],[[9,5],[8,7]]],[[[6,8],[0,8]],[[9,9],[9,0]]]]" + .parse() + .unwrap(); + + let s5: NumberTree = "[[[[6,6],[6,6]],[[6,0],[6,7]]],[[[7,7],[8,9]],[8,[8,1]]]]" + .parse() + .unwrap(); + + let s6: NumberTree = "[[[[6,6],[7,7]],[[0,7],[7,7]]],[[[5,5],[5,6]],9]]" + .parse() + .unwrap(); + + let s7: NumberTree = "[[[[7,8],[6,7]],[[6,8],[0,8]]],[[[7,7],[5,0]],[[5,5],[5,6]]]]" + .parse() + .unwrap(); + + let s8: NumberTree = "[[[[7,7],[7,7]],[[8,7],[8,7]]],[[[7,0],[7,7]],9]]" + .parse() + .unwrap(); + + let s9: NumberTree = "[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]" + .parse() + .unwrap(); + + let mut running_total = nums[0].clone() + &nums[1]; + assert_eq!(running_total, s1); + + running_total = running_total + &nums[2]; + assert_eq!(running_total, s2); + + running_total = running_total + &nums[3]; + assert_eq!(running_total, s3); + + running_total = running_total + &nums[4]; + assert_eq!(running_total, s4); + + running_total = running_total + &nums[5]; + assert_eq!(running_total, s5); + + running_total = running_total + &nums[6]; + assert_eq!(running_total, s6); + + running_total = running_total + &nums[7]; + assert_eq!(running_total, s7); + + running_total = running_total + &nums[8]; + assert_eq!(running_total, s8); + + running_total = running_total + &nums[9]; + assert_eq!(running_total, s9); + } + + #[test] + fn part1_sample_input() { + let input = vec![ + "[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]" + .parse() + .unwrap(), + "[[[5,[2,8]],4],[5,[[9,9],0]]]".parse().unwrap(), + "[6,[[[6,2],[5,6]],[[7,6],[4,7]]]]".parse().unwrap(), + "[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]]".parse().unwrap(), + "[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]]".parse().unwrap(), + "[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]]".parse().unwrap(), + "[[[[5,4],[7,7]],8],[[8,3],8]]".parse().unwrap(), + "[[9,3],[[9,9],[6,[4,9]]]]".parse().unwrap(), + "[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]".parse().unwrap(), + "[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]".parse().unwrap(), + ]; + + let expected = 4140; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]" + .parse() + .unwrap(), + "[[[5,[2,8]],4],[5,[[9,9],0]]]".parse().unwrap(), + "[6,[[[6,2],[5,6]],[[7,6],[4,7]]]]".parse().unwrap(), + "[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]]".parse().unwrap(), + "[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]]".parse().unwrap(), + "[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]]".parse().unwrap(), + "[[[[5,4],[7,7]],8],[[8,3],8]]".parse().unwrap(), + "[[9,3],[[9,9],[6,[4,9]]]]".parse().unwrap(), + "[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]".parse().unwrap(), + "[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]".parse().unwrap(), + ]; + + let expected = 3993; + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day18/src/main.rs b/2021/day18/src/main.rs new file mode 100644 index 0000000..1bab438 --- /dev/null +++ b/2021/day18/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_line_input; +use day18_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day18", read_parsed_line_input, part1, part2) +} diff --git a/2021/day19/Cargo.toml b/2021/day19/Cargo.toml new file mode 100644 index 0000000..89414b2 --- /dev/null +++ b/2021/day19/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day19_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day19_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +itertools = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day19/src/lib.rs b/2021/day19/src/lib.rs new file mode 100644 index 0000000..5d80ad2 --- /dev/null +++ b/2021/day19/src/lib.rs @@ -0,0 +1,645 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::{anyhow, bail}; +use aoc_common::parsing::GroupsParser; +use aoc_solution::Aoc; +use itertools::Itertools; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::ops::{Add, Sub}; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = GroupsParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day19; + +const OVERLAP_THRESHOLD: usize = 12; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct Position { + x: isize, + y: isize, + z: isize, +} + +impl From<(isize, isize, isize)> for Position { + fn from((x, y, z): (isize, isize, isize)) -> Self { + Position { x, y, z } + } +} + +impl Add for Position { + type Output = Position; + + fn add(self, rhs: Position) -> Self::Output { + Position { + x: self.x + rhs.x, + y: self.y + rhs.y, + z: self.z + rhs.z, + } + } +} + +impl Sub for Position { + type Output = Position; + + fn sub(self, rhs: Position) -> Self::Output { + Position { + x: self.x - rhs.x, + y: self.y - rhs.y, + z: self.z - rhs.z, + } + } +} + +impl FromStr for Position { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut split = s.split(','); + let x = split + .next() + .ok_or_else(|| anyhow!("no x value present"))? + .parse()?; + let y = split + .next() + .ok_or_else(|| anyhow!("no y value present"))? + .parse()?; + let z = split + .next() + .ok_or_else(|| anyhow!("no z value present"))? + .parse()?; + Ok(Position { x, y, z }) + } +} + +impl Position { + #[inline] + const fn origin() -> Self { + Position { x: 0, y: 0, z: 0 } + } + + #[inline] + const fn rot_90x(&self) -> Self { + Position { + x: self.x, + y: -self.z, + z: self.y, + } + } + + #[inline] + const fn rot_180x(&self) -> Self { + Position { + x: self.x, + y: -self.y, + z: -self.z, + } + } + + #[inline] + const fn rot_270x(&self) -> Self { + Position { + x: self.x, + y: self.z, + z: -self.y, + } + } + + #[inline] + const fn rot_90y(&self) -> Self { + Position { + x: self.z, + y: self.y, + z: -self.x, + } + } + + #[inline] + const fn rot_180y(&self) -> Self { + Position { + x: -self.x, + y: self.y, + z: -self.z, + } + } + + #[inline] + const fn rot_270y(&self) -> Self { + Position { + x: -self.z, + y: self.y, + z: self.x, + } + } + + #[inline] + const fn rot_90z(&self) -> Self { + Position { + x: -self.y, + y: self.x, + z: self.z, + } + } + + #[inline] + #[allow(unused)] + const fn rot_180z(&self) -> Self { + Position { + x: -self.x, + y: -self.y, + z: self.z, + } + } + + #[inline] + const fn rot_270z(&self) -> Self { + Position { + x: self.y, + y: -self.x, + z: self.z, + } + } + + #[inline] + const fn all_rotations(&self) -> [Self; 24] { + [ + // x0: + *self, + self.rot_90y(), + self.rot_180y(), + self.rot_270y(), + self.rot_90z(), + self.rot_270z(), + // x90: + self.rot_90x(), + self.rot_90x().rot_90y(), + self.rot_90x().rot_180y(), + self.rot_90x().rot_270y(), + self.rot_90x().rot_90z(), + self.rot_90x().rot_270z(), + // x180: + self.rot_180x(), + self.rot_180x().rot_90y(), + self.rot_180x().rot_180y(), + self.rot_180x().rot_270y(), + self.rot_180x().rot_90z(), + self.rot_180x().rot_270z(), + // x270: + self.rot_270x(), + self.rot_270x().rot_90y(), + self.rot_270x().rot_180y(), + self.rot_270x().rot_270y(), + self.rot_270x().rot_90z(), + self.rot_270x().rot_270z(), + ] + } + + #[inline] + const fn manhattan_distance(&self, other: &Self) -> usize { + self.x.abs_diff(other.x) + self.y.abs_diff(other.y) + self.z.abs_diff(other.z) + } +} + +#[derive(Debug, Clone)] +pub struct Scanner { + id: usize, + relative_position: Position, + beacons: BTreeSet, +} + +impl FromStr for Scanner { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + bail!("empty input") + } + let mut lines = s.lines(); + let id_line = lines.next().ok_or_else(|| anyhow!("no id value present"))?; + let prefix_stripped = id_line + .strip_prefix("--- scanner ") + .ok_or_else(|| anyhow!("invalid scanner id"))?; + let id = prefix_stripped + .strip_suffix(" ---") + .ok_or_else(|| anyhow!("invalid scanner id"))? + .parse()?; + + let beacons = lines + .map(FromStr::from_str) + .collect::, _>>()?; + + Ok(Scanner { + id, + relative_position: Position::origin(), + beacons, + }) + } +} + +impl Scanner { + fn all_rotations(&self) -> [Scanner; 24] { + let beacon_rotations = self + .beacons + .iter() + .map(|b| b.all_rotations()) + .collect::>(); + + (0..24) + .map(|i| Scanner { + id: self.id, + relative_position: self.relative_position, + beacons: beacon_rotations.iter().map(|b| b[i]).collect(), + }) + .collect::>() + .try_into() + .unwrap() + } + + fn translate(&self, change: Position) -> Self { + Scanner { + id: self.id, + relative_position: self.relative_position + change, + beacons: self.beacons.iter().map(|&b| b + change).collect(), + } + } + + fn overlap_count(&self, other: &Self) -> usize { + let mut count = 0; + for other_beacon in &other.beacons { + if self.beacons.contains(other_beacon) { + count += 1; + } + } + + count + } + + // we treat 'self' as the source of truth + fn try_align_scanner(&self, other: &Self) -> Option { + for &base in &self.beacons { + for rotation in other.all_rotations() { + for &beacon in &rotation.beacons { + let translation_candidate = base - beacon; + + let translated_scanner = rotation.translate(translation_candidate); + if self.overlap_count(&translated_scanner) >= OVERLAP_THRESHOLD { + // we found it! + return Some(translated_scanner); + } + } + } + } + + None + } +} + +fn try_align_relative_to<'a, I: Iterator>( + base: &Scanner, + unaligned: I, +) -> Vec { + let mut aligned_scanners = Vec::new(); + for scanner in unaligned { + if let Some(aligned) = base.try_align_scanner(scanner) { + aligned_scanners.push(aligned) + } + } + + aligned_scanners +} + +fn reconstruct_absolute_positions(scanners: &[Scanner]) -> Vec { + let mut unaligned = scanners + .iter() + .skip(1) + .map(|s| (s.id, s.clone())) + .collect::>(); + + // we treat scanner 0 as the origin and attempt to align everything relative to it + let mut aligned = vec![]; + + // check leftover scanners only against any newly aligned entries + let mut aligned_last_iter = vec![scanners[0].clone()]; + + while !unaligned.is_empty() { + let mut aligned_this_iter = Vec::new(); + + for known in &aligned_last_iter { + let new_aligned = try_align_relative_to(known, unaligned.values()); + for new_known in new_aligned { + unaligned.remove(&new_known.id); + aligned_this_iter.push(new_known); + } + } + + aligned.append(&mut aligned_last_iter); + aligned_last_iter = aligned_this_iter; + } + aligned.append(&mut aligned_last_iter); + + aligned +} + +pub fn part1(input: Vec) -> usize { + let mut unique_beacons = HashSet::new(); + let aligned_scanners = reconstruct_absolute_positions(&input); + for scanner in aligned_scanners { + for beacon in scanner.beacons { + unique_beacons.insert(beacon); + } + } + + unique_beacons.len() +} + +pub fn part2(input: Vec) -> usize { + reconstruct_absolute_positions(&input) + .into_iter() + .map(|s| s.relative_position) + .tuple_combinations::<(_, _)>() + .map(|(a, b)| a.manhattan_distance(&b)) + .max() + .expect("failed to align the scanners!") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fake_positions() -> Vec { + vec![ + Position { + x: 230, + y: 43, + z: 780, + }, + Position { + x: -230, + y: 43, + z: 780, + }, + Position { + x: 230, + y: -43, + z: 780, + }, + Position { + x: 230, + y: 43, + z: -780, + }, + Position { + x: -230, + y: -43, + z: -780, + }, + Position { + x: 0, + y: -43, + z: 780, + }, + Position { + x: -230, + y: 0, + z: -780, + }, + Position { + x: -230, + y: 43, + z: 0, + }, + ] + } + + #[test] + fn x_rotations() { + for pos in fake_positions() { + assert_eq!(pos.rot_90x().rot_90x(), pos.rot_180x()); + assert_eq!(pos.rot_90x().rot_90x().rot_90x(), pos.rot_270x()); + assert_eq!(pos.rot_180x().rot_90x(), pos.rot_270x()); + } + } + + #[test] + fn y_rotations() { + for pos in fake_positions() { + assert_eq!(pos.rot_90y().rot_90y(), pos.rot_180y()); + assert_eq!(pos.rot_90y().rot_90y().rot_90y(), pos.rot_270y()); + assert_eq!(pos.rot_180y().rot_90y(), pos.rot_270y()); + } + } + + #[test] + fn z_rotations() { + for pos in fake_positions() { + assert_eq!(pos.rot_90z().rot_90z(), pos.rot_180z()); + assert_eq!(pos.rot_90z().rot_90z().rot_90z(), pos.rot_270z()); + assert_eq!(pos.rot_180z().rot_90z(), pos.rot_270z()); + } + } + + fn example_scanners() -> Vec { + let scanner0 = Scanner { + id: 0, + relative_position: Position::origin(), + beacons: vec![ + (404, -588, -901).into(), + (528, -643, 409).into(), + (-838, 591, 734).into(), + (390, -675, -793).into(), + (-537, -823, -458).into(), + (-485, -357, 347).into(), + (-345, -311, 381).into(), + (-661, -816, -575).into(), + (-876, 649, 763).into(), + (-618, -824, -621).into(), + (553, 345, -567).into(), + (474, 580, 667).into(), + (-447, -329, 318).into(), + (-584, 868, -557).into(), + (544, -627, -890).into(), + (564, 392, -477).into(), + (455, 729, 728).into(), + (-892, 524, 684).into(), + (-689, 845, -530).into(), + (423, -701, 434).into(), + (7, -33, -71).into(), + (630, 319, -379).into(), + (443, 580, 662).into(), + (-789, 900, -551).into(), + (459, -707, 401).into(), + ] + .into_iter() + .collect(), + }; + + let scanner1 = Scanner { + id: 1, + relative_position: Position::origin(), + beacons: vec![ + (686, 422, 578).into(), + (605, 423, 415).into(), + (515, 917, -361).into(), + (-336, 658, 858).into(), + (95, 138, 22).into(), + (-476, 619, 847).into(), + (-340, -569, -846).into(), + (567, -361, 727).into(), + (-460, 603, -452).into(), + (669, -402, 600).into(), + (729, 430, 532).into(), + (-500, -761, 534).into(), + (-322, 571, 750).into(), + (-466, -666, -811).into(), + (-429, -592, 574).into(), + (-355, 545, -477).into(), + (703, -491, -529).into(), + (-328, -685, 520).into(), + (413, 935, -424).into(), + (-391, 539, -444).into(), + (586, -435, 557).into(), + (-364, -763, -893).into(), + (807, -499, -711).into(), + (755, -354, -619).into(), + (553, 889, -390).into(), + ] + .into_iter() + .collect(), + }; + + let scanner2 = Scanner { + id: 2, + relative_position: Position::origin(), + beacons: vec![ + (649, 640, 665).into(), + (682, -795, 504).into(), + (-784, 533, -524).into(), + (-644, 584, -595).into(), + (-588, -843, 648).into(), + (-30, 6, 44).into(), + (-674, 560, 763).into(), + (500, 723, -460).into(), + (609, 671, -379).into(), + (-555, -800, 653).into(), + (-675, -892, -343).into(), + (697, -426, -610).into(), + (578, 704, 681).into(), + (493, 664, -388).into(), + (-671, -858, 530).into(), + (-667, 343, 800).into(), + (571, -461, -707).into(), + (-138, -166, 112).into(), + (-889, 563, -600).into(), + (646, -828, 498).into(), + (640, 759, 510).into(), + (-630, 509, 768).into(), + (-681, -892, -333).into(), + (673, -379, -804).into(), + (-742, -814, -386).into(), + (577, -820, 562).into(), + ] + .into_iter() + .collect(), + }; + + let scanner3 = Scanner { + id: 3, + relative_position: Position::origin(), + beacons: vec![ + (-589, 542, 597).into(), + (605, -692, 669).into(), + (-500, 565, -823).into(), + (-660, 373, 557).into(), + (-458, -679, -417).into(), + (-488, 449, 543).into(), + (-626, 468, -788).into(), + (338, -750, -386).into(), + (528, -832, -391).into(), + (562, -778, 733).into(), + (-938, -730, 414).into(), + (543, 643, -506).into(), + (-524, 371, -870).into(), + (407, 773, 750).into(), + (-104, 29, 83).into(), + (378, -903, -323).into(), + (-778, -728, 485).into(), + (426, 699, 580).into(), + (-438, -605, -362).into(), + (-469, -447, -387).into(), + (509, 732, 623).into(), + (647, 635, -688).into(), + (-868, -804, 481).into(), + (614, -800, 639).into(), + (595, 780, -596).into(), + ] + .into_iter() + .collect(), + }; + + let scanner4 = Scanner { + id: 4, + relative_position: Position::origin(), + beacons: vec![ + (727, 592, 562).into(), + (-293, -554, 779).into(), + (441, 611, -461).into(), + (-714, 465, -776).into(), + (-743, 427, -804).into(), + (-660, -479, -426).into(), + (832, -632, 460).into(), + (927, -485, -438).into(), + (408, 393, -506).into(), + (466, 436, -512).into(), + (110, 16, 151).into(), + (-258, -428, 682).into(), + (-393, 719, 612).into(), + (-211, -452, 876).into(), + (808, -476, -593).into(), + (-575, 615, 604).into(), + (-485, 667, 467).into(), + (-680, 325, -822).into(), + (-627, -443, -432).into(), + (872, -547, -609).into(), + (833, 512, 582).into(), + (807, 604, 487).into(), + (839, -516, 451).into(), + (891, -625, 532).into(), + (-652, -548, -490).into(), + (30, -46, -14).into(), + ] + .into_iter() + .collect(), + }; + + vec![scanner0, scanner1, scanner2, scanner3, scanner4] + } + + #[test] + fn part1_sample_input() { + assert_eq!(79, part1(example_scanners())) + } + + #[test] + fn part2_sample_input() { + assert_eq!(3621, part2(example_scanners())) + } +} diff --git a/2021/day19/src/main.rs b/2021/day19/src/main.rs new file mode 100644 index 0000000..ab71f40 --- /dev/null +++ b/2021/day19/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_groups; +use day19_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day19", read_parsed_groups, part1, part2) +} diff --git a/2021/day20/Cargo.toml b/2021/day20/Cargo.toml new file mode 100644 index 0000000..5d62dfa --- /dev/null +++ b/2021/day20/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day20_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day20_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day20/src/lib.rs b/2021/day20/src/lib.rs new file mode 100644 index 0000000..9d2bf0f --- /dev/null +++ b/2021/day20/src/lib.rs @@ -0,0 +1,241 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use std::collections::HashSet; +use std::convert::TryInto; +use std::ops::RangeInclusive; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = TrenchMap)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day20; + +#[derive(Debug, Clone)] +pub struct TrenchMap { + enhancement_algorithm: [bool; 512], + image: HashSet<(isize, isize)>, + infinity: bool, + image_boundary: (RangeInclusive, RangeInclusive), +} + +impl FromStr for TrenchMap { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut lines = s.lines(); + let algo = lines + .next() + .unwrap() + .chars() + .map(|c| c == '#') + .collect::>() + .try_into() + .unwrap(); + + lines.next(); // empty line + + let mut image = HashSet::new(); + for (y, line) in lines.enumerate() { + for (x, pixel) in line.chars().enumerate() { + if pixel == '#' { + image.insert((x as isize, y as isize)); + } + } + } + + let mut map = TrenchMap { + enhancement_algorithm: algo, + image, + infinity: false, + image_boundary: (RangeInclusive::new(0, 0), RangeInclusive::new(0, 0)), + }; + map.update_image_boundary(); + + Ok(map) + } +} + +impl TrenchMap { + fn update_image_boundary(&mut self) { + let mut max_x = 0; + let mut min_x = 0; + let mut max_y = 0; + let mut min_y = 0; + for (x, y) in &self.image { + if *x > max_x { + max_x = *x; + } + if *x < min_x { + min_x = *x + } + if *y > max_y { + max_y = *y; + } + if *y < min_y { + min_y = *y + } + } + + self.image_boundary = ( + RangeInclusive::new(min_x, max_x), + RangeInclusive::new(min_y, max_y), + ); + } + + fn lookup_pixel(&self, pos: (isize, isize)) -> bool { + let (x, y) = pos; + + if !self.image_boundary.0.contains(&x) || !self.image_boundary.1.contains(&y) { + self.infinity + } else { + self.image.contains(&pos) + } + } + + fn enhance_pixel(&self, pos: (isize, isize)) -> bool { + let mut lookup = 0; + + // TL + if self.lookup_pixel((pos.0 - 1, pos.1 - 1)) { + lookup += 1 << 8; + } + + // T + if self.lookup_pixel((pos.0, pos.1 - 1)) { + lookup += 1 << 7; + } + + // TR + if self.lookup_pixel((pos.0 + 1, pos.1 - 1)) { + lookup += 1 << 6; + } + + // L + if self.lookup_pixel((pos.0 - 1, pos.1)) { + lookup += 1 << 5; + } + + // M + if self.lookup_pixel((pos.0, pos.1)) { + lookup += 1 << 4; + } + + // MR + if self.lookup_pixel((pos.0 + 1, pos.1)) { + lookup += 1 << 3; + } + + // BL + if self.lookup_pixel((pos.0 - 1, pos.1 + 1)) { + lookup += 1 << 2; + } + + // B + if self.lookup_pixel((pos.0, pos.1 + 1)) { + lookup += 1 << 1; + } + + // BR + if self.lookup_pixel((pos.0 + 1, pos.1 + 1)) { + lookup += 1 << 0; + } + + self.enhancement_algorithm[lookup] + } + + fn enhance(&mut self) { + let mut new_image = HashSet::new(); + let (x_range, y_range) = &self.image_boundary; + let min_x = x_range.start(); + let max_x = x_range.end(); + let min_y = y_range.start(); + let max_y = y_range.end(); + + for x in min_x - 3..max_x + 3 { + for y in min_y - 3..max_y + 3 { + if self.enhance_pixel((x, y)) { + new_image.insert((x, y)); + } + } + } + + if self.infinity { + self.infinity = self.enhancement_algorithm[511]; + } else { + self.infinity = self.enhancement_algorithm[0] + } + + self.image = new_image; + self.update_image_boundary(); + } +} + +pub fn part1(mut map: TrenchMap) -> usize { + map.enhance(); + map.enhance(); + map.image.len() +} + +pub fn part2(mut map: TrenchMap) -> usize { + for _ in 0..50 { + map.enhance(); + } + map.image.len() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_sample_input() { + let map = "..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..###" + .parse() + .unwrap(); + + let expected = 35; + assert_eq!(expected, part1(map)); + } + + #[test] + fn part2_sample_input() { + let map = "..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..###" + .parse() + .unwrap(); + + let expected = 3351; + assert_eq!(expected, part2(map)); + } +} diff --git a/2021/day20/src/main.rs b/2021/day20/src/main.rs new file mode 100644 index 0000000..1c7b57f --- /dev/null +++ b/2021/day20/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day20_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day20", read_parsed, part1, part2) +} diff --git a/2021/day21/Cargo.toml b/2021/day21/Cargo.toml new file mode 100644 index 0000000..c2014fe --- /dev/null +++ b/2021/day21/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day21_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day21_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day21/src/lib.rs b/2021/day21/src/lib.rs new file mode 100644 index 0000000..9bfcec5 --- /dev/null +++ b/2021/day21/src/lib.rs @@ -0,0 +1,379 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use anyhow::Context; +use aoc_common::parsing::FromStrParser; +use aoc_solution::Aoc; +use std::cmp::max; +use std::collections::HashMap; +use std::mem; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = DiracDice)] +#[aoc(parser = FromStrParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day21; + +#[derive(Debug, Copy, Clone)] +pub enum Player { + One, + Two, +} + +#[derive(Debug, Clone, Copy)] +pub struct DiracDice { + total_rolled: usize, + last_roll: usize, + player1_position: Position, + player2_position: Position, + + player1_score: usize, + player2_score: usize, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Position(usize); + +impl Position { + fn move_pawn(&mut self, val: usize) { + self.0 += val; + self.0 = (self.0 - 1) % 10 + 1 + } +} + +impl FromStr for DiracDice { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut lines = s.lines(); + let p1 = lines + .next() + .context("invalid game")? + .strip_prefix("Player 1 starting position: ") + .context("invalid game")? + .parse()?; + let p2 = lines + .next() + .context("invalid game")? + .strip_prefix("Player 2 starting position: ") + .context("invalid game")? + .parse()?; + + Ok(DiracDice { + total_rolled: 0, + last_roll: 0, + player1_position: Position(p1), + player2_position: Position(p2), + player1_score: 0, + player2_score: 0, + }) + } +} + +impl DiracDice { + fn roll_deterministic_die_once(&mut self) -> usize { + if self.last_roll == 100 { + self.last_roll = 1; + } else { + self.last_roll += 1; + } + self.total_rolled += 1; + self.last_roll + } + + fn roll_deterministic_die_three_times(&mut self) -> usize { + if self.last_roll <= 97 { + let res = 3 * self.last_roll + 6; + self.total_rolled += 3; + self.last_roll += 3; + res + } else { + self.roll_deterministic_die_once() + + self.roll_deterministic_die_once() + + self.roll_deterministic_die_once() + } + } + + fn play_round(&mut self, player: u8) -> bool { + let throw = self.roll_deterministic_die_three_times(); + if player == 1 { + self.player1_position.move_pawn(throw); + self.player1_score += self.player1_position.0; + if self.player1_score >= 1000 { + return true; + } + } else if player == 2 { + self.player2_position.move_pawn(throw); + self.player2_score += self.player2_position.0; + if self.player2_score >= 1000 { + return true; + } + } else { + unreachable!("invalid player") + } + false + } + + fn into_quantum(self) -> QuantumDiracDice { + let mut game = QuantumDiracDice { + simulated_universes: Default::default(), + p1_wins: 0, + p2_wins: 0, + }; + game.simulated_universes.insert( + UniverseState { + player1_position: self.player1_position, + player1_score: self.player1_score, + player2_position: self.player2_position, + player2_score: self.player2_score, + }, + 1, + ); + + game + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct UniverseState { + player1_position: Position, + player1_score: usize, + player2_position: Position, + player2_score: usize, +} + +impl UniverseState { + fn add_throw(&mut self, throw: usize, player: Player) -> bool { + match player { + Player::One => { + self.player1_position.move_pawn(throw); + self.player1_score += self.player1_position.0; + if self.player1_score >= 21 { + return true; + } + } + Player::Two => { + self.player2_position.move_pawn(throw); + self.player2_score += self.player2_position.0; + if self.player2_score >= 21 { + return true; + } + } + } + false + } +} + +struct QuantumDiracDice { + simulated_universes: HashMap, + + p1_wins: usize, + p2_wins: usize, +} + +impl QuantumDiracDice { + // possible outcomes of dice roll: + // 1-1-1 = 3 + // 1-1-2 = 4 + // 1-1-3 = 5 + // 1-2-1 = 4 + // 1-2-2 = 5 + // 1-2-3 = 6 + // 1-3-1 = 5 + // 1-3-2 = 6 + // 1-3-3 = 7 + // 2-1-1 = 4 + // 2-1-2 = 5 + // 2-1-3 = 6 + // 2-2-1 = 5 + // 2-2-2 = 6 + // 2-2-3 = 7 + // 2-3-1 = 6 + // 2-3-2 = 7 + // 2-3-3 = 8 + // 3-1-1 = 5 + // 3-1-2 = 6 + // 3-1-3 = 7 + // 3-2-1 = 6 + // 3-2-2 = 7 + // 3-2-3 = 8 + // 3-3-1 = 7 + // 3-3-2 = 8 + // 3-3-3 = 9 + + // so each 3 rolls produces: + // 1 universe with sum 3 + // 3 universes with sum 4 + // 6 universes with sum 5 + // 7 universes with sum 6 + // 6 universes with sum 7 + // 3 universes with sum 8 + // 1 universe with sum 9 + + fn add_wins(&mut self, count: usize, player: Player) { + match player { + Player::One => self.p1_wins += count, + Player::Two => self.p2_wins += count, + } + } + + fn play_round(&mut self, player: Player) -> bool { + for (universe_state, count) in mem::take(&mut self.simulated_universes) { + let mut sum3 = universe_state; + if sum3.add_throw(3, player) { + self.add_wins(count, player); + } else { + *self.simulated_universes.entry(sum3).or_default() += count + } + + let mut sum4 = universe_state; + if sum4.add_throw(4, player) { + self.add_wins(3 * count, player); + } else { + *self.simulated_universes.entry(sum4).or_default() += 3 * count + } + + let mut sum5 = universe_state; + if sum5.add_throw(5, player) { + self.add_wins(6 * count, player); + } else { + *self.simulated_universes.entry(sum5).or_default() += 6 * count + } + + let mut sum6 = universe_state; + if sum6.add_throw(6, player) { + self.add_wins(7 * count, player); + } else { + *self.simulated_universes.entry(sum6).or_default() += 7 * count + } + + let mut sum7 = universe_state; + if sum7.add_throw(7, player) { + self.add_wins(6 * count, player); + } else { + *self.simulated_universes.entry(sum7).or_default() += 6 * count + } + + let mut sum8 = universe_state; + if sum8.add_throw(8, player) { + self.add_wins(3 * count, player); + } else { + *self.simulated_universes.entry(sum8).or_default() += 3 * count + } + + let mut sum9 = universe_state; + if sum9.add_throw(9, player) { + self.add_wins(count, player); + } else { + *self.simulated_universes.entry(sum9).or_default() += count + } + } + + self.simulated_universes.is_empty() + } +} + +pub fn part1(mut game: DiracDice) -> usize { + loop { + if game.play_round(1) { + return game.total_rolled * game.player2_score; + } + if game.play_round(2) { + return game.total_rolled * game.player1_score; + } + } +} + +pub fn part2(game: DiracDice) -> usize { + let mut quantum_game = game.into_quantum(); + loop { + if quantum_game.play_round(Player::One) { + return max(quantum_game.p1_wins, quantum_game.p2_wins); + } + if quantum_game.play_round(Player::Two) { + return max(quantum_game.p1_wins, quantum_game.p2_wins); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn moving_pawn() { + let mut pos1 = Position(4); + let mut pos2 = Position(8); + + pos1.move_pawn(1 + 2 + 3); + assert_eq!(Position(10), pos1); + + pos2.move_pawn(4 + 5 + 6); + assert_eq!(Position(3), pos2); + + pos1.move_pawn(7 + 8 + 9); + assert_eq!(Position(4), pos1); + + pos2.move_pawn(10 + 11 + 12); + assert_eq!(Position(6), pos2); + + pos1.move_pawn(13 + 14 + 15); + assert_eq!(Position(6), pos1); + + pos2.move_pawn(16 + 17 + 18); + assert_eq!(Position(7), pos2); + + pos1.move_pawn(19 + 20 + 21); + assert_eq!(Position(6), pos1); + + pos2.move_pawn(22 + 23 + 24); + assert_eq!(Position(6), pos2); + } + + #[test] + fn part1_sample_input() { + let game = DiracDice { + total_rolled: 0, + last_roll: 0, + player1_position: Position(4), + player2_position: Position(8), + player1_score: 0, + player2_score: 0, + }; + + let expected = 739785; + assert_eq!(expected, part1(game)) + } + + #[test] + fn part2_sample_input() { + let game = DiracDice { + total_rolled: 0, + last_roll: 0, + player1_position: Position(4), + player2_position: Position(8), + player1_score: 0, + player2_score: 0, + }; + + let expected = 444356092776315; + assert_eq!(expected, part2(game)) + } +} diff --git a/2021/day21/src/main.rs b/2021/day21/src/main.rs new file mode 100644 index 0000000..8a0f1e2 --- /dev/null +++ b/2021/day21/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execution::execute_struct; +use aoc_common::legacy::input_read::read_parsed; +use day21_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_struct("inputs/2021/day21", read_parsed, part1, part2) +} diff --git a/2021/day22/Cargo.toml b/2021/day22/Cargo.toml new file mode 100644 index 0000000..805e283 --- /dev/null +++ b/2021/day22/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day22_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day22_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } +itertools = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day22/src/intersection.rs b/2021/day22/src/intersection.rs new file mode 100644 index 0000000..5524dfc --- /dev/null +++ b/2021/day22/src/intersection.rs @@ -0,0 +1,62 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::Cuboid; +use std::cmp::{max, min}; +use std::ops::RangeInclusive; + +pub trait Intersection: Sized { + fn intersects(&self, other: &Self) -> bool; + + fn intersection(&self, other: &Self) -> Option; +} + +impl Intersection for RangeInclusive +where + T: PartialOrd + Ord + Clone, +{ + fn intersects(&self, other: &Self) -> bool { + !(self.start() > other.end() || other.start() > self.end()) + } + + fn intersection(&self, other: &Self) -> Option { + if !self.intersects(other) { + None + } else { + let start = max(self.start(), other.start()); + let end = min(self.end(), other.end()); + Some(RangeInclusive::new(start.clone(), end.clone())) + } + } +} + +impl Intersection for Cuboid { + fn intersects(&self, other: &Self) -> bool { + self.x_range.intersects(&other.x_range) + && self.y_range.intersects(&other.y_range) + && self.z_range.intersects(&other.z_range) + } + + fn intersection(&self, other: &Self) -> Option { + let x_intersection = self.x_range.intersection(&other.x_range)?; + let y_intersection = self.y_range.intersection(&other.y_range)?; + let z_intersection = self.z_range.intersection(&other.z_range)?; + + Some(Cuboid { + x_range: x_intersection, + y_range: y_intersection, + z_range: z_intersection, + }) + } +} diff --git a/2021/day22/src/lib.rs b/2021/day22/src/lib.rs new file mode 100644 index 0000000..13fa2e5 --- /dev/null +++ b/2021/day22/src/lib.rs @@ -0,0 +1,486 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::intersection::Intersection; +use anyhow::Error; +use aoc_common::legacy::parse_raw_range; +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use itertools::iproduct; +use std::fmt::{Display, Formatter}; +use std::ops::RangeInclusive; +use std::str::FromStr; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day22; + +mod intersection; + +#[derive(Debug, Clone)] +pub struct Step { + on: bool, + cuboid: Cuboid, +} + +impl FromStr for Step { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + let on = s.starts_with("on"); + let mut ranges = if on { + s.strip_prefix("on ") + .ok_or_else(|| Error::msg("incomplete input"))? + .split(',') + } else { + s.strip_prefix("off ") + .ok_or_else(|| Error::msg("incomplete input"))? + .split(',') + }; + + let x_range = parse_raw_range( + ranges + .next() + .ok_or_else(|| Error::msg("incomplete input"))?, + )?; + let y_range = parse_raw_range( + ranges + .next() + .ok_or_else(|| Error::msg("incomplete input"))?, + )?; + let z_range = parse_raw_range( + ranges + .next() + .ok_or_else(|| Error::msg("incomplete input"))?, + )?; + + Ok(Step { + on, + cuboid: Cuboid { + x_range, + y_range, + z_range, + }, + }) + } +} + +#[derive(Debug, Clone)] +pub struct Cuboid { + x_range: RangeInclusive, + y_range: RangeInclusive, + z_range: RangeInclusive, +} + +impl Display for Cuboid { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let cubes = self.clone().into_cubes(); + for cube in cubes { + writeln!(f, "{cube}")?; + } + + Ok(()) + } +} + +impl From for Vec { + fn from(cuboid: Cuboid) -> Self { + iproduct!(cuboid.x_range, cuboid.y_range, cuboid.z_range) + .map(Into::into) + .collect() + } +} + +impl Cuboid { + fn into_cubes(self) -> Vec { + self.into() + } + + fn size(&self) -> usize { + let x_size = (self.x_range.end() - self.x_range.start()).unsigned_abs() + 1; + let y_size = (self.y_range.end() - self.y_range.start()).unsigned_abs() + 1; + let z_size = (self.z_range.end() - self.z_range.start()).unsigned_abs() + 1; + + x_size * y_size * z_size + } +} + +#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)] +pub struct Cube { + x: isize, + y: isize, + z: isize, +} + +impl Display for Cube { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{},{},{}", self.x, self.y, self.z) + } +} + +impl From<(isize, isize, isize)> for Cube { + fn from((x, y, z): (isize, isize, isize)) -> Self { + Cube { x, y, z } + } +} + +struct ReactorCore { + additive_cuboids: Vec, + subtractive_cuboids: Vec, + initialization_area: Cuboid, +} + +impl ReactorCore { + fn new() -> Self { + ReactorCore { + additive_cuboids: vec![], + subtractive_cuboids: vec![], + initialization_area: Cuboid { + x_range: RangeInclusive::new(-50, 50), + y_range: RangeInclusive::new(-50, 50), + z_range: RangeInclusive::new(-50, 50), + }, + } + } + + fn active_region_size(&self) -> usize { + let positive_volume = self + .additive_cuboids + .iter() + .map(|c| c.size()) + .sum::(); + + let negative_volume = self + .subtractive_cuboids + .iter() + .map(|c| c.size()) + .sum::(); + + debug_assert!(positive_volume >= negative_volume); + positive_volume - negative_volume + } + + fn run_initialization_step(&mut self, cuboid: Cuboid, on: bool) { + // since our input consists only of a double digit of cuboids, this naive approach is more than sufficient + let mut new_subs = Vec::new(); + for add in &self.additive_cuboids { + if let Some(intersection) = cuboid.intersection(add) { + new_subs.push(intersection) + } + } + + for sub in &self.subtractive_cuboids { + if let Some(intersection) = cuboid.intersection(sub) { + self.additive_cuboids.push(intersection) + } + } + + self.subtractive_cuboids.append(&mut new_subs); + + if on { + self.additive_cuboids.push(cuboid) + } + } + + fn run_part1_initialization_step(&mut self, step: &Step) { + // filter out cuboids completely outside the area + if let Some(restricted) = self.initialization_area.intersection(&step.cuboid) { + self.run_initialization_step(restricted, step.on) + } + } + + // same as part 1 but without the area restriction + fn run_part2_initialization_step(&mut self, step: &Step) { + self.run_initialization_step(step.cuboid.clone(), step.on) + } +} + +pub fn part1(input: Vec) -> usize { + let mut reactor_core = ReactorCore::new(); + for step in input { + reactor_core.run_part1_initialization_step(&step); + } + + reactor_core.active_region_size() +} + +pub fn part2(input: Vec) -> usize { + let mut reactor_core = ReactorCore::new(); + for step in input { + reactor_core.run_part2_initialization_step(&step); + } + + reactor_core.active_region_size() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cuboid_size() { + assert_eq!( + Cuboid { + x_range: 1..=1, + y_range: 1..=1, + z_range: 1..=1 + } + .size(), + 1 + ); + + assert_eq!( + Cuboid { + x_range: 1..=10, + y_range: 1..=10, + z_range: 1..=10 + } + .size(), + 1000 + ); + + assert_eq!( + Cuboid { + x_range: -10..=-1, + y_range: -10..=-1, + z_range: -10..=-1 + } + .size(), + 1000 + ); + } + + #[test] + fn part1_small_example() { + let input = vec![ + "on x=10..12,y=10..12,z=10..12".parse().unwrap(), + "on x=11..13,y=11..13,z=11..13".parse().unwrap(), + "off x=9..11,y=9..11,z=9..11".parse().unwrap(), + "on x=10..10,y=10..10,z=10..10".parse().unwrap(), + ]; + + let expected = 39; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part1_sample_input() { + let input = vec![ + "on x=-20..26,y=-36..17,z=-47..7".parse().unwrap(), + "on x=-20..33,y=-21..23,z=-26..28".parse().unwrap(), + "on x=-22..28,y=-29..23,z=-38..16".parse().unwrap(), + "on x=-46..7,y=-6..46,z=-50..-1".parse().unwrap(), + "on x=-49..1,y=-3..46,z=-24..28".parse().unwrap(), + "on x=2..47,y=-22..22,z=-23..27".parse().unwrap(), + "on x=-27..23,y=-28..26,z=-21..29".parse().unwrap(), + "on x=-39..5,y=-6..47,z=-3..44".parse().unwrap(), + "on x=-30..21,y=-8..43,z=-13..34".parse().unwrap(), + "on x=-22..26,y=-27..20,z=-29..19".parse().unwrap(), + "off x=-48..-32,y=26..41,z=-47..-37".parse().unwrap(), + "on x=-12..35,y=6..50,z=-50..-2".parse().unwrap(), + "off x=-48..-32,y=-32..-16,z=-15..-5".parse().unwrap(), + "on x=-18..26,y=-33..15,z=-7..46".parse().unwrap(), + "off x=-40..-22,y=-38..-28,z=23..41".parse().unwrap(), + "on x=-16..35,y=-41..10,z=-47..6".parse().unwrap(), + "off x=-32..-23,y=11..30,z=-14..3".parse().unwrap(), + "on x=-49..-5,y=-3..45,z=-29..18".parse().unwrap(), + "off x=18..30,y=-20..-8,z=-3..13".parse().unwrap(), + "on x=-41..9,y=-7..43,z=-33..15".parse().unwrap(), + "on x=-54112..-39298,y=-85059..-49293,z=-27449..7877" + .parse() + .unwrap(), + "on x=967..23432,y=45373..81175,z=27513..53682" + .parse() + .unwrap(), + ]; + + let expected = 590784; + assert_eq!(expected, part1(input)) + } + + #[test] + fn part2_sample_input() { + let input = vec![ + "on x=-5..47,y=-31..22,z=-19..33".parse().unwrap(), + "on x=-44..5,y=-27..21,z=-14..35".parse().unwrap(), + "on x=-49..-1,y=-11..42,z=-10..38".parse().unwrap(), + "on x=-20..34,y=-40..6,z=-44..1".parse().unwrap(), + "off x=26..39,y=40..50,z=-2..11".parse().unwrap(), + "on x=-41..5,y=-41..6,z=-36..8".parse().unwrap(), + "off x=-43..-33,y=-45..-28,z=7..25".parse().unwrap(), + "on x=-33..15,y=-32..19,z=-34..11".parse().unwrap(), + "off x=35..47,y=-46..-34,z=-11..5".parse().unwrap(), + "on x=-14..36,y=-6..44,z=-16..29".parse().unwrap(), + "on x=-57795..-6158,y=29564..72030,z=20435..90618" + .parse() + .unwrap(), + "on x=36731..105352,y=-21140..28532,z=16094..90401" + .parse() + .unwrap(), + "on x=30999..107136,y=-53464..15513,z=8553..71215" + .parse() + .unwrap(), + "on x=13528..83982,y=-99403..-27377,z=-24141..23996" + .parse() + .unwrap(), + "on x=-72682..-12347,y=18159..111354,z=7391..80950" + .parse() + .unwrap(), + "on x=-1060..80757,y=-65301..-20884,z=-103788..-16709" + .parse() + .unwrap(), + "on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856" + .parse() + .unwrap(), + "on x=-52752..22273,y=-49450..9096,z=54442..119054" + .parse() + .unwrap(), + "on x=-29982..40483,y=-108474..-28371,z=-24328..38471" + .parse() + .unwrap(), + "on x=-4958..62750,y=40422..118853,z=-7672..65583" + .parse() + .unwrap(), + "on x=55694..108686,y=-43367..46958,z=-26781..48729" + .parse() + .unwrap(), + "on x=-98497..-18186,y=-63569..3412,z=1232..88485" + .parse() + .unwrap(), + "on x=-726..56291,y=-62629..13224,z=18033..85226" + .parse() + .unwrap(), + "on x=-110886..-34664,y=-81338..-8658,z=8914..63723" + .parse() + .unwrap(), + "on x=-55829..24974,y=-16897..54165,z=-121762..-28058" + .parse() + .unwrap(), + "on x=-65152..-11147,y=22489..91432,z=-58782..1780" + .parse() + .unwrap(), + "on x=-120100..-32970,y=-46592..27473,z=-11695..61039" + .parse() + .unwrap(), + "on x=-18631..37533,y=-124565..-50804,z=-35667..28308" + .parse() + .unwrap(), + "on x=-57817..18248,y=49321..117703,z=5745..55881" + .parse() + .unwrap(), + "on x=14781..98692,y=-1341..70827,z=15753..70151" + .parse() + .unwrap(), + "on x=-34419..55919,y=-19626..40991,z=39015..114138" + .parse() + .unwrap(), + "on x=-60785..11593,y=-56135..2999,z=-95368..-26915" + .parse() + .unwrap(), + "on x=-32178..58085,y=17647..101866,z=-91405..-8878" + .parse() + .unwrap(), + "on x=-53655..12091,y=50097..105568,z=-75335..-4862" + .parse() + .unwrap(), + "on x=-111166..-40997,y=-71714..2688,z=5609..50954" + .parse() + .unwrap(), + "on x=-16602..70118,y=-98693..-44401,z=5197..76897" + .parse() + .unwrap(), + "on x=16383..101554,y=4615..83635,z=-44907..18747" + .parse() + .unwrap(), + "off x=-95822..-15171,y=-19987..48940,z=10804..104439" + .parse() + .unwrap(), + "on x=-89813..-14614,y=16069..88491,z=-3297..45228" + .parse() + .unwrap(), + "on x=41075..99376,y=-20427..49978,z=-52012..13762" + .parse() + .unwrap(), + "on x=-21330..50085,y=-17944..62733,z=-112280..-30197" + .parse() + .unwrap(), + "on x=-16478..35915,y=36008..118594,z=-7885..47086" + .parse() + .unwrap(), + "off x=-98156..-27851,y=-49952..43171,z=-99005..-8456" + .parse() + .unwrap(), + "off x=2032..69770,y=-71013..4824,z=7471..94418" + .parse() + .unwrap(), + "on x=43670..120875,y=-42068..12382,z=-24787..38892" + .parse() + .unwrap(), + "off x=37514..111226,y=-45862..25743,z=-16714..54663" + .parse() + .unwrap(), + "off x=25699..97951,y=-30668..59918,z=-15349..69697" + .parse() + .unwrap(), + "off x=-44271..17935,y=-9516..60759,z=49131..112598" + .parse() + .unwrap(), + "on x=-61695..-5813,y=40978..94975,z=8655..80240" + .parse() + .unwrap(), + "off x=-101086..-9439,y=-7088..67543,z=33935..83858" + .parse() + .unwrap(), + "off x=18020..114017,y=-48931..32606,z=21474..89843" + .parse() + .unwrap(), + "off x=-77139..10506,y=-89994..-18797,z=-80..59318" + .parse() + .unwrap(), + "off x=8476..79288,y=-75520..11602,z=-96624..-24783" + .parse() + .unwrap(), + "on x=-47488..-1262,y=24338..100707,z=16292..72967" + .parse() + .unwrap(), + "off x=-84341..13987,y=2429..92914,z=-90671..-1318" + .parse() + .unwrap(), + "off x=-37810..49457,y=-71013..-7894,z=-105357..-13188" + .parse() + .unwrap(), + "off x=-27365..46395,y=31009..98017,z=15428..76570" + .parse() + .unwrap(), + "off x=-70369..-16548,y=22648..78696,z=-1892..86821" + .parse() + .unwrap(), + "on x=-53470..21291,y=-120233..-33476,z=-44150..38147" + .parse() + .unwrap(), + "off x=-93533..-4276,y=-16170..68771,z=-104985..-24507" + .parse() + .unwrap(), + ]; + + let expected = 2758514936282235; + assert_eq!(expected, part2(input)) + } +} diff --git a/2021/day22/src/main.rs b/2021/day22/src/main.rs new file mode 100644 index 0000000..c0e0c68 --- /dev/null +++ b/2021/day22/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2021-2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_line_input; +use day22_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day22", read_parsed_line_input, part1, part2) +} diff --git a/2021/day24/Cargo.toml b/2021/day24/Cargo.toml new file mode 100644 index 0000000..3ba4774 --- /dev/null +++ b/2021/day24/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "day24_2021" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "day24_2021" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aoc-solution = { path = "../../aoc-solution" } +aoc-common = { path = "../../common" } + +[lints] +workspace = true \ No newline at end of file diff --git a/2021/day24/src/alu/instruction.rs b/2021/day24/src/alu/instruction.rs new file mode 100644 index 0000000..7ae972b --- /dev/null +++ b/2021/day24/src/alu/instruction.rs @@ -0,0 +1,83 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::alu::{Operand, Variable}; +use anyhow::{anyhow, bail}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +const INPUT: &str = "inp"; +const ADD: &str = "add"; +const MUL: &str = "mul"; +const DIV: &str = "div"; +const MOD: &str = "mod"; +const EQUAL: &str = "eql"; + +#[derive(Debug, Copy, Clone)] +pub enum Instruction { + Input(Variable), + Add(Variable, Operand), + Mul(Variable, Operand), + Div(Variable, Operand), + Mod(Variable, Operand), + Equal(Variable, Operand), +} + +impl FromStr for Instruction { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut instr_operands = s.split_ascii_whitespace(); + + let ins = instr_operands + .next() + .ok_or_else(|| anyhow!("no instruction present"))?; + + let op1 = instr_operands + .next() + .ok_or_else(|| anyhow!("no operand 1 present"))? + .parse()?; + + if ins == INPUT { + return Ok(Instruction::Input(op1)); + } + + let op2 = instr_operands + .next() + .ok_or_else(|| anyhow!("no operand 2 present"))? + .parse()?; + + match ins { + ins if ins == ADD => Ok(Instruction::Add(op1, op2)), + ins if ins == MUL => Ok(Instruction::Mul(op1, op2)), + ins if ins == DIV => Ok(Instruction::Div(op1, op2)), + ins if ins == MOD => Ok(Instruction::Mod(op1, op2)), + ins if ins == EQUAL => Ok(Instruction::Equal(op1, op2)), + x => bail!("{} is not a valid instruction", x), + } + } +} + +impl Display for Instruction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Instruction::Input(op1) => write!(f, "{INPUT} {op1}"), + Instruction::Add(op1, op2) => write!(f, "{ADD} {op1} {op2}"), + Instruction::Mul(op1, op2) => write!(f, "{MUL} {op1} {op2}"), + Instruction::Div(op1, op2) => write!(f, "{DIV} {op1} {op2}"), + Instruction::Mod(op1, op2) => write!(f, "{MOD} {op1} {op2}"), + Instruction::Equal(op1, op2) => write!(f, "{EQUAL} {op1} {op2}"), + } + } +} diff --git a/2021/day24/src/alu/mod.rs b/2021/day24/src/alu/mod.rs new file mode 100644 index 0000000..72beefe --- /dev/null +++ b/2021/day24/src/alu/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod instruction; +mod operand; + +pub use instruction::Instruction; +pub use operand::{Operand, Variable}; diff --git a/2021/day24/src/alu/operand.rs b/2021/day24/src/alu/operand.rs new file mode 100644 index 0000000..9526b47 --- /dev/null +++ b/2021/day24/src/alu/operand.rs @@ -0,0 +1,87 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::bail; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Debug, Copy, Clone)] +pub enum Variable { + W, + X, + Y, + Z, +} + +impl FromStr for Variable { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "w" => Ok(Variable::W), + "x" => Ok(Variable::X), + "y" => Ok(Variable::Y), + "z" => Ok(Variable::Z), + _ => bail!("not a valid variable"), + } + } +} + +impl Display for Variable { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Variable::W => write!(f, "w"), + Variable::X => write!(f, "x"), + Variable::Y => write!(f, "y"), + Variable::Z => write!(f, "z"), + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum Operand { + Var(Variable), + Number(isize), +} + +impl Operand { + pub fn get_number(&self) -> Option { + match self { + Operand::Var(_) => None, + Operand::Number(val) => Some(*val), + } + } +} + +impl Display for Operand { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Operand::Var(var) => var.fmt(f), + Operand::Number(num) => num.fmt(f), + } + } +} + +impl FromStr for Operand { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + // try to parse it as a variable, otherwise fallback to a number + if let Ok(var) = Variable::from_str(s) { + Ok(Operand::Var(var)) + } else { + Ok(Operand::Number(s.parse()?)) + } + } +} diff --git a/2021/day24/src/chunk.rs b/2021/day24/src/chunk.rs new file mode 100644 index 0000000..c34f077 --- /dev/null +++ b/2021/day24/src/chunk.rs @@ -0,0 +1,83 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::alu::Instruction; + +// It turns out the input is in the form of the following chunks repeat 14 times: +// inp w +// mul x 0 +// add x z +// mod x 26 +// div z z_div +// add x x_add +// eql x w +// eql x 0 +// mul y 0 +// add y 25 +// mul y x +// add y 1 +// mul z y +// mul y 0 +// add y w +// add y y_add +// mul y x +// add z y +// the only thing linking chunks together is the value of `z`. Both `x` and `y` are irrelevant (and `w` is always overwritten with input) + +#[derive(Debug, Copy, Clone, Hash, PartialOrd, PartialEq, Eq)] +pub struct Chunk { + pub z_div: isize, + pub x_add: isize, + pub y_add: isize, +} + +impl Chunk { + pub fn from_instructions(instructions: &[Instruction]) -> Self { + assert_eq!(instructions.len(), 18, "invalid instructions provided"); + let z_div = if let Instruction::Div(_, op) = instructions[4] { + op.get_number().expect("invalid instructions provided") + } else { + panic!("invalid instructions provided") + }; + + let x_add = if let Instruction::Add(_, op) = instructions[5] { + op.get_number().expect("invalid instructions provided") + } else { + panic!("invalid instructions provided") + }; + + let y_add = if let Instruction::Add(_, op) = instructions[15] { + op.get_number().expect("invalid instructions provided") + } else { + panic!("invalid instructions provided") + }; + + Chunk { + z_div, + x_add, + y_add, + } + } + + pub fn execute(&self, w: isize, input_z: isize) -> isize { + let x = input_z % 26; + let z = input_z / self.z_div; + + if x + self.x_add != w { + 26 * z + w + self.y_add + } else { + z + } + } +} diff --git a/2021/day24/src/lib.rs b/2021/day24/src/lib.rs new file mode 100644 index 0000000..1adb678 --- /dev/null +++ b/2021/day24/src/lib.rs @@ -0,0 +1,112 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +use crate::alu::Instruction; +use crate::chunk::Chunk; +use aoc_common::parsing::LineParser; +use aoc_solution::Aoc; +use std::collections::HashSet; + +#[derive(Aoc)] +#[aoc(input = Vec)] +#[aoc(parser = LineParser)] +#[aoc(part1(output = usize, runner = part1))] +#[aoc(part2(output = usize, runner = part2))] +pub struct Day24; + +mod alu; +mod chunk; + +const DIGITS_ASC: &[isize] = &[1isize, 2, 3, 4, 5, 6, 7, 8, 9]; +const DIGITS_DESC: &[isize] = &[9isize, 8, 7, 6, 5, 4, 3, 2, 1]; + +#[derive(Copy, Clone)] +pub enum SolutionType { + Largest, + Smallest, +} + +// simple bruteforce with pruning +fn check_chunks( + dead_ends: &mut HashSet<(isize, usize)>, + input_z: isize, + chunks: &[Chunk], + prefix: usize, + solution_type: SolutionType, +) -> (usize, bool) { + // have we already seen this input z at this depth? + if dead_ends.contains(&(input_z, chunks.len())) { + // not worth following + return (prefix, false); + } + + // we have reached the final chunk + if chunks.is_empty() { + return (prefix, input_z == 0); + } + + let ws = match solution_type { + SolutionType::Smallest => DIGITS_ASC, + SolutionType::Largest => DIGITS_DESC, + }; + + for &w in ws { + let output_z = chunks[0].execute(w, input_z); + + let (val, found_valid_solution) = check_chunks( + dead_ends, + output_z, + &chunks[1..], + 10 * prefix + w as usize, + solution_type, + ); + // we're done, propagate the answer to the top + if found_valid_solution { + return (val, true); + } + } + + // nothing useful in this branch + dead_ends.insert((input_z, chunks.len())); + (prefix, false) +} + +fn bruteforce(chunks: &[Chunk], solution_type: SolutionType) -> usize { + let mut dead_ends = HashSet::new(); + let (solution, is_solution_valid) = check_chunks(&mut dead_ends, 0, chunks, 0, solution_type); + assert!(is_solution_valid); + solution +} + +pub fn part1(instructions: Vec) -> usize { + let chunks = instructions + .chunks_exact(18) + .map(Chunk::from_instructions) + .collect::>(); + + bruteforce(&chunks, SolutionType::Largest) +} + +pub fn part2(instructions: Vec) -> usize { + let chunks = instructions + .chunks_exact(18) + .map(Chunk::from_instructions) + .collect::>(); + + bruteforce(&chunks, SolutionType::Smallest) +} diff --git a/2021/day24/src/main.rs b/2021/day24/src/main.rs new file mode 100644 index 0000000..4716cc7 --- /dev/null +++ b/2021/day24/src/main.rs @@ -0,0 +1,22 @@ +// Copyright 2022 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aoc_common::legacy::execute_vec; +use aoc_common::legacy::input_read::read_parsed_line_input; +use day24_2021::{part1, part2}; + +#[cfg(not(tarpaulin_include))] +fn main() { + execute_vec("inputs/2021/day24", read_parsed_line_input, part1, part2) +} diff --git a/2022/day01/Cargo.toml b/2022/day01/Cargo.toml index be10a6c..a120b5a 100644 --- a/2022/day01/Cargo.toml +++ b/2022/day01/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day01_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day01/src/lib.rs b/2022/day01/src/lib.rs index 65326d9..623d7f4 100644 --- a/2022/day01/src/lib.rs +++ b/2022/day01/src/lib.rs @@ -57,7 +57,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day02/Cargo.toml b/2022/day02/Cargo.toml index 28d0474..b9e5d38 100644 --- a/2022/day02/Cargo.toml +++ b/2022/day02/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day02_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day02/src/lib.rs b/2022/day02/src/lib.rs index 9cebfcd..34baef3 100644 --- a/2022/day02/src/lib.rs +++ b/2022/day02/src/lib.rs @@ -34,7 +34,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day03/Cargo.toml b/2022/day03/Cargo.toml index efddec4..5418a16 100644 --- a/2022/day03/Cargo.toml +++ b/2022/day03/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day03_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day03/src/lib.rs b/2022/day03/src/lib.rs index 89c59c4..04885e4 100644 --- a/2022/day03/src/lib.rs +++ b/2022/day03/src/lib.rs @@ -40,7 +40,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day04/Cargo.toml b/2022/day04/Cargo.toml index 0a97305..5c1fed7 100644 --- a/2022/day04/Cargo.toml +++ b/2022/day04/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day04_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day04/src/lib.rs b/2022/day04/src/lib.rs index 53cb304..63d1523 100644 --- a/2022/day04/src/lib.rs +++ b/2022/day04/src/lib.rs @@ -40,7 +40,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day04/src/types.rs b/2022/day04/src/types.rs index 156eef8..1ad3e19 100644 --- a/2022/day04/src/types.rs +++ b/2022/day04/src/types.rs @@ -78,7 +78,6 @@ impl AssignmentPair { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day05/Cargo.toml b/2022/day05/Cargo.toml index c9388cb..84e6aa8 100644 --- a/2022/day05/Cargo.toml +++ b/2022/day05/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day05_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day05/src/lib.rs b/2022/day05/src/lib.rs index 1e69a36..a2f7f97 100644 --- a/2022/day05/src/lib.rs +++ b/2022/day05/src/lib.rs @@ -36,7 +36,6 @@ pub fn part2(mut input: Supplies) -> String { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day05/src/types.rs b/2022/day05/src/types.rs index 517c274..401e7fe 100644 --- a/2022/day05/src/types.rs +++ b/2022/day05/src/types.rs @@ -223,7 +223,6 @@ impl FromStr for Supplies { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day06/Cargo.toml b/2022/day06/Cargo.toml index 001e1bf..2650ab8 100644 --- a/2022/day06/Cargo.toml +++ b/2022/day06/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day06_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day06/src/lib.rs b/2022/day06/src/lib.rs index 3d709e2..fabae07 100644 --- a/2022/day06/src/lib.rs +++ b/2022/day06/src/lib.rs @@ -43,7 +43,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2022/day07/Cargo.toml b/2022/day07/Cargo.toml index 0b3b22d..90d5690 100644 --- a/2022/day07/Cargo.toml +++ b/2022/day07/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day07_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day07/src/lib.rs b/2022/day07/src/lib.rs index cd94584..9135fbd 100644 --- a/2022/day07/src/lib.rs +++ b/2022/day07/src/lib.rs @@ -34,7 +34,6 @@ pub fn part2(input: FileSystem) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use crate::types::FileSystem; diff --git a/2022/day08/Cargo.toml b/2022/day08/Cargo.toml index 4edf6a4..e25b7ba 100644 --- a/2022/day08/Cargo.toml +++ b/2022/day08/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day08_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day08/src/lib.rs b/2022/day08/src/lib.rs index 58087e5..00c38d4 100644 --- a/2022/day08/src/lib.rs +++ b/2022/day08/src/lib.rs @@ -34,7 +34,6 @@ pub fn part2(input: Forest) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use crate::types::Forest; diff --git a/2022/day10/Cargo.toml b/2022/day10/Cargo.toml index d701bba..41277cf 100644 --- a/2022/day10/Cargo.toml +++ b/2022/day10/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day10_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day10/src/lib.rs b/2022/day10/src/lib.rs index 302fd70..e993f62 100644 --- a/2022/day10/src/lib.rs +++ b/2022/day10/src/lib.rs @@ -44,7 +44,6 @@ pub fn part2(input: Vec) -> String { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use crate::types::Cpu; diff --git a/2022/day11/Cargo.toml b/2022/day11/Cargo.toml index e98b354..cbce369 100644 --- a/2022/day11/Cargo.toml +++ b/2022/day11/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day11_2022" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2022/day11/src/lib.rs b/2022/day11/src/lib.rs index 595a425..1af4753 100644 --- a/2022/day11/src/lib.rs +++ b/2022/day11/src/lib.rs @@ -38,7 +38,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use crate::types::Monkey; diff --git a/2023/day01/Cargo.toml b/2023/day01/Cargo.toml index eafa631..d7ed9da 100644 --- a/2023/day01/Cargo.toml +++ b/2023/day01/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day01_2023" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2023/day01/src/lib.rs b/2023/day01/src/lib.rs index dc9a132..d822ef9 100644 --- a/2023/day01/src/lib.rs +++ b/2023/day01/src/lib.rs @@ -34,7 +34,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2023/day01/src/types.rs b/2023/day01/src/types.rs index 5c59cf2..fd1c217 100644 --- a/2023/day01/src/types.rs +++ b/2023/day01/src/types.rs @@ -33,8 +33,7 @@ pub fn try_from_ascii_digits(s: &str) -> usize { // ))?; // // // safety: the unwraps here are fine as we've just verified the extracted characters MUST BE ascii digits - // #[allow(clippy::unwrap_used)] - // Ok(first.to_digit(10).unwrap() * 10 + last.to_digit(10).unwrap()) + // // Ok(first.to_digit(10).unwrap() * 10 + last.to_digit(10).unwrap()) // // but for the first days we care about, as Jeremy Clarkson would say, "SPEEEEEED" let bytes = s.as_bytes(); diff --git a/2023/day02/Cargo.toml b/2023/day02/Cargo.toml index 44af9e0..842cd4b 100644 --- a/2023/day02/Cargo.toml +++ b/2023/day02/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day02_2023" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2023/day02/src/lib.rs b/2023/day02/src/lib.rs index c6c2fe1..ac98a9a 100644 --- a/2023/day02/src/lib.rs +++ b/2023/day02/src/lib.rs @@ -40,7 +40,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2023/day03/Cargo.toml b/2023/day03/Cargo.toml index af95724..203e6c1 100644 --- a/2023/day03/Cargo.toml +++ b/2023/day03/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day03_2023" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2023/day03/src/helpers.rs b/2023/day03/src/helpers.rs index 356be4a..e456ba6 100644 --- a/2023/day03/src/helpers.rs +++ b/2023/day03/src/helpers.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +// legacy code +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + pub(crate) fn digits(num: u32) -> usize { // yes, yes, we could have used a more generic solution with a log10 or iterators, like with // `iterate(n, |&n| n / 10).take_while(|&n| n > 0).count().max(1)` @@ -31,7 +35,6 @@ pub(crate) fn digits(num: u32) -> usize { pub(crate) fn digits_to_number(digits: Vec) -> u32 { const RADIX: u32 = 10; // SAFETY: 10 is a valid radix value - #[allow(clippy::unwrap_used)] digits .into_iter() .map(|c| c.to_digit(RADIX).unwrap()) diff --git a/2023/day03/src/lib.rs b/2023/day03/src/lib.rs index 1f54906..edbc1a1 100644 --- a/2023/day03/src/lib.rs +++ b/2023/day03/src/lib.rs @@ -35,7 +35,6 @@ pub fn part2(input: EngineSchematic) -> u32 { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2023/day04/Cargo.toml b/2023/day04/Cargo.toml index 7582e0f..17607e3 100644 --- a/2023/day04/Cargo.toml +++ b/2023/day04/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day04_2023" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2023/day04/src/lib.rs b/2023/day04/src/lib.rs index dac2500..bdf378a 100644 --- a/2023/day04/src/lib.rs +++ b/2023/day04/src/lib.rs @@ -58,7 +58,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2023/day05/Cargo.toml b/2023/day05/Cargo.toml index faeb187..2c8c787 100644 --- a/2023/day05/Cargo.toml +++ b/2023/day05/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day05_2023" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2023/day05/src/lib.rs b/2023/day05/src/lib.rs index ffc4f5b..7ee9333 100644 --- a/2023/day05/src/lib.rs +++ b/2023/day05/src/lib.rs @@ -35,7 +35,6 @@ pub fn part2(mut input: Almanac) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2024/day01/Cargo.toml b/2024/day01/Cargo.toml index 908d614..4867d53 100644 --- a/2024/day01/Cargo.toml +++ b/2024/day01/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day01_2024" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2024/day01/src/lib.rs b/2024/day01/src/lib.rs index dc5d48b..b82b90f 100644 --- a/2024/day01/src/lib.rs +++ b/2024/day01/src/lib.rs @@ -36,7 +36,6 @@ pub fn part2(input: LocationLists) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2024/day02/Cargo.toml b/2024/day02/Cargo.toml index 8b19a6f..abbfba0 100644 --- a/2024/day02/Cargo.toml +++ b/2024/day02/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day02_2024" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2024/day02/src/lib.rs b/2024/day02/src/lib.rs index df728fe..ddf2dfe 100644 --- a/2024/day02/src/lib.rs +++ b/2024/day02/src/lib.rs @@ -34,7 +34,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2024/day03/Cargo.toml b/2024/day03/Cargo.toml index 55a7890..f8df078 100644 --- a/2024/day03/Cargo.toml +++ b/2024/day03/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day03_2024" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2024/day03/src/common.rs b/2024/day03/src/common.rs index 1afd5eb..8022f38 100644 --- a/2024/day03/src/common.rs +++ b/2024/day03/src/common.rs @@ -63,7 +63,6 @@ impl AocInputParser for InstructionsParser { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/2024/day03/src/lib.rs b/2024/day03/src/lib.rs index 4cdeb6d..971ac37 100644 --- a/2024/day03/src/lib.rs +++ b/2024/day03/src/lib.rs @@ -56,7 +56,6 @@ pub fn part2(input: Vec) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use crate::common::Instruction; diff --git a/2024/day04/Cargo.toml b/2024/day04/Cargo.toml index 16bd458..c91cf05 100644 --- a/2024/day04/Cargo.toml +++ b/2024/day04/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day04_2024" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2024/day04/src/common.rs b/2024/day04/src/common.rs index 24c9ebd..086fcae 100644 --- a/2024/day04/src/common.rs +++ b/2024/day04/src/common.rs @@ -168,7 +168,6 @@ impl WordGrid { } } -#[allow(clippy::unwrap_used)] #[cfg(test)] mod tests { use super::*; diff --git a/2024/day04/src/lib.rs b/2024/day04/src/lib.rs index f4a4b4c..f647d5a 100644 --- a/2024/day04/src/lib.rs +++ b/2024/day04/src/lib.rs @@ -52,7 +52,6 @@ pub fn part2(input: WordGrid) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; use aoc_solution::parser::AocInputParser; diff --git a/2024/day05/Cargo.toml b/2024/day05/Cargo.toml index 8e51b9e..74c79c9 100644 --- a/2024/day05/Cargo.toml +++ b/2024/day05/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "day05_2024" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/2024/day05/src/lib.rs b/2024/day05/src/lib.rs index e5df326..63834dc 100644 --- a/2024/day05/src/lib.rs +++ b/2024/day05/src/lib.rs @@ -44,7 +44,6 @@ pub fn part2(input: PrintingRules) -> usize { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*; diff --git a/Cargo.toml b/Cargo.toml index 121bc4a..44edd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,63 @@ members = [ "solution-runner", "common", "tools/aoc-init", + "2019/day01", + "2019/day02", + "2019/day03", + "2019/day04", + "2019/day05", + "2019/day06", + "2019/day07", + "2019/day08", + "2019/day09", + "2020/day01", + "2020/day02", + "2020/day03", + "2020/day04", + "2020/day05", + "2020/day06", + "2020/day07", + "2020/day08", + "2020/day09", + "2020/day10", + "2020/day11", + "2020/day12", + "2020/day13", + "2020/day14", + "2020/day15", + "2020/day16", + "2020/day17", + "2020/day18", + "2020/day19", + "2020/day20", + "2020/day21", + "2020/day22", + "2020/day23", + "2020/day24", + "2020/day25", + "2021/day01", + "2021/day02", + "2021/day03", + "2021/day04", + "2021/day05", + "2021/day06", + "2021/day07", + "2021/day08", + "2021/day09", + "2021/day10", + "2021/day11", + "2021/day12", + "2021/day13", + "2021/day14", + "2021/day15", + "2021/day16", + "2021/day17", + "2021/day18", + "2021/day19", + "2021/day20", + "2021/day21", + "2021/day22", + "2021/day24", "2022/day01", "2022/day02", "2022/day03", @@ -29,17 +86,28 @@ members = [ "2024/day05" ] +[workspace.package] +authors = ["jstuczyn "] +repository = "https://github.com/jstuczyn/AdventOfCode" +edition = "2021" +license = "Apache-2.0" +rust-version = "1.80" +readme = "README.md" + [workspace.dependencies] anyhow = "1.0.93" +bitvec = "1.0.1" cargo-edit = "0.13.0" cargo-generate = "0.22.0" cfg-if = "1.0.0" clap = "4.5.21" criterion = "0.5.1" futures = "0.3.31" +hex = "0.4.3" humantime = "2.1.0" itertools = "0.13.0" num = "0.4.3" +pathfinding = "4.12.0" rayon = "1.10.0" reqwest = "0.12.9" tokio = "1.41.1" diff --git a/README.md b/README.md index 73f8918..3f1721d 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,24 @@ Keeping track of solutions to various puzzles from https://adventofcode.com/ ## About -> Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as a speed contest, interview prep, company training, university coursework, practice problems, or to challenge each other. +> Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that +> can be solved in any programming language you like. People use them as a speed contest, interview prep, company +> training, university coursework, practice problems, or to challenge each other. +> +> You don't need a computer science background to participate - just a little programming knowledge and some problem +> solving skills will get you pretty far. Nor do you need a fancy computer; every problem has a solution that completes +> in +> at most 15 seconds on ten-year-old hardware. > -> You don't need a computer science background to participate - just a little programming knowledge and some problem solving skills will get you pretty far. Nor do you need a fancy computer; every problem has a solution that completes in at most 15 seconds on ten-year-old hardware. -> > - www.adventofcode.com ## Latest puzzle -[![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2023.json)](https://adventofcode.com/2023/about) + +[![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2024.json)](https://adventofcode.com/2024/about) ## Previous years + +[![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2023.json)](https://adventofcode.com/2023/about) [![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2022.json)](https://adventofcode.com/2022/about) [![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2021.json)](https://adventofcode.com/2021/about) [![Completion Status](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/jstuczyn/AdventOfCode/master/.github/badges/completion2020.json)](https://adventofcode.com/2020/about) @@ -25,22 +33,31 @@ Keeping track of solutions to various puzzles from https://adventofcode.com/ ## Running the code -There are two ways of running particular solution. One can either go to the directory associated with the given day, for example: +There are two ways of running particular solution. One can either go to the directory associated with the given day, for +example: + ```shell cd 2022/day01 ``` and run it from there: + ```shell cargo run --release ``` -Alternatively, there's a dedicated `solution-runner` binary that's can run any sub-solution based on arguments provided. For example +Alternatively, there's a dedicated `solution-runner` binary that's can run any sub-solution based on arguments provided. +For example ```shell ./solution-runner --year 2022 --day 1 ``` +### Note: + +solutions from 2019, 2020 and 2021 are not guaranteed to run correctly, +as they got imported from old repositories and have not been written with the current runner framework in mind. + ## Adding new day Run the following command to generate the template: diff --git a/aoc-solution-derive/Cargo.toml b/aoc-solution-derive/Cargo.toml index 9791654..ed86827 100644 --- a/aoc-solution-derive/Cargo.toml +++ b/aoc-solution-derive/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "aoc-derive" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/aoc-solution/Cargo.toml b/aoc-solution/Cargo.toml index d63dd0e..6eaa15b 100644 --- a/aoc-solution/Cargo.toml +++ b/aoc-solution/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "aoc-solution" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..154626e --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +allow-unwrap-in-tests = true diff --git a/common/Cargo.toml b/common/Cargo.toml index 12a1ce8..b71a455 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "aoc-common" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/common/src/legacy.rs b/common/src/legacy.rs new file mode 100644 index 0000000..846fed2 --- /dev/null +++ b/common/src/legacy.rs @@ -0,0 +1,331 @@ +// Copyright 2024 Jedrzej Stuczynski +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use execution::*; +pub use input_read::*; +pub use parsing::*; + +pub mod execution { + + use std::fmt::Display; + use std::io; + use std::path::Path; + use std::time::{Duration, Instant}; + + pub fn execute_slice_with_timing(func: F, args: &[T]) -> (U, Duration) + where + F: Fn(&[T]) -> U, + { + let start = Instant::now(); + let res = func(args); + let time_taken = start.elapsed(); + (res, time_taken) + } + + pub fn execute_vec_with_timing(func: F, args: Vec) -> (U, Duration) + where + F: Fn(Vec) -> U, + { + let start = Instant::now(); + let res = func(args); + let time_taken = start.elapsed(); + (res, time_taken) + } + + pub fn execute_struct_with_timing(func: F, args: T) -> (U, Duration) + where + F: Fn(T) -> U, + { + let start = Instant::now(); + let res = func(args); + let time_taken = start.elapsed(); + (res, time_taken) + } + + pub fn execute_vec( + input_file: P, + input_parser: F, + part1_fn: G, + part2_fn: H, + ) where + P: AsRef, + F: Fn(P) -> io::Result>, + G: Fn(Vec) -> U, + H: Fn(Vec) -> S, + U: Display, + S: Display, + T: Clone, + { + let parsing_start = Instant::now(); + let input = input_parser(input_file).expect("failed to read input file"); + let parsing_time_taken = parsing_start.elapsed(); + + let (part1_result, part1_time_taken) = execute_vec_with_timing(part1_fn, input.clone()); + let (part2_result, part2_time_taken) = execute_vec_with_timing(part2_fn, input); + + println!("It took {parsing_time_taken:?} to parse the input"); + println!(); + println!( + "Part 1 result is {}\nIt took {:?} to compute", + part1_result, part1_time_taken + ); + println!(); + println!( + "Part 2 result is {}\nIt took {:?} to compute", + part2_result, part2_time_taken + ); + } + + // We'll see how it evolves with variety of inputs we get + pub fn execute_slice( + input_file: P, + input_parser: F, + part1_fn: G, + part2_fn: H, + ) where + P: AsRef, + F: Fn(P) -> io::Result>, + G: Fn(&[T]) -> U, + H: Fn(&[T]) -> S, + U: Display, + S: Display, + { + let parsing_start = Instant::now(); + let input = input_parser(input_file).expect("failed to read input file"); + let parsing_time_taken = parsing_start.elapsed(); + + let (part1_result, part1_time_taken) = execute_slice_with_timing(part1_fn, &input); + let (part2_result, part2_time_taken) = execute_slice_with_timing(part2_fn, &input); + + println!("It took {parsing_time_taken:?} to parse the input"); + println!(); + println!( + "Part 1 result is {}\nIt took {:?} to compute", + part1_result, part1_time_taken + ); + println!(); + println!( + "Part 2 result is {}\nIt took {:?} to compute", + part2_result, part2_time_taken + ); + } + + pub fn execute_struct( + input_file: P, + input_parser: F, + part1_fn: G, + part2_fn: H, + ) where + P: AsRef, + F: Fn(P) -> io::Result, + G: Fn(T) -> U, + H: Fn(T) -> S, + U: Display, + S: Display, + T: Clone, + { + let parsing_start = Instant::now(); + let input = input_parser(input_file).expect("failed to read input file"); + let parsing_time_taken = parsing_start.elapsed(); + + let (part1_result, part1_time_taken) = execute_struct_with_timing(part1_fn, input.clone()); + let (part2_result, part2_time_taken) = execute_struct_with_timing(part2_fn, input); + + println!("It took {parsing_time_taken:?} to parse the input"); + println!(); + println!( + "Part 1 result is {}\nIt took {:?} to compute", + part1_result, part1_time_taken + ); + println!(); + println!( + "Part 2 result is {}\nIt took {:?} to compute", + part2_result, part2_time_taken + ); + } +} + +pub mod input_read { + + use std::fmt::Debug; + use std::fs; + use std::fs::File; + use std::io::{self, BufRead}; + use std::path::Path; + use std::str::FromStr; + + /// Reads the file as lines, parsing each of them into desired type. + pub fn read_line_input(path: P) -> io::Result> + where + P: AsRef, + T: FromStr, + ::Err: Debug, + { + let file = File::open(path)?; + + let lines = io::BufReader::new(file).lines(); + let size_hint = lines.size_hint(); + + // use upper bound size hint if available, otherwise use lower bound + let mut results = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0)); + for line in lines { + // the last one is technically not an io error, but I can't be bothered to create a separate error type just for this + results.push(line?.parse().map_err(|parse_err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "input could not be parsed into desired type - {:?}", + parse_err + ), + ) + })?) + } + + Ok(results) + } + + pub fn read_input_lines

(path: P) -> io::Result> + where + P: AsRef, + { + let file = File::open(path)?; + io::BufReader::new(file).lines().collect() + } + + pub fn read_input_lines_with_parser(path: P, parser: F) -> io::Result> + where + P: AsRef, + F: Fn(String) -> io::Result, + { + read_input_lines(path)? + .into_iter() + .map(parser) + .collect::, _>>() + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + } + + /// Reads the file as lines, parsing each of them into desired type. + pub fn read_parsed_line_input(path: P) -> io::Result> + where + P: AsRef, + T: FromStr, + ::Err: Debug, + { + read_input_lines(path)? + .into_iter() + .map(|line| line.parse::()) + .collect::, _>>() + .map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("input could not be parsed into desired type - {err:?}"), + ) + }) + } + + /// Reads the file as a String + pub fn read_to_string>(path: P) -> io::Result { + fs::read_to_string(path) + } + + /// Reads the file and outputs String groups that were originally separated by an empty line + pub fn read_into_string_groups>(path: P) -> io::Result> { + fs::read_to_string(path).map(|string| { + string + .replace("\r\n", "\n") // Windows fix + .split("\n\n") + .map(|split| split.to_owned()) + .collect() + }) + } + + pub fn read_parsed_groups(path: P) -> io::Result> + where + P: AsRef, + T: FromStr, + ::Err: Debug, + { + read_into_string_groups(path)? + .into_iter() + .map(|line| line.parse::()) + .collect::, _>>() + .map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("input could not be parsed into desired type - {err:?}"), + ) + }) + } + + /// Reads the file as a string and parses comma-separated types + pub fn read_parsed_comma_separated_values(path: P) -> io::Result> + where + P: AsRef, + T: FromStr, + ::Err: Debug, + { + fs::read_to_string(path)? + .split(',') + .map(|split| split.parse()) + .collect::, _>>() + .map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("input could not be parsed into desired type - {err:?}"), + ) + }) + } + + pub fn read_parsed(path: P) -> io::Result + where + P: AsRef, + T: FromStr, + ::Err: Debug, + { + fs::read_to_string(path).map(|s| s.parse())?.map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("input could not be parsed into desired type - {err:?}"), + ) + }) + } +} + +pub mod parsing { + use anyhow::{Error, Result}; + use std::ops::RangeInclusive; + + // parses something in the form of x=.. + pub fn parse_raw_range(raw: &str) -> Result> { + let mut bounds = raw.split('='); + let _axis = bounds + .next() + .ok_or_else(|| Error::msg("incomplete range"))?; + let mut values = bounds + .next() + .ok_or_else(|| Error::msg("incomplete range"))? + .split(".."); + + let lower_bound = values + .next() + .ok_or_else(|| Error::msg("incomplete range"))? + .parse()?; + let upper_bound = values + .next() + .ok_or_else(|| Error::msg("incomplete range"))? + .parse()?; + + Ok(RangeInclusive::new(lower_bound, upper_bound)) + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index 0930eec..af7d826 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -18,3 +18,7 @@ pub mod helpers; pub mod input_read; pub mod parsing; pub mod types; + +// to help with import of 2019-2021 solutions; +// eventually they will be properly migrated to current framework +pub mod legacy; diff --git a/solution-runner/Cargo.toml b/solution-runner/Cargo.toml index 12edaab..2fa6f3a 100644 --- a/solution-runner/Cargo.toml +++ b/solution-runner/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "solution-runner" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,6 +16,69 @@ clap = { workspace = true, features = ["derive"] } aoc-solution = { path = "../aoc-solution" } aoc-common = { path = "../common" } +day01_2019 = { path = "../2019/day01" } +day02_2019 = { path = "../2019/day02" } +day03_2019 = { path = "../2019/day03" } +day04_2019 = { path = "../2019/day04" } +day05_2019 = { path = "../2019/day05" } +day06_2019 = { path = "../2019/day06" } +day07_2019 = { path = "../2019/day07" } +day08_2019 = { path = "../2019/day08" } +day09_2019 = { path = "../2019/day09" } + + +day01_2020 = { path = "../2020/day01" } +day02_2020 = { path = "../2020/day02" } +day03_2020 = { path = "../2020/day03" } +day04_2020 = { path = "../2020/day04" } +day05_2020 = { path = "../2020/day05" } +day06_2020 = { path = "../2020/day06" } +day07_2020 = { path = "../2020/day07" } +day08_2020 = { path = "../2020/day08" } +day09_2020 = { path = "../2020/day09" } +day10_2020 = { path = "../2020/day10" } +day11_2020 = { path = "../2020/day11" } +day12_2020 = { path = "../2020/day12" } +day13_2020 = { path = "../2020/day13" } +day14_2020 = { path = "../2020/day14" } +day15_2020 = { path = "../2020/day15" } +day16_2020 = { path = "../2020/day16" } +day17_2020 = { path = "../2020/day17" } +day18_2020 = { path = "../2020/day18" } +day19_2020 = { path = "../2020/day19" } +day20_2020 = { path = "../2020/day20" } +day21_2020 = { path = "../2020/day21" } +day22_2020 = { path = "../2020/day22" } +day23_2020 = { path = "../2020/day23" } +day24_2020 = { path = "../2020/day24" } +day25_2020 = { path = "../2020/day25" } + + +day01_2021 = { path = "../2021/day01" } +day02_2021 = { path = "../2021/day02" } +day03_2021 = { path = "../2021/day03" } +day04_2021 = { path = "../2021/day04" } +day05_2021 = { path = "../2021/day05" } +day06_2021 = { path = "../2021/day06" } +day07_2021 = { path = "../2021/day07" } +day08_2021 = { path = "../2021/day08" } +day09_2021 = { path = "../2021/day09" } +day10_2021 = { path = "../2021/day10" } +day11_2021 = { path = "../2021/day11" } +day12_2021 = { path = "../2021/day12" } +day13_2021 = { path = "../2021/day13" } +day14_2021 = { path = "../2021/day14" } +day15_2021 = { path = "../2021/day15" } +day16_2021 = { path = "../2021/day16" } +day17_2021 = { path = "../2021/day17" } +day18_2021 = { path = "../2021/day18" } +day19_2021 = { path = "../2021/day19" } +day20_2021 = { path = "../2021/day20" } +day21_2021 = { path = "../2021/day21" } +day22_2021 = { path = "../2021/day22" } +day24_2021 = { path = "../2021/day24" } + + day01_2022 = { path = "../2022/day01" } day02_2022 = { path = "../2022/day02" } day03_2022 = { path = "../2022/day03" } @@ -19,13 +87,17 @@ day05_2022 = { path = "../2022/day05" } day06_2022 = { path = "../2022/day06" } day07_2022 = { path = "../2022/day07" } day08_2022 = { path = "../2022/day08" } -day11_2022 = { path = "../2022/day11" } day10_2022 = { path = "../2022/day10" } +day11_2022 = { path = "../2022/day11" } + + day01_2023 = { path = "../2023/day01" } day02_2023 = { path = "../2023/day02" } day03_2023 = { path = "../2023/day03" } day04_2023 = { path = "../2023/day04" } day05_2023 = { path = "../2023/day05" } + + day01_2024 = { path = "../2024/day01" } day02_2024 = { path = "../2024/day02" } day03_2024 = { path = "../2024/day03" } diff --git a/solution-runner/src/main.rs b/solution-runner/src/main.rs index 46a44ff..294998e 100644 --- a/solution-runner/src/main.rs +++ b/solution-runner/src/main.rs @@ -48,6 +48,63 @@ fn main() { let args = Args::parse(); // AUTOGENERATED SOLUTIONS START + define_solution!(args, 2020, 1, "inputs/2019/day01", day01_2019::Day01); + define_solution!(args, 2019, 2, "inputs/2019/day02", day02_2019::Day02); + define_solution!(args, 2019, 3, "inputs/2019/day03", day03_2019::Day03); + define_solution!(args, 2019, 4, "inputs/2019/day04", day04_2019::Day04); + define_solution!(args, 2019, 5, "inputs/2019/day05", day05_2019::Day05); + define_solution!(args, 2019, 6, "inputs/2019/day06", day06_2019::Day06); + define_solution!(args, 2019, 7, "inputs/2019/day07", day07_2019::Day07); + define_solution!(args, 2019, 8, "inputs/2019/day08", day08_2019::Day08); + define_solution!(args, 2019, 9, "inputs/2019/day09", day09_2019::Day09); + define_solution!(args, 2020, 1, "inputs/2020/day01", day01_2020::Day01); + define_solution!(args, 2020, 2, "inputs/2020/day02", day02_2020::Day02); + define_solution!(args, 2020, 3, "inputs/2020/day03", day03_2020::Day03); + define_solution!(args, 2020, 4, "inputs/2020/day04", day04_2020::Day04); + define_solution!(args, 2020, 5, "inputs/2020/day05", day05_2020::Day05); + define_solution!(args, 2020, 6, "inputs/2020/day06", day06_2020::Day06); + define_solution!(args, 2020, 7, "inputs/2020/day07", day07_2020::Day07); + define_solution!(args, 2020, 8, "inputs/2020/day08", day08_2020::Day08); + define_solution!(args, 2020, 9, "inputs/2020/day09", day09_2020::Day09); + define_solution!(args, 2020, 10, "inputs/2020/day10", day10_2020::Day10); + define_solution!(args, 2020, 11, "inputs/2020/day11", day11_2020::Day11); + define_solution!(args, 2020, 12, "inputs/2020/day12", day12_2020::Day12); + define_solution!(args, 2020, 13, "inputs/2020/day13", day13_2020::Day13); + define_solution!(args, 2020, 14, "inputs/2020/day14", day14_2020::Day14); + define_solution!(args, 2020, 15, "inputs/2020/day15", day15_2020::Day15); + define_solution!(args, 2020, 16, "inputs/2020/day16", day16_2020::Day16); + define_solution!(args, 2020, 17, "inputs/2020/day17", day17_2020::Day17); + define_solution!(args, 2020, 18, "inputs/2020/day18", day18_2020::Day18); + define_solution!(args, 2020, 19, "inputs/2020/day19", day19_2020::Day19); + define_solution!(args, 2020, 20, "inputs/2020/day20", day20_2020::Day20); + define_solution!(args, 2020, 21, "inputs/2020/day21", day21_2020::Day21); + define_solution!(args, 2020, 22, "inputs/2020/day22", day22_2020::Day22); + define_solution!(args, 2020, 23, "inputs/2020/day23", day23_2020::Day23); + define_solution!(args, 2020, 24, "inputs/2020/day24", day24_2020::Day24); + define_solution!(args, 2020, 25, "inputs/2020/day25", day25_2020::Day25); + define_solution!(args, 2021, 1, "inputs/2021/day01", day01_2021::Day01); + define_solution!(args, 2021, 2, "inputs/2021/day02", day02_2021::Day02); + define_solution!(args, 2021, 3, "inputs/2021/day03", day03_2021::Day03); + define_solution!(args, 2021, 4, "inputs/2021/day04", day04_2021::Day04); + define_solution!(args, 2021, 5, "inputs/2021/day05", day05_2021::Day05); + define_solution!(args, 2021, 6, "inputs/2021/day06", day06_2021::Day06); + define_solution!(args, 2021, 7, "inputs/2021/day07", day07_2021::Day07); + define_solution!(args, 2021, 8, "inputs/2021/day08", day08_2021::Day08); + define_solution!(args, 2021, 9, "inputs/2021/day09", day09_2021::Day09); + define_solution!(args, 2021, 10, "inputs/2021/day10", day10_2021::Day10); + define_solution!(args, 2021, 11, "inputs/2021/day11", day11_2021::Day11); + define_solution!(args, 2021, 12, "inputs/2021/day12", day12_2021::Day12); + define_solution!(args, 2021, 13, "inputs/2021/day13", day13_2021::Day13); + define_solution!(args, 2021, 14, "inputs/2021/day14", day14_2021::Day14); + define_solution!(args, 2021, 15, "inputs/2021/day15", day15_2021::Day15); + define_solution!(args, 2021, 16, "inputs/2021/day16", day16_2021::Day16); + define_solution!(args, 2021, 17, "inputs/2021/day17", day17_2021::Day17); + define_solution!(args, 2021, 18, "inputs/2021/day18", day18_2021::Day18); + define_solution!(args, 2021, 19, "inputs/2021/day19", day19_2021::Day19); + define_solution!(args, 2021, 20, "inputs/2021/day20", day20_2021::Day20); + define_solution!(args, 2021, 21, "inputs/2021/day21", day21_2021::Day21); + define_solution!(args, 2021, 22, "inputs/2021/day22", day22_2021::Day22); + define_solution!(args, 2021, 24, "inputs/2021/day24", day24_2021::Day24); define_solution!(args, 2022, 1, "inputs/2022/day01", day01_2022::Day01); define_solution!(args, 2022, 2, "inputs/2022/day02", day02_2022::Day02); define_solution!(args, 2022, 3, "inputs/2022/day03", day03_2022::Day03); diff --git a/tools/aoc-init/Cargo.toml b/tools/aoc-init/Cargo.toml index e952efe..3355466 100644 --- a/tools/aoc-init/Cargo.toml +++ b/tools/aoc-init/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "aoc-init" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tools/aoc-init/src/main.rs b/tools/aoc-init/src/main.rs index e9dd90b..5fec817 100644 --- a/tools/aoc-init/src/main.rs +++ b/tools/aoc-init/src/main.rs @@ -186,6 +186,7 @@ fn try_get_input(args: &Args, root: &Path) -> anyhow::Result<()> { let mut file = fs::File::create(input_file)?; let Ok(session_cookie) = env::var("AOC_SESSION") else { + eprintln!("could not find a valid AOC_SESSION cookie"); return Ok(()); }; diff --git a/tools/aoc-init/template/{{year}}/day{{day}}/Cargo.toml b/tools/aoc-init/template/{{year}}/day{{day}}/Cargo.toml index 061fb92..fe8e63e 100644 --- a/tools/aoc-init/template/{{year}}/day{{day}}/Cargo.toml +++ b/tools/aoc-init/template/{{year}}/day{{day}}/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "{{project-name}}" version = "0.1.0" -edition = "2021" +authors.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tools/aoc-init/template/{{year}}/day{{day}}/src/lib.rs b/tools/aoc-init/template/{{year}}/day{{day}}/src/lib.rs index 7142786..d719ba5 100644 --- a/tools/aoc-init/template/{{year}}/day{{day}}/src/lib.rs +++ b/tools/aoc-init/template/{{year}}/day{{day}}/src/lib.rs @@ -28,7 +28,6 @@ pub fn part2(input: ()) -> ! { } #[cfg(test)] -#[allow(clippy::unwrap_used)] mod tests { use super::*;