diff --git a/README.md b/2021/README.md
similarity index 52%
rename from README.md
rename to 2021/README.md
index 32a3254..a05ff96 100644
--- a/README.md
+++ b/2021/README.md
@@ -2,6 +2,10 @@
## All code for advent of code 2021
+### TODO
+
+- [ ] write cli for running a file like: `deno task run 1 1`
+
---
JoΓ«l Kuijper
diff --git a/day-1/input.txt b/2021/day-1/input.txt
similarity index 100%
rename from day-1/input.txt
rename to 2021/day-1/input.txt
diff --git a/day-1/part-1/main.ts b/2021/day-1/part-1/main.ts
similarity index 100%
rename from day-1/part-1/main.ts
rename to 2021/day-1/part-1/main.ts
diff --git a/day-1/part-2/main.ts b/2021/day-1/part-2/main.ts
similarity index 100%
rename from day-1/part-2/main.ts
rename to 2021/day-1/part-2/main.ts
diff --git a/day-10/input.txt b/2021/day-10/input.txt
similarity index 100%
rename from day-10/input.txt
rename to 2021/day-10/input.txt
diff --git a/day-10/main.ts b/2021/day-10/main.ts
similarity index 100%
rename from day-10/main.ts
rename to 2021/day-10/main.ts
diff --git a/day-2/input.txt b/2021/day-2/input.txt
similarity index 100%
rename from day-2/input.txt
rename to 2021/day-2/input.txt
diff --git a/day-2/part-1/main.ts b/2021/day-2/part-1/main.ts
similarity index 100%
rename from day-2/part-1/main.ts
rename to 2021/day-2/part-1/main.ts
diff --git a/day-2/part-2/main.ts b/2021/day-2/part-2/main.ts
similarity index 100%
rename from day-2/part-2/main.ts
rename to 2021/day-2/part-2/main.ts
diff --git a/day-3/input.txt b/2021/day-3/input.txt
similarity index 100%
rename from day-3/input.txt
rename to 2021/day-3/input.txt
diff --git a/day-3/part-1/main.ts b/2021/day-3/part-1/main.ts
similarity index 100%
rename from day-3/part-1/main.ts
rename to 2021/day-3/part-1/main.ts
diff --git a/day-3/part-2/main.ts b/2021/day-3/part-2/main.ts
similarity index 100%
rename from day-3/part-2/main.ts
rename to 2021/day-3/part-2/main.ts
diff --git a/day-3/part-2/temp.ts b/2021/day-3/part-2/temp.ts
similarity index 100%
rename from day-3/part-2/temp.ts
rename to 2021/day-3/part-2/temp.ts
diff --git a/day-4/input.txt b/2021/day-4/input.txt
similarity index 100%
rename from day-4/input.txt
rename to 2021/day-4/input.txt
diff --git a/day-4/main.ts b/2021/day-4/main.ts
similarity index 100%
rename from day-4/main.ts
rename to 2021/day-4/main.ts
diff --git a/day-5/input.txt b/2021/day-5/input.txt
similarity index 100%
rename from day-5/input.txt
rename to 2021/day-5/input.txt
diff --git a/day-5/main.ts b/2021/day-5/main.ts
similarity index 100%
rename from day-5/main.ts
rename to 2021/day-5/main.ts
diff --git a/day-6/input.txt b/2021/day-6/input.txt
similarity index 100%
rename from day-6/input.txt
rename to 2021/day-6/input.txt
diff --git a/day-6/main.ts b/2021/day-6/main.ts
similarity index 100%
rename from day-6/main.ts
rename to 2021/day-6/main.ts
diff --git a/day-7/input.txt b/2021/day-7/input.txt
similarity index 100%
rename from day-7/input.txt
rename to 2021/day-7/input.txt
diff --git a/day-7/main.ts b/2021/day-7/main.ts
similarity index 100%
rename from day-7/main.ts
rename to 2021/day-7/main.ts
diff --git a/day-8/input.txt b/2021/day-8/input.txt
similarity index 100%
rename from day-8/input.txt
rename to 2021/day-8/input.txt
diff --git a/day-8/main.ts b/2021/day-8/main.ts
similarity index 100%
rename from day-8/main.ts
rename to 2021/day-8/main.ts
diff --git a/day-9/input.txt b/2021/day-9/input.txt
similarity index 100%
rename from day-9/input.txt
rename to 2021/day-9/input.txt
diff --git a/day-9/main.ts b/2021/day-9/main.ts
similarity index 100%
rename from day-9/main.ts
rename to 2021/day-9/main.ts
diff --git a/deno.json b/2021/deno.json
similarity index 100%
rename from deno.json
rename to 2021/deno.json
diff --git a/utils.ts b/2021/utils.ts
similarity index 100%
rename from utils.ts
rename to 2021/utils.ts
diff --git a/2022/.cargo/config b/2022/.cargo/config
new file mode 100644
index 0000000..b8ad9a6
--- /dev/null
+++ b/2022/.cargo/config
@@ -0,0 +1,5 @@
+[build]
+rustflags = ["-C", "target-cpu=native"]
+
+[alias]
+rr = "run --release"
diff --git a/2022/.editorconfig b/2022/.editorconfig
new file mode 100644
index 0000000..c075e7e
--- /dev/null
+++ b/2022/.editorconfig
@@ -0,0 +1,16 @@
+# EditorConfig is awesome: http://EditorConfig.org
+root = true
+
+[*]
+indent_size = 4
+indent_style = space
+end_of_line = lf
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.txt]
+insert_final_newline = false
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/2022/.gitignore b/2022/.gitignore
new file mode 100644
index 0000000..81498f2
--- /dev/null
+++ b/2022/.gitignore
@@ -0,0 +1,20 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+
+# Added by cargo
+
+/target
+
+# Advent of Code
+# @see https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3
+inputs
+!inputs/.keep
diff --git a/2022/.vscode/launch.json b/2022/.vscode/launch.json
new file mode 100644
index 0000000..919d015
--- /dev/null
+++ b/2022/.vscode/launch.json
@@ -0,0 +1,64 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug unit tests in executable 'aoc'",
+ "cargo": {
+ "args": [
+ "test",
+ "--no-run",
+ "--bin=aoc",
+ "--package=aoc"
+ ],
+ "filter": {
+ "name": "aoc",
+ "kind": "bin"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug executable 'aoc'",
+ "cargo": {
+ "args": [
+ "build",
+ "--bin=aoc",
+ "--package=aoc"
+ ],
+ "filter": {
+ "name": "aoc",
+ "kind": "bin"
+ }
+ },
+ "args": ["1"],
+ "cwd": "${workspaceFolder}"
+ },
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug unit tests in library 'aoc'",
+ "cargo": {
+ "args": [
+ "test",
+ "--no-run",
+ "--lib",
+ "--package=aoc"
+ ],
+ "filter": {
+ "name": "aoc",
+ "kind": "lib"
+ }
+ },
+ "args": [],
+ "cwd": "${workspaceFolder}"
+ }
+ ]
+}
diff --git a/2022/Cargo.lock b/2022/Cargo.lock
new file mode 100644
index 0000000..a012b5f
--- /dev/null
+++ b/2022/Cargo.lock
@@ -0,0 +1,25 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aoc"
+version = "0.1.0"
+dependencies = [
+ "itertools",
+]
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
diff --git a/2022/Cargo.toml b/2022/Cargo.toml
new file mode 100644
index 0000000..b8cff29
--- /dev/null
+++ b/2022/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "aoc"
+version = "0.1.0"
+authors = ["Felix SpΓΆttel <1682504+fspoettel@users.noreply.github.com>"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[profile.release]
+lto = "thin"
+
+[dependencies]
+itertools = "0.10.1"
diff --git a/2022/LICENSE b/2022/LICENSE
new file mode 100644
index 0000000..b97fd05
--- /dev/null
+++ b/2022/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Felix Spoettel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/2022/README.md b/2022/README.md
new file mode 100644
index 0000000..3cf937f
--- /dev/null
+++ b/2022/README.md
@@ -0,0 +1,121 @@
+
+
+# π [Advent of Code](https://adventofcode.com/)
+
+![Language](https://badgen.net/badge/Language/Rust/orange)
+
+
+
+## 2022 Results
+
+| Day | Part 1 | Part 2 |
+| :--------------------------------------------: | :----: | :----: |
+| [Day 1](https://adventofcode.com/2021/day/1) | β | β |
+| [Day 2](https://adventofcode.com/2021/day/2) | β | β |
+| [Day 3](https://adventofcode.com/2021/day/3) | β | β |
+| [Day 4](https://adventofcode.com/2021/day/4) | β | β |
+| [Day 5](https://adventofcode.com/2021/day/5) | β | β |
+| [Day 6](https://adventofcode.com/2021/day/6) | β | β |
+| [Day 7](https://adventofcode.com/2021/day/7) | β | β |
+| [Day 8](https://adventofcode.com/2021/day/8) | β | β |
+| [Day 9](https://adventofcode.com/2021/day/9) | β | β |
+| [Day 10](https://adventofcode.com/2021/day/10) | β | β |
+| [Day 11](https://adventofcode.com/2021/day/11) | β | β |
+| [Day 12](https://adventofcode.com/2021/day/12) | β | β |
+| [Day 13](https://adventofcode.com/2021/day/13) | β | β |
+| [Day 14](https://adventofcode.com/2021/day/14) | β | β |
+| [Day 15](https://adventofcode.com/2021/day/15) | β | β |
+| [Day 16](https://adventofcode.com/2021/day/16) | β | β |
+| [Day 17](https://adventofcode.com/2021/day/17) | β | β |
+| [Day 18](https://adventofcode.com/2021/day/18) | β | β |
+| [Day 19](https://adventofcode.com/2021/day/19) | β | β |
+| [Day 20](https://adventofcode.com/2021/day/20) | β | β |
+| [Day 21](https://adventofcode.com/2021/day/21) | β | β |
+| [Day 22](https://adventofcode.com/2021/day/22) | β | β |
+| [Day 23](https://adventofcode.com/2021/day/23) | β | β |
+| [Day 24](https://adventofcode.com/2021/day/24) | β | β |
+| [Day 25](https://adventofcode.com/2021/day/25) | β | β |
+
+
+
+---
+
+Generated with the [advent-of-code-rust](https://github.com/fspoettel/advent-of-code-rust) template.
+
+## Commands
+
+### Setup new day
+
+```sh
+# example: `./scripts/scaffold.sh 1`
+./scripts/scaffold.sh
+
+# output:
+# Created module `src/solutions/day01.rs`
+# Created input file `src/inputs/day01.txt`
+# Created example file `src/examples/day01.txt`
+# Linked new module in `src/main.rs`
+# Linked new module in `src/solutions/mod.rs`
+# Done! π
+```
+
+Every solution file has _unit tests_ referencing the example input file. You can use these tests to develop and debug your solution. When editing a solution file, `rust-analyzer` will display buttons for these actions above the unit tests.
+
+### Download inputs for a day
+
+```sh
+# example: `./scripts/download.sh 1`
+./scripts/download.sh
+
+# output:
+# Invoking `aoc` cli...
+# Loaded session cookie from "/home/foo/.adventofcode.session".
+# Downloading input for day 1, 2021...
+# Saving puzzle input to "/tmp/..."...
+# Done!
+# Wrote input to `src/inputs/day01.txt`...
+# Done! π
+```
+
+Puzzle inputs are not checked into git. [See here](https://old.reddit.com/r/adventofcode/comments/k99rod/sharing_input_data_were_we_requested_not_to/gf2ukkf/?context=3) why.
+
+### Run solutions for a day
+
+```sh
+# example: `cargo run 1`
+cargo run
+
+# output:
+# Running `target/debug/aoc 1`
+# ----
+#
+# π Part 1 π
+#
+# 6 (elapsed: 37.03Β΅s)
+#
+# π Part 2 π
+#
+# 9 (elapsed: 33.18Β΅s)
+#
+# ----
+```
+
+To run an optimized version for benchmarking, use the `--release` flag or the alias `cargo rr `.
+
+### Run all solutions against example input
+
+```sh
+cargo test
+```
+
+### Format code
+
+```sh
+cargo fmt
+```
+
+### Lint code
+
+```sh
+cargo clippy
+```
diff --git a/2022/assets/banner.png b/2022/assets/banner.png
new file mode 100644
index 0000000..36ca006
Binary files /dev/null and b/2022/assets/banner.png differ
diff --git a/2022/assets/christmas_ferris.png b/2022/assets/christmas_ferris.png
new file mode 100644
index 0000000..365527a
Binary files /dev/null and b/2022/assets/christmas_ferris.png differ
diff --git a/2022/scripts/download.sh b/2022/scripts/download.sh
new file mode 100755
index 0000000..2860a0e
--- /dev/null
+++ b/2022/scripts/download.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+set -e;
+
+if ! command -v 'aoc' &> /dev/null
+then
+ echo "command \`aoc\` not found. Try running \`cargo install aoc-cli\` to install it."
+ exit 1
+fi
+
+if [ ! -n "$1" ]; then
+ >&2 echo "Argument is required for day."
+ exit 1
+fi
+
+day=$(echo $1 | sed 's/^0*//');
+day_padded=`printf %02d $day`;
+
+filename="day$day_padded";
+input_path="src/inputs/$filename.txt";
+
+tmp_dir=$(mktemp -d);
+tmp_file_path="$tmp_dir/input";
+
+aoc download --day $day --file $tmp_file_path;
+cat $tmp_file_path > $input_path;
+echo "Wrote input to \"$input_path\"...";
+
+cat <&2 echo "Argument is required for day."
+ exit 1
+fi
+
+day=$(echo $1 | sed 's/^0*//');
+day_padded=`printf %02d $day`;
+
+filename="day$day_padded";
+
+input_path="src/inputs/$filename.txt";
+example_path="src/examples/$filename.txt";
+module_path="src/solutions/$filename.rs";
+
+touch $module_path;
+
+cat > $module_path < u32 {
+ 0
+}
+
+pub fn part_two(input: &str) -> u32 {
+ 0
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", day);
+ assert_eq!(part_one(&input), 0);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", day);
+ assert_eq!(part_two(&input), 0);
+ }
+}
+EOF
+
+perl -pi -e "s,day,$day,g" $module_path;
+
+echo "Created module \"$module_path\"";
+
+touch $input_path;
+echo "Created input file \"$input_path\"";
+
+touch $example_path;
+echo "Created example file \"$example_path\"";
+
+line=" $day => solve_day!($filename, &input),"
+perl -pi -le "print '$line' if(/^*.day not solved/);" "src/main.rs";
+
+echo "Linked new module in \"src/main.rs\"";
+
+LINE="pub mod $filename;";
+FILE="src/solutions/mod.rs";
+grep -qF -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE";
+echo "Linked new module in \"$FILE\"";
+
+
+cat < 5,9
+8,0 -> 0,8
+9,4 -> 3,4
+2,2 -> 2,1
+7,0 -> 7,4
+6,4 -> 2,0
+0,9 -> 2,9
+3,4 -> 1,4
+0,0 -> 8,8
+5,5 -> 8,2
\ No newline at end of file
diff --git a/2022/src/examples/day06.txt b/2022/src/examples/day06.txt
new file mode 100644
index 0000000..a7af2b1
--- /dev/null
+++ b/2022/src/examples/day06.txt
@@ -0,0 +1 @@
+3,4,3,1,2
\ No newline at end of file
diff --git a/2022/src/examples/day07.txt b/2022/src/examples/day07.txt
new file mode 100644
index 0000000..2bdd92f
--- /dev/null
+++ b/2022/src/examples/day07.txt
@@ -0,0 +1 @@
+16,1,2,0,4,2,7,1,2,14
\ No newline at end of file
diff --git a/2022/src/examples/day08.txt b/2022/src/examples/day08.txt
new file mode 100644
index 0000000..8614893
--- /dev/null
+++ b/2022/src/examples/day08.txt
@@ -0,0 +1,10 @@
+be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe
+edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc
+fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg
+fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb
+aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea
+fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb
+dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe
+bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef
+egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb
+gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce
\ No newline at end of file
diff --git a/2022/src/examples/day09.txt b/2022/src/examples/day09.txt
new file mode 100644
index 0000000..610bad9
--- /dev/null
+++ b/2022/src/examples/day09.txt
@@ -0,0 +1,5 @@
+2199943210
+3987894921
+9856789892
+8767896789
+9899965678
\ No newline at end of file
diff --git a/2022/src/examples/day10.txt b/2022/src/examples/day10.txt
new file mode 100644
index 0000000..2f182d8
--- /dev/null
+++ b/2022/src/examples/day10.txt
@@ -0,0 +1,10 @@
+[({(<(())[]>[[{[]{<()<>>
+[(()[<>])]({[<{<<[]>>(
+{([(<{}[<>[]}>{[]{[(<()>
+(((({<>}<{<{<>}{[]{[]{}
+[[<[([]))<([[{}[[()]]]
+[{[{({}]{}}([{[{{{}}([]
+{<[[]]>}<{[{[{[]{()[[[]
+[<(<(<(<{}))><([]([]()
+<{([([[(<>()){}]>(<<{{
+<{([{{}}[<[[[<>{}]]]>[]]
\ No newline at end of file
diff --git a/2022/src/examples/day11.txt b/2022/src/examples/day11.txt
new file mode 100644
index 0000000..a3819c9
--- /dev/null
+++ b/2022/src/examples/day11.txt
@@ -0,0 +1,10 @@
+5483143223
+2745854711
+5264556173
+6141336146
+6357385478
+4167524645
+2176841721
+6882881134
+4846848554
+5283751526
\ No newline at end of file
diff --git a/2022/src/examples/day12.txt b/2022/src/examples/day12.txt
new file mode 100644
index 0000000..da6e083
--- /dev/null
+++ b/2022/src/examples/day12.txt
@@ -0,0 +1,18 @@
+fs-end
+he-DX
+fs-he
+start-DX
+pj-DX
+end-zg
+zg-sl
+zg-pj
+pj-he
+RW-he
+fs-DX
+pj-RW
+zg-RW
+start-pj
+he-WI
+zg-he
+pj-fs
+start-RW
\ No newline at end of file
diff --git a/2022/src/examples/day13.txt b/2022/src/examples/day13.txt
new file mode 100644
index 0000000..32a8563
--- /dev/null
+++ b/2022/src/examples/day13.txt
@@ -0,0 +1,21 @@
+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
+
+fold along y=7
+fold along x=5
\ No newline at end of file
diff --git a/2022/src/examples/day14.txt b/2022/src/examples/day14.txt
new file mode 100644
index 0000000..6c1c3a1
--- /dev/null
+++ b/2022/src/examples/day14.txt
@@ -0,0 +1,18 @@
+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
\ No newline at end of file
diff --git a/2022/src/examples/day15.txt b/2022/src/examples/day15.txt
new file mode 100644
index 0000000..7d9d562
--- /dev/null
+++ b/2022/src/examples/day15.txt
@@ -0,0 +1,10 @@
+1163751742
+1381373672
+2136511328
+3694931569
+7463417111
+1319128137
+1359912421
+3125421639
+1293138521
+2311944581
\ No newline at end of file
diff --git a/2022/src/examples/day17.txt b/2022/src/examples/day17.txt
new file mode 100644
index 0000000..f40609b
--- /dev/null
+++ b/2022/src/examples/day17.txt
@@ -0,0 +1 @@
+target area: x=20..30, y=-10..-5
\ No newline at end of file
diff --git a/2022/src/examples/day18.txt b/2022/src/examples/day18.txt
new file mode 100644
index 0000000..2efedbf
--- /dev/null
+++ b/2022/src/examples/day18.txt
@@ -0,0 +1,10 @@
+[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]
+[[[5,[2,8]],4],[5,[[9,9],0]]]
+[6,[[[6,2],[5,6]],[[7,6],[4,7]]]]
+[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]]
+[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]]
+[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]]
+[[[[5,4],[7,7]],8],[[8,3],8]]
+[[9,3],[[9,9],[6,[4,9]]]]
+[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]
+[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]
\ No newline at end of file
diff --git a/2022/src/examples/day19.txt b/2022/src/examples/day19.txt
new file mode 100644
index 0000000..b596cc4
--- /dev/null
+++ b/2022/src/examples/day19.txt
@@ -0,0 +1,136 @@
+--- scanner 0 ---
+404,-588,-901
+528,-643,409
+-838,591,734
+390,-675,-793
+-537,-823,-458
+-485,-357,347
+-345,-311,381
+-661,-816,-575
+-876,649,763
+-618,-824,-621
+553,345,-567
+474,580,667
+-447,-329,318
+-584,868,-557
+544,-627,-890
+564,392,-477
+455,729,728
+-892,524,684
+-689,845,-530
+423,-701,434
+7,-33,-71
+630,319,-379
+443,580,662
+-789,900,-551
+459,-707,401
+
+--- scanner 1 ---
+686,422,578
+605,423,415
+515,917,-361
+-336,658,858
+95,138,22
+-476,619,847
+-340,-569,-846
+567,-361,727
+-460,603,-452
+669,-402,600
+729,430,532
+-500,-761,534
+-322,571,750
+-466,-666,-811
+-429,-592,574
+-355,545,-477
+703,-491,-529
+-328,-685,520
+413,935,-424
+-391,539,-444
+586,-435,557
+-364,-763,-893
+807,-499,-711
+755,-354,-619
+553,889,-390
+
+--- scanner 2 ---
+649,640,665
+682,-795,504
+-784,533,-524
+-644,584,-595
+-588,-843,648
+-30,6,44
+-674,560,763
+500,723,-460
+609,671,-379
+-555,-800,653
+-675,-892,-343
+697,-426,-610
+578,704,681
+493,664,-388
+-671,-858,530
+-667,343,800
+571,-461,-707
+-138,-166,112
+-889,563,-600
+646,-828,498
+640,759,510
+-630,509,768
+-681,-892,-333
+673,-379,-804
+-742,-814,-386
+577,-820,562
+
+--- scanner 3 ---
+-589,542,597
+605,-692,669
+-500,565,-823
+-660,373,557
+-458,-679,-417
+-488,449,543
+-626,468,-788
+338,-750,-386
+528,-832,-391
+562,-778,733
+-938,-730,414
+543,643,-506
+-524,371,-870
+407,773,750
+-104,29,83
+378,-903,-323
+-778,-728,485
+426,699,580
+-438,-605,-362
+-469,-447,-387
+509,732,623
+647,635,-688
+-868,-804,481
+614,-800,639
+595,780,-596
+
+--- scanner 4 ---
+727,592,562
+-293,-554,779
+441,611,-461
+-714,465,-776
+-743,427,-804
+-660,-479,-426
+832,-632,460
+927,-485,-438
+408,393,-506
+466,436,-512
+110,16,151
+-258,-428,682
+-393,719,612
+-211,-452,876
+808,-476,-593
+-575,615,604
+-485,667,467
+-680,325,-822
+-627,-443,-432
+872,-547,-609
+833,512,582
+807,604,487
+839,-516,451
+891,-625,532
+-652,-548,-490
+30,-46,-14
\ No newline at end of file
diff --git a/2022/src/examples/day20.txt b/2022/src/examples/day20.txt
new file mode 100644
index 0000000..000a554
--- /dev/null
+++ b/2022/src/examples/day20.txt
@@ -0,0 +1,7 @@
+..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..#
+
+#..#.
+#....
+##..#
+..#..
+..###
\ No newline at end of file
diff --git a/2022/src/examples/day21.txt b/2022/src/examples/day21.txt
new file mode 100644
index 0000000..3f69194
--- /dev/null
+++ b/2022/src/examples/day21.txt
@@ -0,0 +1,2 @@
+Player 1 starting position: 4
+Player 2 starting position: 8
diff --git a/2022/src/examples/day22.txt b/2022/src/examples/day22.txt
new file mode 100644
index 0000000..2790bed
--- /dev/null
+++ b/2022/src/examples/day22.txt
@@ -0,0 +1,60 @@
+on x=-5..47,y=-31..22,z=-19..33
+on x=-44..5,y=-27..21,z=-14..35
+on x=-49..-1,y=-11..42,z=-10..38
+on x=-20..34,y=-40..6,z=-44..1
+off x=26..39,y=40..50,z=-2..11
+on x=-41..5,y=-41..6,z=-36..8
+off x=-43..-33,y=-45..-28,z=7..25
+on x=-33..15,y=-32..19,z=-34..11
+off x=35..47,y=-46..-34,z=-11..5
+on x=-14..36,y=-6..44,z=-16..29
+on x=-57795..-6158,y=29564..72030,z=20435..90618
+on x=36731..105352,y=-21140..28532,z=16094..90401
+on x=30999..107136,y=-53464..15513,z=8553..71215
+on x=13528..83982,y=-99403..-27377,z=-24141..23996
+on x=-72682..-12347,y=18159..111354,z=7391..80950
+on x=-1060..80757,y=-65301..-20884,z=-103788..-16709
+on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856
+on x=-52752..22273,y=-49450..9096,z=54442..119054
+on x=-29982..40483,y=-108474..-28371,z=-24328..38471
+on x=-4958..62750,y=40422..118853,z=-7672..65583
+on x=55694..108686,y=-43367..46958,z=-26781..48729
+on x=-98497..-18186,y=-63569..3412,z=1232..88485
+on x=-726..56291,y=-62629..13224,z=18033..85226
+on x=-110886..-34664,y=-81338..-8658,z=8914..63723
+on x=-55829..24974,y=-16897..54165,z=-121762..-28058
+on x=-65152..-11147,y=22489..91432,z=-58782..1780
+on x=-120100..-32970,y=-46592..27473,z=-11695..61039
+on x=-18631..37533,y=-124565..-50804,z=-35667..28308
+on x=-57817..18248,y=49321..117703,z=5745..55881
+on x=14781..98692,y=-1341..70827,z=15753..70151
+on x=-34419..55919,y=-19626..40991,z=39015..114138
+on x=-60785..11593,y=-56135..2999,z=-95368..-26915
+on x=-32178..58085,y=17647..101866,z=-91405..-8878
+on x=-53655..12091,y=50097..105568,z=-75335..-4862
+on x=-111166..-40997,y=-71714..2688,z=5609..50954
+on x=-16602..70118,y=-98693..-44401,z=5197..76897
+on x=16383..101554,y=4615..83635,z=-44907..18747
+off x=-95822..-15171,y=-19987..48940,z=10804..104439
+on x=-89813..-14614,y=16069..88491,z=-3297..45228
+on x=41075..99376,y=-20427..49978,z=-52012..13762
+on x=-21330..50085,y=-17944..62733,z=-112280..-30197
+on x=-16478..35915,y=36008..118594,z=-7885..47086
+off x=-98156..-27851,y=-49952..43171,z=-99005..-8456
+off x=2032..69770,y=-71013..4824,z=7471..94418
+on x=43670..120875,y=-42068..12382,z=-24787..38892
+off x=37514..111226,y=-45862..25743,z=-16714..54663
+off x=25699..97951,y=-30668..59918,z=-15349..69697
+off x=-44271..17935,y=-9516..60759,z=49131..112598
+on x=-61695..-5813,y=40978..94975,z=8655..80240
+off x=-101086..-9439,y=-7088..67543,z=33935..83858
+off x=18020..114017,y=-48931..32606,z=21474..89843
+off x=-77139..10506,y=-89994..-18797,z=-80..59318
+off x=8476..79288,y=-75520..11602,z=-96624..-24783
+on x=-47488..-1262,y=24338..100707,z=16292..72967
+off x=-84341..13987,y=2429..92914,z=-90671..-1318
+off x=-37810..49457,y=-71013..-7894,z=-105357..-13188
+off x=-27365..46395,y=31009..98017,z=15428..76570
+off x=-70369..-16548,y=22648..78696,z=-1892..86821
+on x=-53470..21291,y=-120233..-33476,z=-44150..38147
+off x=-93533..-4276,y=-16170..68771,z=-104985..-24507
\ No newline at end of file
diff --git a/2022/src/examples/day24.txt b/2022/src/examples/day24.txt
new file mode 100644
index 0000000..b5b5961
--- /dev/null
+++ b/2022/src/examples/day24.txt
@@ -0,0 +1,11 @@
+inp w
+add z w
+mod z 2
+div w 2
+add y w
+mod y 2
+div w 2
+add x w
+mod x 2
+div w 2
+mod w 2
\ No newline at end of file
diff --git a/2022/src/examples/day25.txt b/2022/src/examples/day25.txt
new file mode 100644
index 0000000..73a37cc
--- /dev/null
+++ b/2022/src/examples/day25.txt
@@ -0,0 +1,9 @@
+v...>>.vv>
+.vv>>.vv..
+>>.>v>...v
+>>v>>.>.v.
+v>v.vv.v..
+>.>>..v...
+.vv..>.>v.
+v.v..>>v.v
+....v..v.>
\ No newline at end of file
diff --git a/2022/src/helpers/grid.rs b/2022/src/helpers/grid.rs
new file mode 100644
index 0000000..1448977
--- /dev/null
+++ b/2022/src/helpers/grid.rs
@@ -0,0 +1,63 @@
+use std::fmt::Debug;
+
+/// A point describes a location `x, y` in a grid with two axis.
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct Point(pub usize, pub usize);
+
+impl Point {
+ /// Get a unique id for a point in a given grid.
+ pub fn to_id(self, width: usize) -> usize {
+ self.0 + width * self.1
+ }
+
+ /// Get a point from a unique id in a grid.
+ pub fn from_id(id: usize, width: usize) -> Self {
+ Point(id % width, id / width)
+ }
+
+ /// Get all neighbors for a point in a grid, respecting the boundaries of the input.
+ pub fn neighbors(self, max_x: usize, max_y: usize, include_diagonals: bool) -> Vec {
+ let mut neighbors: Vec = Vec::new();
+ let Point(x, y) = self;
+
+ let bound_top = y == 0;
+ let bound_left = x == 0;
+
+ let bound_bottom = y == max_y;
+ let bound_right = x == max_x;
+
+ if !bound_top {
+ neighbors.push(Point(x, y - 1));
+
+ if include_diagonals && !bound_left {
+ neighbors.push(Point(x - 1, y - 1));
+ }
+
+ if include_diagonals && !bound_right {
+ neighbors.push(Point(x + 1, y - 1));
+ }
+ }
+
+ if !bound_bottom {
+ neighbors.push(Point(x, y + 1));
+
+ if include_diagonals && !bound_left {
+ neighbors.push(Point(x - 1, y + 1));
+ }
+
+ if include_diagonals && !bound_right {
+ neighbors.push(Point(x + 1, y + 1));
+ }
+ }
+
+ if !bound_left {
+ neighbors.push(Point(x - 1, y));
+ }
+
+ if !bound_right {
+ neighbors.push(Point(x + 1, y));
+ }
+
+ neighbors
+ }
+}
diff --git a/2022/src/helpers/math.rs b/2022/src/helpers/math.rs
new file mode 100644
index 0000000..b9ad938
--- /dev/null
+++ b/2022/src/helpers/math.rs
@@ -0,0 +1,37 @@
+/// get a vector's median value.
+/// the median is the value separating the higher half from the lower half of a data sample.
+/// [Wikipedia](https://en.wikipedia.org/wiki/Median)
+pub fn median(vec: &mut Vec) -> u64 {
+ let len = vec.len();
+ let mid = len / 2;
+
+ vec.sort_unstable();
+
+ if len % 2 == 0 {
+ (vec[mid - 1] + vec[mid]) / 2
+ } else {
+ vec[mid]
+ }
+}
+
+/// sum a sequence of integers. (e.g. `1, 2, 3, 4`)
+/// [Wikipedia](https://en.wikipedia.org/wiki/Triangular_number)
+pub fn nth_triangular(a: u64) -> u64 {
+ a * (a + 1) / 2
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_median() {
+ assert_eq!(median(&mut [1, 4, 7].to_vec()), 4);
+ assert_eq!(median(&mut [3, 10, 36, 255, 79, 24, 5, 8].to_vec()), 17);
+ }
+
+ #[test]
+ fn test_nth_triangular() {
+ assert_eq!(nth_triangular(7), 28);
+ }
+}
diff --git a/2022/src/helpers/mod.rs b/2022/src/helpers/mod.rs
new file mode 100644
index 0000000..e25af20
--- /dev/null
+++ b/2022/src/helpers/mod.rs
@@ -0,0 +1,2 @@
+pub mod grid;
+pub mod math;
diff --git a/2022/src/lib.rs b/2022/src/lib.rs
new file mode 100644
index 0000000..4696f5c
--- /dev/null
+++ b/2022/src/lib.rs
@@ -0,0 +1,14 @@
+use std::env;
+use std::fs;
+
+pub fn read_file(folder: &str, day: u8) -> String {
+ let cwd = env::current_dir().unwrap();
+
+ let filepath = cwd
+ .join("src")
+ .join(folder)
+ .join(format!("day{:02}.txt", day));
+
+ let f = fs::read_to_string(filepath);
+ f.expect("could not open input file")
+}
diff --git a/2022/src/main.rs b/2022/src/main.rs
new file mode 100644
index 0000000..8444f0a
--- /dev/null
+++ b/2022/src/main.rs
@@ -0,0 +1,75 @@
+use crate::solutions::*;
+use aoc::read_file;
+use std::env;
+use std::fmt::Display;
+use std::time::Instant;
+
+mod helpers;
+mod solutions;
+
+static ANSI_ITALIC: &str = "\x1b[3m";
+static ANSI_BOLD: &str = "\x1b[1m";
+static ANSI_RESET: &str = "\x1b[0m";
+
+fn print_result(func: impl FnOnce(&str) -> T, input: &str) {
+ let timer = Instant::now();
+ let result = func(input);
+ let time = timer.elapsed();
+ println!(
+ "{} {}(elapsed: {:.2?}){}",
+ result, ANSI_ITALIC, time, ANSI_RESET
+ );
+}
+
+macro_rules! solve_day {
+ ($day:path, $input:expr) => {{
+ use $day::*;
+ println!("----");
+ println!("");
+ println!("π {}Part 1{} π", ANSI_BOLD, ANSI_RESET);
+ println!("");
+ print_result(part_one, $input);
+ println!("");
+ println!("π {}Part 2{} π", ANSI_BOLD, ANSI_RESET);
+ println!("");
+ print_result(part_two, $input);
+ println!("");
+ println!("----");
+ }};
+}
+
+fn main() {
+ let args: Vec = env::args().collect();
+ let day: u8 = args[1].clone().parse().unwrap();
+ let input = read_file("inputs", day);
+
+ match day {
+ 1 => solve_day!(day01, &input),
+ 2 => solve_day!(day02, &input),
+ 3 => solve_day!(day03, &input),
+ 4 => solve_day!(day04, &input),
+ 5 => solve_day!(day05, &input),
+ 6 => solve_day!(day06, &input),
+ 7 => solve_day!(day07, &input),
+ 8 => solve_day!(day08, &input),
+ 9 => solve_day!(day09, &input),
+ 10 => solve_day!(day10, &input),
+ 11 => solve_day!(day11, &input),
+ 12 => solve_day!(day12, &input),
+ 13 => solve_day!(day13, &input),
+ 14 => solve_day!(day14, &input),
+ 15 => solve_day!(day15, &input),
+ 16 => solve_day!(day16, &input),
+ 17 => solve_day!(day17, &input),
+ 18 => solve_day!(day18, &input),
+ 19 => solve_day!(day19, &input),
+ 20 => solve_day!(day20, &input),
+ 21 => solve_day!(day21, &input),
+ 22 => solve_day!(day22, &input),
+ 23 => solve_day!(day23, &input),
+ 24 => solve_day!(day24, &input),
+ 25 => solve_day!(day25, &input),
+ 1 => solve_day!(day01, &input),
+ _ => println!("day not solved: {}", day),
+ }
+}
diff --git a/2022/src/solutions/.keep b/2022/src/solutions/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/2022/src/solutions/day01.rs b/2022/src/solutions/day01.rs
new file mode 100644
index 0000000..f011ec9
--- /dev/null
+++ b/2022/src/solutions/day01.rs
@@ -0,0 +1,26 @@
+pub fn part_one(input: &str) -> u32 {
+ 0
+}
+
+pub fn part_two(input: &str) -> u32 {
+ 0
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 1);
+ assert_eq!(part_one(&input), 0);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 1);
+ assert_eq!(part_two(&input), 0);
+ }
+}
diff --git a/2022/src/solutions/day02.rs b/2022/src/solutions/day02.rs
new file mode 100644
index 0000000..29da7dd
--- /dev/null
+++ b/2022/src/solutions/day02.rs
@@ -0,0 +1,76 @@
+struct Instruction<'a> {
+ direction: &'a str,
+ value: i32,
+}
+
+struct Position {
+ x: i32,
+ y: i32,
+ aim: i32,
+}
+
+fn to_instruction(line: &str) -> Instruction {
+ let (direction, _value) = line.split_once(' ').unwrap();
+
+ Instruction {
+ direction,
+ value: _value.parse().unwrap(),
+ }
+}
+
+fn update_position(pos: Position, Instruction { direction, value }: Instruction) -> Position {
+ match direction {
+ "forward" => Position {
+ x: pos.x + value,
+ y: pos.y + pos.aim * value,
+ ..pos
+ },
+ "down" => Position {
+ aim: pos.aim + value,
+ ..pos
+ },
+ "up" => Position {
+ aim: pos.aim - value,
+ ..pos
+ },
+ val => panic!("bad direction input: {}", val),
+ }
+}
+
+pub fn part_one(input: &str) -> i32 {
+ let pos = input
+ .lines()
+ .map(to_instruction)
+ .fold(Position { x: 0, y: 0, aim: 0 }, update_position);
+
+ // optimization: `aim` in part two mirrors `depth` in part one which allows us to reuse the positioning logic.
+ pos.x * pos.aim
+}
+
+pub fn part_two(input: &str) -> i32 {
+ let pos = input
+ .lines()
+ .map(to_instruction)
+ .fold(Position { x: 0, y: 0, aim: 0 }, update_position);
+
+ pos.x * pos.y
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 2);
+ assert_eq!(part_one(&input), 150);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 2);
+ assert_eq!(part_two(&input), 900);
+ }
+}
diff --git a/2022/src/solutions/day03.rs b/2022/src/solutions/day03.rs
new file mode 100644
index 0000000..b3f6456
--- /dev/null
+++ b/2022/src/solutions/day03.rs
@@ -0,0 +1,89 @@
+use itertools::Itertools;
+use std::collections::HashMap;
+
+pub fn arr_to_int(bits: &[bool]) -> u32 {
+ bits.iter().fold(0, |acc, &b| acc * 2 + (b as u32))
+}
+
+pub fn str_to_int(str: &str) -> u32 {
+ u32::from_str_radix(str, 2).unwrap()
+}
+
+pub fn part_one(input: &str) -> u32 {
+ // counter that maps character indices to signed integers.
+ // we can later calculate the gamma value for a given index by checking the entry's sign.
+ let mut counter: HashMap = HashMap::new();
+
+ // for every character in a line:
+ // increment (1) or decrement (0) the counter entry for this index.
+ input.lines().for_each(|l| {
+ l.chars().enumerate().for_each(|(i, c)| {
+ let val = counter.entry(i).or_default();
+ *val += if c == '1' { 1 } else { -1 };
+ })
+ });
+
+ // collect counter into a sorted byte array.
+ let gamma = counter
+ .keys()
+ .sorted_unstable()
+ .map(|i| *counter.get(i).unwrap() >= 0)
+ .collect_vec();
+
+ // derive epsilon by flipping each bit of gamma.
+ let epsilon = gamma.iter().map(|b| !(*b)).collect_vec();
+
+ arr_to_int(&gamma) * arr_to_int(&epsilon)
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let lines = input.lines().collect_vec();
+
+ let oxy_rating =
+ find_line_by_bit_criteria(|a, b| if a.len() >= b.len() { a } else { b }, &lines);
+
+ let co2_rating =
+ find_line_by_bit_criteria(|a, b| if a.len() >= b.len() { b } else { a }, &lines);
+
+ str_to_int(oxy_rating) * str_to_int(co2_rating)
+}
+
+fn find_line_by_bit_criteria<'a>(
+ bit_criteria: impl Fn(Vec<&'a str>, Vec<&'a str>) -> Vec<&'a str>,
+ candidates: &[&'a str],
+) -> &'a str {
+ let mut i = 0;
+ let mut survivors = candidates.to_vec();
+
+ while survivors.len() > 1 {
+ // partition lines by the dominant bit (1 or 0) in column `i`.
+ let (one_dominant, zero_dominant): (Vec<&str>, Vec<&str>) = survivors
+ .iter()
+ .partition(|s| s.chars().nth(i).unwrap() == '1');
+
+ // determine which group should be continued with and assign it as survivors, discarding the rest.
+ survivors = bit_criteria(one_dominant, zero_dominant);
+ i += 1;
+ }
+
+ survivors.pop().unwrap()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 3);
+ assert_eq!(part_one(&input), 198);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 3);
+ assert_eq!(part_two(&input), 230);
+ }
+}
diff --git a/2022/src/solutions/day04.rs b/2022/src/solutions/day04.rs
new file mode 100644
index 0000000..d45d4dc
--- /dev/null
+++ b/2022/src/solutions/day04.rs
@@ -0,0 +1,143 @@
+use std::collections::{HashMap, HashSet};
+
+static BOARD_SIZE: usize = 5;
+
+// boards are stored as a hash_map. this facilitates the removal of winning boards in the game loop.
+struct Board {
+ // holds unique numbers on this board in a set. this makes summing uncrossed numbers easy and efficient.
+ nums: HashSet,
+ // holds both `horizontal` and `vertical` rows of ths board.
+ // optimization: pre-calculate a 'transposed' set of rows & columns once in the beginning.
+ // optimization: use signed integers for keys (- for column ids) to avoid expensive string operations.
+ rows: HashMap>,
+}
+
+type Boards = HashMap;
+
+pub fn str_to_u32(s: &str) -> u32 {
+ s.trim().parse().unwrap()
+}
+
+fn to_draw(line: &str) -> Vec {
+ line.split(',').map(str_to_u32).collect()
+}
+
+fn to_board(lines: &[&str]) -> Board {
+ lines.iter().enumerate().fold(
+ Board {
+ nums: HashSet::new(),
+ rows: HashMap::new(),
+ },
+ |mut board, (_row, l)| {
+ l.trim()
+ .split_whitespace()
+ .enumerate()
+ .for_each(|(_col, s)| {
+ let parsed: u32 = str_to_u32(s);
+ let col: i32 = _col as i32;
+ let row: i32 = _row as i32;
+ board.nums.insert(parsed);
+ board.rows.entry(-(col + 1)).or_default().push(parsed);
+ board.rows.entry(row + 1).or_default().push(parsed);
+ });
+
+ board
+ },
+ )
+}
+
+fn to_boards<'a>(lines: impl Iterator- ) -> Boards {
+ let mut boards = HashMap::new();
+
+ lines
+ .filter(|l| !l.is_empty())
+ .collect::>()
+ .chunks(BOARD_SIZE)
+ .map(to_board)
+ .enumerate()
+ .for_each(|(i, b)| {
+ boards.insert(i, b);
+ });
+
+ boards
+}
+
+fn find_winners(draw: &[u32], boards: &Boards, i: usize) -> Vec<(usize, u32)> {
+ let current_draw = &draw[i - 1];
+ let draws = &draw[..i];
+
+ boards
+ .iter()
+ .filter(|(_, b)| {
+ // optimization: skip board processing if it does not contain the drawn number.
+ b.nums.contains(current_draw)
+ // check if any row consists of crossed numbers only.
+ && b.rows.values().any(|v| v.iter().all(|n| draws.contains(n)))
+ })
+ .map(|(key, b)| {
+ let uncrossed_nums: u32 = b.nums.iter().filter(|n| !draws.contains(n)).sum();
+ (*key, current_draw * uncrossed_nums)
+ })
+ .collect()
+}
+
+fn find_first_winner(draw: &[u32], boards: &mut Boards, i: usize) -> u32 {
+ let winners = find_winners(draw, boards, i);
+
+ if winners.is_empty() {
+ find_first_winner(draw, boards, i + 1)
+ } else {
+ let (_, score) = winners.first().unwrap();
+ *score
+ }
+}
+
+fn find_last_winner(draw: &[u32], boards: &mut Boards) -> u32 {
+ let mut winners: Vec = Vec::new();
+
+ for i in BOARD_SIZE..draw.len() {
+ find_winners(draw, boards, i)
+ .iter()
+ .for_each(|(key, score)| {
+ boards.remove(key);
+ winners.push(*score);
+ });
+ }
+
+ *winners.last().unwrap()
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let mut lines = input.lines();
+ let draw: Vec = to_draw(lines.next().unwrap());
+ let mut boards = to_boards(lines);
+
+ find_first_winner(&draw, &mut boards, BOARD_SIZE)
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let mut lines = input.lines();
+ let draw: Vec = to_draw(lines.next().unwrap());
+ let mut boards = to_boards(lines);
+
+ find_last_winner(&draw, &mut boards)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 4);
+ assert_eq!(part_one(&input), 4512);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 4);
+ assert_eq!(part_two(&input), 1924);
+ }
+}
diff --git a/2022/src/solutions/day05.rs b/2022/src/solutions/day05.rs
new file mode 100644
index 0000000..3571039
--- /dev/null
+++ b/2022/src/solutions/day05.rs
@@ -0,0 +1,92 @@
+use std::{collections::HashMap, convert::TryInto};
+
+struct Point {
+ x: i32,
+ y: i32,
+}
+
+type Line = (Point, Point);
+type Grid = HashMap<(i32, i32), u32>;
+
+trait PointGrid {
+ fn add_point(&mut self, x: i32, y: i32);
+ fn add_points(&mut self, line: &str, skip_diagonals: bool);
+ fn overlaps(&self) -> u32;
+}
+
+impl PointGrid for Grid {
+ fn add_point(&mut self, x: i32, y: i32) {
+ *self.entry((x, y)).or_default() += 1;
+ }
+
+ fn add_points(&mut self, line: &str, skip_diagonals: bool) {
+ let (p1, p2) = parse_line(line);
+
+ if skip_diagonals && (p1.x != p2.x && p1.y != p2.y) {
+ return;
+ }
+
+ let mut x = p1.x;
+ let mut y = p1.y;
+
+ let dx = (p2.x - p1.x).signum();
+ let dy = (p2.y - p1.y).signum();
+
+ while (x, y) != (p2.x + dx, p2.y + dy) {
+ *self.entry((x, y)).or_default() += 1;
+ x += dx;
+ y += dy;
+ }
+ }
+
+ fn overlaps(&self) -> u32 {
+ self.values()
+ .filter(|v| **v > 1)
+ .count()
+ .try_into()
+ .unwrap()
+ }
+}
+
+fn parse_line(l: &str) -> Line {
+ let mut parts = l.split(" -> ").map(|p| {
+ let mut nums = p.split(',').map(|x| x.parse().unwrap());
+ Point {
+ x: nums.next().unwrap(),
+ y: nums.next().unwrap(),
+ }
+ });
+
+ (parts.next().unwrap(), parts.next().unwrap())
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let mut grid: Grid = HashMap::new();
+ input.lines().for_each(|l| grid.add_points(l, true));
+ grid.overlaps()
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let mut grid: Grid = HashMap::new();
+ input.lines().for_each(|l| grid.add_points(l, false));
+ grid.overlaps()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 5);
+ assert_eq!(part_one(&input), 5);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 5);
+ assert_eq!(part_two(&input), 12);
+ }
+}
diff --git a/2022/src/solutions/day06.rs b/2022/src/solutions/day06.rs
new file mode 100644
index 0000000..b3a3a0d
--- /dev/null
+++ b/2022/src/solutions/day06.rs
@@ -0,0 +1,81 @@
+static REPRO_INTERVAL_INITIAL: usize = 9;
+static REPRO_INTERVAL: usize = 7;
+
+fn get_og_fishes(input: &str) -> Vec {
+ input
+ .split(',')
+ .map(|s| s.trim().parse().unwrap())
+ .collect()
+}
+
+fn project_population(members: Vec, generations: &mut [usize]) -> usize {
+ let interval_count = generations.len();
+ let mut pop = members.len();
+
+ // for each fish in the OG generation, spawn an offspring at {rest_timer}.
+ // we have to project their adult life as well since OGs continue reproducing.
+ members.iter().for_each(|f| {
+ generations[*f] += 1;
+ project_adult_generation(generations, 1, *f, interval_count);
+ });
+
+ // a generation of fishes hatches every day.
+ // we can project them as a whole since there is no variance in repro. rate in a generation.
+ for i in 0..interval_count {
+ let generation_size = generations[i];
+ // add hatched fishes to the population counter.
+ pop += generation_size;
+
+ // initial reproduction is delayed for hatched fishes. we handle it with a special case.
+ let adults_at = i + REPRO_INTERVAL_INITIAL;
+
+ if adults_at < interval_count {
+ generations[adults_at] += generation_size;
+ // Once fishes are adults, we can project the rest of {interval_count} in a loop.
+ project_adult_generation(generations, generation_size, adults_at, interval_count);
+ }
+ }
+
+ pop
+}
+
+fn project_adult_generation(
+ generations: &mut [usize],
+ generation_size: usize,
+ current_interval: usize,
+ interval_count: usize,
+) {
+ let mut spawns_at = current_interval + REPRO_INTERVAL;
+ // increase the generation_size at {interval} by the current generation_size for rest of observed interval.
+ while spawns_at < interval_count {
+ generations[spawns_at] += generation_size;
+ spawns_at += REPRO_INTERVAL;
+ }
+}
+
+pub fn part_one(input: &str) -> usize {
+ project_population(get_og_fishes(input), &mut [0; 80])
+}
+
+pub fn part_two(input: &str) -> usize {
+ project_population(get_og_fishes(input), &mut [0; 256])
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 6);
+ assert_eq!(part_one(&input), 5934);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 6);
+ assert_eq!(part_two(&input), 26984457539);
+ }
+}
diff --git a/2022/src/solutions/day07.rs b/2022/src/solutions/day07.rs
new file mode 100644
index 0000000..b6e728b
--- /dev/null
+++ b/2022/src/solutions/day07.rs
@@ -0,0 +1,54 @@
+use crate::helpers::math::{median, nth_triangular};
+
+fn parse(input: &str) -> Vec {
+ input
+ .lines()
+ .next()
+ .unwrap()
+ .split(',')
+ .map(|x| x.parse().unwrap())
+ .collect()
+}
+
+pub fn part_one(input: &str) -> u64 {
+ let mut positions = parse(input);
+ let median = median(&mut positions);
+ positions
+ .iter()
+ .map(|x| (*x as i32 - median as i32).abs() as u64)
+ .sum()
+}
+
+pub fn part_two(input: &str) -> u64 {
+ let mut positions = parse(input);
+ positions.sort_unstable();
+
+ (0..*positions.last().unwrap())
+ .map(|i| {
+ positions
+ .iter()
+ .map(|p| nth_triangular((*p as i32 - i as i32).abs() as u64))
+ .sum()
+ })
+ .min()
+ .unwrap()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 7);
+ assert_eq!(part_one(&input), 37);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 7);
+ assert_eq!(part_two(&input), 168);
+ }
+}
diff --git a/2022/src/solutions/day08.rs b/2022/src/solutions/day08.rs
new file mode 100644
index 0000000..3ccb63f
--- /dev/null
+++ b/2022/src/solutions/day08.rs
@@ -0,0 +1,185 @@
+use std::collections::HashMap;
+
+use itertools::Itertools;
+
+pub fn part_one(input: &str) -> usize {
+ input
+ .lines()
+ .flat_map(|l| {
+ l.split(" | ").last().unwrap().split(' ').filter(|s| {
+ let len = s.len();
+ (2..=4).contains(&len) || len == 7
+ })
+ })
+ .count()
+}
+
+type Pattern = Vec;
+
+/// The display is made up of segments `a-g` mapped as follows:
+/// aaaa
+/// b c
+/// b c
+/// dddd
+/// e f
+/// e f
+/// gggg
+type Display = HashMap;
+
+trait DisplayMethods {
+ fn decode(&self, pattern: &[char]) -> char;
+}
+
+impl DisplayMethods for Display {
+ /// Once a display is fully reconstructed, we can decode digits with it.
+ /// Individual digits are returned as strings since we need to join 4 digits in the caller.
+ fn decode(&self, pattern: &[char]) -> char {
+ if is_one(pattern) {
+ '1'
+ } else if is_four(pattern) {
+ '4'
+ } else if is_seven(pattern) {
+ '7'
+ } else if is_eight(pattern) {
+ '8'
+ } else {
+ let displayed: String = pattern
+ .iter()
+ .map(|c| self.get(c).unwrap())
+ .sorted_unstable()
+ .collect();
+
+ match displayed.as_ref() {
+ "abcefg" => '0',
+ "acdeg" => '2',
+ "acdfg" => '3',
+ "abdfg" => '5',
+ "abdefg" => '6',
+ "abcdfg" => '9',
+ val => panic!("unexpected decoded pattern: {}", val),
+ }
+ }
+ }
+}
+
+// c,f
+fn is_one(pattern: &[char]) -> bool {
+ pattern.len() == 2
+}
+
+// b,c,d,f
+fn is_four(pattern: &[char]) -> bool {
+ pattern.len() == 4
+}
+
+// a,c,f
+fn is_seven(pattern: &[char]) -> bool {
+ pattern.len() == 3
+}
+
+// a,b,c,d,e,f,g
+fn is_eight(pattern: &[char]) -> bool {
+ pattern.len() == 7
+}
+
+fn is_six(p: &[char], one: &[char]) -> bool {
+ one.iter().any(|c| !p.contains(c))
+}
+
+fn is_zero(p: &[char], four: &[char]) -> bool {
+ four.iter().any(|c| !p.contains(c))
+}
+
+/// Helper trait to make `.find()`ing the first digits less verbose.
+fn find_by(signal: &[Vec], find_fn: impl Fn(&[char]) -> bool) -> Pattern {
+ signal.iter().find(|x| find_fn(x)).unwrap().to_owned()
+}
+
+pub fn part_two(input: &str) -> u32 {
+ input
+ .lines()
+ .map(|l| {
+ let patterns: Vec = l
+ .replace(" |", "")
+ .split(' ')
+ .map(|s| s.chars().collect())
+ .collect();
+
+ let signal = patterns[0..10].to_vec();
+ let outputs = patterns[10..14].to_vec();
+
+ let mut display = Display::new();
+
+ let one = find_by(&signal, is_one);
+ let seven = find_by(&signal, is_seven);
+ let four = find_by(&signal, is_four);
+ let eight = find_by(&signal, is_eight);
+
+ // once we know `c` and `f`, we can isolate `a` by looking at `7`
+ display.insert(*seven.iter().find(|c| !&one.contains(c)).unwrap(), 'a');
+
+ // at this point, we can decode the full signal by looking at six-segment components.
+ for p in signal.iter().filter(|x| x.len() == 6) {
+ if is_six(p, &one) {
+ for c in &one {
+ if p.contains(c) {
+ display.insert(*c, 'f');
+ } else {
+ display.insert(*c, 'c');
+ }
+ }
+ } else if is_zero(p, &four) {
+ for c in &four {
+ if !p.contains(c) {
+ display.insert(*c, 'd');
+ } else if !&one.contains(c) {
+ display.insert(*c, 'b');
+ }
+ }
+ } else {
+ for c in &eight {
+ if !p.contains(c) {
+ display.insert(*c, 'e');
+ }
+ }
+ }
+ }
+
+ // whatever segment is left over maps to the last needed segment `g`.
+ // we can use `eight` to identify it since it has all segments.
+ for c in &eight {
+ if !(display.contains_key(c)) {
+ display.insert(*c, 'g');
+ }
+ }
+
+ // the display is ready for decoding now.
+ // We decode the 4-digit number to a string and then parse it to an int.
+ let num = outputs.iter().fold(String::new(), |mut acc, p| {
+ acc.push(display.decode(p));
+ acc
+ });
+
+ num.parse::().unwrap()
+ })
+ .sum()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 8);
+ assert_eq!(part_one(&input), 26);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 8);
+ assert_eq!(part_two(&input), 61229);
+ }
+}
diff --git a/2022/src/solutions/day09.rs b/2022/src/solutions/day09.rs
new file mode 100644
index 0000000..ab299af
--- /dev/null
+++ b/2022/src/solutions/day09.rs
@@ -0,0 +1,131 @@
+type Matrix = Vec>;
+
+#[derive(Clone, Copy, PartialEq)]
+struct Point {
+ x: usize,
+ y: usize,
+ val: u32,
+}
+
+fn parse(input: &str) -> Matrix {
+ input
+ .lines()
+ .map(|l| {
+ l.chars()
+ .map(|c| c.to_digit(10).unwrap())
+ .collect::>()
+ })
+ .collect()
+}
+
+fn surounding_points(matrix: &[Vec], p: &Point) -> [Point; 4] {
+ let line = &matrix[p.y];
+ // using an array istd. of a vec here achieves a 5x speedup by utilising the stack.
+ // in this case, we need to return "bogus" values for points that would otherwise result in `-1`.
+ // convention: we use index `99` and set value to `9` to mark it as an edge.
+ [
+ Point {
+ x: if p.x > 0 { p.x - 1 } else { 99 },
+ y: p.y,
+ val: if p.x > 0 { line[p.x - 1] } else { 9 },
+ },
+ Point {
+ x: p.x + 1,
+ y: p.y,
+ val: if p.x < line.len() - 1 {
+ line[p.x + 1]
+ } else {
+ 9
+ },
+ },
+ Point {
+ x: p.x,
+ y: if p.y > 0 { p.y - 1 } else { 99 },
+ val: if p.y > 0 { matrix[p.y - 1][p.x] } else { 9 },
+ },
+ Point {
+ x: p.x,
+ y: p.y + 1,
+ val: if p.y < matrix.len() - 1 {
+ matrix[p.y + 1][p.x]
+ } else {
+ 9
+ },
+ },
+ ]
+}
+
+fn is_minimum(matrix: &[Vec], p: &Point) -> bool {
+ surounding_points(matrix, p).iter().all(|x| x.val > p.val)
+}
+
+fn get_minimums(matrix: &[Vec]) -> Vec {
+ let mut minimums: Vec = Vec::new();
+
+ for y in 0..matrix.len() {
+ for x in 0..matrix[0].len() {
+ let val = matrix[y][x];
+ let p = Point { x, y, val };
+ if is_minimum(matrix, &p) {
+ minimums.push(p);
+ }
+ }
+ }
+
+ minimums
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let matrix = parse(input);
+ get_minimums(&matrix).iter().map(|p| p.val + 1).sum()
+}
+
+fn flood_fill<'a>(matrix: &'a [Vec], p: &'a Point, basin: &'a mut Vec) {
+ surounding_points(matrix, p)
+ .iter()
+ .filter(|x| x.val != 9 && x.val > p.val)
+ .for_each(|x| {
+ flood_fill(matrix, x, basin);
+ });
+
+ if !basin.contains(p) {
+ basin.push(*p);
+ }
+}
+
+pub fn part_two(input: &str) -> usize {
+ let matrix = parse(input);
+
+ let mut basins = get_minimums(&matrix)
+ .iter()
+ .map(|p| {
+ let mut basin: Vec = Vec::new();
+ flood_fill(&matrix, p, &mut basin);
+ basin.len()
+ })
+ .collect::>();
+
+ let len = basins.len();
+ basins.sort_unstable();
+
+ basins[len - 1] * basins[len - 2] * basins[len - 3]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 9);
+ assert_eq!(part_one(&input), 15);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 9);
+ assert_eq!(part_two(&input), 1134);
+ }
+}
diff --git a/2022/src/solutions/day10.rs b/2022/src/solutions/day10.rs
new file mode 100644
index 0000000..4f6a457
--- /dev/null
+++ b/2022/src/solutions/day10.rs
@@ -0,0 +1,119 @@
+use crate::helpers::math::median;
+
+/// tracks open tokens (e.g. `(`) in sequence of occurence.
+type CharacterStack = Vec;
+
+/// error thrown if parser fails to parse a line.
+struct ParsingError {
+ token: char,
+}
+
+type ParsingResult = Result;
+
+fn opener(c: char) -> Option {
+ match c {
+ ')' => Some('('),
+ ']' => Some('['),
+ '}' => Some('{'),
+ '>' => Some('<'),
+ _ => None,
+ }
+}
+
+/// go through the line char-by-char.
+/// opening chars are added to a stack.
+/// when closing char is encountered, pop the first item of the stack.
+/// if the closing char can be used to close the pair, continue processing the line.
+/// if it does not match, throw a `ParsingError` referencing the offending token.
+/// once the line completes parsing without errors, return the rest of the stack.
+fn parse(line: &str) -> ParsingResult {
+ let mut stack: CharacterStack = Vec::new();
+ let mut offending_token: Option = None;
+
+ for c in line.chars() {
+ let opener = opener(c);
+
+ if let Some(opener) = opener {
+ match stack.pop() {
+ Some(last_open) => {
+ if opener != last_open {
+ offending_token = Some(c);
+ break;
+ }
+ }
+ // case doesn't seem to occur in puzzle input.
+ None => {
+ offending_token = Some(c);
+ break;
+ }
+ }
+ } else {
+ stack.push(c);
+ }
+ }
+
+ match offending_token {
+ Some(token) => Err(ParsingError { token }),
+ None => Ok(stack),
+ }
+}
+
+pub fn part_one(input: &str) -> u32 {
+ input
+ .lines()
+ .map(|l| match parse(l) {
+ Ok(_) => 0,
+ Err(err) => match err.token {
+ ')' => 3,
+ ']' => 57,
+ '}' => 1197,
+ '>' => 25137,
+ _ => 0,
+ },
+ })
+ .sum()
+}
+
+pub fn part_two(input: &str) -> u64 {
+ let mut scores: Vec = input
+ .lines()
+ .filter_map(|l| match parse(l) {
+ Ok(stack) => {
+ let score = stack.iter().rev().fold(0, |acc, char| {
+ acc * 5
+ + match char {
+ '(' => 1,
+ '[' => 2,
+ '{' => 3,
+ '<' => 4,
+ _ => 0,
+ }
+ });
+
+ Some(score)
+ }
+ Err(_) => None,
+ })
+ .collect();
+
+ median(&mut scores)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 10);
+ assert_eq!(part_one(&input), 26397);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 10);
+ assert_eq!(part_two(&input), 288957);
+ }
+}
diff --git a/2022/src/solutions/day11.rs b/2022/src/solutions/day11.rs
new file mode 100644
index 0000000..c34d5a7
--- /dev/null
+++ b/2022/src/solutions/day11.rs
@@ -0,0 +1,122 @@
+use crate::helpers::grid::Point;
+use std::collections::HashSet;
+
+static OCTOPUS_ROWS: usize = 10;
+static OCTOPUS_COLS: usize = 10;
+
+type Grid = [Line; 10];
+type Line = [u32; 10];
+
+fn parse(input: &str) -> Grid {
+ input
+ .lines()
+ .map(|l| {
+ l.chars()
+ .map(|c| c.to_digit(10).unwrap())
+ .collect::>()
+ .try_into()
+ .unwrap()
+ })
+ .collect::>()
+ .try_into()
+ .unwrap()
+}
+
+fn process_step(grid: &mut Grid, all_points: &[Point]) -> u32 {
+ let mut flashed: HashSet = HashSet::new();
+ // start the flash cascade by incrementing all points in the grid.
+ // `tick` calls itself recursively until either all octopus have flashed or there
+ // is a `tick` where no octopus reaches required energy levels.
+ tick(grid, &mut flashed, all_points);
+ // reset all flashed octopus to `0`.
+ reset_energy_levels(grid, all_points);
+ // once there is no more processing to do for a step, we return the count of flashes observed.
+ flashed.len() as u32
+}
+
+fn tick(grid: &mut Grid, flashed: &mut HashSet, points: &[Point]) {
+ points.iter().for_each(|Point(x, y)| {
+ grid[*y][*x] += 1;
+ if grid[*y][*x] > 9 && !flashed.contains(&Point(*x, *y)) {
+ let p = Point(*x, *y);
+ flashed.insert(p);
+ tick(
+ grid,
+ flashed,
+ // when an octopus flashes, it increments all neighbors.
+ &p.neighbors(OCTOPUS_COLS - 1, OCTOPUS_ROWS - 1, true),
+ );
+ }
+ });
+}
+
+fn reset_energy_levels(grid: &mut Grid, points: &[Point]) {
+ points.iter().for_each(|Point(x, y)| {
+ if grid[*y][*x] > 9 {
+ grid[*y][*x] = 0;
+ }
+ });
+}
+
+fn all_points() -> Vec {
+ let mut points = Vec::new();
+
+ for x in 0..OCTOPUS_COLS {
+ for y in 0..OCTOPUS_ROWS {
+ points.push(Point(x, y));
+ }
+ }
+
+ points
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let mut grid = parse(input);
+ // optimization: keep a reference of all points in the grid to avoid recomputing this constantly.
+ let points = all_points();
+
+ let mut flash_count: u32 = 0;
+ for _ in 0..100 {
+ flash_count += process_step(&mut grid, &points);
+ }
+
+ flash_count
+}
+
+pub fn part_two(input: &str) -> usize {
+ let mut grid = parse(input);
+ // optimization: keep a reference of all points in the grid to avoid recomputing this constantly.
+ let points = all_points();
+
+ let mut index: usize = 0;
+ let mut all_flashed = false;
+
+ while !all_flashed {
+ if process_step(&mut grid, &points) == (OCTOPUS_COLS * OCTOPUS_ROWS) as u32 {
+ all_flashed = true
+ } else {
+ index += 1;
+ }
+ }
+
+ index + 1
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 11);
+ assert_eq!(part_one(&input), 1656);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 11);
+ assert_eq!(part_two(&input), 195);
+ }
+}
diff --git a/2022/src/solutions/day12.rs b/2022/src/solutions/day12.rs
new file mode 100644
index 0000000..9c821f5
--- /dev/null
+++ b/2022/src/solutions/day12.rs
@@ -0,0 +1,109 @@
+use std::collections::{HashMap, HashSet};
+
+static START: &str = "start";
+static END: &str = "end";
+
+#[derive(Debug)]
+struct Graph<'a> {
+ // nodes are stored as map where size is a boolean.
+ // `true` indicates that a room is big, `false` small.
+ nodes: HashMap<&'a str, bool>,
+ // edges are stored as an adjacency list for each node.
+ edges: HashMap<&'a str, HashSet<&'a str>>,
+}
+
+impl Graph<'_> {
+ fn new() -> Self {
+ Graph {
+ nodes: HashMap::new(),
+ edges: HashMap::new(),
+ }
+ }
+
+ fn get_adjacent_nodes(&self, node_id: &str) -> Vec<&&str> {
+ self.edges.get(node_id).unwrap().iter().collect()
+ }
+}
+
+fn parse(input: &str) -> Graph {
+ let mut graph = Graph::new();
+
+ input.lines().for_each(|l| {
+ let mut node_pair = l.split('-').map(|id| (id, id.to_uppercase() == id));
+
+ let (from, from_type) = node_pair.next().unwrap();
+ let (to, to_type) = node_pair.next().unwrap();
+
+ graph.nodes.insert(from, from_type);
+ graph.nodes.insert(to, to_type);
+ graph.edges.entry(from).or_default().insert(to);
+ graph.edges.entry(to).or_default().insert(from);
+ });
+
+ graph
+}
+
+fn search(graph: &Graph, seen: &HashSet<&str>, id: &str, small_room_counter: u8) -> u32 {
+ let mut small_room_counter = small_room_counter;
+
+ if id == END {
+ return 1;
+ }
+
+ if seen.contains(&id) {
+ if id == START {
+ return 0;
+ // in part one, any small room can only be visited once.
+ // in part two, **one** room may be visited twice.
+ // to reflect that, a counter is decremented the first time a small room is encountered.
+ } else if !*graph.nodes.get(id).unwrap() {
+ if small_room_counter == 0 {
+ return 0;
+ } else {
+ small_room_counter -= 1;
+ }
+ }
+ }
+
+ let mut seen_here = seen.clone();
+ seen_here.insert(id);
+
+ let result = graph
+ .get_adjacent_nodes(id)
+ .iter()
+ .map(|adjacent_node| search(graph, &seen_here, adjacent_node, small_room_counter))
+ .sum();
+
+ result
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let graph = parse(input);
+ let seen: HashSet<&str> = HashSet::new();
+ search(&graph, &seen, START, 0)
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let graph = parse(input);
+ let seen: HashSet<&str> = HashSet::new();
+ search(&graph, &seen, START, 1)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 12);
+ assert_eq!(part_one(&input), 226);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 12);
+ assert_eq!(part_two(&input), 3509);
+ }
+}
diff --git a/2022/src/solutions/day13.rs b/2022/src/solutions/day13.rs
new file mode 100644
index 0000000..3623652
--- /dev/null
+++ b/2022/src/solutions/day13.rs
@@ -0,0 +1,157 @@
+#![allow(clippy::needless_range_loop)]
+use crate::helpers::grid::Point;
+use std::cmp::max;
+
+type Points = Vec;
+
+type Line = Vec;
+type Grid = Vec;
+
+#[derive(Debug)]
+enum Instruction {
+ X(usize),
+ Y(usize),
+}
+type Instructions = Vec;
+
+fn parse(input: &str) -> (Grid, Instructions) {
+ let mut points: Points = Vec::new();
+ let mut instructions: Instructions = Vec::new();
+
+ let mut width: usize = 0;
+ let mut height: usize = 0;
+
+ input.lines().for_each(|l| {
+ // line is an instruction.
+ if l.starts_with('f') {
+ let instruction = parse_instruction(l);
+
+ // infer grid size from first instructions.
+ // looking at max. point size might fail if last lines or columns are empty.
+ if width == 0 || height == 0 {
+ match instruction {
+ Instruction::X(x) => width = max(x * 2 + 1, width),
+ Instruction::Y(y) => height = max(y * 2 + 1, height),
+ }
+ }
+
+ instructions.push(instruction);
+ // line is a point.
+ } else if !l.is_empty() {
+ points.push(parse_point(l));
+ }
+ });
+
+ (make_grid(&points, width, height), instructions)
+}
+
+fn parse_point(l: &str) -> Point {
+ let mut coords = l.split(',');
+ let x: usize = coords.next().unwrap().parse().unwrap();
+ let y: usize = coords.next().unwrap().parse().unwrap();
+ Point(x, y)
+}
+
+fn parse_instruction(l: &str) -> Instruction {
+ let mut instr = l.split(' ').last().unwrap().split('=');
+ let axis = instr.next().unwrap();
+ let amount: usize = instr.next().unwrap().parse().unwrap();
+
+ if axis == "x" {
+ Instruction::X(amount)
+ } else {
+ Instruction::Y(amount)
+ }
+}
+
+fn make_grid(points: &[Point], width: usize, height: usize) -> Grid {
+ let mut grid: Grid = vec![vec![false; width]; height];
+
+ for Point(x, y) in points {
+ grid[*y][*x] = true;
+ }
+
+ grid
+}
+
+fn fold_y(grid: &[Line], fold_at: usize, width: usize, height: usize) -> Grid {
+ let mut points: Points = Vec::new();
+
+ for y in 0..fold_at {
+ for x in 0..width {
+ if grid[y][x] || grid[height - y - 1][x] {
+ points.push(Point(x, y));
+ }
+ }
+ }
+
+ make_grid(&points, width, fold_at)
+}
+
+fn fold_x(grid: &[Line], fold_at: usize, width: usize, height: usize) -> Grid {
+ let mut points: Points = Vec::new();
+
+ for y in 0..height {
+ for x in 0..fold_at {
+ if grid[y][x] || grid[y][width - x - 1] {
+ points.push(Point(x, y));
+ }
+ }
+ }
+
+ make_grid(&points, fold_at, height)
+}
+
+fn fold(grid: &[Line], instruction: &Instruction) -> Grid {
+ let height = grid.len();
+ let width = grid[0].len();
+
+ match instruction {
+ Instruction::X(fold_at) => fold_x(grid, *fold_at, width, height),
+ Instruction::Y(fold_at) => fold_y(grid, *fold_at, width, height),
+ }
+}
+
+fn count_grid(grid: &[Line]) -> u32 {
+ grid.iter().flatten().filter(|x| **x).count() as u32
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let (grid, instructions) = parse(input);
+ count_grid(&fold(&grid, &instructions[0]))
+}
+
+fn print_grid(grid: &[Line]) {
+ for line in grid {
+ let chars: String = line.iter().map(|x| if *x { '#' } else { '.' }).collect();
+ println!("{}", chars);
+ }
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let (grid, instructions) = parse(input);
+
+ let code = instructions.iter().fold(grid, |acc, curr| fold(&acc, curr));
+
+ print_grid(&code);
+ count_grid(&code)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 13);
+ assert_eq!(part_one(&input), 17);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 13);
+ assert_eq!(part_two(&input), 16);
+ }
+}
diff --git a/2022/src/solutions/day14.rs b/2022/src/solutions/day14.rs
new file mode 100644
index 0000000..c0b2759
--- /dev/null
+++ b/2022/src/solutions/day14.rs
@@ -0,0 +1,116 @@
+use itertools::{Itertools, MinMaxResult};
+use std::collections::HashMap;
+
+type Pair = (char, char);
+type Rules = HashMap;
+
+/// Compact representation of a polymer string. This is a necessity to solve part 2 where my naive first approach failed.
+/// `pairs` counts the # of times a character combination is present in the polymer.
+/// example: `{ (N, N): 1, (N, C): 1, (C, B): 1 }` for the example polymer.
+/// A polymerization step creates a new set of pairs by processing `rules` for every pair currently in the polymer.
+/// example: `{ (N, N): 3 }` produces `{ (N, C): 2, (C, N): 2 }`.
+/// Looking at `pairs` is not enough to derive character counts, so we need a `chars` map to count these as well.
+/// whenever a character is added to a pair, we increment a running counter for that character.
+/// example: `{ (N, N): 3 }` would increment `C` by 2.
+#[derive(Clone)]
+struct Polymer {
+ pairs: HashMap,
+ characters: HashMap,
+}
+
+impl Polymer {
+ fn new() -> Self {
+ Polymer {
+ pairs: HashMap::new(),
+ characters: HashMap::new(),
+ }
+ }
+
+ fn from_string(line: &str) -> Self {
+ let mut polymer = Polymer::new();
+ let chars = line.chars();
+
+ chars.clone().for_each(|c| {
+ *polymer.characters.entry(c).or_default() += 1;
+ });
+
+ chars.clone().tuple_windows().for_each(|(a, b)| {
+ *polymer.pairs.entry((a, b)).or_default() += 1;
+ });
+
+ polymer
+ }
+
+ fn expand(&mut self, rules: &Rules) {
+ let pairs = self.pairs.clone();
+ self.pairs.clear();
+
+ for (pair, count) in pairs {
+ let to_add = rules.get(&pair).unwrap().to_owned();
+ *self.pairs.entry((pair.0, to_add)).or_default() += count;
+ *self.pairs.entry((to_add, pair.1)).or_default() += count;
+ *self.characters.entry(to_add).or_default() += count;
+ }
+ }
+
+ fn expand_times(&mut self, times: u8, rules: &Rules) -> &Self {
+ for _ in 0..times {
+ self.expand(rules);
+ }
+ self
+ }
+
+ fn delta(&self) -> u64 {
+ match self.characters.values().minmax() {
+ MinMaxResult::MinMax(a, b) => b - a,
+ _ => unreachable!(),
+ }
+ }
+}
+
+fn parse(input: &str) -> (Polymer, Rules) {
+ let mut lines = input.lines();
+
+ let polymer = Polymer::from_string(lines.next().unwrap());
+
+ let rules = lines.fold(HashMap::new(), |mut acc, l| {
+ if !l.is_empty() {
+ let parts: Vec = l.replace(" -> ", "").chars().collect();
+ let pair = (parts[0], parts[1]);
+ acc.insert(pair, parts[2]);
+ }
+
+ acc
+ });
+
+ (polymer, rules)
+}
+
+pub fn part_one(input: &str) -> u64 {
+ let (mut polymer, rules) = parse(input);
+ polymer.expand_times(10, &rules).delta()
+}
+
+pub fn part_two(input: &str) -> u64 {
+ let (mut polymer, rules) = parse(input);
+ polymer.expand_times(40, &rules).delta()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 14);
+ assert_eq!(part_one(&input), 1588);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 14);
+ assert_eq!(part_two(&input), 2188189693529);
+ }
+}
diff --git a/2022/src/solutions/day15.rs b/2022/src/solutions/day15.rs
new file mode 100644
index 0000000..6d2fbd0
--- /dev/null
+++ b/2022/src/solutions/day15.rs
@@ -0,0 +1,148 @@
+/// implementation of Dijkstra's algorithm for a 2d grid.
+/// borrows from the example found in the [rust docs](https://doc.rust-lang.org/std/collections/binary_heap/index.html#examples).
+/// in contrast to the example, we do not create a directed graph but work with the supplied grid directly.
+/// for further information, see:
+/// [Wikipedia](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) |
+/// [Introduction to the A* Algorithm](https://www.redblobgames.com/pathfinding/a-star/introduction.html).
+mod shortest_path {
+ use crate::helpers::grid::Point;
+ use std::cmp::Ordering;
+ use std::collections::BinaryHeap;
+
+ // while performing the search, track a sorted list of candidates (=state) to visit next on a priority queue.
+ #[derive(Copy, Clone, Eq, PartialEq)]
+ struct State {
+ cost: usize,
+ position: usize,
+ }
+
+ /// the algorithm expects a `min-heap` priority queue as frontier.
+ /// the default std. lib implementation is a `max-heap`, so the sort order needs to be flipped for state values.
+ /// also adds a tie breaker based on position. see [rust docs](https://doc.rust-lang.org/std/collections/struct.BinaryHeap.html#min-heap)
+ impl Ord for State {
+ fn cmp(&self, other: &Self) -> Ordering {
+ other
+ .cost
+ .cmp(&self.cost)
+ .then_with(|| self.position.cmp(&other.position))
+ }
+ }
+
+ impl PartialOrd for State {
+ fn partial_cmp(&self, other: &Self) -> Option {
+ Some(self.cmp(other))
+ }
+ }
+
+ pub fn shortest_path(grid: &[Vec]) -> Option {
+ let height = grid.len();
+ let width = grid[0].len();
+
+ // dist[node] = current shortest distance from `start` to `node`.
+ let mut dist: Vec<_> = (0..(height * width)).map(|_| usize::MAX).collect();
+
+ let mut frontier = BinaryHeap::new();
+
+ let start_node = Point(0, 0).to_id(width);
+ let target_node = Point(height - 1, width - 1).to_id(width);
+
+ // initialize start with a zero cost.
+ dist[start_node] = 0;
+ frontier.push(State {
+ cost: 0,
+ position: start_node,
+ });
+
+ // examine the frontier starting with the lowest cost nodes.
+ while let Some(State { cost, position }) = frontier.pop() {
+ if position == target_node {
+ return Some(cost);
+ }
+
+ // skip: there is a better path to this node already.
+ if cost > dist[position] {
+ continue;
+ }
+
+ // see if we can find a path with a lower cost than previous paths for any adjacent nodes.
+ for point in Point::from_id(position, width).neighbors(width - 1, height - 1, false) {
+ let next = State {
+ cost: cost + grid[point.1][point.0] as usize,
+ position: point.to_id(width),
+ };
+
+ // if so, add it to the frontier and continue.
+ if next.cost < dist[next.position] {
+ frontier.push(next);
+ dist[next.position] = next.cost;
+ }
+ }
+ }
+
+ None
+ }
+}
+
+use self::shortest_path::shortest_path;
+
+type Row = Vec;
+type Grid = Vec
;
+
+fn parse(input: &str) -> Grid {
+ input
+ .lines()
+ .map(|l| l.chars().map(|c| c.to_digit(10).unwrap()).collect())
+ .collect()
+}
+
+pub fn part_one(input: &str) -> u32 {
+ shortest_path(&parse(input)).unwrap() as u32
+}
+
+pub fn part_two(input: &str) -> u32 {
+ let grid = parse(input);
+
+ let height = grid.len();
+ let width = grid[0].len();
+
+ let expanded: Grid = (0..(5 * grid.len()))
+ .map(|y| {
+ (0..(5 * grid[0].len()))
+ .map(|x| {
+ // increment grows by one with every horizontal *and* vertical tile.
+ let x_increment = (x / width) as u32;
+ let y_increment = (y / height) as u32;
+
+ // each individual value can be derived from the original value and the current distance to it.
+ let cost = grid[x % width][y % height] + x_increment + y_increment;
+ if cost == 9 {
+ cost
+ } else {
+ cost % 9
+ }
+ })
+ .collect()
+ })
+ .collect();
+
+ shortest_path(&expanded).unwrap() as u32
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 15);
+ assert_eq!(part_one(&input), 40);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 15);
+ assert_eq!(part_two(&input), 315);
+ }
+}
diff --git a/2022/src/solutions/day16.rs b/2022/src/solutions/day16.rs
new file mode 100644
index 0000000..2608a0a
--- /dev/null
+++ b/2022/src/solutions/day16.rs
@@ -0,0 +1,214 @@
+mod decoder {
+ #[derive(Clone, Debug)]
+ pub enum Packet {
+ Operator(Operator),
+ Literal(Literal),
+ }
+
+ #[derive(Clone, Debug)]
+ pub struct Literal {
+ pub header: Header,
+ pub value: u64,
+ size: usize,
+ }
+
+ #[derive(Clone, Debug)]
+ pub struct Operator {
+ pub header: Header,
+ pub children: Vec,
+ size: usize,
+ }
+
+ #[derive(Clone, Debug)]
+ pub struct Header {
+ pub version: u64,
+ pub type_id: u64,
+ }
+
+ pub fn decode(message: &str) -> Packet {
+ decode_packet(&decode_message(message))
+ }
+
+ fn decode_packet(bits: &[u8]) -> Packet {
+ if to_u64(&bits[3..6]) == 4 {
+ decode_literal(bits)
+ } else {
+ decode_operator(bits)
+ }
+ }
+
+ fn decode_literal(bits: &[u8]) -> Packet {
+ let rest = &bits[6..];
+ let mut index = 0;
+ let mut num = Vec::new();
+
+ while index <= rest.len() - 5 {
+ let next = index + 5;
+ let chunk = &rest[index..next];
+ let signal = chunk[0];
+
+ num.extend(&chunk[1..5]);
+ index = next;
+
+ if signal == 0 {
+ break;
+ }
+ }
+
+ Packet::Literal(Literal {
+ header: decode_header(bits),
+ value: to_u64(&num),
+ size: 6 + index,
+ })
+ }
+
+ fn decode_operator(bits: &[u8]) -> Packet {
+ let mode = bits[6];
+ let content_offset = 7 + if mode == 0 { 15 } else { 11 };
+ let len = to_u64(&bits[7..content_offset]) as usize;
+
+ let mut children = Vec::new();
+ let mut index = 0;
+
+ while (mode == 0 && index < len) || (mode == 1 && children.len() < len) {
+ let packet = decode_packet(&bits[(content_offset + index)..]);
+
+ match &packet {
+ Packet::Literal(data) => index += data.size,
+ Packet::Operator(data) => index += data.size,
+ }
+
+ children.push(packet);
+ }
+
+ Packet::Operator(Operator {
+ header: decode_header(bits),
+ children,
+ size: content_offset + index,
+ })
+ }
+
+ fn decode_header(bits: &[u8]) -> Header {
+ Header {
+ version: to_u64(&bits[0..3]),
+ type_id: to_u64(&bits[3..6]),
+ }
+ }
+
+ // instruction set is small, use a lookup table.
+ fn decode_message(message: &str) -> Vec {
+ message
+ .chars()
+ .flat_map(|c| match c {
+ '0' => [0, 0, 0, 0],
+ '1' => [0, 0, 0, 1],
+ '2' => [0, 0, 1, 0],
+ '3' => [0, 0, 1, 1],
+ '4' => [0, 1, 0, 0],
+ '5' => [0, 1, 0, 1],
+ '6' => [0, 1, 1, 0],
+ '7' => [0, 1, 1, 1],
+ '8' => [1, 0, 0, 0],
+ '9' => [1, 0, 0, 1],
+ 'A' => [1, 0, 1, 0],
+ 'B' => [1, 0, 1, 1],
+ 'C' => [1, 1, 0, 0],
+ 'D' => [1, 1, 0, 1],
+ 'E' => [1, 1, 1, 0],
+ 'F' => [1, 1, 1, 1],
+ c => panic!("unexpected token in message: {}", c),
+ })
+ .collect()
+ }
+
+ fn to_u64(bits: &[u8]) -> u64 {
+ bits.iter().fold(0, |acc, &b| acc * 2 + (b as u64))
+ }
+}
+
+mod interpreter {
+ use super::decoder::Packet;
+
+ pub fn interpret(packet: Packet) -> u64 {
+ match packet {
+ Packet::Literal(_) => panic!("unexpected literal on root level."),
+ Packet::Operator(data) => {
+ let values: Vec = data
+ .children
+ .iter()
+ .map(|child| match child {
+ Packet::Literal(child_data) => child_data.value as u64,
+ Packet::Operator(child_data) => {
+ interpret(Packet::Operator(child_data.clone()))
+ }
+ })
+ .collect();
+
+ match data.header.type_id {
+ 0 => values.iter().sum(),
+ 1 => values.iter().product(),
+ 2 => *values.iter().min().unwrap(),
+ 3 => *values.iter().max().unwrap(),
+ 5 => (values[0] > values[1]) as u64,
+ 6 => (values[0] < values[1]) as u64,
+ 7 => (values[0] == values[1]) as u64,
+ c => panic!("unknown type_id {}", c),
+ }
+ }
+ }
+ }
+
+ pub fn sum_versions(packet: Packet) -> u64 {
+ match packet {
+ Packet::Literal(_) => panic!("unexpected literal on root level."),
+ Packet::Operator(data) => {
+ data.children
+ .iter()
+ .fold(data.header.version, |acc, curr| match curr {
+ Packet::Literal(child_data) => acc + child_data.header.version,
+ Packet::Operator(child_data) => {
+ acc + sum_versions(Packet::Operator(child_data.clone()))
+ }
+ })
+ }
+ }
+ }
+}
+
+use self::decoder::decode;
+use self::interpreter::{interpret, sum_versions};
+
+pub fn part_one(input: &str) -> u64 {
+ let packet = decode(input.lines().next().unwrap());
+ sum_versions(packet)
+}
+
+pub fn part_two(input: &str) -> u64 {
+ let packet = decode(input.lines().next().unwrap());
+ interpret(packet)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ assert_eq!(part_one("8A004A801A8002F478"), 16);
+ assert_eq!(part_one("A0016C880162017C3686B18A3D4780"), 31);
+ assert_eq!(part_one("620080001611562C8802118E34"), 12);
+ assert_eq!(part_one("C0015000016115A2E0802F182340"), 23);
+ }
+
+ #[test]
+ fn test_part_two() {
+ assert_eq!(part_two("C200B40A82"), 3);
+ assert_eq!(part_two("04005AC33890"), 54);
+ assert_eq!(part_two("880086C3E88112"), 7);
+ assert_eq!(part_two("CE00C43D881120"), 9);
+ assert_eq!(part_two("D8005AC2A8F0"), 1);
+ assert_eq!(part_two("F600BC2D8F"), 0);
+ assert_eq!(part_two("9C005AC2F8F0"), 0);
+ assert_eq!(part_two("9C0141080250320F1802104A08"), 1);
+ }
+}
diff --git a/2022/src/solutions/day17.rs b/2022/src/solutions/day17.rs
new file mode 100644
index 0000000..a4f32fe
--- /dev/null
+++ b/2022/src/solutions/day17.rs
@@ -0,0 +1,105 @@
+use std::cmp::max;
+
+type Point = (isize, isize);
+type Velocity = (isize, isize);
+
+struct Bounds {
+ left: isize,
+ right: isize,
+ top: isize,
+ bottom: isize,
+}
+
+impl Bounds {
+ fn contains(&self, (x, y): &Point) -> bool {
+ x >= &self.left && x <= &self.right && y <= &self.top && y >= &self.bottom
+ }
+}
+
+fn parse(line: &str) -> Bounds {
+ let values: Vec = line
+ .split(',')
+ .flat_map(|part| {
+ part.split('=')
+ .last()
+ .unwrap()
+ .split("..")
+ .map(|x| x.parse().unwrap())
+ })
+ .collect();
+
+ Bounds {
+ left: values[0],
+ right: values[1],
+ bottom: values[2],
+ top: values[3],
+ }
+}
+
+fn simulate_point(
+ initial_point: Point,
+ initial_velocity: Velocity,
+ bounds: &Bounds,
+) -> Option {
+ let mut point = initial_point;
+ let mut velocity = initial_velocity;
+ let mut y_max = point.1;
+
+ // terminate if point has overshot bounds
+ while point.0 <= bounds.right && point.1 >= bounds.bottom {
+ point = (point.0 + velocity.0, point.1 + velocity.1);
+ y_max = max(point.1, y_max);
+
+ if bounds.contains(&point) {
+ return Some(y_max);
+ } else {
+ velocity = (max(0, velocity.0 - 1), velocity.1 - 1);
+ }
+ }
+
+ None
+}
+
+fn find_hits(bounds: &Bounds) -> Vec {
+ let mut max_y = Vec::new();
+ let initial_position = (0, 0);
+
+ for x in 0..=bounds.right {
+ for y in bounds.bottom..=-bounds.bottom {
+ if let Some(y) = simulate_point(initial_position, (x, y), bounds) {
+ max_y.push(y);
+ }
+ }
+ }
+
+ max_y
+}
+
+pub fn part_one(input: &str) -> isize {
+ let bounds = parse(input.lines().next().unwrap());
+ *find_hits(&bounds).iter().max().unwrap()
+}
+
+pub fn part_two(input: &str) -> usize {
+ let bounds = parse(input.lines().next().unwrap());
+ find_hits(&bounds).len()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 17);
+ assert_eq!(part_one(&input), 45);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 17);
+ assert_eq!(part_two(&input), 112);
+ }
+}
diff --git a/2022/src/solutions/day18.rs b/2022/src/solutions/day18.rs
new file mode 100644
index 0000000..5d4acdd
--- /dev/null
+++ b/2022/src/solutions/day18.rs
@@ -0,0 +1,187 @@
+use itertools::Itertools;
+use std::cmp::max;
+
+#[derive(Clone, Copy)]
+enum Symbol {
+ Open,
+ Close,
+ Comma,
+ Num(u32),
+}
+
+impl Symbol {
+ fn from_char(c: char) -> Self {
+ match c {
+ '[' => Symbol::Open,
+ ']' => Symbol::Close,
+ ',' => Symbol::Comma,
+ c => Symbol::Num(c.to_digit(10).unwrap()),
+ }
+ }
+}
+
+type Snail = Vec;
+
+fn add(a: &[Symbol], b: &[Symbol]) -> Snail {
+ let mut snail: Snail = vec![Symbol::Open];
+ snail.extend(a.iter().cloned());
+ snail.push(Symbol::Comma);
+ snail.extend(b.iter().cloned());
+ snail.push(Symbol::Close);
+ reduce(&mut snail);
+ snail
+}
+
+fn from_str(s: &str) -> Snail {
+ s.chars().map(Symbol::from_char).collect()
+}
+
+fn reduce(snail: &mut Snail) {
+ while explode(snail) || split(snail) {}
+}
+
+fn split(snail: &mut Snail) -> bool {
+ let pos = snail.iter().position(|s| match s {
+ Symbol::Num(x) => *x > 9,
+ _ => false,
+ });
+
+ match pos {
+ None => false,
+ Some(index) => {
+ if let Symbol::Num(x) = snail[index] {
+ let l = x / 2;
+ let r = x - l;
+ snail.splice(
+ index..index + 1,
+ [
+ Symbol::Open,
+ Symbol::Num(l),
+ Symbol::Comma,
+ Symbol::Num(r),
+ Symbol::Close,
+ ],
+ );
+ true
+ } else {
+ unreachable!()
+ }
+ }
+ }
+}
+
+fn explode(snail: &mut Snail) -> bool {
+ let mut depth = 0;
+
+ let pos = snail.iter().position(|s| {
+ match s {
+ Symbol::Open => {
+ depth += 1;
+ }
+ Symbol::Close => {
+ depth -= 1;
+ }
+ _ => (),
+ };
+
+ depth == 5
+ });
+
+ match pos {
+ None => false,
+ Some(index) => {
+ let l = match snail[index + 1] {
+ Symbol::Num(x) => x,
+ _ => unreachable!(),
+ };
+
+ let r = match snail[index + 3] {
+ Symbol::Num(x) => x,
+ _ => unreachable!(),
+ };
+
+ snail[..index].iter_mut().rev().find_map(|x| {
+ if let Symbol::Num(x) = x {
+ *x += l;
+ Some(())
+ } else {
+ None
+ }
+ });
+
+ snail[index + 4..].iter_mut().find_map(|x| {
+ if let Symbol::Num(x) = x {
+ *x += r;
+ Some(())
+ } else {
+ None
+ }
+ });
+
+ snail.splice(index..index + 5, [Symbol::Num(0)]);
+ true
+ }
+ }
+}
+
+fn parse(input: &str) -> Vec {
+ input.lines().map(from_str).collect()
+}
+
+// this previously used a recursive function based on casting to json.
+// found this on a random reddit thread and it is both faster and more elegant.
+fn calc_magnitude(snail: &[Symbol]) -> u32 {
+ let mut multiplier = 1;
+ let mut output = 0;
+
+ for symbol in snail {
+ match symbol {
+ Symbol::Close => multiplier /= 2,
+ Symbol::Comma => multiplier = (multiplier / 3) * 2,
+ Symbol::Num(x) => output += *x * multiplier,
+ Symbol::Open => multiplier *= 3,
+ }
+ }
+
+ output
+}
+
+pub fn part_one(input: &str) -> u32 {
+ calc_magnitude(
+ &parse(input)
+ .into_iter()
+ .fold1(|acc, curr| add(&acc, &curr))
+ .unwrap(),
+ )
+}
+
+pub fn part_two(input: &str) -> u32 {
+ parse(input).iter().combinations(2).fold(0, |acc, snails| {
+ max(
+ acc,
+ max(
+ calc_magnitude(&add(snails[0], snails[1])),
+ calc_magnitude(&add(snails[1], snails[0])),
+ ),
+ )
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 18);
+ assert_eq!(part_one(&input), 4140);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 18);
+ assert_eq!(part_two(&input), 3993);
+ }
+}
diff --git a/2022/src/solutions/day19.rs b/2022/src/solutions/day19.rs
new file mode 100644
index 0000000..632b4e6
--- /dev/null
+++ b/2022/src/solutions/day19.rs
@@ -0,0 +1,216 @@
+use itertools::Itertools;
+use std::{
+ collections::HashSet,
+ ops::{Add, Sub},
+};
+
+#[derive(Clone, Copy, Eq, Hash, PartialEq)]
+struct Point(i32, i32, i32);
+
+impl Add for Point {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(self.0 + other.0, self.1 + other.1, self.2 + other.2)
+ }
+}
+
+impl Sub for Point {
+ type Output = Self;
+
+ fn sub(self, other: Self) -> Self {
+ Self(self.0 - other.0, self.1 - other.1, self.2 - other.2)
+ }
+}
+
+impl Point {
+ fn distance(&self, other: &Point) -> i32 {
+ (other.0 - self.0).pow(2) + (other.1 - self.1).pow(2) + (other.2 - self.2).pow(2)
+ }
+
+ fn manhattan_distance(&self, other: &Point) -> i32 {
+ (self.0 - other.0).abs() + (self.1 - other.1).abs() + (self.2 - other.2).abs()
+ }
+
+ fn rotate(&self, rot: u8) -> Self {
+ let &Point(x, y, z) = self;
+
+ match rot {
+ // translation of http://www.euclideanspace.com/maths/algebra/matrix/transforms/examples/index.htm
+ 0 => Point(x, y, z),
+ 1 => Point(x, z, -y),
+ 2 => Point(x, -y, -z),
+ 3 => Point(x, -z, y),
+ 4 => Point(y, -x, z),
+ 5 => Point(y, z, x),
+ 6 => Point(y, x, -z),
+ 7 => Point(y, -z, -x),
+ 8 => Point(-x, -y, z),
+ 9 => Point(-x, -z, -y),
+ 10 => Point(-x, y, -z),
+ 11 => Point(-x, z, y),
+ 12 => Point(-y, x, z),
+ 13 => Point(-y, -z, x),
+ 14 => Point(-y, -x, -z),
+ 15 => Point(-y, z, -x),
+ 16 => Point(z, y, -x),
+ 17 => Point(z, x, y),
+ 18 => Point(z, -y, x),
+ 19 => Point(z, -x, -y),
+ 20 => Point(-z, -y, -x),
+ 21 => Point(-z, -x, y),
+ 22 => Point(-z, y, x),
+ 23 => Point(-z, x, -y),
+ v => panic!("unexpected rotation {}", v),
+ }
+ }
+}
+
+type Neighbors = (usize, usize);
+
+type Report = Vec;
+type Reports = Vec;
+
+type Distances = HashSet;
+
+fn parse(input: &str) -> Reports {
+ input.lines().fold(Vec::new(), |mut acc, l| {
+ if l.starts_with("---") {
+ acc.push(vec![]);
+ } else if !l.is_empty() {
+ let mut coords = l.split(',').map(|s| s.parse::().unwrap());
+ let last = acc.len() - 1;
+
+ acc[last].push(Point(
+ coords.next().unwrap(),
+ coords.next().unwrap(),
+ coords.next().unwrap(),
+ ));
+ }
+
+ acc
+ })
+}
+
+fn distances(reports: &[Report]) -> Vec {
+ reports
+ .iter()
+ .map(|r| {
+ r.iter()
+ .tuple_combinations()
+ .map(|(p1, p2)| p1.distance(p2))
+ .collect()
+ })
+ .collect()
+}
+
+fn find_neighbors(distances: &[Distances]) -> Vec {
+ distances
+ .iter()
+ .enumerate()
+ .tuple_combinations()
+ .filter(|((_, d1), (_, d2))| d1.intersection(d2).count() >= 66)
+ .flat_map(|((i, _), (j, _))| [(i, j), (j, i)])
+ .collect()
+}
+
+fn unaligned_neighbors(neighbors: &[Neighbors], aligned: &[Report]) -> Option {
+ neighbors
+ .iter()
+ .find(|(a, b)| !aligned[*a].is_empty() && aligned[*b].is_empty())
+ .map(|(a, b)| (*a, *b))
+}
+
+fn find_pair_by_distance(reports: &[Point], distance: i32) -> (&Point, &Point) {
+ reports
+ .iter()
+ .tuple_combinations()
+ .find(|(a, b)| a.distance(b) == distance)
+ .unwrap()
+}
+
+fn align(reports: &[Report]) -> (Vec, Vec) {
+ let distances = distances(reports);
+ let neighbors = find_neighbors(&distances);
+
+ let mut alignments: Vec = vec![Point(0, 0, 0)];
+ let mut aligned: Vec = vec![vec![]; reports.len()];
+ aligned[0] = reports[0].clone();
+
+ while let Some((a, b)) = unaligned_neighbors(&neighbors, &aligned) {
+ let common_distance = distances[a].intersection(&distances[b]).next().unwrap();
+
+ let (c0, c1) = find_pair_by_distance(&aligned[a], *common_distance);
+ let (t0, t1) = find_pair_by_distance(&reports[b], *common_distance);
+
+ let mut alignment: Option = None;
+ let mut rot = 0;
+
+ while rot < 24 {
+ let r0 = t0.rotate(rot);
+ let r1 = t1.rotate(rot);
+
+ let saligned = r0 - *c0 == r1 - *c1;
+ let asaligned = r0 - *c1 == r1 - *c0;
+
+ if saligned || asaligned {
+ alignment = if saligned {
+ Some(*c0 - r0)
+ } else {
+ Some(*c1 - r0)
+ };
+ break;
+ } else {
+ rot += 1;
+ }
+ }
+
+ if let Some(alignment) = alignment {
+ alignments.push(alignment);
+ aligned[b] = reports[b]
+ .iter()
+ .map(|p| p.rotate(rot) + alignment)
+ .collect();
+ } else {
+ panic!("could not find a canonical orientation for all reports!");
+ }
+ }
+
+ (aligned, alignments)
+}
+
+pub fn part_one(input: &str) -> usize {
+ let reports = parse(input);
+ align(&reports).0.iter().flatten().unique().count()
+}
+
+pub fn part_two(input: &str) -> i32 {
+ let reports = parse(input);
+ let (_, alignments) = align(&reports);
+
+ alignments
+ .iter()
+ .tuple_combinations()
+ .map(|(a, b)| a.manhattan_distance(b))
+ .max()
+ .unwrap()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 19);
+ assert_eq!(part_one(&input), 79);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 19);
+ assert_eq!(part_two(&input), 3621);
+ }
+}
diff --git a/2022/src/solutions/day20.rs b/2022/src/solutions/day20.rs
new file mode 100644
index 0000000..6b0adb0
--- /dev/null
+++ b/2022/src/solutions/day20.rs
@@ -0,0 +1,105 @@
+type Pixels = Vec;
+type Grid = Vec;
+
+pub fn arr_to_int(bits: &[bool]) -> usize {
+ bits.iter().fold(0, |acc, &b| acc * 2 + (b as usize))
+}
+
+fn to_pixels(s: &str) -> Pixels {
+ s.chars().map(|c| c == '#').collect()
+}
+
+fn parse(input: &str) -> (Pixels, Grid) {
+ let mut lines = input.lines();
+ let cipher = to_pixels(lines.next().unwrap());
+ let grid = lines.filter(|l| !l.is_empty()).map(to_pixels).collect();
+
+ (cipher, grid)
+}
+
+fn pad(grid: &mut Vec, state: bool) {
+ let empty_line = vec![state; grid.len()];
+ grid.insert(0, empty_line.clone());
+ grid.push(empty_line);
+
+ for row in grid.iter_mut() {
+ row.insert(0, state);
+ row.push(state);
+ }
+}
+
+fn expand(grid: &mut Vec, cipher: &[bool], state: bool) -> bool {
+ pad(grid, state);
+
+ let w = grid[0].len();
+ let h = grid.len();
+
+ let mut next_grid = grid.clone();
+
+ for x in 0..w {
+ for y in 0..h {
+ let t = y == 0;
+ let l = x == 0;
+ let r = x == w - 1;
+ let b = y == h - 1;
+ let id = [
+ if !t && !l { grid[y - 1][x - 1] } else { state },
+ if !t { grid[y - 1][x] } else { state },
+ if !t && !r { grid[y - 1][x + 1] } else { state },
+ if !l { grid[y][x - 1] } else { state },
+ grid[y][x],
+ if !r { grid[y][x + 1] } else { state },
+ if !b && !l { grid[y + 1][x - 1] } else { state },
+ if !b { grid[y + 1][x] } else { state },
+ if !b && !r { grid[y + 1][x + 1] } else { state },
+ ];
+
+ next_grid[y][x] = cipher[arr_to_int(&id)];
+ }
+ }
+
+ *grid = next_grid;
+ cipher[arr_to_int(&[state; 9])]
+}
+
+fn expand_times(input: &str, times: u32) -> Vec {
+ let (cipher, mut grid) = parse(input);
+ let mut state = false;
+
+ for _ in 0..times {
+ state = expand(&mut grid, &cipher, state);
+ }
+
+ grid
+}
+
+fn count(arr: &[Pixels]) -> usize {
+ arr.iter().flatten().filter(|&&x| x).count()
+}
+
+pub fn part_one(input: &str) -> usize {
+ count(&expand_times(input, 2))
+}
+
+pub fn part_two(input: &str) -> usize {
+ count(&expand_times(input, 50))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 20);
+ assert_eq!(part_one(&input), 35);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 20);
+ assert_eq!(part_two(&input), 3351);
+ }
+}
diff --git a/2022/src/solutions/day21.rs b/2022/src/solutions/day21.rs
new file mode 100644
index 0000000..247be35
--- /dev/null
+++ b/2022/src/solutions/day21.rs
@@ -0,0 +1,107 @@
+use std::collections::HashMap;
+
+fn parse(input: &str) -> Vec {
+ input
+ .lines()
+ .filter(|l| !l.is_empty())
+ .map(|l| l.split(": ").last().unwrap().parse::().unwrap())
+ .collect()
+}
+
+fn deterministic_roll(position: &mut u64, score: &mut u64, dice: &mut u64) {
+ let roll: u64 = (*dice..(*dice + 3)).sum();
+ let next_position = ((*position + roll - 1) % 10) + 1;
+ *position = next_position;
+ *score += next_position;
+ *dice += 3;
+}
+
+pub fn part_one(input: &str) -> u64 {
+ let positions = parse(input);
+ let mut p1_position = positions[0];
+ let mut p2_position = positions[1];
+ let mut p1_score = 0;
+ let mut p2_score = 0;
+ let mut dice = 1;
+
+ loop {
+ deterministic_roll(&mut p1_position, &mut p1_score, &mut dice);
+ if p1_score >= 1000 {
+ return p2_score * (dice - 1);
+ }
+
+ deterministic_roll(&mut p2_position, &mut p2_score, &mut dice);
+ if p2_score >= 1000 {
+ return p1_score * (dice - 1);
+ }
+ }
+}
+
+// possible rolls for a 3-sided die.
+static ROLLS: [(u64, u64); 7] = [(3, 1), (4, 3), (5, 6), (6, 7), (7, 6), (8, 3), (9, 1)];
+
+type Cache = HashMap<(u64, u64, u64, u64), (u64, u64)>;
+
+fn play(
+ p1_position: u64,
+ p2_position: u64,
+ p1_score: u64,
+ p2_score: u64,
+ cache: &mut Cache,
+) -> (u64, u64) {
+ let cache_key = (p1_position, p2_position, p1_score, p2_score);
+
+ // if there is a cached resolution for this game state, return it.
+ if cache.contains_key(&cache_key) {
+ return *cache.get(&cache_key).unwrap();
+ }
+
+ // the game loop works by swapping p1 & p2 every iteration:
+ // 1. increment position and score for p1 for every possible dice roll in a turn.
+ // 2. call play with p1 and p2 swapped since it's now p2's turn.
+ // 3. if any of the previous positions won, count it as a win (=p2 is previous p1) and return.
+ // 4. once a loop returns, increment the win counter **in a swapped fashion** since the wins reported for p2 belong to p1.
+ if p2_score >= 21 {
+ (0, 1)
+ } else {
+ let res = ROLLS.iter().fold((0, 0), |acc, (roll, n)| {
+ let position = ((p1_position + roll - 1) % 10) + 1;
+ let wins = play(p2_position, position, p2_score, p1_score + position, cache);
+ (acc.0 + n * wins.1, acc.1 + n * wins.0)
+ });
+
+ cache.insert(cache_key, res);
+
+ res
+ }
+}
+
+pub fn part_two(input: &str) -> u64 {
+ let mut cache = HashMap::new();
+
+ let positions = parse(input);
+ let p1_position = positions[0];
+ let p2_position = positions[1];
+
+ let (p1_wins, p2_wins) = play(p1_position, p2_position, 0, 0, &mut cache);
+ std::cmp::max(p1_wins, p2_wins)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 21);
+ assert_eq!(part_one(&input), 739785);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 21);
+ assert_eq!(part_two(&input), 444356092776315);
+ }
+}
diff --git a/2022/src/solutions/day22.rs b/2022/src/solutions/day22.rs
new file mode 100644
index 0000000..d5486de
--- /dev/null
+++ b/2022/src/solutions/day22.rs
@@ -0,0 +1,131 @@
+use std::cmp::{max, min};
+
+#[derive(Clone)]
+struct Range {
+ from: i64,
+ to: i64,
+}
+
+#[derive(Clone)]
+struct Ranges {
+ x: Range,
+ y: Range,
+ z: Range,
+}
+
+#[derive(Clone)]
+struct Cube {
+ on: bool,
+ ranges: Ranges,
+}
+
+fn parse_range(s: &str) -> Range {
+ let mut range = s.split('=').last().unwrap().split("..");
+ let from = range.next().unwrap().parse().unwrap();
+ let to = range.next().unwrap().parse().unwrap();
+
+ Range { from, to }
+}
+
+fn parse(input: &str) -> Vec {
+ input
+ .lines()
+ .map(|l| {
+ let on = l.starts_with("on");
+ let mut ranges = l.split(' ').last().unwrap().split(',').map(parse_range);
+
+ let x = ranges.next().unwrap();
+ let y = ranges.next().unwrap();
+ let z = ranges.next().unwrap();
+ let ranges = Ranges { x, y, z };
+
+ Cube { on, ranges }
+ })
+ .collect()
+}
+
+fn intersect(a: &Range, b: &Range) -> Option {
+ if b.from > a.to || a.from > b.to {
+ None
+ } else {
+ Some(Range {
+ from: max(a.from, b.from),
+ to: min(a.to, b.to),
+ })
+ }
+}
+
+fn intersection(a: &Cube, b: &Cube, on: bool) -> Option {
+ let x = intersect(&a.ranges.x, &b.ranges.x)?;
+ let y = intersect(&a.ranges.y, &b.ranges.y)?;
+ let z = intersect(&a.ranges.z, &b.ranges.z)?;
+
+ Some(Cube {
+ on,
+ ranges: Ranges { x, y, z },
+ })
+}
+
+fn cube_diffs(instructions: Vec) -> Vec {
+ instructions.iter().fold(Vec::new(), |mut acc, curr| {
+ // add `on` instructions to the diff list.
+ let mut to_add = if curr.on { vec![curr.clone()] } else { vec![] };
+ // for every intersect with a previous diff, add a diff with opposite sign.
+ // this works because we only track `on` cubes and subtractions to them by default:
+ // 1. `off` instructions turn off intersecting parts of previous diffs.
+ // 2. previously `off` diffs can be turned back on by `on` instructions.
+ // 3. if both instructions were `on`, the new instruction cancel out old diffs to prevent duplicates.
+ to_add.extend(acc.iter().filter_map(|c| intersection(curr, c, !c.on)));
+ acc.extend(to_add);
+ acc
+ })
+}
+
+fn vol(r: Range) -> i64 {
+ r.to - r.from + 1
+}
+
+fn volume(c: Cube) -> i64 {
+ let sign = if c.on { 1 } else { -1 };
+ sign * vol(c.ranges.x) * vol(c.ranges.y) * vol(c.ranges.z)
+}
+
+pub fn part_one(input: &str) -> i64 {
+ let bounds = Cube {
+ on: true,
+ ranges: Ranges {
+ x: Range { from: -50, to: 50 },
+ y: Range { from: -50, to: 50 },
+ z: Range { from: -50, to: 50 },
+ },
+ };
+
+ cube_diffs(parse(input))
+ .into_iter()
+ .filter_map(|c| intersection(&c, &bounds, c.on))
+ .map(volume)
+ .sum()
+}
+
+pub fn part_two(input: &str) -> i64 {
+ cube_diffs(parse(input)).into_iter().map(volume).sum()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 22);
+ assert_eq!(part_one(&input), 474140);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 22);
+ assert_eq!(part_two(&input), 2758514936282235);
+ }
+}
diff --git a/2022/src/solutions/day23.rs b/2022/src/solutions/day23.rs
new file mode 100644
index 0000000..db50246
--- /dev/null
+++ b/2022/src/solutions/day23.rs
@@ -0,0 +1,28 @@
+/// Solved today by hand on a whiteboard with my family.
+/// This file contains a helper to sum moves and the correct solutions for my input.
+/// Here are some pictures of the whiteboard:
+/// [#1](https://user-images.githubusercontent.com/1682504/147255802-bf21c955-7a1f-412f-9cb0-05627d359635.jpeg)
+/// [#2](https://user-images.githubusercontent.com/1682504/147255905-00f1ac8a-3d5b-4c01-b310-a1a2655a77f4.jpeg)
+
+fn sum(l: &str, factor: u64) -> u64 {
+ l.split(' ')
+ .map(|x| x.parse::().unwrap() * factor)
+ .sum()
+}
+
+fn add_lines(pink: &str, blue: &str, green: &str, purple: &str) -> u64 {
+ sum(pink, 1) + sum(blue, 10) + sum(green, 100) + sum(purple, 1000)
+}
+
+pub fn part_one(_: &str) -> u64 {
+ add_lines("3 3 5 8", "2 3 5", "2 3 4", "9 9")
+}
+
+pub fn part_two(_: &str) -> u64 {
+ add_lines(
+ "8 8 4 5 5 5 9 9",
+ "7 4 5 8 7 7",
+ "7 2 5 6 5 6",
+ "11 11 11 11",
+ )
+}
diff --git a/2022/src/solutions/day24.rs b/2022/src/solutions/day24.rs
new file mode 100644
index 0000000..72e3bae
--- /dev/null
+++ b/2022/src/solutions/day24.rs
@@ -0,0 +1,80 @@
+use std::collections::HashMap;
+
+fn calculate_step(w: i64, z: i64, a: i64, b: i64, c: i64) -> i64 {
+ let x = ((z % 26 + b) != w) as i64;
+ (z / a) * (25 * x + 1) + ((w + c) * x)
+}
+
+fn solve() -> Vec {
+ let steps = [
+ (1, 13, 10),
+ (1, 11, 16),
+ (1, 11, 0),
+ (1, 10, 13),
+ (26, -14, 7),
+ (26, -4, 11),
+ (1, 11, 11),
+ (26, -3, 10),
+ (1, 12, 16),
+ (26, -12, 8),
+ (1, 13, 15),
+ (26, -12, 2),
+ (26, -15, 5),
+ (26, -12, 10),
+ ];
+
+ let mut step = 0;
+ let mut z_values: HashMap> = HashMap::new();
+
+ for w in 1..=9 {
+ let (a, b, c) = steps[step];
+ z_values
+ .entry(calculate_step(w, 0, a, b, c))
+ .or_default()
+ .push(w);
+ }
+
+ step += 1;
+
+ while step < 14 {
+ let values: Vec<(i64, Vec)> = z_values.drain().collect();
+
+ values.iter().for_each(|(z, nums)| {
+ let (a, b, c) = steps[step];
+
+ for w in 1..=9 {
+ let next_z = calculate_step(w, *z, a, b, c);
+ // optimization: remove z values above threshold.
+ // optimization: remove z values that do not shrink when divided / 26.
+ if (a == 1 || next_z < *z) && next_z < 1000000 {
+ let c = w.to_string().chars().next().unwrap();
+ let entry = z_values.entry(next_z).or_default();
+
+ let next_nums: Vec = nums
+ .iter()
+ .map(|n| {
+ let mut digits: Vec = n.to_string().chars().collect();
+ digits.push(c);
+ digits.iter().collect::().parse().unwrap()
+ })
+ .collect();
+
+ entry.push(*next_nums.iter().min().unwrap());
+ entry.push(*next_nums.iter().max().unwrap());
+ }
+ }
+ });
+
+ step += 1;
+ }
+
+ z_values.get(&0).unwrap().to_owned()
+}
+
+pub fn part_one(_: &str) -> i64 {
+ *solve().iter().max().unwrap()
+}
+
+pub fn part_two(_: &str) -> i64 {
+ *solve().iter().min().unwrap()
+}
diff --git a/2022/src/solutions/day25.rs b/2022/src/solutions/day25.rs
new file mode 100644
index 0000000..5a80b10
--- /dev/null
+++ b/2022/src/solutions/day25.rs
@@ -0,0 +1,109 @@
+#[derive(Clone)]
+enum Occupant {
+ EastBound,
+ SouthBound,
+ Empty,
+}
+
+type Line = Vec;
+
+fn parse(input: &str) -> Vec {
+ input
+ .lines()
+ .filter_map(|l| {
+ if l.is_empty() {
+ None
+ } else {
+ Some(
+ l.chars()
+ .map(|c| match c {
+ '>' => Occupant::EastBound,
+ 'v' => Occupant::SouthBound,
+ '.' => Occupant::Empty,
+ c => panic!("unexpected input: {}", c),
+ })
+ .collect(),
+ )
+ }
+ })
+ .collect()
+}
+
+fn simulate_step(grid: &mut Vec) -> u32 {
+ let mut moved = 0;
+
+ let w = grid[0].len();
+ let h = grid.len();
+
+ let reference = grid.clone();
+ // eastbound traffic
+ for y in 0..h {
+ for x in 0..w {
+ let x2 = if x == 0 { w - 1 } else { x - 1 };
+
+ if let Occupant::Empty = reference[y][x] {
+ if let Occupant::EastBound = reference[y][x2] {
+ grid[y][x2] = Occupant::Empty;
+ grid[y][x] = Occupant::EastBound;
+ moved += 1;
+ }
+ }
+ }
+ }
+
+ let reference = grid.clone();
+ // southbound traffic
+ for y in 0..h {
+ for x in 0..w {
+ let y2 = if y == 0 { h - 1 } else { y - 1 };
+
+ if let Occupant::Empty = reference[y][x] {
+ if let Occupant::SouthBound = reference[y2][x] {
+ grid[y2][x] = Occupant::Empty;
+ grid[y][x] = Occupant::SouthBound;
+ moved += 1;
+ }
+ }
+ }
+ }
+
+ moved
+}
+
+pub fn part_one(input: &str) -> u32 {
+ let mut grid = parse(input);
+ let mut step = 0;
+
+ loop {
+ step += 1;
+
+ if simulate_step(&mut grid) == 0 {
+ break;
+ }
+ }
+
+ step
+}
+
+pub fn part_two(_input: &str) -> u32 {
+ 0
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_part_one() {
+ use aoc::read_file;
+ let input = read_file("examples", 25);
+ assert_eq!(part_one(&input), 58);
+ }
+
+ #[test]
+ fn test_part_two() {
+ use aoc::read_file;
+ let input = read_file("examples", 25);
+ assert_eq!(part_two(&input), 0);
+ }
+}
diff --git a/2022/src/solutions/mod.rs b/2022/src/solutions/mod.rs
new file mode 100644
index 0000000..d5bc06f
--- /dev/null
+++ b/2022/src/solutions/mod.rs
@@ -0,0 +1,25 @@
+pub mod day01;
+pub mod day02;
+pub mod day03;
+pub mod day04;
+pub mod day05;
+pub mod day06;
+pub mod day07;
+pub mod day08;
+pub mod day09;
+pub mod day10;
+pub mod day11;
+pub mod day12;
+pub mod day13;
+pub mod day14;
+pub mod day15;
+pub mod day16;
+pub mod day17;
+pub mod day18;
+pub mod day19;
+pub mod day20;
+pub mod day21;
+pub mod day22;
+pub mod day23;
+pub mod day24;
+pub mod day25;