diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee304ee..52f642a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/checkout@v3 + - uses: actions/cache@v3 with: path: | ~/.cargo/registry @@ -28,6 +28,6 @@ jobs: - name: Build run: cargo build --locked --verbose - name: Lint - run: cargo clippy --all-targets --all-features -- -D warnings + run: cargo clippy - name: Run tests run: cargo test --verbose diff --git a/.gitignore b/.gitignore index 67f7931..f84186c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target .idea config.toml +.DS_Store +.project +input diff --git a/Cargo.lock b/Cargo.lock index d321d1c..f872d87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,10 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "advent-of-code" -version = "0.2020.0" +version = "0.2022.0" dependencies = [ "hashers", "lazy_static", @@ -14,13 +14,14 @@ dependencies = [ "serde_derive", "toml", "unicode-segmentation", + "uuid", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -31,6 +32,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + [[package]] name = "byteorder" version = "1.4.2" @@ -43,16 +50,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "crossbeam-channel" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.1" @@ -93,6 +90,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "fxhash" version = "0.2.1" @@ -102,6 +105,24 @@ dependencies = [ "byteorder", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashers" version = "1.0.1" @@ -112,31 +133,32 @@ dependencies = [ ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "indexmap" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ - "libc", + "equivalent", + "hashbrown", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "memchr" -version = "2.4.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -147,16 +169,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "once_cell" version = "1.13.0" @@ -165,51 +177,59 @@ checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.5.3" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ - "autocfg", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] name = "regex" -version = "1.6.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -218,9 +238,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "scopeguard" @@ -230,26 +250,38 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.142" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" -version = "1.0.143" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "syn" -version = "1.0.98" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -258,13 +290,45 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", ] +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28391a4201ba7eb1984cfeb6862c0b3ea2cfe23332298967c749dddc0d6cd976" + [[package]] name = "unicode-ident" version = "1.0.1" @@ -273,6 +337,106 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml index 23a3216..12b062a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "advent-of-code" -version = "0.2020.0" +version = "0.2022.0" authors = ["Carlos Macasaet "] edition = "2018" @@ -8,10 +8,15 @@ edition = "2018" [dependencies] hashers = "1.0.1" -lazy_static = "1.4.0" -rayon = "1.5.3" +lazy_static = "1.5.0" +rayon = "1.10.0" regex = "1" -serde = "1.0.142" -serde_derive = "1.0.143" -toml = "0.5.9" -unicode-segmentation = "1.9.0" \ No newline at end of file +serde = "1.0.219" +serde_derive = "1.0.188" +toml = "0.8.21" +unicode-segmentation = "1.12.0" +[dependencies.uuid] +version = "1.16.0" +features = [ + "v4", +] \ No newline at end of file diff --git a/README.md b/README.md index 93ee912..4e19014 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# Advent of Code 2020 (Rust) +# Advent of Code 2022 (Rust) ## Other Editions -* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java)) +* 2020 Advent of Code ( [Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust/tree/y2020) ) +* 2021 Advent of Code ( [Java](https://github.com/l0s/advent-of-code-java/tree/2021) ) +* 2022 Advent of Code ( [Java](https://github.com/l0s/advent-of-code-java) ) diff --git a/sample/day-01.txt b/sample/day-01.txt new file mode 100644 index 0000000..444e241 --- /dev/null +++ b/sample/day-01.txt @@ -0,0 +1,14 @@ +1000 +2000 +3000 + +4000 + +5000 +6000 + +7000 +8000 +9000 + +10000 \ No newline at end of file diff --git a/sample/day-02.txt b/sample/day-02.txt new file mode 100644 index 0000000..25097e8 --- /dev/null +++ b/sample/day-02.txt @@ -0,0 +1,3 @@ +A Y +B X +C Z \ No newline at end of file diff --git a/sample/day-03.txt b/sample/day-03.txt new file mode 100644 index 0000000..9919ffa --- /dev/null +++ b/sample/day-03.txt @@ -0,0 +1,6 @@ +vJrwpWtwJgWrhcsFMMfFFhFp +jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL +PmmdzqPrVvPwwTWBwg +wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn +ttgJtRGJQctTZtZT +CrZsJsPPZsGzwwsLwLmpwMDw \ No newline at end of file diff --git a/sample/day-04.txt b/sample/day-04.txt new file mode 100644 index 0000000..99a66c5 --- /dev/null +++ b/sample/day-04.txt @@ -0,0 +1,6 @@ +2-4,6-8 +2-3,4-5 +5-7,7-9 +2-8,3-7 +6-6,4-6 +2-6,4-8 \ No newline at end of file diff --git a/sample/day-05.txt b/sample/day-05.txt new file mode 100644 index 0000000..e98aba4 --- /dev/null +++ b/sample/day-05.txt @@ -0,0 +1,9 @@ + [D] +[N] [C] +[Z] [M] [P] + 1 2 3 + +move 1 from 2 to 1 +move 3 from 1 to 3 +move 2 from 2 to 1 +move 1 from 1 to 2 \ No newline at end of file diff --git a/sample/day-06.txt b/sample/day-06.txt new file mode 100644 index 0000000..5a2b0a7 --- /dev/null +++ b/sample/day-06.txt @@ -0,0 +1 @@ +mjqjpqmgbljsphdztnvjfqwrcgsmlb \ No newline at end of file diff --git a/sample/day-07.txt b/sample/day-07.txt new file mode 100644 index 0000000..bcbb513 --- /dev/null +++ b/sample/day-07.txt @@ -0,0 +1,23 @@ +$ cd / +$ ls +dir a +14848514 b.txt +8504156 c.dat +dir d +$ cd a +$ ls +dir e +29116 f +2557 g +62596 h.lst +$ cd e +$ ls +584 i +$ cd .. +$ cd .. +$ cd d +$ ls +4060174 j +8033020 d.log +5626152 d.ext +7214296 k \ No newline at end of file diff --git a/sample/day-08.txt b/sample/day-08.txt new file mode 100644 index 0000000..6557024 --- /dev/null +++ b/sample/day-08.txt @@ -0,0 +1,5 @@ +30373 +25512 +65332 +33549 +35390 \ No newline at end of file diff --git a/sample/day-09.txt b/sample/day-09.txt new file mode 100644 index 0000000..cbea2b3 --- /dev/null +++ b/sample/day-09.txt @@ -0,0 +1,8 @@ +R 4 +U 4 +L 3 +D 1 +R 4 +D 1 +L 5 +R 2 \ No newline at end of file diff --git a/sample/day-1-input.txt b/sample/day-1-input.txt deleted file mode 100644 index e3fb011..0000000 --- a/sample/day-1-input.txt +++ /dev/null @@ -1,6 +0,0 @@ -1721 -979 -366 -299 -675 -1456 diff --git a/sample/day-10-input.txt b/sample/day-10-input.txt deleted file mode 100644 index be5c492..0000000 --- a/sample/day-10-input.txt +++ /dev/null @@ -1,31 +0,0 @@ -28 -33 -18 -42 -31 -14 -46 -20 -48 -47 -24 -23 -49 -45 -19 -38 -39 -11 -1 -32 -25 -35 -8 -17 -7 -9 -4 -2 -34 -10 -3 \ No newline at end of file diff --git a/sample/day-10.txt b/sample/day-10.txt new file mode 100644 index 0000000..94cd0a8 --- /dev/null +++ b/sample/day-10.txt @@ -0,0 +1,146 @@ +addx 15 +addx -11 +addx 6 +addx -3 +addx 5 +addx -1 +addx -8 +addx 13 +addx 4 +noop +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx -35 +addx 1 +addx 24 +addx -19 +addx 1 +addx 16 +addx -11 +noop +noop +addx 21 +addx -15 +noop +noop +addx -3 +addx 9 +addx 1 +addx -3 +addx 8 +addx 1 +addx 5 +noop +noop +noop +noop +noop +addx -36 +noop +addx 1 +addx 7 +noop +noop +noop +addx 2 +addx 6 +noop +noop +noop +noop +noop +addx 1 +noop +noop +addx 7 +addx 1 +noop +addx -13 +addx 13 +addx 7 +noop +addx 1 +addx -33 +noop +noop +noop +addx 2 +noop +noop +noop +addx 8 +noop +addx -1 +addx 2 +addx 1 +noop +addx 17 +addx -9 +addx 1 +addx 1 +addx -3 +addx 11 +noop +noop +addx 1 +noop +addx 1 +noop +noop +addx -13 +addx -19 +addx 1 +addx 3 +addx 26 +addx -30 +addx 12 +addx -1 +addx 3 +addx 1 +noop +noop +noop +addx -9 +addx 18 +addx 1 +addx 2 +noop +noop +addx 9 +noop +noop +noop +addx -1 +addx 2 +addx -37 +addx 1 +addx 3 +noop +addx 15 +addx -21 +addx 22 +addx -6 +addx 1 +noop +addx 2 +addx 1 +noop +addx -10 +noop +noop +addx 20 +addx 1 +addx 2 +addx 2 +addx -6 +addx -11 +noop +noop +noop \ No newline at end of file diff --git a/sample/day-11-input.txt b/sample/day-11-input.txt deleted file mode 100644 index 1beaede..0000000 --- a/sample/day-11-input.txt +++ /dev/null @@ -1,10 +0,0 @@ -L.LL.LL.LL -LLLLLLL.LL -L.L.L..L.. -LLLL.LL.LL -L.LL.LL.LL -L.LLLLL.LL -..L.L..... -LLLLLLLLLL -L.LLLLLL.L -L.LLLLL.LL diff --git a/sample/day-11.txt b/sample/day-11.txt new file mode 100644 index 0000000..c04eddb --- /dev/null +++ b/sample/day-11.txt @@ -0,0 +1,27 @@ +Monkey 0: + Starting items: 79, 98 + Operation: new = old * 19 + Test: divisible by 23 + If true: throw to monkey 2 + If false: throw to monkey 3 + +Monkey 1: + Starting items: 54, 65, 75, 74 + Operation: new = old + 6 + Test: divisible by 19 + If true: throw to monkey 2 + If false: throw to monkey 0 + +Monkey 2: + Starting items: 79, 60, 97 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 3: + Starting items: 74 + Operation: new = old + 3 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 1 \ No newline at end of file diff --git a/sample/day-12-input.txt b/sample/day-12-input.txt deleted file mode 100644 index d382291..0000000 --- a/sample/day-12-input.txt +++ /dev/null @@ -1,5 +0,0 @@ -F10 -N3 -F7 -R90 -F11 diff --git a/sample/day-12.txt b/sample/day-12.txt new file mode 100644 index 0000000..86e9cac --- /dev/null +++ b/sample/day-12.txt @@ -0,0 +1,5 @@ +Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi diff --git a/sample/day-13-input.txt b/sample/day-13-input.txt deleted file mode 100644 index d76f619..0000000 --- a/sample/day-13-input.txt +++ /dev/null @@ -1,2 +0,0 @@ -939 -7,13,x,x,59,x,31,19 diff --git a/sample/day-13.txt b/sample/day-13.txt new file mode 100644 index 0000000..27c8912 --- /dev/null +++ b/sample/day-13.txt @@ -0,0 +1,23 @@ +[1,1,3,1,1] +[1,1,5,1,1] + +[[1],[2,3,4]] +[[1],4] + +[9] +[[8,7,6]] + +[[4,4],4,4] +[[4,4],4,4,4] + +[7,7,7,7] +[7,7,7] + +[] +[3] + +[[[]]] +[[]] + +[1,[2,[3,[4,[5,6,7]]]],8,9] +[1,[2,[3,[4,[5,6,0]]]],8,9] \ No newline at end of file diff --git a/sample/day-14-input.txt b/sample/day-14-input.txt deleted file mode 100644 index b4b4e06..0000000 --- a/sample/day-14-input.txt +++ /dev/null @@ -1,4 +0,0 @@ -mask = 000000000000000000000000000000X1001X -mem[42] = 100 -mask = 00000000000000000000000000000000X0XX -mem[26] = 1 \ No newline at end of file diff --git a/sample/day-15-input.txt b/sample/day-15-input.txt deleted file mode 100644 index c84ffe7..0000000 --- a/sample/day-15-input.txt +++ /dev/null @@ -1 +0,0 @@ -0,3,6 diff --git a/sample/day-16-input.txt b/sample/day-16-input.txt deleted file mode 100644 index 29738e6..0000000 --- a/sample/day-16-input.txt +++ /dev/null @@ -1,12 +0,0 @@ -departure: 1-3 or 5-7 -row: 6-11 or 33-44 -seat: 13-40 or 45-50 - -your ticket: -7,1,14 - -nearby tickets: -7,3,47 -40,4,50 -55,2,20 -38,6,12 diff --git a/sample/day-17-input.txt b/sample/day-17-input.txt deleted file mode 100644 index eedd3d2..0000000 --- a/sample/day-17-input.txt +++ /dev/null @@ -1,3 +0,0 @@ -.#. -..# -### diff --git a/sample/day-18-input.txt b/sample/day-18-input.txt deleted file mode 100644 index 9e1ad36..0000000 --- a/sample/day-18-input.txt +++ /dev/null @@ -1,4 +0,0 @@ -2 * 3 + (4 * 5) -5 + (8 * 3 + 9 + 3 * 4 * 3) -5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4)) -((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2 diff --git a/sample/day-19-input.txt b/sample/day-19-input.txt deleted file mode 100644 index 1500b6f..0000000 --- a/sample/day-19-input.txt +++ /dev/null @@ -1,12 +0,0 @@ -0: 4 1 5 -1: 2 3 | 3 2 -2: 4 4 | 5 5 -3: 4 5 | 5 4 -4: "a" -5: "b" - -ababbb -bababa -abbbab -aaabbb -aaaabbb diff --git a/sample/day-2-input.txt b/sample/day-2-input.txt deleted file mode 100644 index fe19c03..0000000 --- a/sample/day-2-input.txt +++ /dev/null @@ -1,3 +0,0 @@ -1-3 a: abcde -1-3 b: cdefg -2-9 c: ccccccccc diff --git a/sample/day-20-input.txt b/sample/day-20-input.txt deleted file mode 100644 index 546e71d..0000000 --- a/sample/day-20-input.txt +++ /dev/null @@ -1,108 +0,0 @@ -Tile 2311: -..##.#..#. -##..#..... -#...##..#. -####.#...# -##.##.###. -##...#.### -.#.#.#..## -..#....#.. -###...#.#. -..###..### - -Tile 1951: -#.##...##. -#.####...# -.....#..## -#...###### -.##.#....# -.###.##### -###.##.##. -.###....#. -..#.#..#.# -#...##.#.. - -Tile 1171: -####...##. -#..##.#..# -##.#..#.#. -.###.####. -..###.#### -.##....##. -.#...####. -#.##.####. -####..#... -.....##... - -Tile 1427: -###.##.#.. -.#..#.##.. -.#.##.#..# -#.#.#.##.# -....#...## -...##..##. -...#.##### -.#.####.#. -..#..###.# -..##.#..#. - -Tile 1489: -##.#.#.... -..##...#.. -.##..##... -..#...#... -#####...#. -#..#.#.#.# -...#.#.#.. -##.#...##. -..##.##.## -###.##.#.. - -Tile 2473: -#....####. -#..#.##... -#.##..#... -######.#.# -.#...#.#.# -.######### -.###.#..#. -########.# -##...##.#. -..###.#.#. - -Tile 2971: -..#.#....# -#...###... -#.#.###... -##.##..#.. -.#####..## -.#..####.# -#..#.#..#. -..####.### -..#.#.###. -...#.#.#.# - -Tile 2729: -...#.#.#.# -####.#.... -..#.#..... -....#..#.# -.##..##.#. -.#.####... -####.#.#.. -##.####... -##..#.##.. -#.##...##. - -Tile 3079: -#.#.#####. -.#..###### -..#....... -######.... -####.#..#. -.#...#.##. -#.#####.## -..#.###... -..#....... -..#.###... - diff --git a/sample/day-21-input.txt b/sample/day-21-input.txt deleted file mode 100644 index 1c0ea2f..0000000 --- a/sample/day-21-input.txt +++ /dev/null @@ -1,4 +0,0 @@ -mxmxvkd kfcds sqjhc nhms (contains dairy, fish) -trh fvjkl sbzzf mxmxvkd (contains dairy) -sqjhc fvjkl (contains soy) -sqjhc mxmxvkd sbzzf (contains fish) diff --git a/sample/day-22-input.txt b/sample/day-22-input.txt deleted file mode 100644 index 391cd24..0000000 --- a/sample/day-22-input.txt +++ /dev/null @@ -1,13 +0,0 @@ -Player 1: -9 -2 -6 -3 -1 - -Player 2: -5 -8 -4 -7 -10 diff --git a/sample/day-23-input.txt b/sample/day-23-input.txt deleted file mode 100644 index 17f006f..0000000 --- a/sample/day-23-input.txt +++ /dev/null @@ -1 +0,0 @@ -364289715 diff --git a/sample/day-24-input.txt b/sample/day-24-input.txt deleted file mode 100644 index 3dc2f67..0000000 --- a/sample/day-24-input.txt +++ /dev/null @@ -1,20 +0,0 @@ -sesenwnenenewseeswwswswwnenewsewsw -neeenesenwnwwswnenewnwwsewnenwseswesw -seswneswswsenwwnwse -nwnwneseeswswnenewneswwnewseswneseene -swweswneswnenwsewnwneneseenw -eesenwseswswnenwswnwnwsewwnwsene -sewnenenenesenwsewnenwwwse -wenwwweseeeweswwwnwwe -wsweesenenewnwwnwsenewsenwwsesesenwne -neeswseenwwswnwswswnw -nenwswwsewswnenenewsenwsenwnesesenew -enewnwewneswsewnwswenweswnenwsenwsw -sweneswneswneneenwnewenewwneswswnese -swwesenesewenwneswnwwneseswwne -enesenwswwswneneswsenwnewswseenwsese -wnwnesenesenenwwnenwsewesewsesesew -nenewswnwewswnenesenwnesewesw -eneswnwswnwsenenwnwnwwseeswneewsenese -neswnwewnwnwseenwseesewsenwsweewe -wseweeenwnesenwwwswnew diff --git a/sample/day-3-input.txt b/sample/day-3-input.txt deleted file mode 100644 index 7e88cdc..0000000 --- a/sample/day-3-input.txt +++ /dev/null @@ -1,11 +0,0 @@ -..##....... -#...#...#.. -.#....#..#. -..#.#...#.# -.#...##..#. -..#.##..... -.#.#.#....# -.#........# -#.##...#... -#...##....# -.#..#...#.# diff --git a/sample/day-4-input.txt b/sample/day-4-input.txt deleted file mode 100644 index 0ff208e..0000000 --- a/sample/day-4-input.txt +++ /dev/null @@ -1,13 +0,0 @@ -ecl:gry pid:860033327 eyr:2020 hcl:#fffffd -byr:1937 iyr:2017 cid:147 hgt:183cm - -iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 -hcl:#cfa07d byr:1929 - -hcl:#ae17e1 iyr:2013 -eyr:2024 -ecl:brn pid:760753108 byr:1931 -hgt:179cm - -hcl:#cfa07d eyr:2025 pid:166559648 -iyr:2011 ecl:brn hgt:59in diff --git a/sample/day-5-input.txt b/sample/day-5-input.txt deleted file mode 100644 index 6b89eb8..0000000 --- a/sample/day-5-input.txt +++ /dev/null @@ -1,4 +0,0 @@ -FBFBBFFRLR -BFFFBBFRRR -FFFBBBFRRR -BBFFBBFRLL diff --git a/sample/day-6-input.txt b/sample/day-6-input.txt deleted file mode 100644 index 0f5b3bc..0000000 --- a/sample/day-6-input.txt +++ /dev/null @@ -1,15 +0,0 @@ -abc - -a -b -c - -ab -ac - -a -a -a -a - -b diff --git a/sample/day-7-input.txt b/sample/day-7-input.txt deleted file mode 100644 index 1cec74f..0000000 --- a/sample/day-7-input.txt +++ /dev/null @@ -1,9 +0,0 @@ -light red bags contain 1 bright white bag, 2 muted yellow bags. -dark orange bags contain 3 bright white bags, 4 muted yellow bags. -bright white bags contain 1 shiny gold bag. -muted yellow bags contain 2 shiny gold bags, 9 faded blue bags. -shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags. -dark olive bags contain 3 faded blue bags, 4 dotted black bags. -vibrant plum bags contain 5 faded blue bags, 6 dotted black bags. -faded blue bags contain no other bags. -dotted black bags contain no other bags. diff --git a/sample/day-8-input.txt b/sample/day-8-input.txt deleted file mode 100644 index 178df53..0000000 --- a/sample/day-8-input.txt +++ /dev/null @@ -1,9 +0,0 @@ -nop +0 -acc +1 -jmp +4 -acc +3 -jmp -3 -acc -99 -acc +1 -jmp -4 -acc +6 diff --git a/sample/day-9-input.txt b/sample/day-9-input.txt deleted file mode 100644 index 05803e5..0000000 --- a/sample/day-9-input.txt +++ /dev/null @@ -1,40 +0,0 @@ -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -9223372036854775807 -35 -20 -15 -25 -47 -40 -62 -55 -65 -95 -103 -117 -150 -182 -127 -219 -299 -277 -309 -576 \ No newline at end of file diff --git a/src/day01.rs b/src/day01.rs index b1e43b7..3b1b39b 100644 --- a/src/day01.rs +++ b/src/day01.rs @@ -1,66 +1,77 @@ -use crate::get_lines; +/// --- Day 1: Calorie Counting --- +/// https://adventofcode.com/2022/day/1 +use crate::get_block_strings; +use std::cmp::Ordering; -pub fn get_input() -> Vec { - let mut items: Vec = get_lines("day-1-input.txt") - .map(|line| line.parse::().unwrap()) - .filter(|candidate| *candidate <= 2020u16) // filter out anything that is obviously too big - .collect(); - items.sort_unstable(); // sort to reduce the search space for the complement - items +type CalorieCount = u32; + +pub fn get_elves(max: usize) -> Vec { + let mut result = vec![]; + for elf in get_block_strings("day-01.txt") + .map(|block| { + block + .split('\n') + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(|line| line.parse::().expect("Invalid calorie count")) + .sum() + }) + .map(|calories_carried| Elf { calories_carried }) + { + let index = match result.binary_search(&elf) { + Ok(index) => index, + Err(index) => index, + }; + result.insert(index, elf); + if result.len() > max { + result.remove(result.len() - 1); + } + } + result +} + +#[derive(Debug)] +pub struct Elf { + calories_carried: CalorieCount, +} + +impl Eq for Elf {} + +impl PartialEq for Elf { + fn eq(&self, other: &Self) -> bool { + self.calories_carried == other.calories_carried + } +} + +impl PartialOrd for Elf { + fn partial_cmp(&self, other: &Self) -> Option { + other.calories_carried.partial_cmp(&self.calories_carried) + } +} + +impl Ord for Elf { + fn cmp(&self, other: &Self) -> Ordering { + other.calories_carried.cmp(&self.calories_carried) + } } #[cfg(test)] mod tests { - use std::cmp::Ordering; + use crate::day01::{get_elves, CalorieCount}; #[test] fn part1() { - let items = super::get_input(); + let elves = get_elves(1); + let result: CalorieCount = elves.iter().map(|elf| elf.calories_carried).sum(); - for i in 0..items.len() - 1 { - let x: u32 = items[i].into(); - for y in items.iter().skip(i + 1) { - let y = *y as u32; - let result = x + y; - match result.cmp(&2020) { - Ordering::Equal => { - println!("Part 1: {}", (x * y)); - return; - } - Ordering::Greater => { - break; - } - Ordering::Less => {} - } - } - } + println!("Part 1: {}", result); } #[test] fn part2() { - let items = super::get_input(); - for i in 0..items.len() - 2 { - let x: u32 = items[i].into(); - for j in i + 1..items.len() - 1 { - let y: u32 = items[j].into(); - if x + y > 2020 { - break; - } - for z in items.iter().skip(j + 1) { - let z = *z as u32; - let result = x + y + z; - match result.cmp(&2020u32) { - Ordering::Less => {} - Ordering::Equal => { - println!("Part 2: {}", (x * y * z)); - return; - } - Ordering::Greater => { - break; - } - } - } - } - } + let elves = get_elves(3); + let result: CalorieCount = elves.iter().map(|elf| elf.calories_carried).sum(); + + println!("Part 2: {}", result); } } diff --git a/src/day02.rs b/src/day02.rs index 3ef99cc..6a1173c 100644 --- a/src/day02.rs +++ b/src/day02.rs @@ -1,109 +1,161 @@ -use unicode_segmentation::UnicodeSegmentation; +/// --- Day 2: Rock Paper Scissors --- +/// https://adventofcode.com/2022/day/2 +use std::str::FromStr; + +use crate::day02::ResponseStrategy::{Draw, Lose, Win}; +use crate::day02::Shape::{Paper, Rock, Scissors}; use crate::get_lines; -pub fn get_input() -> impl Iterator { - get_lines("day-2-input.txt") +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum Shape { + Rock, + Paper, + Scissors, } -trait Entry { - fn is_valid(&self) -> bool; - fn build(line: String) -> Self; // TODO impl FromStr +impl Shape { + pub fn beats(&self) -> Self { + match self { + Rock => Scissors, + Paper => Rock, + Scissors => Paper, + } + } + + pub fn beaten_by(&self) -> Self { + match self { + Rock => Paper, + Paper => Scissors, + Scissors => Rock, + } + } + + pub fn value(&self) -> u16 { + match self { + Rock => 1, + Paper => 2, + Scissors => 3, + } + } } -pub struct SledEntry { - c: String, - password: String, - min_iterations: usize, - max_iterations: usize, +impl FromStr for Shape { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "A" | "X" => Ok(Rock), + "B" | "Y" => Ok(Paper), + "C" | "Z" => Ok(Scissors), + _ => Err(()), + } + } } -impl Entry for SledEntry { - fn is_valid(&self) -> bool { - let count: usize = self - .password - .graphemes(true) - .filter(|grapheme| *grapheme == self.c) - .count() as usize; - count >= self.min_iterations && count <= self.max_iterations +pub enum ResponseStrategy { + Lose, + Draw, + Win, +} + +impl ResponseStrategy { + pub fn respond_to(&self, opponent: &Shape) -> Shape { + match self { + Lose => opponent.beats(), + Draw => *opponent, + Win => opponent.beaten_by(), + } } +} - fn build(line: String) -> Self { - let mut components = line.split_whitespace(); - let range_string = components.next().unwrap(); - let c = components.next().unwrap().replace(':', ""); - let password = components.next().unwrap().to_owned(); - - let mut range_components = range_string - .split('-') - .map(|component| component.parse::()) - .map(Result::unwrap); - let min_iterations = range_components.next().unwrap(); - let max_iterations = range_components.next().unwrap(); - - SledEntry { - c, - password, - min_iterations, - max_iterations, +impl FromStr for ResponseStrategy { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "X" => Ok(Lose), + "Y" => Ok(Draw), + "Z" => Ok(Win), + _ => Err(()), } } } -pub struct TobogganEntry { - c: String, - password: String, - first_position: usize, - second_position: usize, +pub struct Round { + opponent_shape: Shape, + potential_response: Shape, + response_strategy: ResponseStrategy, } -impl Entry for TobogganEntry { - fn is_valid(&self) -> bool { - let password_chars = &self.password.graphemes(true).collect::>(); - let x = password_chars[&self.first_position - 1]; - let y = password_chars[&self.second_position - 1]; - (x == self.c) ^ (y == self.c) +impl Round { + pub fn naïve_score(&self) -> u16 { + outcome(&self.opponent_shape, &self.potential_response) + self.potential_response.value() + } + + pub fn score(&self) -> u16 { + let response = self.response_strategy.respond_to(&self.opponent_shape); + outcome(&self.opponent_shape, &response) + response.value() } +} - fn build(line: String) -> Self { +impl FromStr for Round { + type Err = &'static str; + + fn from_str(line: &str) -> Result { let mut components = line.split_whitespace(); - let position_str = components.next().unwrap(); - let c = components.next().unwrap().replace(':', ""); - let password = components.next().unwrap().to_owned(); - let mut position_components = position_str - .split('-') - .map(|component| component.parse::()) - .map(Result::unwrap); - let first_position: usize = position_components.next().unwrap(); - let second_position: usize = position_components.next().unwrap(); - TobogganEntry { - c, - password, - first_position, - second_position, - } + let opponent_shape = components + .next() + .ok_or("Opponent's shape code not found")? + .parse::() + .ok() + .ok_or("Could not decipher opponent's shape")?; + let strategy_code = components.next().ok_or("Strategy code not found")?; + let potential_response = strategy_code + .parse::() + .ok() + .ok_or("Could not decipher potential response")?; + let response_strategy = strategy_code + .parse::() + .ok() + .ok_or("Could not decipher response strategy")?; + Ok(Round { + opponent_shape, + potential_response, + response_strategy, + }) } } +fn outcome(opponent: &Shape, player: &Shape) -> u16 { + if opponent == player { + 3 + } else if opponent.beaten_by() == *player { + 6 + } else { + 0 + } +} + +pub fn get_input() -> impl Iterator { + get_lines("day-02.txt").map(|line| line.parse::().expect("Unable to parse round")) +} + #[cfg(test)] mod tests { - use crate::day02::{get_input, Entry, SledEntry, TobogganEntry}; + use crate::day02::get_input; #[test] fn part1() { - let count = get_input() - .map(SledEntry::build) - .filter(SledEntry::is_valid) - .count(); - println!("Part 1: {}", count); + let result: u16 = get_input().map(|round| round.naïve_score()).sum(); + + println!("Part 1: {}", result); } #[test] fn part2() { - let count = get_input() - .map(TobogganEntry::build) - .filter(TobogganEntry::is_valid) - .count(); - println!("Part 2: {}", count); + let result: u16 = get_input().map(|round| round.score()).sum(); + + println!("Part 2: {}", result); } } diff --git a/src/day03.rs b/src/day03.rs index cc2085f..d00a366 100644 --- a/src/day03.rs +++ b/src/day03.rs @@ -1,99 +1,131 @@ +use std::collections::HashSet; +/// --- Day 3: Rucksack Reorganization --- +/// https://adventofcode.com/2022/day/3 +use std::str::FromStr; + use crate::get_lines; -fn get_input() -> impl Iterator { - get_lines("day-3-input.txt") +/// A container with supplies for a jungle journey. "Each rucksack has two large compartments. All +/// items of a given type are meant to go into exactly one of the two compartments." +pub struct Rucksack { + items: HashSet, + compartments: (HashSet, HashSet), } -/// A map of the trees in the area -pub fn get_map() -> Vec> { - get_input() - .map(|line| { - line.chars() - .map(|square| square == '#') - .collect::>() - }) - .collect() +impl Rucksack { + pub fn priority(&self) -> Result { + let mut intersection = self.compartments.0.intersection(&self.compartments.1); + if let Some(common_item) = intersection.next() { + if intersection.next().is_some() { + return Err("Multiple common items between the compartments"); + } + return Ok(priority(*common_item)); + } + Err("No common items between the compartments") + } } -/// Returns the number of trees a toboggan will encounter when traveling from the top-left of a map -/// to the bottom. -/// -/// Parameters -/// * `forest` - a map of the area with `true` squares denoting the presence of a tree -/// * `slope` - the angle at which the toboggan travels -pub fn count_trees(forest: &[Vec], slope: &Slope) -> u8 { - let mut num_trees: u8 = 0; - let mut row_index: usize = 0; - let mut column_index: usize = 0; +/// A value to assist in item reärrangement +fn priority(item: char) -> u32 { + let item = item as u32; + if item >= 'a' as u32 && item <= 'z' as u32 { + item - 'a' as u32 + 1 + } else { + item - 'A' as u32 + 27 + } +} - loop { - let row = &forest[row_index]; - let cell = &row[column_index]; - if *cell { - num_trees += 1; - } - // waiting for destructuring assignment: https://github.com/rust-lang/rfcs/issues/372 - let (new_row, new_column) = slope.move_toboggan(row_index, column_index, row.len()); - row_index = new_row; - column_index = new_column; +impl FromStr for Rucksack { + type Err = &'static str; - if row_index >= forest.len() { - break num_trees; + fn from_str(s: &str) -> Result { + let items = s.chars().collect::>(); + if items.len() % 2 != 0 { + return Err("The items cannot be evenly divided between the two compartments"); } + let compartments = items.split_at(items.len() / 2); + let compartments = ( + compartments.0.iter().copied().collect::>(), + compartments.1.iter().copied().collect::>(), + ); + let items = items.iter().copied().collect::>(); + Ok(Self { + items, + compartments, + }) } } -pub struct Slope { - right: usize, - down: usize, +/// A group of elves' rucksacks. There is exactly one common item shared by each group member. +pub struct Group { + members: (Rucksack, Rucksack, Rucksack), } -impl Slope { - /// Returns the new coordinates of the toboggan on the map - pub fn move_toboggan( - &self, - current_row: usize, - current_column: usize, - num_columns: usize, - ) -> (usize, usize) { - let row = current_row + self.down; - let column = current_column + self.right; - // These aren't the only trees, though; due to something you read about once involving - // arboreal genetics and biome stability, the same pattern repeats to the right many times - let column = column % num_columns; - (row, column) +impl Group { + /// The priority of shared item + pub fn badge_priority(&self) -> Result { + let badge = self.badge()?; + Ok(priority(badge)) } + fn badge(&self) -> Result { + let intersection = self + .members + .0 + .items + .intersection(&self.members.1.items) + .copied() + .collect::>(); + let mut intersection = intersection.intersection(&self.members.2.items); + if let Some(badge) = intersection.next() { + if intersection.next().is_some() { + return Err("Multiple items in common between members of the group"); + } + return Ok(*badge); + } + Err("No items in common between members of the group") + } +} + +pub fn get_input() -> impl Iterator { + get_lines("day-03.txt") + .map(|line| line.parse::()) + .map(Result::unwrap) } #[cfg(test)] mod tests { - use crate::day03::{count_trees, get_map, Slope}; + + use crate::day03::{get_input, Group}; #[test] fn part1() { - let slope = Slope { right: 3, down: 1 }; - - let num_trees = count_trees(&get_map(), &slope); + let result: u32 = get_input() + .map(|rucksack| rucksack.priority().unwrap()) + .sum(); - println!("Part 1: {}", num_trees); + println!("Part 1: {}", result); } #[test] fn part2() { - let slopes = vec![ - Slope { right: 1, down: 1 }, - Slope { right: 3, down: 1 }, - Slope { right: 5, down: 1 }, - Slope { right: 7, down: 1 }, - Slope { right: 1, down: 2 }, - ]; - let forest = get_map(); - - let num_trees = slopes + let mut groups = vec![]; + let mut iter = get_input(); + loop { + if let Some(first) = iter.next() { + let second = iter.next().expect("Group only has one item"); + let third = iter.next().expect("Group only has two items"); + let members = (first, second, third); + groups.push(Group { members }); + } else { + break; + } + } + let result: u32 = groups .iter() - .map(|slope| count_trees(&forest, slope)) - .fold(1u64, |accumulator, count| accumulator * (count as u64)); + .map(Group::badge_priority) + .map(Result::unwrap) + .sum(); - println!("Part 2: {}", num_trees); + println!("Part 2: {}", result); } } diff --git a/src/day04.rs b/src/day04.rs index 1dae851..3ae072b 100644 --- a/src/day04.rs +++ b/src/day04.rs @@ -1,118 +1,113 @@ -use std::collections::HashMap; +/// --- Day 4: Camp Cleanup --- +/// https://adventofcode.com/2022/day/4 +use std::str::FromStr; -use crate::get_block_strings; +use crate::get_lines; -fn get_input() -> impl Iterator { - get_block_strings("day-4-input.txt") +pub type SectionId = u8; + +/// Someone responsible for cleaning a section of the camp +pub struct Elf { + lower_section_id: SectionId, + upper_section_id: SectionId, +} + +impl Elf { + /// Determine if this Elf's realm of responsibility fully encompasses that of the other elf + pub fn fully_contains(&self, other: &Self) -> bool { + self.lower_section_id <= other.lower_section_id + && self.upper_section_id >= other.upper_section_id + } +} + +impl FromStr for Elf { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let mut components = s.split('-'); + let lower_section_id = components + .next() + .ok_or("No section range defined")? + .parse::() + .map_err(|_| "Unparseable lower bound")?; + let upper_section_id = components + .next() + .ok_or("Range has no upper bound")? + .parse::() + .map_err(|_| "Unparseable upper bound")?; + if components.next().is_some() { + return Err("Invalid section range"); + } + Ok(Self { + lower_section_id, + upper_section_id, + }) + } +} + +/// Two crew members responsible for cleaning part of the camp +pub struct Pair(Elf, Elf); + +impl Pair { + /// Identify an inefficiency in which one elf's responsibility fully encompasses the other's + pub fn one_fully_contains_the_other(&self) -> bool { + self.0.fully_contains(&self.1) || self.1.fully_contains(&self.0) + } + + /// Identity an inefficiency in which there is at least one section for which both elves are + /// responsible + pub fn sections_overlap(&self) -> bool { + (self.0.lower_section_id <= self.1.lower_section_id + && self.0.upper_section_id >= self.1.lower_section_id) + || (self.1.lower_section_id <= self.0.lower_section_id + && self.1.upper_section_id >= self.0.lower_section_id) + } } -pub fn get_blocks() -> impl Iterator> { - get_input().map(|block| -> HashMap { - block - .split_whitespace() - .map(|string| string.trim()) - .filter(|block_entry| !block_entry.is_empty()) - .map(|block_entry| -> (String, String) { - let (key, value) = block_entry.split_once(':').unwrap(); - (key.trim().to_owned(), value.trim().to_owned()) - }) - .collect() - }) +impl FromStr for Pair { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let mut components = s.split(','); + let x = components + .next() + .ok_or("No pair of elves defined")? + .parse::()?; + let y = components + .next() + .ok_or("Only one elf found")? + .parse::()?; + if components.next().is_some() { + return Err("Too many elves specified"); + } + Ok(Self(x, y)) + } +} + +pub fn get_input() -> impl Iterator { + get_lines("day-04.txt") + .map(|line| line.parse::()) + .map(Result::unwrap) } #[cfg(test)] mod tests { - use std::collections::HashSet; - - use regex::Regex; - use crate::day04::get_blocks; + use crate::day04::{get_input, Pair}; #[test] fn part1() { - let count_valid = get_blocks() - .filter(|block| block.contains_key("byr")) - .filter(|block| block.contains_key("iyr")) - .filter(|block| block.contains_key("eyr")) - .filter(|block| block.contains_key("hgt")) - .filter(|block| block.contains_key("hcl")) - .filter(|block| block.contains_key("ecl")) - .filter(|block| block.contains_key("pid")) + let result = get_input() + .filter(Pair::one_fully_contains_the_other) .count(); - println!("Part 1: {}", count_valid); + + println!("Part 1: {}", result); } #[test] fn part2() { - let valid_hair_colour = Regex::new(r"^#[0-9a-f]{6}$").unwrap(); - let valid_eye_colours: HashSet<&'static str> = - ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"] - .iter() - .cloned() - .collect(); - let valid_passport_id = Regex::new(r"^[0-9]{9}$").unwrap(); - - let count_valid = get_blocks() - .filter(|block| -> bool { - let birth_year: Option = block - .get("byr") - .and_then(|string| string.parse::().ok()) - .filter(|year| *year >= 1920 && *year <= 2002); - birth_year.is_some() - }) - .filter(|block| -> bool { - let issue_year: Option = block - .get("iyr") - .and_then(|year| year.parse::().ok()) - .filter(|year| *year >= 2010 && *year <= 2020); - issue_year.is_some() - }) - .filter(|block| -> bool { - let expiration_year: Option = block - .get("eyr") - .map(|year| year.parse::().unwrap()) - .filter(|year| *year >= 2020 && *year <= 2030); - expiration_year.is_some() - }) - .filter(|block| -> bool { - let height = block.get("hgt").filter(|string| -> bool { - if string.ends_with("cm") { - let cm = string.replace("cm", "").parse::(); - if cm.is_err() { - return false; - } - let cm = cm.unwrap(); - (150..=193).contains(&cm) - } else if string.ends_with("in") { - let inches = string.replace("in", "").parse::(); - if inches.is_err() { - return false; - } - let inches = inches.unwrap(); - (59..=76).contains(&inches) - } else { - false - } - }); - height.is_some() - }) - .filter(|block| -> bool { - let hair_colour = block - .get("hcl") - .filter(|colour| valid_hair_colour.is_match(colour)); - hair_colour.is_some() - }) - .filter(|block| -> bool { - let eye_colour = block - .get("ecl") - .filter(|colour| valid_eye_colours.contains(colour as &str)); - eye_colour.is_some() - }) - .filter(|block| -> bool { - let passport_id = block.get("pid").filter(|id| valid_passport_id.is_match(id)); - passport_id.is_some() - }) - .count(); - println!("Part 2: {}", count_valid); + let result = get_input().filter(Pair::sections_overlap).count(); + + println!("Part 2: {}", result); } } diff --git a/src/day05.rs b/src/day05.rs index 0c9469d..6edb225 100644 --- a/src/day05.rs +++ b/src/day05.rs @@ -1,79 +1,188 @@ -use crate::get_lines; +/// --- Day 5: --- +/// https://adventofcode.com/2022/day/5 +use crate::get_block_strings; +use std::borrow::BorrowMut; +use std::collections::VecDeque; +use std::str::FromStr; -fn get_input() -> impl Iterator { - get_lines("day-5-input.txt") +pub fn get_part1_input() -> (Vec>, Vec) { + let mut iterator = get_block_strings("day-05.txt"); + let stacks = iterator.next().expect("Stack specification is missing"); + let stacks = parse_stacks(&stacks); + let instructions = iterator.next().expect("Instructions missing"); + let instructions = instructions + .split('\n') + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(|line| line.parse::()) + .map(Result::unwrap) + .collect::>(); + (stacks, instructions) } -pub fn get_sorted_seat_ids() -> Vec { - let mut result = get_input() - .map(|id| -> u16 { - let mut chars = id.chars(); - - let mut max_row_exclusive = 128u8; - let mut row = 0; - for i in 0..7_usize { - let partition = chars - .next() - .expect(&("Invalid id: ".to_owned() + &*id + " at index " + &i.to_string())); - if partition == 'B' { - row = ((max_row_exclusive - row) / 2u8) + row; - } else if partition == 'F' { - max_row_exclusive = max_row_exclusive - ((max_row_exclusive - row) / 2); - } else { - panic!( - "Invalid character '{}' at index {} for id {}.", - partition, i, id - ); - } - } +pub fn get_part2_input() -> (Vec>, Vec) { + let mut iterator = get_block_strings("day-05.txt"); + let stacks = iterator.next().expect("Stack specification is missing"); + let stacks = parse_stacks(&stacks); + let instructions = iterator.next().expect("Instructions missing"); + let instructions = instructions + .split('\n') + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(|line| line.parse::()) + .map(Result::unwrap) + .collect::>(); + (stacks, instructions) +} - let mut column = 0u8; - let mut max_column_exclusive = 8u8; - for i in 7..10_usize { - let partition = chars - .next() - .expect(&("Invalid id: ".to_owned() + &*id + " at index " + &i.to_string())); - if partition == 'R' { - column = ((max_column_exclusive - column) / 2u8) + column; - } else if partition == 'L' { - max_column_exclusive = - max_column_exclusive - ((max_column_exclusive - column) / 2); - } else { - panic!( - "Invalid character '{}' at index {} for id {}.", - partition, i, id - ); - } +fn parse_stacks(lines: &str) -> Vec> { + let mut stacks = vec![VecDeque::new(); 9]; + for line in lines.split('\n') { + let mut stack_index = None; + for (i, c) in line.chars().enumerate() { + if c == '[' { + stack_index = Some(i / 4); + } else if let Some(index) = stack_index { + stacks[index].push_back(c); + stack_index = None; } + } + } + stacks +} + +pub trait Instruction: FromStr { + fn execute(&self, stacks: Vec>) -> Vec>; +} + +pub struct CrateMover9000Instruction { + count: usize, + from: usize, + to: usize, +} - row as u16 * 8u16 + column as u16 - }) - .collect::>(); - result.sort_unstable(); +impl FromStr for CrateMover9000Instruction { + type Err = String; + + fn from_str(line: &str) -> Result { + let mut components = line.split(' '); + let count = components + .nth(1) + .ok_or_else(|| "count not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse count: {}", parse_error))?; + let from = components + .nth(1) + .ok_or_else(|| "Source stack not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse source stack: {}", parse_error))? + - 1; + let to = components + .nth(1) + .ok_or_else(|| "Destination stack not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse destination stack: {}", parse_error))? + - 1; + Ok(Self { count, from, to }) + } +} + +impl Instruction for CrateMover9000Instruction { + fn execute(&self, mut stacks: Vec>) -> Vec> { + let s: &mut [VecDeque] = stacks.borrow_mut(); + for _ in 0..self.count { + let tmp = s[self.from].pop_front(); + s[self.to].push_front( + tmp.expect("CrateMover attempted to remove an item from an empty stack"), + ); + } + s.to_vec() + } +} + +pub struct CrateMover9001Instruction { + count: usize, + from: usize, + to: usize, +} + +impl FromStr for CrateMover9001Instruction { + type Err = String; + + fn from_str(line: &str) -> Result { + let mut components = line.split(' '); + let count = components + .nth(1) + .ok_or_else(|| "count not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse count: {}", parse_error))?; + let from = components + .nth(1) + .ok_or_else(|| "Source stack not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse source stack: {}", parse_error))? + - 1; + let to = components + .nth(1) + .ok_or_else(|| "Destination stack not specified".to_string())? + .parse::() + .map_err(|parse_error| format!("Unable to parse destination stack: {}", parse_error))? + - 1; + Ok(Self { count, from, to }) + } +} + +impl Instruction for CrateMover9001Instruction { + fn execute(&self, mut stacks: Vec>) -> Vec> { + let mut buffer = VecDeque::with_capacity(self.count); + for _ in 0..self.count { + buffer.push_front( + stacks[self.from] + .pop_front() + .expect("CrateMover attempted to remove an item from an empty stack"), + ); + } + while let Some(item) = buffer.pop_front() { + stacks[self.to].push_front(item); + } + stacks + } +} + +pub fn summarise_stacks(stacks: &Vec>) -> String { + let mut result = String::new(); + for stack in stacks { + if let Some(c) = stack.front() { + result.push(*c); + } + } result } #[cfg(test)] mod tests { - use crate::day05::get_sorted_seat_ids; + + use crate::day05::{get_part1_input, get_part2_input, summarise_stacks, Instruction}; #[test] fn part1() { - let seat_ids = get_sorted_seat_ids(); - let last_seat_id = seat_ids.last().expect("No seats found"); - println!("Part 1: {}", last_seat_id); + let (mut stacks, instructions) = get_part1_input(); + for instruction in instructions { + stacks = instruction.execute(stacks); + } + let result = summarise_stacks(&stacks); + + println!("Part 1: {}", result); } #[test] fn part2() { - let seat_ids = get_sorted_seat_ids(); - for i in 1..seat_ids.len() { - let x = seat_ids.get(i - 1).unwrap(); - let y = seat_ids.get(i).unwrap(); - if y - x > 1 { - println!("Part 2: {}", (x + 1)); - return; - } + let (mut stacks, instructions) = get_part2_input(); + for instruction in instructions { + stacks = instruction.execute(stacks); } + let result = summarise_stacks(&stacks); + + println!("Part 2: {}", result); } } diff --git a/src/day06.rs b/src/day06.rs index 1b1ac9a..731b8b9 100644 --- a/src/day06.rs +++ b/src/day06.rs @@ -1,58 +1,67 @@ -use crate::get_block_strings; - -pub fn get_input() -> impl Iterator> { - get_block_strings("day-6-input.txt").map(|string| { - string - .split_whitespace() - .map(|str| str.trim().to_owned()) - .collect() - }) +/// --- Day 6: Tuning Trouble --- +/// https://adventofcode.com/2022/day/6 +use crate::get_lines; +use std::collections::{BTreeSet, VecDeque}; + +/// Characters received by the Elves' handheld communication device +pub fn get_signal() -> String { + get_lines("day-06.txt").next().expect("No signal detected") +} + +/// Determine which character of the buffer contains the start of a packet +/// Returns: +/// - `Some(usize)` - the index of the first packet if one exists +/// - `None` - if no packet is found +pub fn get_start_of_packet(data_stream: String) -> Result { + let distinct_characters = 4; + let error = "No start of packet found"; + get_marker_position(data_stream, distinct_characters, error) +} + +/// Determine which character of the buffer contains the start of the message +/// Returns: +/// - `Some(usize)` - the index of the start of the message if one exists +/// - `None` if there is no message +pub fn get_start_of_message(data_stream: String) -> Result { + get_marker_position(data_stream, 14, "No message found") +} + +fn get_marker_position( + data_stream: String, + distinct_characters: usize, + error: &str, +) -> Result { + let mut buffer = VecDeque::new(); + for (index, c) in data_stream.chars().enumerate() { + if buffer.len() < distinct_characters { + buffer.push_back(c); + continue; + } + let set = buffer.iter().copied().collect::>(); + if set.len() >= buffer.len() { + return Ok(index); + } + buffer.pop_front(); + buffer.push_back(c); + } + Err(error) } #[cfg(test)] mod tests { - use std::collections::HashSet; - - use crate::day06::get_input; + use crate::day06::{get_signal, get_start_of_message, get_start_of_packet}; #[test] fn part1() { - let sum: u16 = get_input() - .map(|group| -> u16 { - // number of questions to which group answered yes - group - .iter() - .flat_map(|string| string.chars()) - .collect::>() - .len() as u16 - }) - .sum(); - println!("Part 1: {}", sum); + let result = get_start_of_packet(get_signal()).unwrap(); + + println!("Part 1: {}", result); } #[test] fn part2() { - let sum: u16 = get_input() - .map(|group| -> u16 { - // TODO support all UTF-8 code points - let unique_characters = group - .iter() - .flat_map(|string| string.chars()) - .collect::>(); - unique_characters - .iter() - .filter(|c| -> bool { - // whether or not c was answered by all in group - group - .iter() - .map(|string| string.chars().collect::>()) - .filter(|chars| chars.contains(c)) - .count() - == group.len() - }) - .count() as u16 - }) - .sum(); - println!("Part 2: {}", sum); + let result = get_start_of_message(get_signal()).unwrap(); + + println!("Part 2: {}", result); } } diff --git a/src/day07.rs b/src/day07.rs index aac276a..ef1b901 100644 --- a/src/day07.rs +++ b/src/day07.rs @@ -1,134 +1,328 @@ +use crate::day07::Line::{ChangeDirectory, DirectoryListing, FileListing, ListContents}; +/// --- Day 7: No Space Left On Device --- +/// https://adventofcode.com/2022/day/7 +use crate::get_lines; use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; use std::str::FromStr; +use uuid::Uuid; -use regex::Regex; - -use crate::day07::ParseError::{ - BagColourMissing, BagCountMissing, ContainerColourMissing, NothingContained, -}; -use crate::get_lines; +pub struct Session { + file_system: FileSystem, + working_directory: Uuid, +} -pub fn get_input() -> HashMap { - get_lines("day-7-input.txt") - .flat_map(|sentence| sentence.parse::()) - .map(|rule| (rule.container_colour.to_owned(), rule)) - .collect() +impl Session { + pub fn find_directories_smaller_than(&self, max_size: usize) -> Vec<(Uuid, usize)> { + let root = self + .file_system + .directories + .get(&self.file_system.root) + .unwrap(); // FIXME error handling + self.file_system + .find_directories_smaller_than(root, max_size) + .0 + } + pub fn find_directories_larger_than(&self, min_size: usize) -> Vec<(Uuid, usize)> { + let root = self + .file_system + .directories + .get(&self.file_system.root) + .unwrap(); // FIXME error handling + self.file_system + .find_directories_larger_than(root, min_size) + .0 + } } -pub struct Rule { - container_colour: String, - contained_counts: HashMap, +impl Default for Session { + fn default() -> Self { + let file_system = FileSystem::default(); + let working_directory = file_system.root; + Self { + file_system, + working_directory, + } + } } -pub enum ParseError { - ContainerColourMissing, - NothingContained, - BagCountMissing, - BagColourMissing, +pub struct FileSystem { + directories: HashMap, + parents: HashMap, + files: HashMap, + root: Uuid, } -impl FromStr for Rule { - type Err = ParseError; +impl FileSystem { + pub fn consumed_space(&self) -> usize { + self.directory_size(self.directories.get(&self.root).unwrap()) + } - fn from_str(sentence: &str) -> Result { - lazy_static! { - static ref BAG_SUFFIX: Regex = Regex::new(" bag.*$").unwrap(); + fn directory_size(&self, directory: &Directory) -> usize { + let mut result = 0; + for sub in directory.sub_directories.values() { + let sub = self.directories.get(sub).unwrap(); + result += self.directory_size(sub); } - let mut components = sentence.splitn(2, " bags contain "); - let container_colour = components.next(); - if container_colour.is_none() { - return Err(ContainerColourMissing); + for file in &directory.files { + let file = self.files.get(file).unwrap(); + result += file.size; } - let container_colour = container_colour.unwrap().trim().to_owned(); - let contained = components.next(); - if contained.is_none() { - return Err(NothingContained); + result + } + + pub fn insert_file(&mut self, parent_directory_id: Uuid, file: File) { + let file_id = file.id; + self.files.insert(file_id, file); + // TODO should return Result + let parent_directory = self + .directories + .get_mut(&parent_directory_id) + .expect("No such parent directory"); + parent_directory.files.push(file_id); + self.parents.insert(file_id, parent_directory_id); + } + + pub fn insert_directory(&mut self, parent_directory_id: Uuid, directory: Directory) { + let directory_id = directory.id; + let directory_name = directory.name.clone(); + self.directories.insert(directory_id, directory); + // TODO should return Result + let parent_directory = self + .directories + .get_mut(&parent_directory_id) + .expect("No such parent directory"); + parent_directory + .sub_directories + .insert(directory_name, directory_id); + self.parents.insert(directory_id, parent_directory_id); + } + + pub fn find_directories_smaller_than( + &self, + parent: &Directory, + max_size: usize, + ) -> (Vec<(Uuid, usize)>, usize) { + let mut list = vec![]; + let mut total_size = 0; + for sub_directory_id in parent.sub_directories.values() { + let sub_directory = self.directories.get(sub_directory_id).unwrap(); // TODO better error handling + let (items, sub_directory_size) = + self.find_directories_smaller_than(sub_directory, max_size); + total_size += sub_directory_size; + list = vec![list, items].concat(); } - let contained = contained.unwrap().trim(); - if contained.eq("no other bags.") { - return Ok(Rule { - container_colour, - contained_counts: HashMap::new(), - }); + for file_id in &parent.files { + if total_size > max_size { + break; + } + let file = self.files.get(file_id).unwrap(); // TODO better error handling + total_size += file.size; } - let contained_counts = contained - .split(", ") - .flat_map(|phrase| -> Result<(String, u32), Self::Err> { - let phrase = BAG_SUFFIX.replace(phrase, ""); - let mut phrase_components = phrase.splitn(2, ' '); - - let count = phrase_components - .next() - .and_then(|string| string.parse::().ok()); - if count.is_none() { - return Err(BagCountMissing); - } - - let colour = phrase_components.next(); - if colour.is_none() { - return Err(BagColourMissing); - } + if total_size < max_size { + list.push((parent.id, total_size)); + } + (list, total_size) + } - Ok((colour.unwrap().trim().to_owned(), count.unwrap())) - }) - .collect::>(); - Ok(Rule { - container_colour, - contained_counts, - }) + pub fn find_directories_larger_than( + &self, + parent: &Directory, + min_size: usize, + ) -> (Vec<(Uuid, usize)>, usize) { + let mut list = vec![]; + let mut total_size = 0; + for sub_directory_id in parent.sub_directories.values() { + let sub_directory = self.directories.get(sub_directory_id).unwrap(); // TODO better error handling + let (items, sub_directory_size) = + self.find_directories_larger_than(sub_directory, min_size); + total_size += sub_directory_size; + list = vec![list, items].concat(); + } + for file_id in &parent.files { + let file = self.files.get(file_id).unwrap(); // TODO better error handling + total_size += file.size; + } + if total_size >= min_size { + list.push((parent.id, total_size)); + } + (list, total_size) } } -impl Rule { - pub fn can_contain(&self, colour: &str, rule_map: &HashMap) -> bool { - if self.contained_counts.contains_key(colour) { - return true; +impl Default for FileSystem { + fn default() -> Self { + let root = Directory { + id: Default::default(), + name: "/".to_string(), + sub_directories: Default::default(), + files: Default::default(), + }; + let root_id = root.id; + let mut directories = HashMap::new(); + directories.insert(root_id, root); + Self { + directories, + parents: HashMap::new(), + files: HashMap::new(), + root: root_id, } - self.contained_counts - .keys() - .map(|key| rule_map.get(key)) - .any(|rule| -> bool { - match rule { - None => false, - Some(rule) => rule.can_contain(colour, rule_map), + } +} + +pub struct Directory { + id: Uuid, + name: String, + sub_directories: HashMap, + files: Vec, +} + +impl Debug for Directory { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Directory{{ name: {}, id: {} }}", self.name, self.id) + } +} + +pub struct File { + id: Uuid, + // name: String, + size: usize, +} + +#[derive(Debug)] +pub enum Line { + ListContents, + ChangeDirectory(String), + DirectoryListing(String), + FileListing(usize, String), +} + +impl Line { + pub fn execute(&self, session: &mut Session) { + match self { + ListContents => {} + ChangeDirectory(target) => { + if target == ".." { + let parent_id = session + .file_system + .parents + .get(&session.working_directory) + .unwrap(); + session.working_directory = *parent_id; + } else if target == "/" { + session.working_directory = session.file_system.root; + } else { + let current = session + .file_system + .directories + .get(&session.working_directory) + .unwrap(); + let target = current.sub_directories.get(target).unwrap_or_else(|| { + panic!( + "Directory {:?} does not contain a sub-directory named {}", + current, target + ) + }); + session.working_directory = *target; } - }) + } + DirectoryListing(name) => { + let directory = Directory { + id: Uuid::new_v4(), + name: name.to_string(), + sub_directories: Default::default(), + files: Default::default(), + }; + session + .file_system + .insert_directory(session.working_directory, directory); + } + FileListing(size, _name) => { + let file = File { + id: Uuid::new_v4(), + // name: _name.to_string(), + size: *size, + }; + session + .file_system + .insert_file(session.working_directory, file); + } + } } +} - pub fn count_contained(&self, rule_map: &HashMap) -> u32 { - let sum = &self - .contained_counts - .iter() - .flat_map(|(colour, multiplier)| -> Option { - rule_map.get(colour).map(|sub_rule| -> u32 { - let base = sub_rule.count_contained(rule_map); - base * multiplier - }) - }) - .sum(); - sum + 1u32 +impl FromStr for Line { + type Err = (); + + fn from_str(line: &str) -> Result { + let mut components = line.split(' '); + let first_token = components.next().unwrap(); // TODO error handling + match first_token { + "$" => { + let second_token = components.next().unwrap(); // TODO error handling + match second_token { + "cd" => { + let argument = components.next().unwrap().to_string(); + Ok(ChangeDirectory(argument)) + } + "ls" => Ok(ListContents), + other_command => { + eprintln!("Unknown command: {}", other_command); + panic!() + } + } + } + "dir" => { + let name = components.next().unwrap().to_string(); + Ok(DirectoryListing(name)) + } + number => { + let size = number.parse::().unwrap(); + let name = components.next().unwrap().to_string(); + Ok(FileListing(size, name)) + } + } } } +pub fn get_input() -> impl Iterator { + get_lines("day-07.txt") + .map(|line| line.parse::()) + .map(Result::unwrap) +} + #[cfg(test)] mod tests { - use crate::day07::get_input; + + use crate::day07::{get_input, Session}; #[test] fn part1() { - let map = get_input(); - let count = map - .values() - .filter(|rule| rule.can_contain("shiny gold", &map)) - .count(); - println!("Part 1: {}", count); + let mut session = Session::default(); + get_input().for_each(|line| line.execute(&mut session)); + let result: usize = session + .find_directories_smaller_than(100_000) + .iter() + .map(|item| item.1) + .sum(); + + println!("Part 1: {}", result); } #[test] fn part2() { - let map = get_input(); - let bag = map.get("shiny gold").unwrap(); - let result = bag.count_contained(&map) - 1; + let mut session = Session::default(); + get_input().for_each(|line| line.execute(&mut session)); + let consumed = session.file_system.consumed_space(); + let unused = 70_000_000 - consumed; + let required = 30_000_000 - unused; + let result: usize = session + .find_directories_larger_than(required) + .iter() + .map(|item| item.1) + .min() + .unwrap(); + println!("Part 2: {}", result); } } diff --git a/src/day08.rs b/src/day08.rs index 4e8d69d..f4579e2 100644 --- a/src/day08.rs +++ b/src/day08.rs @@ -1,192 +1,153 @@ -use std::str::FromStr; - +/// --- Day 8: Treetop Tree House --- +/// https://adventofcode.com/2022/day/8 use crate::get_lines; -use crate::day08::Operation::{Accumulate, Jump, NoOp}; -use crate::day08::ParseError::{ - InvalidArgument, InvalidOperation, MissingArgument, MissingOperation, -}; -use std::num::ParseIntError; - -#[derive(Eq, PartialEq)] -pub enum Operation { - /// Increase or decrease the accumulator by the value in the argument. - /// Move to the instruction directly below. - Accumulate, - /// Do nothing then move to the instruction directly below. - NoOp, - /// Jump to a new instruction relative to the current one offset by the argument. - Jump, +/// A "peculiar patch of tall trees all planted carefully in a grid" as part of a reforestation +/// effort. +pub struct Forest { + /// The heights of the trees + heights: Vec>, } -impl Operation { - pub fn update_total(&self, previous_total: i32, argument: i32) -> i32 { - match &self { - Accumulate => previous_total + argument, - NoOp => previous_total, - Jump => previous_total, +impl Forest { + /// Determine how many trees in the forest are visible from outside the forest (from any side) + pub fn count_visible_trees(&self) -> usize { + let mut result = 0; + for i in 0..self.heights.len() { + for j in 0..self.heights[i].len() { + if self.is_visible(i, j) { + result += 1; + } + } } + result } - pub fn update_index(&self, previous_index: usize, argument: i32) -> usize { - match &self { - Accumulate => previous_index + 1, - NoOp => previous_index + 1, - Jump => (previous_index as i32 + argument) as usize, + /// Determine if a tree is visible from outside the forest, that is, not obstructed by other + /// trees from any side + fn is_visible(&self, x: usize, y: usize) -> bool { + let tree_height = self.heights[x][y]; + let mut visible_from_north = true; + for i in 0..x { + if self.heights[i][y] >= tree_height { + visible_from_north = false; + break; + } } - } -} - -pub enum ParseError { - InvalidOperation(String), - MissingOperation(String), - MissingArgument(String), - InvalidArgument(ParseIntError), -} - -impl FromStr for Operation { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "acc" => Ok(Accumulate), - "nop" => Ok(NoOp), - "jmp" => Ok(Jump), - _ => Err(InvalidOperation(String::from(s))), + if visible_from_north { + return true; + } + let mut visible_from_south = true; + for i in x + 1..self.heights.len() { + if self.heights[i][y] >= tree_height { + visible_from_south = false; + break; + } } + if visible_from_south { + return true; + } + let mut visible_from_west = true; + for j in 0..y { + if self.heights[x][j] >= tree_height { + visible_from_west = false; + break; + } + } + if visible_from_west { + return true; + } + for j in y + 1..self.heights[x].len() { + if self.heights[x][j] >= tree_height { + return false; + } + } + true } -} -/// A boot code instruction -pub struct Instruction { - operation: Operation, - argument: i32, -} - -impl Instruction { - /// Compute the new value of the accumulator - /// - /// Parameters: - /// - `previous_total` - the current accumulator value - /// - /// Returns: the new accumulator value - pub fn update_total(&self, previous_total: i32) -> i32 { - self.operation.update_total(previous_total, self.argument) + /// Determine the best possible view from any treetop in the forest + pub fn max_scenic_score(&self) -> usize { + let mut result = 0; + for i in 0..self.heights.len() { + for j in 0..self.heights[i].len() { + let score = self.scenic_score(i, j); + if score > result { + result = score; + } + } + } + result } - /// Identify the index of the next instruction to execute + /// A score that rewards coördinates from which the most trees are visible /// /// Parameters: - /// - `previous_index` - the current instruction index - /// - /// Returns: the index of the next instruction to execute - pub fn update_index(&self, previous_index: usize) -> usize { - self.operation.update_index(previous_index, self.argument) + /// - `(x, y)` - the potential position in the forest to build a treetop tree house + fn scenic_score(&self, x: usize, y: usize) -> usize { + let tree_height = self.heights[x][y]; + let mut north_score = 0; + for i in (0..x).rev() { + let height = self.heights[i][y]; + north_score += 1; + if height >= tree_height { + break; + } + } + let mut south_score = 0; + for i in x + 1..self.heights.len() { + let height = self.heights[i][y]; + south_score += 1; + if height >= tree_height { + break; + } + } + let mut west_score = 0; + for j in (0..y).rev() { + let height = self.heights[x][j]; + west_score += 1; + if height >= tree_height { + break; + } + } + let mut east_score = 0; + for j in y + 1..self.heights[x].len() { + let height = self.heights[x][j]; + east_score += 1; + if height >= tree_height { + break; + } + } + north_score * east_score * south_score * west_score } } -impl FromStr for Instruction { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - let mut components = s.splitn(2, ' '); - let operation = match components.next() { - None => return Err(MissingOperation(String::from(s))), - Some(operation) => match operation.parse::() { - Ok(operation) => operation, - Err(e) => return Err(e), - }, - }; - - let argument = match components.next() { - None => return Err(MissingArgument(String::from(s))), - Some(argument) => match argument.parse::() { - Ok(argument) => argument, - Err(parse_int_error) => return Err(InvalidArgument(parse_int_error)), - }, - }; - - Ok(Instruction { - operation, - argument, +pub fn get_input() -> Forest { + let heights = get_lines("day-08.txt") + .map(|line| { + line.chars() + .map(|height| height.to_digit(10).expect("Invalid tree height") as u8) + .collect::>() }) - } -} - -pub fn get_instructions() -> Vec { - get_lines("day-8-input.txt") - .flat_map(|line| line.parse()) - .collect() + .collect::>>(); + Forest { heights } } #[cfg(test)] mod tests { - use crate::day08::Operation::{Accumulate, Jump, NoOp}; - use crate::day08::{get_instructions, Instruction}; + + use crate::day08::get_input; #[test] fn part1() { - let instructions = get_instructions(); - let mut visited = vec![false; instructions.len()]; - let mut index = 0_usize; - let mut accumulator = 0_i32; + let result = get_input().count_visible_trees(); - while !visited[index] { - visited[index] |= true; - let instruction = &instructions[index]; - accumulator = instruction.update_total(accumulator); - index = instruction.update_index(index); - } - println!("Part 1: {}", accumulator); + println!("Part 1: {}", result); } #[test] fn part2() { - let instructions = get_instructions(); - // "After some careful analysis, you believe that exactly one instruction is corrupted." - 'instructions: for i in 0..instructions.len() { - let to_replace = &instructions[i]; - if to_replace.operation == Accumulate { - // "No acc instructions were harmed in the corruption of this boot code." - continue; - } - let operation = if to_replace.operation == NoOp { - Jump - } else { - NoOp - }; - let replacement = Instruction { - operation, - argument: to_replace.argument, - }; - - let mut index = 0_usize; - let mut accumulator = 0_i32; - let mut visited = vec![false; instructions.len()]; - - // "The program is supposed to terminate by attempting to execute an instruction - // immediately after the last instruction in the file." - while index < instructions.len() { - let instruction = if index == i { - &replacement - } else { - &instructions[index] - }; - if visited[index] { - // replacing the instruction causes an infinite loop - // try replacing a different one + let result = get_input().max_scenic_score(); - // NB: Simply re-visiting this instruction is an infinite loop because the - // argument to each instruction is constant and never dependent on the value of - // "accumulator" - continue 'instructions; - } - visited[index] |= true; - accumulator = instruction.update_total(accumulator); - index = instruction.update_index(index); - } - println!("Part 2: {}", accumulator); - break; - } + println!("Part 2: {}", result); } } diff --git a/src/day09.rs b/src/day09.rs index 5b893b6..4566e2d 100644 --- a/src/day09.rs +++ b/src/day09.rs @@ -1,97 +1,193 @@ +use crate::day09::Direction::{Down, Left, Right, Up}; use crate::get_lines; +use std::cmp::Ordering; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; -// https://adventofcode.com/2020/day/9 - -/// "Though the port is non-standard, you manage to connect it to your computer through the clever -/// use of several paperclips. Upon connection, the port outputs a series of numbers (your puzzle -/// input). -/// -/// The data appears to be encrypted with the eXchange-Masking Addition System (XMAS) which, -/// conveniently for you, is an old cypher with an important weakness." -pub fn get_data() -> impl Iterator { - get_lines("day-9-input.txt") - .map(|line| line.parse()) - .map(|result| result.unwrap()) // panic on invalid XMAS code +/// --- Day 9: Rope Bridge --- +/// https://adventofcode.com/2022/day/9 + +#[derive(Default, Copy, Clone, Eq, PartialEq)] +pub struct Coordinate { + x: i32, + y: i32, } -#[cfg(test)] -mod tests { - use std::collections::LinkedList; +impl Coordinate { + pub fn distance(&self, other: &Self) -> usize { + ((self.x as f64 - other.x as f64).powf(2f64) + (self.y as f64 - other.y as f64).powf(2f64)) + .sqrt() as usize + } - use crate::day09::get_data; - use std::cmp::Ordering; + pub fn step(&mut self, distance: (i32, i32)) { + self.x += distance.0; + self.y += distance.1; + } - #[test] - fn test() { - let preamble_size = 25_usize; - let mut buffer: LinkedList = LinkedList::new(); - let mut data = get_data(); - // "XMAS starts by transmitting a preamble of 25 numbers." - for _ in 0..preamble_size { - buffer.push_back(data.next().unwrap()); + pub fn step_towards(&mut self, leader: &Self) { + let x_distance = match leader.x.cmp(&self.x) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + }; + let y_distance = match leader.y.cmp(&self.y) { + Ordering::Less => -1, + Ordering::Equal => 0, + Ordering::Greater => 1, + }; + self.step((x_distance, y_distance)); + } +} + +pub enum Direction { + Up, + Down, + Left, + Right, +} + +impl Direction { + pub fn step(&self) -> (i32, i32) { + match self { + Up => (-1, 0), + Down => (1, 0), + Left => (0, -1), + Right => (0, 1), } - let invalid = 'candidates: loop { - match data.next() { - None => break None, - Some(number) => { - // "each number you receive should be the sum of any two of the 25 immediately - // previous numbers" - let mut addends = buffer - .iter() - // no negative numbers in the input, so filter out anything larger than our - // target - .filter(|previous| **previous <= number) - .collect::>(); - addends.sort(); // sort so we can quit search early - for i in 0..addends.len() - 1 { - let x = addends[i]; - for y in addends.iter().skip(i + 1) { - let sum = x + *y; - if sum == number { - buffer.push_back(number); - buffer.pop_front(); - continue 'candidates; - } - } - } - break Some(number); - } + } +} + +impl FromStr for Direction { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "U" => Ok(Up), + "D" => Ok(Down), + "L" => Ok(Left), + "R" => Ok(Right), + _ => Err(()), + } + } +} + +pub struct Instruction { + direction: Direction, + distance: u16, +} + +impl FromStr for Instruction { + type Err = (); + + fn from_str(line: &str) -> Result { + let mut components = line.split(' '); + let direction = components + .next() + .expect("No direction specified") + .parse::() + .expect("Unparseable direction"); + let distance = components + .next() + .expect("No distance specified") + .parse::() + .expect("Unparseable distance"); + Ok(Self { + direction, + distance, + }) + } +} + +pub struct Rope { + knot_coordinates: Vec, + visited: HashMap>, +} + +impl Rope { + pub fn count_visited(&self) -> usize { + let mut result = 0; + for bucket in self.visited.values() { + result += bucket.len(); + } + result + } + + pub fn process(&mut self, instruction: &Instruction) { + let step = instruction.direction.step(); + for _ in 0..instruction.distance { + self.knot_coordinates[0].step(step); + for j in 1..self.knot_coordinates.len() { + self.move_knot(j); } - }; - let invalid = invalid.unwrap(); - println!("Part 1: {}", invalid); - - // I avoided putting all the input into memory until this point, but for the next part, it - // seems to be the most elegant approach. Alternatively, I could use two iterators over the - // input file, one to iterate through the potential left bounds and one to find the right - // bound for each potential left bound. - let data = get_data().collect::>(); - 'left_bounds: for i in 0..data.len() - 1 { - let mut total = data[i]; - let mut min = total; - let mut max = total; - for current in data.iter().skip(i + 1) { - if current < &min { - min = *current; - } - if current > &max { - max = *current; + } + } + + fn move_knot(&mut self, knot_index: usize) { + let leader = self.knot_coordinates[knot_index - 1]; + let mut follower = self.knot_coordinates[knot_index]; + if leader == follower || leader.distance(&follower) <= 1 { + return; + } + + follower.step_towards(&leader); + self.knot_coordinates[knot_index] = follower; + + if knot_index == &self.knot_coordinates.len() - 1 { + match self.visited.entry(follower.x) { + Entry::Occupied(o) => { + o.into_mut().insert(follower.y); } - total += current; - match total.cmp(&invalid) { - Ordering::Equal => { - let result = min + max; - println!("Part 2: {}", result); - return; - } - Ordering::Less => {} - Ordering::Greater => { - // Since there are no negative numbers, stop the search when the numbers become - // too big. - continue 'left_bounds; - } + Entry::Vacant(v) => { + let mut bucket = HashSet::new(); + bucket.insert(follower.y); + v.insert(bucket); } } } } } + +impl From for Rope { + fn from(num_knots: usize) -> Self { + let knot_coordinates = vec![Coordinate::default(); num_knots]; + let mut visited = HashMap::default(); + let mut bucket = HashSet::new(); + bucket.insert(0i32); + visited.insert(0, bucket); + Self { + knot_coordinates, + visited, + } + } +} + +pub fn get_input() -> impl Iterator { + get_lines("day-09.txt") + .map(|line| line.parse::()) + .map(Result::unwrap) +} + +#[cfg(test)] +mod tests { + + use crate::day09::{get_input, Rope}; + + #[test] + fn part1() { + let mut rope: Rope = 2usize.into(); + get_input().for_each(|instruction| rope.process(&instruction)); + let result = rope.count_visited(); + + println!("Part 1: {}", result); + } + + #[test] + fn part2() { + let mut rope: Rope = 10usize.into(); + get_input().for_each(|instruction| rope.process(&instruction)); + let result = rope.count_visited(); + + println!("Part 2: {}", result); + } +} diff --git a/src/day10.rs b/src/day10.rs index 524be58..5bed630 100644 --- a/src/day10.rs +++ b/src/day10.rs @@ -1,123 +1,193 @@ -use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; - +use crate::day10::Instruction::{AddX, NoOp}; use crate::get_lines; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +/// --- Day 10: Cathode-Ray Tube --- +/// https://adventofcode.com/2022/day/10 -// https://adventofcode.com/2020/day/10 - -/// Get a sorted, unique list of the joltages of all the available adapters. -/// -/// *Panics* if any line in the input contains a malformed joltage -/// -/// "Always prepared, you make a list of all of the joltage adapters in your bag." -pub fn get_joltage_adapters() -> Vec { - let mut adapters = get_lines("day-10-input.txt") - .map(|line| line.parse()) - .map(|result| result.unwrap()) // panic on invalid joltage - .collect::>(); - adapters.sort_unstable(); - adapters +/// The state of the central processing unit in the Elves' handheld device at a given point in time +pub struct ProcessorState { + /// The clock cycle indicating the point in time this state was in effect + cycle: u16, + /// The `X` register of the processor + register: i32, } -/// Count the number of ways _adapters_ can be arranged to connect a charging outlet to a device. -/// -/// Parameters: -/// * `adapters` - a unique and sorted list of the joltages of the available adapters. This method -/// will not return a correct value if the vector is not unique and sorted. -/// * `from` - the joltage of the charging outlet -/// * `to` - the joltage of the device that needs to be charged -/// * `cache` - a history of precomputed arrangement counts. This will be updated prior to the -/// method returning. -pub fn count_adapter_arrangements( - adapters: &[u8], - from: u8, - to: u8, - cache: &mut HashMap, -) -> u64 { - let hash = || { - let mut hasher = DefaultHasher::new(); - hasher.write_u8(from); - hasher.write_u8(to); - adapters.hash(&mut hasher); - hasher.finish() - }; - - let hash = hash(); - if cache.contains_key(&hash) { - return cache[&hash]; +impl ProcessorState { + /// A measurable aspect of the processor that is derived from the current clock cycle and the + /// register's value + pub fn signal_strength(&self) -> i32 { + (self.cycle as i32) * self.register } +} - if adapters.is_empty() { - return if from + 3 == to { 1 } else { 0 }; - } else if adapters.len() == 1 { - let last = adapters[0]; - return if last == to - 3 { 1 } else { 0 }; +impl Default for ProcessorState { + fn default() -> Self { + Self { + cycle: 1, + register: 1, + } + } +} + +/// A low-level instruction for the Elves' handheld device +pub enum Instruction { + /// Do nothing + NoOp, + + /// Increase the `X` register by the value specified + AddX(i32), +} + +impl Instruction { + /// The number of clock cycles it takes this instruction to complete + fn cycles(&self) -> usize { + match self { + AddX(_) => 2, + _ => 1, + } } - let mut retval = 0u64; - for i in 0..3usize { - if i >= adapters.len() { - break; + /// Execute a single instruction. The instruction may take multiple cycles to complete and a + /// separate processor state is emitted for each cycle elapsed. + pub fn execute(&self, current_state: &ProcessorState) -> Vec { + let mut result = vec![]; + let mut inc = 1; + for _ in 0..self.cycles() - 1 { + result.push(ProcessorState { + cycle: current_state.cycle + inc, + register: current_state.register, + }); + inc += 1; + } + let last = match self { + NoOp => ProcessorState { + cycle: current_state.cycle + inc, + register: current_state.register, + }, + AddX(argument) => ProcessorState { + cycle: current_state.cycle + inc, + register: current_state.register + argument, + }, + }; + result.push(last); + result + } +} + +impl FromStr for Instruction { + type Err = &'static str; + + fn from_str(line: &str) -> Result { + let mut components = line.split(' '); + let instruction = components.next().expect("Instruction not specified"); + if instruction == "noop" { + return Ok(NoOp); + } else if instruction == "addx" { + let argument = components + .next() + .expect("Argument required") + .parse::() + .expect("Unparseable argument"); + return Ok(AddX(argument)); + } + Err("Unrecognised instruction") + } +} + +/// The CRT display on the Elves' handheld device +pub struct HandheldDisplay { + pixels: [[char; 40]; 6], +} + +impl HandheldDisplay { + /// Update the pixels based on the current processor state + pub fn update(&mut self, state: &ProcessorState) { + let pixel_index = (state.cycle - 1) as usize; + + // Part or all of the sprite might be off screen + let mut sprite_positions = vec![]; + if state.register > 0 { + sprite_positions.push((state.register - 1) as usize); + } + if state.register >= 0 { + sprite_positions.push(state.register as usize); } - let first = adapters[i]; - let difference = first - from; - if (1..=3).contains(&difference) { - let remaining = if i < adapters.len() { - adapters.split_at(i + 1).1 + if state.register + 1 >= 0 { + sprite_positions.push((state.register + 1) as usize); + } + + // determine which pixel is currently being drawn + let row = pixel_index / 40; + let column = pixel_index % 40; + if row < self.pixels.len() { + self.pixels[row][column] = if sprite_positions.contains(&column) { + '#' } else { - &[0u8] + '.' }; - retval += count_adapter_arrangements(remaining, first, to, cache); } } - cache.insert(hash, retval); - retval +} + +impl Display for HandheldDisplay { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for i in 0..self.pixels.len() { + writeln!(f, "{}", self.pixels[i].iter().collect::())?; + } + Ok(()) + } +} + +impl Default for HandheldDisplay { + fn default() -> Self { + Self { + pixels: [['.'; 40]; 6], + } + } +} + +pub fn get_input() -> impl Iterator { + get_lines("day-10.txt") + .map(|line| line.parse::()) + .map(Result::unwrap) } #[cfg(test)] mod tests { - use std::collections::HashMap; - use crate::day10::{count_adapter_arrangements, get_joltage_adapters}; + use crate::day10::{get_input, HandheldDisplay, ProcessorState}; + + #[test] + fn part1() { + let interesting_cycles = vec![20u16, 60, 100, 140, 180, 220]; + let mut state: ProcessorState = Default::default(); + let mut total_signal_strength = 0; + for instruction in get_input() { + for result in instruction.execute(&state) { + if interesting_cycles.contains(&result.cycle) { + total_signal_strength += result.signal_strength(); + } + state = result; + } + } + + println!("Part 1: {}", total_signal_strength); + } #[test] - fn test() { - let joltages = get_joltage_adapters(); - let target_joltage = joltages - .last() - .expect("At least one joltage adapter is required.") - + 3u8; - - // "If you use every adapter in your bag at once, what is the distribution of joltage - // differences between the charging outlet, the adapters, and your device?" - // "In addition, your device has a built-in joltage adapter rated for 3 jolts higher than - // the highest-rated adapter in your bag." - let mut difference_distribution = [0u8, 0u8, 1u8]; - - // "Treat the charging outlet near your seat as having an effective joltage rating of 0." - let mut current_joltage = 0u8; - - while current_joltage < target_joltage - 3 { - let next = joltages - .iter() - .find(|candidate| { - **candidate > current_joltage - && **candidate - current_joltage >= 1 - && **candidate - current_joltage <= 3 - }) - .expect("Required joltage adapter not available."); - let difference = *next as usize - current_joltage as usize; - difference_distribution[difference - 1] += 1; - current_joltage = *next; + fn part2() { + let mut state: ProcessorState = Default::default(); + let mut display: HandheldDisplay = Default::default(); + display.update(&state); + for instruction in get_input() { + for result in instruction.execute(&state) { + display.update(&result); + state = result; + } } - println!( - "Part 1: {}", - difference_distribution[0] as u16 * difference_distribution[2] as u16 - ); - println!( - "Part 2: {}", - count_adapter_arrangements(&joltages, 0, target_joltage, &mut HashMap::new()) - ); + + println!("Part 2:\n{}", display); } } diff --git a/src/day11.rs b/src/day11.rs index 8f68dd1..e334213 100644 --- a/src/day11.rs +++ b/src/day11.rs @@ -1,386 +1,272 @@ -// https://adventofcode.com/2020/day/11 - -use std::iter::repeat; +use crate::get_block_strings; +use lazy_static::lazy_static; +use regex::Regex; use std::str::FromStr; +use Operator::{Add, Multiply}; +use ValueSupplier::{Literal, OldValue}; -use unicode_segmentation::UnicodeSegmentation; +/// --- Day 11: Monkey in the Middle --- +/// https://adventofcode.com/2022/day/11 -use crate::get_lines; +pub type Int = u64; -use crate::day11::Direction::{ - East, North, NorthEast, NorthWest, South, SouthEast, SouthWest, West, -}; -use crate::day11::Position::{EmptySeat, Floor, OccupiedSeat}; +/// A description of how a specific monkey handles your belongings in response to your worry level. +#[derive(Clone)] +pub struct Monkey { + /// Your worry level for each item the monkey has + items: Vec, + /// How your worry level changes when this monkey inspects each item + operation: Operation, + /// A factor in what the monkey chooses to do with each item + divisor: Int, + /// The recipient ID if the test passes + target_if_true: usize, + /// The recipient ID if the test fails + target_if_false: usize, + /// The total number of times this monkey has inspected any item + items_inspected: usize, +} -/// A position in the seating layout -#[derive(Clone, Copy, Eq, PartialEq)] -pub enum Position { - Floor, - EmptySeat, - OccupiedSeat, +impl Monkey { + /// Inspect the items in possession and throw them to other monkeys as necessary. + /// + /// Note this method assumes that a monkey cannot throw an item to themself. + pub fn inspect_items(&mut self, worry_updater: &dyn Fn(&Int) -> Int) -> Vec { + let result = self + .items + .iter() + .map(|worry_level| -> Throw { + let worry_level = self.operation.apply(worry_level); + let worry_level = worry_updater(&worry_level); + let destination_monkey_id = if worry_level % self.divisor == 0 { + self.target_if_true + } else { + self.target_if_false + }; + Throw { + destination_monkey_id, + worry_level, + } + }) + .collect::>(); + self.items_inspected += result.len(); + self.items.clear(); + result + } } -impl FromStr for Position { +impl FromStr for Monkey { type Err = (); - fn from_str(s: &str) -> Result { - match s { - "." => Ok(Floor), - "L" => Ok(EmptySeat), - "#" => Ok(OccupiedSeat), - _ => Err(()), + fn from_str(block: &str) -> Result { + lazy_static! { + static ref NOT_A_NUMBER: Regex = Regex::new("[^0-9]").unwrap(); } + let mut lines = block.split('\n').skip(1); + let items = lines + .next() + .expect("Starting items not specified") + .trim() + .replace("Starting items: ", "") + .split(", ") + .map(|item_string| item_string.parse::()) + .map(Result::unwrap) + .collect::>(); + let operation = lines + .next() + .expect("Operation not specified") + .parse::() + .expect("Unparseable operation"); + let divisor = NOT_A_NUMBER + .replace_all(lines.next().expect("Divisor not specified"), "") + .parse::() + .expect("Unparseable divisor"); + let target_if_true = NOT_A_NUMBER + .replace_all(lines.next().expect("Target if true not specified"), "") + .parse::() + .expect("Unparseable target if true"); + let target_if_false = NOT_A_NUMBER + .replace_all(lines.next().expect("Target if false not specified"), "") + .parse::() + .expect("Unparseable target if true"); + Ok(Self { + items, + operation, + divisor, + target_if_true, + target_if_false, + items_inspected: 0, + }) } } -/// "The seat layout fits neatly on a grid." -pub fn read_seat_layout() -> Vec> { - get_lines("day-11-input.txt") - .map(|line| -> Vec { - line.graphemes(true) - .flat_map(Position::from_str) - .collect() - }) - .collect() +/// The side effect of a monkey inspecting an item, a throw to another monkey. +pub struct Throw { + #[allow(dead_code)] + destination_monkey_id: usize, + #[allow(dead_code)] + worry_level: Int, } -/// "Now, you just need to model the people who will be arriving shortly. Fortunately, people are -/// entirely predictable and always follow a simple set of rules. All decisions are based on the -/// number of occupied seats adjacent to a given seat (one of the eight positions immediately up, -/// down, left, right, or diagonal from the seat)." -pub fn update_seat_based_on_neighbours( - row_index: usize, - column_index: usize, - source: &[Vec], -) -> Position { - let row = &source[row_index]; - let has_preceding_row = row_index > 0; - let has_subsequent_row = row_index < source.len() - 1; - let has_preceding_column = column_index > 0; - let has_subsequent_column = column_index < row.len() - 1; - match source[row_index][column_index] { - Floor => Floor, // "Floor (.) never changes; seats don't move, and nobody sits on the floor" - EmptySeat => { - // "If a seat is empty and there are no occupied seats adjacent to it, the seat becomes - // occupied." - let mut has_occupied_neighbour = false; - - // check current row - if has_preceding_column { - has_occupied_neighbour |= row[column_index - 1] == OccupiedSeat; - } - if has_subsequent_column { - has_occupied_neighbour |= row[column_index + 1] == OccupiedSeat; - } - if has_preceding_row && !has_occupied_neighbour { - // check preceding row - let preceding_row = &source[row_index - 1]; - has_occupied_neighbour |= preceding_row[column_index] == OccupiedSeat; - if has_preceding_column { - has_occupied_neighbour |= preceding_row[column_index - 1] == OccupiedSeat; - } - if has_subsequent_column { - has_occupied_neighbour |= preceding_row[column_index + 1] == OccupiedSeat; - } - } - if has_subsequent_row && !has_occupied_neighbour { - // check subsequent row - let subsequent_row = &source[row_index + 1]; - has_occupied_neighbour |= subsequent_row[column_index] == OccupiedSeat; - if has_preceding_column { - has_occupied_neighbour |= subsequent_row[column_index - 1] == OccupiedSeat; - } - if has_subsequent_column { - has_occupied_neighbour |= subsequent_row[column_index + 1] == OccupiedSeat; - } - } +#[derive(Copy, Clone)] +enum Operator { + Add, + Multiply, +} - if has_occupied_neighbour { - EmptySeat - } else { - OccupiedSeat - } +impl Operator { + pub fn apply(&self, x: &Int, y: &Int) -> Int { + match self { + Add => x + y, + Multiply => x * y, } - OccupiedSeat => { - // "If a seat is occupied (#) and four or more seats adjacent to it are also occupied, - // the seat becomes empty." - - let mut occupied_neighbours = 0u8; - - // check current row - if has_preceding_column && row[column_index - 1] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_subsequent_column && row[column_index + 1] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_preceding_row { - // check preceding row - let preceding_row = &source[row_index - 1]; + } +} - if preceding_row[column_index] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_preceding_column && preceding_row[column_index - 1] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_subsequent_column && preceding_row[column_index + 1] == OccupiedSeat { - occupied_neighbours += 1; - } - } - if has_subsequent_row { - // check subsequent row - let subsequent_row = &source[row_index + 1]; +impl FromStr for Operator { + type Err = (); - if subsequent_row[column_index] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_preceding_column && subsequent_row[column_index - 1] == OccupiedSeat { - occupied_neighbours += 1; - } - if has_subsequent_column && subsequent_row[column_index + 1] == OccupiedSeat { - occupied_neighbours += 1; - } - } - if occupied_neighbours >= 4 { - EmptySeat - } else { - OccupiedSeat - } + fn from_str(s: &str) -> Result { + match s { + "*" => Ok(Multiply), + "+" => Ok(Add), + _ => Err(()), } } } -/// Update each seat based on its adjacent seats. Perform all updates simultaneously. -pub fn update_seats_based_on_neighbours(seat_layout: &[Vec]) -> Vec> { - let mut new_layout = seat_layout.to_owned(); - for i in 0..seat_layout.len() { - let row = &seat_layout[i]; - for j in 0..row.len() { - new_layout[i][j] = update_seat_based_on_neighbours(i, j, seat_layout); - } - } - new_layout +#[derive(Copy, Clone)] +enum ValueSupplier { + OldValue, + Literal(Int), } -/// Count the number of occupied seats in the given layout. -pub fn count_occupied(seat_layout: &[Vec]) -> u16 { - let mut count = 0u16; - for row in seat_layout { - for cell in row { - count += match cell { - OccupiedSeat => 1, - _ => 0, - } +impl ValueSupplier { + fn get_value(&self, old_value: &Int) -> Int { + match self { + OldValue => *old_value, + Literal(literal) => *literal, } } - count } -pub enum Direction { - North, - NorthWest, - West, - SouthWest, - South, - SouthEast, - East, - NorthEast, +#[derive(Copy, Clone)] +struct Operation { + operator: Operator, + x_value_supplier: ValueSupplier, + y_value_supplier: ValueSupplier, } -impl Direction { - fn has_occupied_seat_in_sight( - &self, - vantage_row: usize, - vantage_column: usize, - source: &[Vec], - max_columns: usize, - ) -> bool { - for (i, j) in - self.get_visible_seat_indices(vantage_row, vantage_column, source.len(), max_columns) - { - match source[i][j] { - EmptySeat => return false, - OccupiedSeat => return true, - _ => continue, - } - } - false - } - fn get_visible_seat_indices( - &self, - vantage_row: usize, - vantage_column: usize, - max_rows: usize, - max_columns: usize, - ) -> Box> { - match self { - Direction::North => { - let row_indices = (0..vantage_row).rev(); - let col_indices = repeat(vantage_column); - Box::new(row_indices.zip(col_indices)) - } - Direction::NorthWest => { - let row_indices = (0..vantage_row).rev(); - let col_indices = (0..vantage_column).rev(); - Box::new(row_indices.zip(col_indices)) - } - Direction::West => { - let row_indices = repeat(vantage_row); - let col_indices = (0..vantage_column).rev(); - Box::new(row_indices.zip(col_indices)) - } - Direction::SouthWest => { - let row_indices = vantage_row + 1..max_rows; - let col_indices = (0..vantage_column).rev(); - Box::new(row_indices.zip(col_indices)) - } - Direction::South => { - let row_indices = vantage_row + 1..max_rows; - let col_indices = repeat(vantage_column); - Box::new(row_indices.zip(col_indices)) - } - Direction::SouthEast => { - let row_indices = vantage_row + 1..max_rows; - let col_indices = vantage_column + 1..max_columns; - Box::new(row_indices.zip(col_indices)) - } - Direction::East => { - let row_indices = repeat(vantage_row); - let col_indices = vantage_column + 1..max_columns; - Box::new(row_indices.zip(col_indices)) - } - Direction::NorthEast => { - let row_indices = (0..vantage_row).rev(); - let col_indices = vantage_column + 1..max_columns; - Box::new(row_indices.zip(col_indices)) - } - } +impl Operation { + pub fn apply(&self, old_value: &Int) -> Int { + let x = self.x_value_supplier.get_value(old_value); + let y = self.y_value_supplier.get_value(old_value); + self.operator.apply(&x, &y) } } -/// Update a seat based on what is visible from that seat's vantage point. -/// -/// "People don't just care about adjacent seats - they care about the first seat they can see in -/// each of those eight directions! -/// -/// Now, instead of considering just the eight immediately adjacent seats, consider the first seat -/// in each of those eight directions." -/// -/// Parameters: -/// * `row_index` - the row of the seat to update -/// * `column_index` - the column of the seat to update -/// * `source` - the full seating layout -/// -/// Returns: the new seating status -pub fn update_seat_based_on_visibility( - row_index: usize, - column_index: usize, - source: &[Vec], -) -> Position { - match source[row_index][column_index] { - Floor => Floor, - EmptySeat => { - // "empty seats that see no occupied seats become occupied" - let row = &source[row_index]; - let mut has_occupied_seat_in_sight = false; - for direction in [ - North, NorthWest, West, SouthWest, South, SouthEast, East, NorthEast, - ] - .iter() - { - has_occupied_seat_in_sight |= direction.has_occupied_seat_in_sight( - row_index, - column_index, - source, - row.len(), - ); - if has_occupied_seat_in_sight { - break; - } - } - if has_occupied_seat_in_sight { - EmptySeat - } else { - OccupiedSeat - } - } - OccupiedSeat => { - // "it now takes five or more visible occupied seats for an occupied seat to become - // empty" - let row = &source[row_index]; - let mut visible_occupied_seats = 0u8; - for direction in [ - North, NorthWest, West, SouthWest, South, SouthEast, East, NorthEast, - ] - .iter() - { - let has_occupied_seat_in_sight = direction.has_occupied_seat_in_sight( - row_index, - column_index, - source, - row.len(), - ); - if has_occupied_seat_in_sight { - visible_occupied_seats += 1; - } - if visible_occupied_seats >= 5 { - break; - } +impl FromStr for Operation { + type Err = (); + + fn from_str(line: &str) -> Result { + let trimmed = line.trim(); + assert!(trimmed.starts_with("Operation:")); + let mut components = trimmed.split(' ').skip(3); + let x_value_supplier = match components.next().expect("First parameter is missing") { + "old" => OldValue, + numeric => { + let literal = numeric.parse::().expect("Unparseable parameter"); + Literal(literal) } - if visible_occupied_seats >= 5 { - EmptySeat - } else { - OccupiedSeat + }; + let operator = components + .next() + .expect("Operator is missing") + .parse::() + .expect("Unparseable operator"); + let y_value_supplier = match components.next().expect("Second parameter is missing") { + "old" => OldValue, + numeric => { + let literal = numeric.parse::().expect("Unparseable parameter"); + Literal(literal) } - } + }; + Ok(Self { + operator, + x_value_supplier, + y_value_supplier, + }) } } -/// Update all of the seats simultaneously based on what is visible from each individual seat. -pub fn update_seats_based_on_visibility(seat_layout: &[Vec]) -> Vec> { - let mut new_layout = seat_layout.to_owned(); - for i in 0..seat_layout.len() { - let row = &seat_layout[i]; - for j in 0..row.len() { - new_layout[i][j] = update_seat_based_on_visibility(i, j, seat_layout); - } - } - new_layout +/// Parse the descriptions of the monkeys' behaviour. The Monkey's ID is its index in the vector. +pub fn get_input() -> Vec { + get_block_strings("day-11.txt") + .map(|block| block.parse::()) + .map(Result::unwrap) + .collect() } #[cfg(test)] -mod tests { - use crate::day11::{ - count_occupied, read_seat_layout, update_seats_based_on_neighbours, - update_seats_based_on_visibility, - }; +pub mod tests { + + use crate::day11::{get_input, Int}; #[test] - fn part1() { - let mut layout = read_seat_layout(); - // "Simulate your seating area by applying the seating rules repeatedly until no seats - // change state." - loop { - let new_layout = update_seats_based_on_neighbours(&layout); - if new_layout == layout { - break; + pub fn part1() { + let mut monkeys = get_input(); + fn update_worry_level(worry_level: &Int) -> Int { + worry_level / 3 + } + for _round in 0..20 { + for i in 0..monkeys.len() { + let mut monkey = monkeys[i].clone(); + let throws = monkey.inspect_items(&update_worry_level); + monkeys[i] = monkey; + for throw in throws { + let mut target = monkeys[throw.destination_monkey_id].clone(); + target.items.push(throw.worry_level); + monkeys[throw.destination_monkey_id] = target; + } } - layout = new_layout; } - println!("Part 1: {}", count_occupied(&layout)); + let mut counts = monkeys + .iter() + .map(|monkey| monkey.items_inspected) + .collect::>(); + counts.sort_unstable_by(|x, y| y.cmp(x)); + let result = counts[0] * counts[1]; + println!("Part 1: {}", result); } #[test] - fn part2() { - let mut layout = read_seat_layout(); - loop { - let new_layout = update_seats_based_on_visibility(&layout); - if new_layout == layout { - break; + pub fn part2() { + let mut monkeys = get_input(); + let product_of_divisors = monkeys + .iter() + .map(|monkey| monkey.divisor) + .reduce(|x, y| x * y) + .expect("No monkeys found"); + let update_worry_level = + move |worry_level: &Int| -> Int { worry_level % product_of_divisors }; + for _round in 0..10_000 { + for i in 0..monkeys.len() { + let mut monkey = monkeys[i].clone(); + let throws = monkey.inspect_items(&update_worry_level); + monkeys[i] = monkey; + for throw in throws { + let mut target = monkeys[throw.destination_monkey_id].clone(); + target.items.push(throw.worry_level); + monkeys[throw.destination_monkey_id] = target; + } } - layout = new_layout; } - println!("Part 2: {}", count_occupied(&layout)); + let mut counts = monkeys + .iter() + .map(|monkey| monkey.items_inspected) + .collect::>(); + counts.sort_unstable_by(|x, y| y.cmp(x)); + let result = counts[0] * counts[1]; + println!("Part 2: {}", result); } } diff --git a/src/day12.rs b/src/day12.rs index 9ad6937..2b3cfff 100644 --- a/src/day12.rs +++ b/src/day12.rs @@ -1,360 +1,191 @@ -// https://adventofcode.com/2020/day/12 - -use core::fmt; -use std::fmt::{Display, Formatter}; -use std::num::ParseIntError; -use std::str::FromStr; - -use crate::day12::Action::{East, Forward, Left, North, Right, South, West}; -use crate::day12::ParseError::{ - InstructionTooShort, InvalidAction, InvalidValue, ValueInappropriateForAction, -}; use crate::get_lines; - -#[derive(Debug)] -pub enum ParseError { - InvalidAction(String), - InstructionTooShort(String), - InvalidValue(ParseIntError), - ValueInappropriateForAction(Action, i16), +use std::cmp::Ordering; +use std::collections::{BinaryHeap, HashMap}; + +/// --- Day 12: Hill Climbing Algorithm --- +/// https://adventofcode.com/2022/day/12 + +/// A map of the local area in a grid +pub struct HeightMap { + grid: Vec>, + #[allow(dead_code)] + starting_point: (usize, usize), + destination: (usize, usize), } -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - ParseError::InvalidAction(action) => write!(f, "Invalid action: {}", action), - ParseError::InstructionTooShort(instruction) => { - write!(f, "Instruction too short: {}", instruction) +impl HeightMap { + pub fn length_of_shortest_path(&self, starting_point: &(usize, usize)) -> usize { + let mut shortest_path_to_node: HashMap<(usize, usize), usize> = HashMap::new(); + shortest_path_to_node.insert(*starting_point, 0usize); + let mut estimated_cost_to_destination: HashMap<(usize, usize), usize> = HashMap::new(); + estimated_cost_to_destination.insert( + *starting_point, + Self::estimate_distance(starting_point, &self.destination), + ); + let mut open_set: BinaryHeap = BinaryHeap::new(); + open_set.push(Node { + coordinate: *starting_point, + estimated_cost_to_destination: Self::estimate_distance( + starting_point, + &self.destination, + ), + }); + while let Some(current) = open_set.pop() { + if current.coordinate == self.destination { + return shortest_path_to_node[¤t.coordinate]; } - ParseError::InvalidValue(error) => write!(f, "Could not parse value: {}", error), - ParseError::ValueInappropriateForAction(action, value) => { - write!( - f, - "Value {} is not appropriate for action {:?}", - value, action - ) + for neighbour in self.neighbours(¤t.coordinate) { + let tentative_score = shortest_path_to_node[¤t.coordinate] + 1; + if tentative_score < *shortest_path_to_node.get(&neighbour).unwrap_or(&usize::MAX) { + shortest_path_to_node.insert(neighbour, tentative_score); + let estimate = + tentative_score + Self::estimate_distance(&neighbour, &self.destination); + estimated_cost_to_destination.insert(neighbour, estimate); + let node = Node { + coordinate: neighbour, + estimated_cost_to_destination: estimate, + }; + open_set.push(node); + } } } + usize::MAX } -} -/// An action that the ferry's navigation computer can produce. -#[derive(Debug)] -pub enum Action { - North, - South, - East, - West, - Left, - Right, - Forward, -} + pub fn potential_trail_heads(&self) -> Vec<(usize, usize)> { + let mut result = vec![]; + for i in 0..self.grid.len() { + let row = &self.grid[i]; + for (j, c) in row.iter().enumerate() { + if *c == 0 { + result.push((i, j)); + } + } + } + result + } -impl FromStr for Action { - type Err = ParseError; + fn height(&self, coördinate: &(usize, usize)) -> u8 { + self.grid[coördinate.0][coördinate.1] + } - fn from_str(code: &str) -> Result { - match code { - "N" => Ok(North), - "S" => Ok(South), - "E" => Ok(East), - "W" => Ok(West), - "L" => Ok(Left), - "R" => Ok(Right), - "F" => Ok(Forward), - _ => Err(InvalidAction(code.to_owned())), + fn neighbours(&self, coördinate: &(usize, usize)) -> Vec<(usize, usize)> { + let mut result = Vec::with_capacity(4); + if coördinate.0 > 0 { + let up = (coördinate.0 - 1, coördinate.1); + if self.height(coördinate) + 1 >= self.height(&up) { + result.push(up); + } + } + if coördinate.0 < self.grid.len() - 1 { + let down = (coördinate.0 + 1, coördinate.1); + if self.height(coördinate) + 1 >= self.height(&down) { + result.push(down); + } + } + if coördinate.1 > 0 { + let left = (coördinate.0, coördinate.1 - 1); + if self.height(coördinate) + 1 >= self.height(&left) { + result.push(left); + } + } + let row = &self.grid[coördinate.0]; + if coördinate.1 < row.len() - 1 { + let right = (coördinate.0, coördinate.1 + 1); + if self.height(coördinate) + 1 >= self.height(&right) { + result.push(right); + } } + result + } + + fn estimate_distance(from: &(usize, usize), to: &(usize, usize)) -> usize { + from.0.abs_diff(to.0) + from.1.abs_diff(to.1) } } -/// An instruction produced by the ferry's navigation computer. A series of NavigationInstructions -/// will allow the ferry/ship to evade the storm and make it to the island. -/// -/// Each instruction consists of an Action and an integer value indicating the magnitude of that -/// action. -pub struct NavigationInstruction { - action: Action, - value: i16, +struct Node { + coordinate: (usize, usize), + estimated_cost_to_destination: usize, } -impl FromStr for NavigationInstruction { - type Err = ParseError; +impl Eq for Node {} - fn from_str(s: &str) -> Result { - if s.len() < 2 { - return Err(InstructionTooShort(s.to_owned())); - } - let (action, value) = s.split_at(1); - action - .parse::() - .and_then(|action| -> Result { - value.parse::().map_err(InvalidValue).and_then( - |value| -> Result { - match action { - Left | Right => { - if value % 90 != 0 { - return Err(ValueInappropriateForAction(action, value)); - } - } - _ => {} - } - Ok(NavigationInstruction { action, value }) - }, - ) - }) +impl PartialEq for Node { + fn eq(&self, other: &Self) -> bool { + self.coordinate.eq(&other.coordinate) } } -pub struct Point { - /// longitudinal position, - /// positive numbers are to the East and negative numbers are to the West - x: i16, - /// latitudinal position, - /// positive numbers are to the North and negative numbers are to the South - y: i16, +impl PartialOrd for Node { + fn partial_cmp(&self, other: &Self) -> Option { + other + .estimated_cost_to_destination + .partial_cmp(&self.estimated_cost_to_destination) + } } -impl Point { - /// The sum of the absolute longitudinal and latitudinal distances from the origin - pub fn get_manhattan_distance(&self) -> u16 { - self.x.unsigned_abs() + self.y.unsigned_abs() +impl Ord for Node { + fn cmp(&self, other: &Self) -> Ordering { + other + .estimated_cost_to_destination + .cmp(&self.estimated_cost_to_destination) } } -impl NavigationInstruction { - /// Interpret the instruction to move the ferry - /// - /// Parameters - /// - `bearing` - (degrees) the direction the ferry is currently facing. - /// 0° means it's facing North, - /// 90° means it's facing East, - /// 180° means it's facing South, - /// 270° means it's facing West, - /// - `position` - the current position of the ferry - /// - /// Returns - /// - `Ok` - the ferry's new bearing and new position - /// - `Err` - if the instruction is to move "forward" but the provided `bearing` is not - /// supported - pub fn move_ship(&self, bearing: u16, position: Point) -> Result<(u16, Point), String> { - match self.action { - North => Ok(( - bearing, - Point { - x: position.x, - y: position.y + self.value, - }, - )), - South => Ok(( - bearing, - Point { - x: position.x, - y: position.y - self.value, - }, - )), - East => Ok(( - bearing, - Point { - x: position.x + self.value, - y: position.y, - }, - )), - West => Ok(( - bearing, - Point { - x: position.x - self.value, - y: position.y, - }, - )), - Left => { - let relative_bearing = bearing as i16 - self.value; - let bearing = if relative_bearing < 0 { - (relative_bearing + 360) as u16 - } else { - relative_bearing as u16 - }; - Ok((bearing, position)) +pub fn get_input() -> HeightMap { + let lines = get_lines("day-12.txt") + .map(|line| line.chars().collect::>()) + .collect::>>(); + let mut grid: Vec> = vec![vec![]; lines.len()]; + let (mut start_x, mut start_y) = (0usize, 0usize); + let (mut destination_x, mut destination_y) = (0usize, 0usize); + for i in 0..lines.len() { + let row = &lines[i]; + grid[i] = vec![0; row.len()]; + for (j, c) in row.iter().enumerate() { + if *c == 'S' { + start_x = i; + start_y = j; + grid[i][j] = 0; + } else if *c == 'E' { + destination_x = i; + destination_y = j; + grid[i][j] = b'z' - b'a'; + } else { + grid[i][j] = *c as u8 - b'a'; } - Right => Ok(((bearing + self.value as u16) % 360, position)), - Forward => match bearing { - 0 => Ok(( - bearing, - Point { - x: position.x, - y: position.y + self.value, - }, - )), - 90 => Ok(( - bearing, - Point { - x: position.x + self.value, - y: position.y, - }, - )), - 180 => Ok(( - bearing, - Point { - x: position.x, - y: position.y - self.value, - }, - )), - 270 => Ok(( - bearing, - Point { - x: position.x - self.value, - y: position.y, - }, - )), - _ => Err("Unsupported bearing: ".to_owned() + &bearing.to_string()), - }, } } - - /// Interpret the instruction to move the ferry's waypoint and possibly also the ferry itself - /// - /// Parameters - /// - `waypoint` - the current position of the ferry's waypoint - /// - `ship` - the current position of the ferry - /// - /// Returns - The new positions of the waypoint and the ferry - pub fn move_ship_using_waypoint(&self, waypoint: Point, ship: Point) -> (Point, Point) { - match self.action { - North => ( - Point { - x: waypoint.x, - y: waypoint.y + self.value, - }, - ship, - ), - South => ( - Point { - x: waypoint.x, - y: waypoint.y - self.value, - }, - ship, - ), - East => ( - Point { - x: waypoint.x + self.value, - y: waypoint.y, - }, - ship, - ), - West => ( - Point { - x: waypoint.x - self.value, - y: waypoint.y, - }, - ship, - ), - Left => { - // rotate the waypoint around the ship, anti-clockwise (when viewed from above), - // by the number of degrees specified in the instruction - - // capture the vector between the waypoint and the ship - let mut longitudinal_difference = waypoint.x - ship.x; - let mut latitudinal_difference = waypoint.y - ship.y; - - // relocate the waypoint relative to the ship - let mut waypoint = Point { - x: waypoint.x, - y: waypoint.y, - }; - for _ in 0..self.value / 90 { - waypoint.x = ship.x - latitudinal_difference; - waypoint.y = ship.y + longitudinal_difference; - longitudinal_difference = waypoint.x - ship.x; - latitudinal_difference = waypoint.y - ship.y; - } - (waypoint, ship) - } - Right => { - // rotate the waypoint around the ship clockwise (when viewed from above), - // by the number of degrees specified in the instruction - - // capture the vector between the waypoint and the ship - let mut longitudinal_difference = waypoint.x - ship.x; - let mut latitudinal_difference = waypoint.y - ship.y; - - // relocate the waypoint relative to the ship - let mut waypoint = Point { - x: waypoint.x, - y: waypoint.y, - }; - for _ in 0..self.value / 90 { - waypoint.x = ship.x + latitudinal_difference; - waypoint.y = ship.y - longitudinal_difference; - longitudinal_difference = waypoint.x - ship.x; - latitudinal_difference = waypoint.y - ship.y; - } - (waypoint, ship) - } - Forward => { - // capture the vector between the waypoint and the ship - let longitudinal_difference = waypoint.x - ship.x; - let latitudinal_difference = waypoint.y - ship.y; - - let mut waypoint = Point { - x: waypoint.x, - y: waypoint.y, - }; - let mut ship = Point { - x: ship.x, - y: ship.y, - }; - - ship.x += longitudinal_difference * self.value; - ship.y += latitudinal_difference * self.value; - - // "The waypoint is relative to the ship; that is, if the ship moves, the waypoint - // moves with it." - waypoint.x = ship.x + longitudinal_difference; - waypoint.y = ship.y + latitudinal_difference; - - (waypoint, ship) - } - } + HeightMap { + grid, + starting_point: (start_x, start_y), + destination: (destination_x, destination_y), } } -pub fn get_instructions() -> impl Iterator> { - get_lines("day-12-input.txt") - .map(|line| line.parse::()) - .map(|parse_result| parse_result.map_err(|error| error.to_string())) -} - #[cfg(test)] -mod tests { - use crate::day12::{get_instructions, Point}; +pub mod tests { - #[test] - fn part1() { - let (_, position) = get_instructions() - .try_fold( - (90u16, Point { x: 0, y: 0 }), // "The ship starts by facing east." - |coordinates, result| { - result - .and_then(|instruction| instruction.move_ship(coordinates.0, coordinates.1)) - }, - ) - .expect("Invalid instruction found"); + use crate::day12::get_input; - println!("Part 1: {}", position.get_manhattan_distance()); + #[test] + pub fn part1() { + let map = get_input(); + let result = map.length_of_shortest_path(&map.starting_point); + println!("Part 1: {}", result); } #[test] - fn part2() { - let (_, ship) = get_instructions() - // "The waypoint starts 10 units east and 1 unit north relative to the ship." - .try_fold( - (Point { x: 10, y: 1 }, Point { x: 0, y: 0 }), - |positions, result| { - result.map(|instruction| { - instruction.move_ship_using_waypoint(positions.0, positions.1) - }) - }, - ) - .expect("Invalid instruction found"); - println!("Part 2: {}", ship.get_manhattan_distance()); + pub fn part2() { + let map = get_input(); + let mut result = usize::MAX; + for potential_trail_head in map.potential_trail_heads() { + let distance = map.length_of_shortest_path(&potential_trail_head); + if distance < result { + result = distance; + } + } + println!("Part 2: {}", result); } } diff --git a/src/day13.rs b/src/day13.rs index d235d3e..db6cf73 100644 --- a/src/day13.rs +++ b/src/day13.rs @@ -1,263 +1,143 @@ -// https://adventofcode.com/2020/day/13 - -use core::fmt; +use crate::get_block_strings; use std::cmp::Ordering; -use std::fmt::{Display, Formatter}; -use std::num::ParseIntError; - -use crate::get_lines; - -use crate::day13::ParseError::{ - EarliestDepartureNotSpecified, InvalidBusId, InvalidDepartureTime, NoBusesSpecified, -}; - -/// A shuttle bus that departs from the sea port, travels to the airport, then several other -/// destinations, then returns to the sea port -#[derive(Debug, Copy, Clone)] -pub struct Bus { - /// The time it takes the bus to complete a circuit in minutes, which also uniquely identifies - /// the bus - id: u16, - - /// The bus' position in the time table - index: usize, -} +use std::cmp::Ordering::Equal; +use std::str::FromStr; +use PacketItem::{List, Literal}; -impl Bus { - /// Determine how long a passenger would have to wait (in minutes) to board this bus at the sea - /// port. - /// - /// Parameters: - /// - `earliest_departure` - the earliest time (in minutes) at which the passenger can board - /// _any_ bus at the sea port - /// - /// Returns: the amount of time in minutes the passenger would have to wait for this particular - /// bus - fn get_time_to_wait(&self, earliest_departure: u32) -> u32 { - let missed_count: u32 = earliest_departure / self.id as u32; // round down - ((missed_count + 1) * self.id as u32) - earliest_departure - } -} +/// --- Day 13: Distress Signal --- +/// https://adventofcode.com/2022/day/13 -impl Display for Bus { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Bus( id: {}, index: {} )", self.id, self.index) - } +#[derive(Clone)] +pub enum PacketItem { + List(Box>), + Literal(u16), } -impl Eq for Bus {} +impl Eq for PacketItem {} -impl PartialEq for Bus { +impl PartialEq for PacketItem { fn eq(&self, other: &Self) -> bool { - self.id == other.id + self.cmp(other) == Equal } } -/// A bus that **a specific passenger** is considering boarding from the sea port -#[derive(Debug, Copy, Clone)] -pub struct BusCandidate { - /// The bus under consideration - bus: Bus, - - /// The earliest time the passenger can board _any_ bus from the sea port - earliest_departure: u32, - - /// The amount of time the passenger would need to wait for this particular bus at the sea port - time_to_wait: u32, -} - -impl Display for BusCandidate { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "BusCandidate( bus: {}, earliest_departure: {}, time_to_wait: {}", - self.bus, self.earliest_departure, self.time_to_wait - ) - } -} - -impl Eq for BusCandidate {} - -impl PartialEq for BusCandidate { - fn eq(&self, other: &Self) -> bool { - self.bus == other.bus - } -} - -impl PartialOrd for BusCandidate { +impl PartialOrd for PacketItem { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for BusCandidate { +impl Ord for PacketItem { fn cmp(&self, other: &Self) -> Ordering { - self.time_to_wait.cmp(&other.time_to_wait) + match self { + List(items) => match other { + List(other_items) => { + let common_length = items.len().min(other_items.len()); + let mut x_iter = items.iter().take(common_length); + let mut y_iter = other_items.iter().take(common_length); + while let (Some(x), Some(y)) = (x_iter.next(), y_iter.next()) { + let result = x.cmp(y); + if result != Equal { + return result; + } + } + items.len().cmp(&other_items.len()) + } + Literal(other_value) => self.cmp(&List(Box::new(vec![Literal(*other_value)]))), + }, + Literal(value) => match other { + List(other_items) => { + List(Box::new(vec![Literal(*value)])).cmp(&List(Box::new(other_items.to_vec()))) + } + Literal(other_value) => value.cmp(other_value), + }, + } } } -/// An error that could arise from parsing the problem input -#[derive(Debug, Clone)] -pub enum ParseError { - InvalidBusId(ParseIntError), - EarliestDepartureNotSpecified, - InvalidDepartureTime(ParseIntError), - NoBusesSpecified, -} - -/// Parse a bus time table on behalf of a specific passenger. -/// -/// Parameters: -/// - `earliest_departure` - The earliest time in minutes the passenger can board a shuttle bus -/// - `string` - A comma-delimited bus time table. Each entry will either be an 'x' representing an -/// out-of-service bus or a bus ID. -/// -/// Returns: All of the bus candidates. For any malformed bus value, a `ParseError` is returned. -fn parse_buses(earliest_departure: u32, string: String) -> Vec> { - string - .split(',') - .enumerate() - .filter(|(_, id)| *id != "x") - .map(|(index, id)| -> Result { - match id.parse::() { - Ok(bus_id) => { - let bus = Bus { id: bus_id, index }; - if earliest_departure % bus_id as u32 == 0 { - return Ok(BusCandidate { - bus, - earliest_departure, - time_to_wait: 0, - }); - } - Ok(BusCandidate { - bus, - earliest_departure, - time_to_wait: bus.get_time_to_wait(earliest_departure), - }) +impl FromStr for PacketItem { + type Err = (); + + fn from_str(line: &str) -> Result { + let mut stack: Vec> = vec![]; + + let mut number_buffer = String::new(); + for c in line.chars() { + if c == '[' { + stack.push(vec![]); + } else if c == ']' { + if !number_buffer.is_empty() { + let number = number_buffer.parse::().unwrap(); + number_buffer.clear(); + let mut last = stack.pop().unwrap(); + last.push(Literal(number)); + stack.push(last); + } + if stack.len() > 1 { + let completed = stack.pop().unwrap(); + let mut last = stack.pop().unwrap(); + last.push(List(Box::new(completed))); + stack.push(last); } - Err(e) => Err(InvalidBusId(e)), + } else if c == ',' { + if !number_buffer.is_empty() { + let number = number_buffer.parse::().unwrap(); + number_buffer.clear(); + let mut last = stack.pop().unwrap(); + last.push(Literal(number)); + stack.push(last); + } + } else { + number_buffer.push(c); } - }) - .collect() -} - -/// Parse the problem input -/// -/// Returns: -/// - `Ok` - All of the bus candidates given the passenger's earliest departure time -/// - `Err(ParseError)` - The first parsing error encountered. -pub fn parse_input() -> Result, ParseError> { - let mut lines = get_lines("day-13-input.txt"); - let earliest_departure = match lines.next() { - None => return Err(EarliestDepartureNotSpecified), - Some(line) => match line.parse::() { - Err(e) => return Err(InvalidDepartureTime(e)), - Ok(integer) => integer, - }, - }; - let buses = match lines.next() { - None => return Err(NoBusesSpecified), - Some(line) => parse_buses(earliest_departure, line), - }; + } - let mut errors = buses - .iter() - .filter(|result| result.is_err()) - .map(move |result| result.as_ref().unwrap_err().to_owned()); - if let Some(error) = errors.next() { - return Err(error); + // currently cannot parse a number literal on its own + Ok(List(Box::new(stack.pop().unwrap()))) } - let buses = buses - .iter() - .map(move |result| result.as_ref().unwrap().to_owned()) - .collect::>(); - Ok(buses) } -/// Find the modular multiplicative inverse -/// -/// Parameters: -/// - `partial_product` - the number by which the result can be multiplied to yield a value -/// equivalent to `1 (mod mod_space)` -/// - `mod_space` - the modulus -/// -/// Returns: The smallest number _x_ such that `mod_space` evenly divides -/// `( partial_product * x ) - 1`. -pub fn find_inverse(partial_product: u64, mod_space: u16) -> u64 { - let mod_space = mod_space as u64; - let mut multiplier = 1u64; - loop { - if (partial_product * multiplier) % mod_space == 1 { - return multiplier; - } - multiplier += 1; - } +pub fn get_input() -> impl Iterator { + get_block_strings("day-13.txt").map(|block| -> (PacketItem, PacketItem) { + let mut lines = block.split('\n'); + let left = lines.next().unwrap().parse::().unwrap(); + let right = lines.next().unwrap().parse::().unwrap(); + (left, right) + }) } #[cfg(test)] -mod tests { - use crate::day13::{find_inverse, parse_input, Bus}; +pub mod tests { + + use crate::day13::PacketItem::{List, Literal}; + use crate::day13::{get_input, PacketItem}; - /// Find the earliest bus you can take to the airport. #[test] - fn part1() { - let mut buses = parse_input().expect("Error parsing input"); - buses.sort(); - let first_bus = buses.first(); - let first_bus = first_bus.expect("No buses found."); - let result = first_bus.bus.id as u32 * first_bus.time_to_wait; + pub fn part1() { + let mut result = 0; + for (i, pair) in get_input().enumerate() { + if pair.0 < pair.1 { + eprintln!("{}", i + 1); + result += i + 1; + } + } println!("Part 1: {}", result); } - /// Find the earliest timestamp such that the first bus departs at that time and each subsequent - /// listed bus departs at that subsequent minute. #[test] - fn part2() { - let buses = parse_input() - .expect("Error parsing input") - .iter() - .map(|candidate| candidate.bus) - .collect::>(); - - // Apply the Chinese Remainder Theorem - // Treat each bus as a modular expression of the form `x ≡ id - index (mod id)`. - // Solve for _x_. + pub fn part2() { + let mut packets = get_input() + .flat_map(|(x, y)| vec![x, y]) + .collect::>(); + packets.sort(); - // For each bus, multiply its placement (index) in the mod space (circuit time), the product - // of the other mod spaces, and its inverse with regard to the other mod spaces, then sum - // the result. This is _a_ timestamp at which each bus departs offset by its position in the - // time table. However, initially, it will not contain the earliest departure time. - let mut sum = 0u64; + let divider_x = List(Box::new(vec![List(Box::new(vec![Literal(2)]))])); + let divider_y = List(Box::new(vec![List(Box::new(vec![Literal(6)]))])); - // Multiply all of the mod spaces (circuit times or bus IDs) to yield the spacing between - // each possible solution. - let mut product_of_mod_spaces = 1u64; - for bus in buses.clone() { - product_of_mod_spaces *= bus.id as u64; + let x_index = packets.binary_search(÷r_x).unwrap_err() + 1; + let y_index = packets.binary_search(÷r_y).unwrap_err() + 2; + let result = x_index * y_index; - // Multiply by all of the other mod spaces (circuit times or bus IDs) in order to cancel - // them out while applying `mod bus_id`. - let product_of_other_mod_spaces = buses - .clone() - .iter() - .filter(|other| **other != bus) - .fold(1u64, |acc, other| acc * other.id as u64); - - let inverse = find_inverse(product_of_other_mod_spaces, bus.id); - - let index = bus.index as u16; - if bus.id > bus.index as u16 { - let remainder = ((bus.id - index) % bus.id) as u64; - sum += remainder * product_of_other_mod_spaces * inverse; - } else { - let remainder = ((index - bus.id) % bus.id) as u64; - sum -= remainder * product_of_other_mod_spaces * inverse; - } - } - // Repeatedly subtract the product of all the mod spaces to find the earliest possible - // departure time that satisfies the constraints. - let iteration = sum / product_of_mod_spaces; - sum -= product_of_mod_spaces * iteration; - println!("Part 2: {}", sum); + println!("Part 2: {}", result); } } diff --git a/src/day14.rs b/src/day14.rs deleted file mode 100644 index 21e13c5..0000000 --- a/src/day14.rs +++ /dev/null @@ -1,272 +0,0 @@ -// https://adventofcode.com/2020/day/14 - -use std::num::ParseIntError; -use std::ops::BitXor; -use std::str::FromStr; - -use regex::Regex; - -use crate::day14::Command::{SetMask, SetMemory}; -use crate::day14::ParseError::{ - InvalidAddress, InvalidInstruction, InvalidMask, InvalidValue, MissingLeftValue, - MissingRightValue, -}; -use crate::get_lines; - -/// An instruction in the sea port's computer system initialisation programme -/// -/// A ferry will use a series of commands to dock at the sea port. -pub enum Command { - /// Update the current bit mask - /// Parameters: - /// - bitmask - A 36-character String containing only the characters '0', '1', or 'X'. - /// The most significant bit is to the left and the least significant bit is to the - /// right. - SetMask(String), - - /// Update the program's memory - /// Parameters - /// - `address` - /// - `value` - SetMemory(u64, u64), -} - -/// An error that may be raised when parsing the initialisation programme. -#[derive(Debug)] -pub enum ParseError { - MissingLeftValue(String), - MissingRightValue(String), - InvalidInstruction(String), - InvalidAddress(String), - InvalidValue(ParseIntError), - InvalidMask(String), -} - -/// Apply a mask to an integer value prior to storing it in memory. This is used by version 1 of the -/// ferry's docking programme decoder chip. -/// -/// Parameters -/// - `mask` - a 36-bit, big-endian mask consisting of the characters '0', '1', and 'X' -/// - `value` - a 36-bit, unsigned integer (the most-significant 28 bits are unmasked) -/// -/// Returns: The result of masking the least-significant 36 bits -pub fn mask_value(mask: &str, value: &u64) -> u64 { - let mask_chars = mask.chars().collect::>(); - (0..36usize).fold(*value, |result, i| -> u64 { - let flag = 1u64 << i; - let mask_value = mask_chars.get(mask.len() - i - 1).expect("Invalid mask"); - match mask_value { - 'X' => result, // "an X leaves the bit in the value unchanged" - '0' => result & std::u64::MAX.bitxor(flag), - '1' => result | flag, - _ => panic!("Invalid mask value: {}", mask_value), - } - }) -} - -/// Apply a mask to a memory address. This is used by version 2 of the ferry's docking programme -/// decoder chip. -/// -/// Parameters: -/// - `mask` - a 36-bit, big-endian mask consisting of the characters '0', '1', and 'X' -/// - `address` - a 36-bit, unsigned integer representing a memory address (the most-significant 28 -/// bits are unmasked) -/// -/// Returns: All the memory addresses that should be updated -pub fn mask_address(mask: &str, address: &u64) -> Vec { - let mask_chars = mask.chars().collect::>(); - let spec = (0..36usize) - .map(|i| -> char { - let mask_value = mask_chars.get(mask.len() - i - 1).expect("Invalid mask"); - let address_value = (address & (1 << i)) >> i; - match mask_value { - // "If the bitmask bit is X, the corresponding memory address bit is floating." - // "If the bitmask bit is 1, the corresponding memory address bit is overwritten with - // 1." - 'X' | '1' => mask_value.to_owned(), - // "If the bitmask bit is 0, the corresponding memory address bit is unchanged." - '0' => address_value - .to_string() - .chars() - .next() - .expect("Invalid address bit"), - _ => panic!("Invalid mask value: {}", mask_value), - } - }) - .collect::>(); - explode(spec) -} - -/// Expand an address specification into all the matching addresses. -/// -/// Parameters: -/// - `spec` - a 36-character address specification consisting of the characters '0', '1', and 'X' -/// For every 'X', two variants will be generated, one in which it is replaced by '0', -/// and one in which it is replaced by '1'. -/// -/// Returns: All the possible memory locations. The length is 2^(number of Xs in `spec`). -fn explode(spec: Vec) -> Vec { - let floating_indices = spec - .iter() - .enumerate() - .filter_map(|(index, bit)| if *bit == 'X' { Some(index) } else { None }) - .collect::>(); - explode_indices(spec, floating_indices.as_slice()) -} - -/// Expand an address specification into all the matching addresses. -/// -/// Parameters: -/// - `spec` - a 36-character address specification consisting of the characters '0', '1', and 'X' -/// For every 'X', two variants will be generated, one in which it is replaced by '0', -/// and one in which it is replaced by '1'. -/// - `floating_indices` - the indices of every 'X' in `spec`. -/// -/// Returns: All the possible memory locations. The length is 2^floating_indices.len(). -fn explode_indices(spec: Vec, floating_indices: &[usize]) -> Vec { - if floating_indices.is_empty() { - return vec![to_int(spec)]; - } - let floating_index = floating_indices[0]; - let mut result: Vec = Vec::new(); - let mut copy = spec; - let sub = floating_indices.split_at(1).1; - copy[floating_index] = '0'; - explode_indices(copy.clone(), sub) - .iter() - .for_each(|address| result.push(*address)); - copy[floating_index] = '1'; - explode_indices(copy, sub) - .iter() - .for_each(|address| result.push(*address)); - result -} - -/// Convert a vector of 0s and 1s to an integer. -/// -/// Parameters: -/// - `chars` - a vector of length 36 for which each character is either '0' or '1'. -/// -/// Returns: the integer representation of `chars` -fn to_int(chars: Vec) -> u64 { - (0..36usize).fold(0u64, |result, i| -> u64 { - let bit = chars[i]; - match bit { - '0' => result, - '1' => result | (1u64 << i), - _ => panic!("Invalid bit: {}", bit), - } - }) -} - -impl FromStr for Command { - type Err = ParseError; - - fn from_str(line: &str) -> Result { - let mut components = line.splitn(2, " = "); - let left_value = match components.next() { - None => return Err(MissingLeftValue(line.to_owned())), - Some(string) => string.trim(), - }; - let right_value = match components.next() { - None => return Err(MissingRightValue(line.to_owned())), - Some(string) => string.trim(), - }; - match left_value { - "mask" => { - let mask = right_value.to_owned(); - if right_value.len() != 36 { - // "The bitmask is always given as a string of 36 bits" - return Err(InvalidMask(mask)); - } - if right_value - .chars() - .any(|c| c != '0' && c != '1' && c != 'X') - { - return Err(InvalidMask(mask)); - } - Ok(SetMask(mask)) - } - _ => { - lazy_static! { - /// A regular expression that matches a memory-assignment lvalue. - /// It includes a single capture group that contains the memory address. - static ref MEM: Regex = Regex::new("mem\\[([0-9]+)\\]").unwrap(); - } - match MEM.captures(left_value) { - None => Err(InvalidInstruction(left_value.to_owned())), - Some(captures) => { - if let Some(matcher) = captures.get(1) { - match matcher.as_str().parse::() { - Err(_) => return Err(InvalidAddress(matcher.as_str().to_owned())), - Ok(address) => match right_value.parse::() { - Err(e) => Err(InvalidValue(e)), - Ok(value) => Ok(SetMemory(address, value)), - }, - } - } else { - Err(InvalidAddress(left_value.to_owned())) - } - } - } - } - } - } -} - -/// Parse the puzzle input -pub fn parse_initialisation_programme() -> impl Iterator { - get_lines("day-14-input.txt") - .map(|line| line.parse::()) - .map(|result| result.expect("Unparseable line")) -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use crate::day14::{mask_address, mask_value, parse_initialisation_programme, Command}; - - #[test] - fn part1() { - let (memory, _) = parse_initialisation_programme().fold( - (HashMap::new(), "X".repeat(36)), - move |mut state, command| -> (HashMap, String) { - match command { - Command::SetMask(mask) => (state.0, mask), - Command::SetMemory(address, value) => { - let value = mask_value(&state.1, &value); - state.0.insert(address, value); - state - } - } - }, - ); - // "To initialize your ferry's docking program, you need the sum of all values left in - // memory after the initialization program completes." - let result = memory.values().sum::(); - println!("Part 1: {}", result); - } - - #[test] - fn part2() { - let (memory, _) = parse_initialisation_programme().fold( - (HashMap::new(), "0".repeat(36)), - move |mut state, command| -> (HashMap, String) { - match command { - Command::SetMask(mask) => (state.0, mask), - Command::SetMemory(address, value) => { - mask_address(&state.1, &address).iter().for_each(|address| { - state.0.insert(address.to_owned(), value); - }); - state - } - } - }, - ); - // "To initialize your ferry's docking program, you need the sum of all values left in - // memory after the initialization program completes." - let result = memory.values().sum::(); - println!("Part 2: {}", result); - } -} diff --git a/src/day15.rs b/src/day15.rs deleted file mode 100644 index 72cee92..0000000 --- a/src/day15.rs +++ /dev/null @@ -1,133 +0,0 @@ -// https://adventofcode.com/2020/day/15 - -use std::collections::hash_map::{Entry, RandomState}; -use std::collections::HashMap; -use std::hash::{BuildHasher, BuildHasherDefault}; - -use hashers::fx_hash::FxHasher; - -use crate::get_lines; - -/// Parse the puzzle input -/// -/// *Panics* if any of the input numbers are not valid array indices -pub fn parse_numbers() -> Vec { - get_lines("day-15-input.txt") - .flat_map(|line| { - line.split(',') - .map(|slice| slice.to_owned()) - .collect::>() - }) - .map(|string| string.parse::()) - .map(|result| result.expect("Invalid number")) - .collect() -} - -/// A variant of [Van Eck's sequence](http://oeis.org/A181391) that starts with a specific seed of -/// numbers. -/// -/// This Iterator has as many elements as the maximum value of a `usize`. -/// -/// Parameters: -/// - `S` - the hash function to use for keeping track of the last time a number was spoken -pub struct VanEckSequence { - /// the first items in the sequence - seed: Vec, - /// A mapping of sequence value to the last index into the sequence at which it appeared - oral_history: HashMap, - /// The index of the _next_ number to speak - index: usize, - last_number_spoken: usize, -} - -impl VanEckSequence { - /// Create a new sequence - /// - /// Parameters: - /// - `seed` - the first items in the sequence. - /// - `oral_history` - a cache to store the last time each number was spoken. - fn with_cache(seed: Vec, oral_history: HashMap) -> VanEckSequence { - VanEckSequence { - seed, - oral_history, - index: 0usize, - last_number_spoken: 0usize, - } - } -} - -impl VanEckSequence> { - /// Create a new sequence - /// - /// Parameters: - /// - `seed` - the first items in the sequence. - pub fn new(seed: Vec) -> VanEckSequence> { - VanEckSequence::with_cache( - seed, - HashMap::with_hasher(BuildHasherDefault::::default()), - ) - } -} - -impl Iterator for VanEckSequence { - type Item = usize; - - fn next(&mut self) -> Option { - let next_number_to_speak = if self.index < self.seed.len() { - let next_number_to_speak = self.seed[self.index]; - self.oral_history - .insert(next_number_to_speak, self.index + 1); - next_number_to_speak - } else { - match self.oral_history.entry(self.last_number_spoken) { - Entry::Occupied(mut entry) => { - let last_mention = entry.insert(self.index); - self.index - last_mention - } - Entry::Vacant(entry) => { - entry.insert(self.index); - 0usize - } - } - }; - - self.index += 1; - self.last_number_spoken = next_number_to_speak; - Some(next_number_to_speak) - } -} - -/// Get the last number spoken after playing the Elves' memory game for _num_rounds_ turns. -/// -/// Parameters: -/// - `num_rounds` - the number of turns in the game. Each turn involves a player speaking one -/// number. -/// -/// Returns: The last number spoken after the specified number of turns/rounds. -pub fn get_last_number_spoken(num_rounds: usize) -> usize { - let numbers = parse_numbers(); - VanEckSequence::new(numbers) - .nth(num_rounds - 1) - .expect("Sequence should be unbounded") -} - -#[cfg(test)] -mod tests { - use crate::day15::get_last_number_spoken; - - #[test] - fn part1() { - // "Their question for you is: what will be the 2020th number spoken?" - let num_rounds = 2020usize; - let last_number_spoken = get_last_number_spoken(num_rounds); - println!("Part 1: {}", last_number_spoken); - } - - #[test] - fn part2() { - // "Impressed, the Elves issue you a challenge: determine the 30,000,000th number spoken." - let num_rounds = 30_000_000usize; - let last_number_spoken = get_last_number_spoken(num_rounds); - println!("Part 2: {}", last_number_spoken); - } -} diff --git a/src/day16.rs b/src/day16.rs deleted file mode 100644 index 92e7bd5..0000000 --- a/src/day16.rs +++ /dev/null @@ -1,256 +0,0 @@ -// --- Day 16: Ticket Translation --- -// https://adventofcode.com/2020/day/16 - -use std::collections::HashSet; -use std::hash::{Hash, Hasher}; -use std::ops::Range; - -use crate::get_lines; - -/// A high-speed train ticket -pub struct Ticket { - /// The numbers that appear on the ticket in the order that they appear - numbers: Vec, -} - -impl Ticket { - /// Determine if the ordered list of numbers represents a valid ticket given the field - /// definitions. - /// - /// Parameters - /// - `fields` - the known rules that ticket fields must follow - /// - /// Returns: true if and only if this ticket satisfies the specified rules - pub fn is_valid(&self, fields: &HashSet) -> bool { - self.get_invalid_numbers(fields).is_empty() - } - - /// Find all of the numbers on the ticket that cannot be valid - /// - /// Parameters - /// - `fields` the known rules that ticket fields must follow - /// - /// Returns: all of the numbers on the ticket that cannot correspond to any of the fields - fn get_invalid_numbers(&self, fields: &HashSet) -> Vec { - /* - Using a generator here would be ideal once that feature is stable: - https://doc.rust-lang.org/std/ops/trait.Generator.html . - This will allow us to support short-circuiting - */ - let mut result: Vec = Vec::new(); - 'outer: for number in self.numbers.iter() { - for field in fields.iter() { - if field.contains(*number) { - continue 'outer; - } - } - result.push(*number); - } - result - } -} - -/// The rules for a ticket field -#[derive(Eq, Clone)] -pub struct Field { - /// The name of the field - label: String, - - /// The valid ranges for this field on a ticket - ranges: Vec>, -} - -impl Field { - /// Determine if this field can contain a certain number - /// - /// Parameters - /// - `number` - a number on a ticket - /// - /// Returns: true if and only if the number _can_ correspond to this field - pub fn contains(&self, number: usize) -> bool { - self.ranges.iter().any(|range| range.contains(&number)) - } -} - -impl Hash for Field { - fn hash(&self, state: &mut H) { - self.label.hash(state); - state.finish(); - } -} - -impl PartialEq for Field { - fn eq(&self, other: &Self) -> bool { - self.label.eq(&other.label) - } -} - -/// Parse the problem input -/// -/// Returns a tuple with the following values: -/// - The high-speed train ticket assigned to you -/// - the valid ranges for the ticket fields -/// - the numbers on all the nearby tickets, sourced via the airport security cameras -pub fn get_input() -> (Ticket, HashSet, Vec) { - let mut fields: HashSet = HashSet::new(); - let mut nearby_tickets: Vec = Vec::new(); - - let mut section = 0u8; - let mut my_ticket: Option = None; - for line in get_lines("day-16-input.txt") { - if line.is_empty() { - section += 1; - continue; - } else if line.trim().eq_ignore_ascii_case("your ticket:") - || line.trim().eq_ignore_ascii_case("nearby tickets:") - { - continue; - } - match section { - 0 => { - let mut sections = line.splitn(2, ": "); - let label = sections - .next() - .expect("Expected field label and ranges delimited by \": \"") - .trim() - .to_owned(); - let ranges = sections - .next() - .expect("No ranges specified") - .trim() - .split(" or ") - .map(|string| -> Range { - let mut bounds = string.splitn(2, '-'); - let start = bounds - .next() - .expect("Missing lower bound of range") - .trim() - .parse::() - .expect("Cannot parse range start."); - let end = bounds - .next() - .expect("Missing upper bound of range") - .trim() - .parse::() - .expect("Cannot parse range end.") - + 1; - Range { start, end } - }) - .collect::>>(); - fields.insert(Field { label, ranges }); - } - 1 => { - my_ticket = Some(Ticket { - numbers: line - .split(',') - .map(|section| { - section - .trim() - .parse::() - .expect("Cannot parse ticket number.") - }) - .collect::>(), - }); - } - 2 => { - nearby_tickets.push(Ticket { - numbers: line - .split(',') - .map(|section| { - section - .trim() - .parse::() - .expect("Cannot parse nearby ticket number.") - }) - .collect::>(), - }); - } - _ => panic!("Unexpected section starting with: {}", line), - } - } - ( - my_ticket.expect("Ticket not issued."), - fields, - nearby_tickets, - ) -} - -#[cfg(test)] -mod tests { - use std::collections::{HashMap, HashSet}; - - use crate::day16::{get_input, Field, Ticket}; - - #[test] - fn part1() { - let (_, fields, nearby_tickets) = get_input(); - let ticket_scanning_error_rate: usize = nearby_tickets - .iter() - .flat_map(|ticket| ticket.get_invalid_numbers(&fields)) - .sum(); - println!("Part 1: {}", ticket_scanning_error_rate); - } - - #[test] - fn part2() { - let (my_ticket, fields, nearby_tickets) = get_input(); - - // "Now that you've identified which tickets contain invalid values, discard those tickets - // entirely. Use the remaining valid tickets to determine which field is which." - let valid_tickets = nearby_tickets - .iter() - .filter(|candidate| candidate.is_valid(&fields)) - .collect::>(); - - // "Using the valid ranges for each field, determine what order the fields appear on the - // tickets. The order is consistent between all tickets" - let mut unmapped_indices = (0..my_ticket.numbers.len()).collect::>(); - let mut field_table: HashMap = HashMap::new(); - let mut unmapped_fields = fields.iter().collect::>(); - while !unmapped_fields.is_empty() { - let mut indices_to_remove: HashSet = HashSet::new(); - for field_index in &unmapped_indices { - let mut candidates = unmapped_fields.clone(); - let mut to_remove: HashSet<&Field> = HashSet::new(); - for ticket in &valid_tickets { - let number = ticket.numbers[*field_index]; - for potential_field in &candidates { - if !potential_field.contains(number) { - to_remove.insert(potential_field); - } - } - for disqualified in &to_remove { - candidates.remove(disqualified); - } - } - if candidates.is_empty() { - panic!("No candidate fields for index: {}", field_index); - } else if candidates.len() == 1 { - // map candidate to index - let field = candidates - .drain() - .next() - .expect("There should be exactly one candidate."); - field_table.insert(field.label.clone(), *field_index); - unmapped_fields.remove(&field); - indices_to_remove.insert(*field_index); - } - } - for index in indices_to_remove { - unmapped_indices.remove(&index); - } - } - // "Once you work out which field is which, look for the six fields on your ticket that - // start with the word departure. What do you get if you multiply those six values - // together?" - let numbers = &my_ticket.numbers; - let product = field_table - .iter() - .filter(|(label, _)| label.starts_with("departure")) - .map(|(_, number)| *number) - .map(|number| numbers[number]) - .reduce(|x, y| x * y) - .expect("Unable to deduce any field position mappings"); - println!("Part 2: {}", product); - } -} diff --git a/src/day17.rs b/src/day17.rs deleted file mode 100644 index 8f9d703..0000000 --- a/src/day17.rs +++ /dev/null @@ -1,431 +0,0 @@ -// --- Day 17: Conway Cubes --- -// https://adventofcode.com/2020/day/17 - -use std::cmp; -use std::collections::{HashMap, HashSet}; - -use rayon::prelude::*; - -use crate::get_lines; - -/// A signed integer for indexing into an infinite 3-dimensional space -/// -/// This can be sized to accommodate the maximum-needed distance from the origin. -type Int = i8; - -/// The location of a Conway Cube in three-dimensional space -/// -/// Each location has 26 adjacent neighbours. -#[derive(PartialEq, Eq, Hash, Copy, Clone)] -pub struct SpatialCoordinate { - x: Int, - y: Int, - z: Int, -} - -impl SpatialCoordinate { - /// Find the coordinate at the specified offset - /// - /// Returns: - /// - `None` - if all the offsets are zero - /// - `Some(SpatialCoordinate)` - The coordinate at the specified offset - pub fn offset(&self, x_offset: Int, y_offset: Int, z_offset: Int) -> Option { - if x_offset == 0 && y_offset == 0 && z_offset == 0 { - None - } else { - Some(SpatialCoordinate { - x: self.x + x_offset, - y: self.y + y_offset, - z: self.z + z_offset, - }) - } - } -} - -/// The location of a Hyper Conway Cube in four-dimensional space -/// -/// Each location has 80 adjacent neighbours. -#[derive(PartialEq, Eq, Hash, Copy, Clone)] -pub struct SpaceTimeCoordinate { - x: Int, - y: Int, - z: Int, - w: Int, -} - -impl SpaceTimeCoordinate { - /// Find the coordinate at the specified offset - /// - /// Returns: - /// - `None` - if all the offsets are zero - /// - `Some(SpatialCoordinate)` - The coordinate at the specified offset - pub fn offset( - &self, - x_offset: Int, - y_offset: Int, - z_offset: Int, - w_offset: Int, - ) -> Option { - if x_offset == 0 && y_offset == 0 && z_offset == 0 && w_offset == 0 { - None - } else { - Some(SpaceTimeCoordinate { - x: self.x + x_offset, - y: self.y + y_offset, - z: self.z + z_offset, - w: self.w + w_offset, - }) - } - } -} - -/// Identifies the boundaries of the known space for a single dimension -/// -/// Note that during a cycle, the cubes one unit beyond the bounds *may* update. -#[derive(Debug)] -pub struct Bounds { - /// The lower bound, inclusive and strictly less than or equal to `upper` - lower: Int, - /// The upper bound, inclusive and strictly greater than or equal to `lower` - upper: Int, -} - -/// An infinite, 3-dimensional grid of Conway Cubes. Each cube is either active or inactive as -/// represented by a `bool`. -pub struct SpatialGrid { - x_bounds: Bounds, - y_bounds: Bounds, - z_bounds: Bounds, - map: HashMap>>, -} - -impl SpatialGrid { - pub fn new(known_cubes: Vec) -> SpatialGrid { - let mut map = HashMap::new(); - let mut x_min: Int = 0; - let mut x_max: Int = 0; - let mut y_min: Int = 0; - let mut y_max: Int = 0; - let mut z_min: Int = 0; - let mut z_max: Int = 0; - - for coordinate in known_cubes { - let x_dimension = map.entry(coordinate.x).or_insert_with(HashMap::new); - let y_dimension = x_dimension.entry(coordinate.y).or_insert_with(HashSet::new); - y_dimension.insert(coordinate.z); - - x_min = cmp::min(x_min, coordinate.x); - x_max = cmp::max(x_max, coordinate.x); - y_min = cmp::min(y_min, coordinate.y); - y_max = cmp::max(y_max, coordinate.y); - z_min = cmp::min(z_min, coordinate.z); - z_max = cmp::max(z_max, coordinate.z); - } - - SpatialGrid { - x_bounds: Bounds { - lower: x_min, - upper: x_max, - }, - y_bounds: Bounds { - lower: y_min, - upper: y_max, - }, - z_bounds: Bounds { - lower: z_min, - upper: z_max, - }, - map, - } - } - - /// Returns: the total number of active Conway Cubes in the unbounded grid - pub fn count_active(&self) -> usize { - self.map - .values() - .map(|x_dimension| -> usize { x_dimension.values().map(HashSet::len).sum() }) - .sum() - } - - /// Determine if the Conway Cube at the given three-dimensional coordinates is active or not. - fn is_active(&self, coordinates: &SpatialCoordinate) -> bool { - if let Some(x_dimension) = self.map.get(&coordinates.x) { - if let Some(y_dimension) = x_dimension.get(&coordinates.y) { - return y_dimension.contains(&coordinates.z); - } - } - false - } - - /// Create a new generation from the current one. - /// - /// Returns: a new Grid based on the evaluation of the current state - pub fn cycle(&self) -> SpatialGrid { - let known_cubes = (&self.x_bounds.lower - 1..=&self.x_bounds.upper + 1) - .into_par_iter() - .flat_map(move |x| { - (&self.y_bounds.lower - 1..=&self.y_bounds.upper + 1) - .into_par_iter() - .flat_map(move |y| { - (&self.z_bounds.lower - 1..=&self.z_bounds.upper + 1) - .into_par_iter() - .flat_map(move |z| { - let coordinate = SpatialCoordinate { x, y, z }; - if self.cycle_cube(&coordinate) { - Some(coordinate) - } else { - None - } - }) - }) - }) - .collect::>(); - SpatialGrid::new(known_cubes) - } - - fn cycle_cube(&self, coordinates: &SpatialCoordinate) -> bool { - let active_neighbours = self - .get_neighbouring_coordinates(coordinates) - .filter(|neighbour| self.is_active(neighbour)) - .count(); - if self.is_active(coordinates) { - active_neighbours == 2 || active_neighbours == 3 - } else { - active_neighbours == 3 - } - } - - fn get_neighbouring_coordinates<'a>( - &self, - coordinates: &'a SpatialCoordinate, - ) -> impl Iterator + 'a { - (-1..=1).flat_map(move |x_offset| { - (-1..=1).flat_map(move |y_offset| { - (-1..=1).flat_map(move |z_offset| coordinates.offset(x_offset, y_offset, z_offset)) - }) - }) - } -} - -pub struct SpaceTimeGrid { - x_bounds: Bounds, - y_bounds: Bounds, - z_bounds: Bounds, - w_bounds: Bounds, - - map: HashMap>>>, -} - -impl SpaceTimeGrid { - pub fn new(known_cubes: Vec) -> SpaceTimeGrid { - let mut map = HashMap::new(); - let mut x_min: Int = 0; - let mut x_max: Int = 0; - let mut y_min: Int = 0; - let mut y_max: Int = 0; - let mut z_min: Int = 0; - let mut z_max: Int = 0; - let mut w_min: Int = 0; - let mut w_max: Int = 0; - - for coordinate in known_cubes { - let x_dimension = map.entry(coordinate.x).or_insert_with(HashMap::new); - let y_dimension = x_dimension.entry(coordinate.y).or_insert_with(HashMap::new); - let z_dimension = y_dimension.entry(coordinate.z).or_insert_with(HashSet::new); - z_dimension.insert(coordinate.w); - - x_min = cmp::min(x_min, coordinate.x); - x_max = cmp::max(x_max, coordinate.x); - y_min = cmp::min(y_min, coordinate.y); - y_max = cmp::max(y_max, coordinate.y); - z_min = cmp::min(z_min, coordinate.z); - z_max = cmp::max(z_max, coordinate.z); - w_min = cmp::min(w_min, coordinate.w); - w_max = cmp::max(w_max, coordinate.w); - } - - SpaceTimeGrid { - x_bounds: Bounds { - lower: x_min, - upper: x_max, - }, - y_bounds: Bounds { - lower: y_min, - upper: y_max, - }, - z_bounds: Bounds { - lower: z_min, - upper: z_max, - }, - w_bounds: Bounds { - lower: w_min, - upper: w_max, - }, - map, - } - } - - /// Returns: the total number of active Conway Cubes in the unbounded grid - pub fn count_active(&self) -> usize { - self.map - .values() - .map(|x_dimension| -> usize { - x_dimension - .values() - .map(|y_dimension| -> usize { y_dimension.values().map(HashSet::len).sum() }) - .sum() - }) - .sum() - } - - /// Determine if the Hyper Conway Cube at the given three-dimensional coordinates is active or not. - fn is_active(&self, coordinates: &SpaceTimeCoordinate) -> bool { - if let Some(x_dimension) = self.map.get(&coordinates.x) { - if let Some(y_dimension) = x_dimension.get(&coordinates.y) { - if let Some(z_dimension) = y_dimension.get(&coordinates.z) { - return z_dimension.contains(&coordinates.w); - } - } - } - false - } - - /// Create a new generation from the current one. - /// - /// Returns: a new Grid based on the evaluation of the current state - pub fn cycle(&self) -> SpaceTimeGrid { - let known_cubes = (&self.x_bounds.lower - 1..=&self.x_bounds.upper + 1) - .into_par_iter() - .flat_map(move |x| { - (&self.y_bounds.lower - 1..=&self.y_bounds.upper + 1) - .into_par_iter() - .flat_map(move |y| { - (&self.z_bounds.lower - 1..=&self.z_bounds.upper + 1) - .into_par_iter() - .flat_map(move |z| { - (&self.w_bounds.lower - 1..=&self.w_bounds.upper + 1) - .into_par_iter() - .flat_map(move |w| { - let coordinate = SpaceTimeCoordinate { x, y, z, w }; - if self.cycle_cube(&coordinate) { - Some(coordinate) - } else { - None - } - }) - }) - }) - }) - .collect::>(); - SpaceTimeGrid::new(known_cubes) - } - - fn cycle_cube(&self, coordinates: &SpaceTimeCoordinate) -> bool { - let active_neighbours = self - .get_neighbouring_coordinates(coordinates) - .filter(|neighbour| self.is_active(neighbour)) - .count(); - if self.is_active(coordinates) { - active_neighbours == 2 || active_neighbours == 3 - } else { - active_neighbours == 3 - } - } - - fn get_neighbouring_coordinates<'a>( - &self, - coordinates: &'a SpaceTimeCoordinate, - ) -> impl Iterator + 'a { - (-1..=1).flat_map(move |x_offset| { - (-1..=1).flat_map(move |y_offset| { - (-1..=1).flat_map(move |z_offset| { - (-1..=1).flat_map(move |w_offset| { - coordinates.offset(x_offset, y_offset, z_offset, w_offset) - }) - }) - }) - }) - } -} - -/// Parse the problem input. -/// -/// "In the initial state of the pocket dimension, almost all cubes start inactive. The only -/// exception to this is a small flat region of cubes (your puzzle input); the cubes in this region -/// start in the specified active (#) or inactive (.) state." -pub fn parse_3d_grid() -> SpatialGrid { - let known_cubes = get_lines("day-17-input.txt") - .enumerate() - .flat_map(|(x, line)| { - line.chars() - .enumerate() - .flat_map(|(y, state)| -> Option { - if state == '#' { - Some(SpatialCoordinate { - x: x as Int, - y: y as Int, - z: 0, - }) - } else { - None - } - }) - .collect::>() - }) - .collect::>(); - SpatialGrid::new(known_cubes) -} - -/// Parse the problem input. -/// -/// "In the initial state of the pocket dimension, almost all cubes start inactive. The only -/// exception to this is a small flat region of cubes (your puzzle input); the cubes in this region -/// start in the specified active (#) or inactive (.) state." -pub fn parse_4d_grid() -> SpaceTimeGrid { - let known_cubes = get_lines("day-17-input.txt") - .enumerate() - .flat_map(|(x, line)| { - line.chars() - .enumerate() - .flat_map(|(y, state)| -> Option { - if state == '#' { - Some(SpaceTimeCoordinate { - x: x as Int, - y: y as Int, - z: 0, - w: 0, - }) - } else { - None - } - }) - .collect::>() - }) - .collect::>(); - SpaceTimeGrid::new(known_cubes) -} - -#[cfg(test)] -mod tests { - use crate::day17::{parse_3d_grid, parse_4d_grid}; - - #[test] - fn part1() { - let mut grid = parse_3d_grid(); - for _ in 0..6 { - grid = grid.cycle(); - } - let num_active = grid.count_active(); - println!("Part 1: {}", num_active); - } - - #[test] - fn part2() { - let mut grid = parse_4d_grid(); - for _ in 0..6 { - grid = grid.cycle(); - } - let num_active = grid.count_active(); - println!("Part 2: {}", num_active); - } -} diff --git a/src/day18.rs b/src/day18.rs deleted file mode 100644 index 7fcc93b..0000000 --- a/src/day18.rs +++ /dev/null @@ -1,242 +0,0 @@ -// --- Day 18: Operation Order --- -// https://adventofcode.com/2020/day/18 - -use crate::get_lines; - -type Int = u64; - -/// All of the supported operators -/// -/// All are binary, left-associative operators. -#[derive(Debug, Copy, Clone)] -pub enum Operator { - Add, - Multiply, -} - -/// All of the possible values in a single math problem -#[derive(Eq, PartialEq, Debug)] -pub enum Token { - /// A numerical literal - Number(Int), - - /// An opening parenthesis that indicates the start of an expression that must be evaluated - /// before it can be used by the surrounding expression. A corresponding `EndGroup` token must - /// follow eventually. - StartGroup, - - /// A closing parenthesis indicating the end of an expression that must be evaluated before it - /// can be used by the surrounding expression. A corresponding `StartGroup` token must precede - /// this. - EndGroup, - - /// An infix operator that sums the adjacent expressions - Plus, - - /// An infix operator that produces the product of the adjacent expressions - Times, -} - -/// An element in the postfix representation of a mathematical expression -pub enum Instruction { - /// An instruction to push a number onto the stack - Number(Int), - - /// An instruction to add the top two elements on the stack and push the result - /// This cannot be interpreted as a unary operator. - Add, - - /// An instruction to multiply the top two elements on the stack and push the result - Multiply, -} - -/// Tokenise the problem input -/// -/// Returns: an `Iterator` in which each entry is a mathematical expression using infix notation -pub fn get_input() -> impl Iterator> { - get_lines("day-18-input.txt").map(|line| -> Vec { - line.chars() - .flat_map(|c| -> Option { - match c { - '0'..='9' => Some(Token::Number(c.to_digit(10).unwrap() as Int)), - '+' => Some(Token::Plus), - '*' => Some(Token::Times), - '(' => Some(Token::StartGroup), - ')' => Some(Token::EndGroup), - ' ' => None, - _ => panic!("Invalid char: {}", c), - } - }) - .collect() - }) -} - -/// Convert an expression from infix notation to postfix notation. -/// -/// This uses the Shunting-yard algorithm. -/// -/// Parameters: -/// - `tokens` - a mathematical expression that uses infix notation -/// - `precedences` - a function that returns larger numbers for higher precedence operators -pub fn convert_to_postfix(tokens: Vec, precedence: F) -> Vec -where - F: Fn(&Operator) -> u8, -{ - /// A subset of the tokens stored as operators in the Shunting-yard algorithm - enum Op { - /// An add operation with a precedence value - Add(u8), - - /// A multiplication operation with a precedence value - Multiply(u8), - - /// An indicator of the start of a group - Group, - } - - let mut operators = vec![]; - let mut result = vec![]; - - let process_operator = |current_operator: Op, - current_operator_precedence: u8, - operators: &mut Vec| - -> Vec { - let mut result = vec![]; - while let Some(previous_operator) = operators.last() { - match previous_operator { - Op::Group => break, - Op::Add(previous_operator_precedence) => { - if *previous_operator_precedence >= current_operator_precedence { - operators.pop(); - result.push(Instruction::Add); - } else { - break; - } - } - Op::Multiply(previous_operator_precedence) => { - if *previous_operator_precedence >= current_operator_precedence { - operators.pop(); - result.push(Instruction::Multiply); - } else { - break; - } - } - } - } - operators.push(current_operator); - result - }; - - for token in tokens { - match token { - Token::Number(i) => result.push(Instruction::Number(i)), - Token::StartGroup => operators.push(Op::Group), - Token::EndGroup => loop { - let operator = operators.pop().expect("Unexpected group ending"); - match operator { - Op::Group => break, - Op::Add(_) => { - assert!(!operators.is_empty(), "Missing opening parenthesis"); - result.push(Instruction::Add); - } - Op::Multiply(_) => { - assert!(!operators.is_empty(), "Missing opening parenthesis"); - result.push(Instruction::Multiply); - } - } - }, - Token::Plus => { - let current_operator_precedence = precedence(&Operator::Add); - result.append(&mut process_operator( - Op::Add(current_operator_precedence), - current_operator_precedence, - &mut operators, - )) - } - Token::Times => { - let current_operator_precedence = precedence(&Operator::Multiply); - result.append(&mut process_operator( - Op::Multiply(current_operator_precedence), - current_operator_precedence, - &mut operators, - )) - } - } - } - while !operators.is_empty() { - let operator = operators.pop().unwrap(); - match operator { - Op::Add(_) => result.push(Instruction::Add), - Op::Multiply(_) => result.push(Instruction::Multiply), - Op::Group => panic!("All groups should have been removed by now"), - } - } - result -} - -/// Evaluate a mathematical expression in postfix notation -/// -/// Parameters: -/// - `elements` - The mathematical instructions in postfix order -/// -/// Returns: the result of evaluating the expression -pub fn evaluate(elements: Vec) -> Int { - let mut stack = vec![]; - for element in elements { - match element { - Instruction::Number(i) => stack.push(i), - Instruction::Add => { - let right_value = stack.pop().unwrap(); - let left_value = stack.pop().unwrap(); - stack.push(left_value + right_value); - } - Instruction::Multiply => { - let right_value = stack.pop().unwrap(); - let left_value = stack.pop().unwrap(); - stack.push(left_value * right_value); - } - } - } - stack.pop().unwrap() -} - -#[cfg(test)] -mod tests { - use crate::day18::Operator::{Add, Multiply}; - use crate::day18::{convert_to_postfix, evaluate, get_input, Int}; - - #[test] - fn part1() { - let sum = get_input() - .map(|tokens| { - convert_to_postfix(tokens, |operator| match operator { - // "However, the rules of operator precedence have changed. Rather than - // evaluating multiplication before addition, the operators have the same - // precedence, and are evaluated left-to-right regardless of the order in which - // they appear." - Add => 1, - Multiply => 1, - }) - }) - .map(evaluate) - .sum::(); - println!("Part 1: {}", sum); - } - - #[test] - fn part2() { - let sum = get_input() - .map(|tokens| { - convert_to_postfix(tokens, |operator| match operator { - // "Now, addition and multiplication have different precedence levels, but - // they're not the ones you're familiar with. Instead, addition is evaluated - // before multiplication." - Add => 2, - Multiply => 1, - }) - }) - .map(evaluate) - .sum::(); - println!("Part 2: {}", sum); - } -} diff --git a/src/day19.rs b/src/day19.rs deleted file mode 100644 index a2e7308..0000000 --- a/src/day19.rs +++ /dev/null @@ -1,224 +0,0 @@ -// --- Day 19: Monster Messages --- -// https://adventofcode.com/2020/day/19 - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use crate::day19::Rule::{MatchAll, MatchAnySet, MatchSingleCharacter}; -use crate::get_lines; - -/// A rule that valid messages (or partial messages) should obey -pub enum Rule { - /// The message must match a single character exactly - /// Parameters: - /// - the character to match - MatchSingleCharacter(char), - - /// Each sub rule referenced must match a subsequent portion of the message - /// Parameters: - /// - an ordered list of Rule IDs to match - MatchAll(Vec), - - /// The message must match at least one of the sub-rules - /// Parameters: - /// - two or more rule sets - MatchAnySet(Vec>), // FIXME outer container does not need to be ordered -} - -impl<'a> Rule { - /// Determine if a message matches this rule exactly - /// - /// Parameters: - /// - `message` - the message to evaluate - /// - `rules` - a dictionary of rule ID to rule - /// Returns: true if and only if the message matches this rule in its entirety with no remaining - /// characters. - pub fn matches(&self, message: &'a str, rules: &HashMap) -> bool { - let mut prefixes = HashSet::new(); - prefixes.insert(message); - self.matching_suffixes(&prefixes, rules) - .iter() - .any(|suffix| suffix.is_empty()) - } - - /// Return every possible suffix for the messages that match this rule. - /// - /// Apply every permutation of this rule to the provided messages. For each message permutation - /// combination that results in a partial match, emit the remainder of the message that has not - /// yet been matched. Since a rule may reference other rules, including itself, this method is - /// expected to be called recursively. - /// - /// Parameters: - /// - `messages` - The strings to evaluate for a match. It is expected that each of these is a - /// suffix of a single originating message. - /// - `rules` - A dictionary of the other rules for this rule to reference. - /// - /// Returns: All of the possible matching suffixes. If the return value is empty, there were no - /// matches. If one of the entries is the empty string, it means that there was an - /// exact match. If one of the entries is non-empty, it means there was a partial match - /// and the entry represents the remaining portion. - fn matching_suffixes( - &self, - messages: &HashSet<&'a str>, - rules: &HashMap, - ) -> HashSet<&'a str> { - match self { - MatchSingleCharacter(c) => messages - .iter() - .flat_map(|prefix| -> HashSet<&'a str> { - let mut result = HashSet::new(); - if prefix.starts_with(*c) { - result.insert(&prefix[1..prefix.len()]); - } - result - }) - .collect(), - MatchAll(ids) => messages - .iter() - .flat_map(|prefix| -> HashSet<&'a str> { - let mut result = HashSet::new(); - result.insert(*prefix); - for id in ids { - let rule = rules.get(id).unwrap(); - let suffixes = rule.matching_suffixes(&result, rules); - result = suffixes; - if result.is_empty() { - break; - } - } - result - }) - .collect(), - MatchAnySet(id_sets) => messages - .iter() - .flat_map(|prefix| -> HashSet<&'a str> { - id_sets - .iter() - .flat_map(|set| -> HashSet<&'a str> { - let mut result = HashSet::new(); - result.insert(*prefix); - MatchAll(set.to_owned()).matching_suffixes(&result, rules) - }) - .collect() - }) - .collect(), - } - } -} - -impl FromStr for Rule { - type Err = (); - - fn from_str(s: &str) -> Result { - if s.starts_with('"') { - let c = s - .strip_prefix('"') - .and_then(|s| s.strip_suffix('"')) - .and_then(|s| -> Option { - if s.len() != 1 { - return None; - } - s.chars().next() - }) - .expect(&*format!("Error parsing character: {}", s)); - Ok(MatchSingleCharacter(c)) - } else if s.contains(" | ") { - let potential_rule_sets = s - .split(" | ") - .map(|section| { - section - .trim() - .split(' ') - .map(|c| { - c.parse::() - .expect(&*format!("Invalid rule ID: {}", c)) - }) - .collect::>() - }) - .collect(); - Ok(MatchAnySet(potential_rule_sets)) - } else { - let rule_ids = s - .trim() - .split(' ') - .map(|c| c.parse::().unwrap()) - .collect::>(); - Ok(MatchAll(rule_ids)) - } - } -} - -fn parse_rule(string: &str) -> (usize, Rule) { - let mut sections = string.trim().split(": "); - if let Some(id_string) = sections.next() { - let id = id_string - .trim() - .parse::() - .expect(&*format!("Unparseable id: {}", id_string)); - if let Some(value_string) = sections.next() { - let value_string = value_string.trim(); - if let Ok(rule) = Rule::from_str(value_string) { - let result = (id, rule); - assert!(sections.next().is_none(), "Too many sections: {}", string); - return result; - } - } - } - - panic!("Malformed rule: {}", string); -} - -/// Get the puzzle input -/// -/// Returns -/// - the rules that valid messages should obey -/// - the received messages -pub fn get_input() -> (HashMap, Vec) { - let mut rules = HashMap::new(); - let mut messages = vec![]; - let mut section = 0; - for line in get_lines("day-19-input.txt") { - if line.is_empty() { - section += 1; - continue; - } - if section == 0 { - let (id, rule) = parse_rule(&*line); - rules.insert(id, rule); - } else if section == 1 { - messages.push(line); - } else { - panic!("Unexpected section"); - } - } - (rules, messages) -} - -#[cfg(test)] -mod tests { - use crate::day19::{get_input, Rule}; - - #[test] - fn part1() { - let (rules, messages) = get_input(); - let rule = rules.get(&0usize).unwrap(); - let count = messages - .iter() - .filter(|message| rule.matches(message, &rules)) - .count(); - println!("Part 1: {}", count); - } - - #[test] - fn part2() { - let (mut rules, messages) = get_input(); - rules.insert(8, "42 | 42 8".parse::().unwrap()); - rules.insert(11, "42 31 | 42 11 31".parse::().unwrap()); - let rule = rules.get(&0usize).unwrap(); - let count = messages - .iter() - .filter(|message| rule.matches(message, &rules)) - .count(); - println!("Part 2: {}", count); - } -} diff --git a/src/day20.rs b/src/day20.rs deleted file mode 100644 index 84bb42b..0000000 --- a/src/day20.rs +++ /dev/null @@ -1,680 +0,0 @@ -// --- Day 20: Jurassic Jigsaw --- -// https://adventofcode.com/2020/day/20 - -use std::fmt::{Display, Formatter}; -use std::iter::FromIterator; -use std::ops::{Index, IndexMut}; - -use Transformation::*; - -use crate::get_lines; - -type Id = u64; - -/// A square, monochrome portion of a satellite image. Because the satellite's camera array is -/// malfunctioning, it may have been rotated or flipped randomly. Each tile's image border matches -/// with another tile. -pub struct Tile { - /// A random unique identifier provided by the camera - id: Id, - - /// The square grid of pixels. The length of the outer vector is the same as the length of each - /// inner vector. - pixels: Vec>, -} - -impl Index for Tile { - type Output = Vec; - - fn index(&self, index: usize) -> &Self::Output { - &self.pixels[index] - } -} - -impl IndexMut for Tile { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.pixels[index] - } -} - -impl Clone for Tile { - fn clone(&self) -> Self { - Tile { - id: self.id, - pixels: self.pixels.clone(), - } - } -} - -impl Display for Tile { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let picture = self - .pixels - .iter() - .map(|row| -> String { - let mut joined: String = row.iter().cloned().collect::(); - joined.push('\n'); - joined - }) - .collect::(); - write!(f, "{}:\n{}\n", self.id, picture) - } -} - -impl Tile { - /// Determine all of the possible ways the tile may be flipped and/or rotated - /// - /// Returns: the unique set of ways the tile may be oriented, including the original - fn permutations(&self) -> Vec { - let original = OrientedTile { - tile: self, - transformations: vec![], - }; - let r90 = original.rotate90(); - vec![ - original.flip_horizontally(), - original.flip_vertically(), - original.rotate180(), - original.rotate270(), - r90.flip_horizontally(), - r90.flip_vertically(), - r90, - /* these are redundant: - original.rotate180().flip_horizontally(), original.rotate180().flip_vertically(), - original.rotate270().flip_horizontally(), original.rotate270().flip_vertically(), - */ - original, - ] - } - - /// Determines how rough the waters are in the sea monsters' habitat - /// - /// Returns: the number of '#' pixels in the image - pub fn roughness(&self) -> usize { - let mut result = 0usize; - for row in &self.pixels { - for cell in row { - if cell == &'#' { - result += 1; - } - } - } - result - } - - /// Remove one row of pixels from each edge of the tile - /// - /// Returns: a new tile with the borders removed - pub fn crop_borders(&self) -> Self { - let mut wide_rows = self.pixels.clone(); - wide_rows.remove(wide_rows.len() - 1); - wide_rows.remove(0); - - Tile { - id: self.id, - pixels: wide_rows - .iter() - .cloned() - .map(|mut row| -> Vec { - // let mut row = wide_row.clone(); - row.remove(row.len() - 1); - row.remove(0); - row - }) - .collect(), - } - } -} - -/// A rotation or flip operation on a tile -#[derive(Copy, Clone, Debug)] -enum Transformation { - Rotate90, - Rotate180, - Rotate270, - FlipHorizontally, - FlipVertically, -} - -impl Transformation { - /// Translate the coördinates from an oriented tile to the coördinates on the original tile - /// - /// Parameters: - /// - `x` - the row number in the oriented tile - /// - `y` - the column number in the oriented tile - /// - `length` - the number of pixels on each side of the square tile - /// - /// Returns: `(row, column)` that index into the non-oriented tile - fn transform(&self, x: usize, y: usize, length: usize) -> (usize, usize) { - match self { - Rotate90 => (y, length - x - 1), - Rotate180 => (length - x - 1, length - y - 1), - Rotate270 => (length - y - 1, length - x - 1), - FlipHorizontally => (x, length - y - 1), - FlipVertically => (length - x - 1, y), - } - } -} - -/// A satellite image tile that has been oriented in a specific way. -/// -/// Parameters: -/// `'t` - The lifetime of the non-oriented tile to ensure it outlives the oriented tile -pub struct OrientedTile<'t> { - /// The non-oriented tile - tile: &'t Tile, - - /// ordered list of flip or rotate operations to apply, may be empty - transformations: Vec, -} - -impl<'t> Clone for OrientedTile<'t> { - fn clone(&self) -> Self { - OrientedTile { - tile: self.tile, - transformations: self.transformations.clone(), - } - } -} - -impl<'t> Display for OrientedTile<'t> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let picture = self - .pixels() - .iter() - .map(|row| -> String { - let mut joined: String = row.iter().cloned().collect::(); - joined.push('\n'); - joined - }) - .collect::(); - write!(f, "{}:\n{}\n", self.tile.id, picture) - } -} - -impl<'t> OrientedTile<'t> { - /// The reference pattern of what a Sea Monster looks like - const SEA_MONSTER: [[char; 20]; 3] = [ - [ - ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', - ' ', '#', ' ', - ], - [ - '#', ' ', ' ', ' ', ' ', '#', '#', ' ', ' ', ' ', ' ', '#', '#', ' ', ' ', ' ', ' ', - '#', '#', '#', - ], - [ - ' ', '#', ' ', ' ', '#', ' ', ' ', '#', ' ', ' ', '#', ' ', ' ', '#', ' ', ' ', '#', - ' ', ' ', ' ', - ], - ]; - - pub fn id(&self) -> Id { - self.tile.id - } - - /// Freeze the orientation of this tile - /// - /// Returns: a new `Tile` that has been reöriented according to the _transformations_. - pub fn tile(&self) -> Tile { - Tile { - id: self.tile.id, - pixels: self.pixels(), - } - } - - /// Calculate the raw pixels of the oriented tile. - /// - /// Returns: a new matrix of pixels, generated by applying the _transformations_. - pub fn pixels(&self) -> Vec> { - (0..self.edge_length()) // final row indices - .map(|i| -> Vec { - (0..self.edge_length()) // final column indices - .map(|j| self.translate(i, j)) // original coördinates - .map(|(x, y)| self.tile[x][y]) // original char - .collect() - }) - .collect() - } - - /// the number of pixels on each edge of the square tile - fn edge_length(&self) -> usize { - self.tile.pixels.len() - } - - /// Convert the coördinates from the oriented tile to the corresponding coördinates in the - /// non-oriented tile. - fn translate(&self, x: usize, y: usize) -> (usize, usize) { - self.transformations - .iter() - .fold((x, y), |previous, transformation| { - transformation.transform(previous.0, previous.1, self.edge_length()) - }) - } - - fn item_at(&self, x: usize, y: usize) -> char { - let coordinates = self.translate(x, y); - self.tile[coordinates.0][coordinates.1] - } - - fn left_border(&'t self) -> impl Iterator + 't { - (0..self.edge_length()).map(move |i| self.item_at(i, 0)) - } - - fn right_border(&'t self) -> impl Iterator + 't { - let last_index = self.edge_length() - 1; - (0..self.edge_length()).map(move |i| self.item_at(i, last_index)) - } - - fn top_border(&'t self) -> impl Iterator + 't { - (0..self.edge_length()).map(move |j| self.item_at(0, j)) - } - - fn bottom_border(&'t self) -> impl Iterator + 't { - let last_index = self.edge_length() - 1; - (0..self.edge_length()).map(move |j| self.item_at(last_index, j)) - } - - fn flip_horizontally(&self) -> Self { - let mut transformations = self.transformations.clone(); - transformations.push(Transformation::FlipHorizontally); - OrientedTile { - tile: self.tile, - transformations, - } - } - - fn flip_vertically(&self) -> Self { - let mut transformations = self.transformations.clone(); - transformations.push(Transformation::FlipVertically); - OrientedTile { - tile: self.tile, - transformations, - } - } - - fn rotate90(&self) -> Self { - let mut transformations = self.transformations.clone(); - transformations.push(Transformation::Rotate90); - OrientedTile { - tile: self.tile, - transformations, - } - } - - fn rotate180(&self) -> Self { - let mut transformations = self.transformations.clone(); - transformations.push(Transformation::Rotate180); - OrientedTile { - tile: self.tile, - transformations, - } - } - - fn rotate270(&self) -> Self { - let mut transformations = self.transformations.clone(); - transformations.push(Transformation::Rotate270); - OrientedTile { - tile: self.tile, - transformations, - } - } - - fn fits_to_left_of(&self, right_candidate: &OrientedTile) -> bool { - OrientedTile::edges_match(right_candidate.left_border(), self.right_border()) - } - - fn fits_above(&self, bottom_candidate: &OrientedTile) -> bool { - OrientedTile::edges_match(bottom_candidate.top_border(), self.bottom_border()) - } - - fn edges_match(mut x: impl Iterator, mut y: impl Iterator) -> bool { - while let (Some(x), Some(y)) = (x.next(), y.next()) { - if x != y { - return false; - } - } - x.next().is_none() && y.next().is_none() - } - - /// Highlights sea monsters with 'O' - /// - /// Returns: the number of sea monsters identified and a copy of the tile with the sea monsters - /// highlighted - pub fn highlight_seamonsters(&'t self) -> (usize, Tile) { - let window_height = OrientedTile::SEA_MONSTER.len(); - let window_width = OrientedTile::SEA_MONSTER[0].len(); - let vertical_windows = self.edge_length() - window_height; - let horizontal_windows = self.edge_length() - window_width; - - let mut pixels = self.pixels(); - - let mut sum = 0usize; - for i in 0..vertical_windows { - for j in 0..horizontal_windows { - if self.contains_sea_monster(&pixels, i, j) { - sum += 1; - self.highlight_seamonster(&mut pixels, i, j); - } - } - } - let tile = Tile { - id: self.tile.id, - pixels, - }; - (sum, tile) - } - - /// Paints a sea monster using '0' in the given window, overwriting any existing pixels - /// - /// Parameters: - /// - `vertical_offset` - how far "down" from the origin that the image starts - /// - `horizontal_offset` - how far "right" from the origin that the image starts - fn highlight_seamonster( - &'t self, - pixels: &mut [Vec], - vertical_offset: usize, - horizontal_offset: usize, - ) { - for i in 0..OrientedTile::SEA_MONSTER.len() { - let pattern_row = OrientedTile::SEA_MONSTER[i]; - for j in 0..pattern_row.len() { - let pattern = pattern_row[j]; - let image_row = &mut pixels[i + vertical_offset]; - if pattern == '#' { - image_row[j + horizontal_offset] = '0'; - } - } - } - } - - /// Determine whether or not the window whose origin is at the specified coördinates contains a - /// sea monster. - /// - /// Parameters: - /// - `vertical_offset` - the vertical origin of the window in question - /// - `horizontal_offset` - the horizontal origin of the window in question - /// - /// Returns: true if and only if the window contains a sea monster - fn contains_sea_monster( - &'t self, - pixels: &[Vec], - vertical_offset: usize, - horizontal_offset: usize, - ) -> bool { - for i in 0..OrientedTile::SEA_MONSTER.len() { - let pattern_row = OrientedTile::SEA_MONSTER[i]; - let image_row = &pixels[i + vertical_offset]; - for j in 0..pattern_row.len() { - let pattern = pattern_row[j]; - // spaces can be anything - if pattern == '#' && image_row[j + horizontal_offset] != '#' { - // only the '#' pixels need to match - return false; - } - } - } - true - } -} - -/// Retrieve all of the image tiles from the Mythical Information Bureau's satellite's camera array. -/// Due to a malfunction in the array, the tiles arrive in a random order and rotated or flipped -/// randomly. -pub fn get_input() -> Vec { - let mut result = vec![]; - let mut id: Id = 0; - let mut rows = vec![]; - for line in get_lines("day-20-input.txt") { - if line.starts_with("Tile") { - let mut components = line.split(' '); - components.next(); // "Tile" - id = components - .next() - .and_then(|string| string.strip_suffix(':')) - .map(|string| string.parse::().unwrap()) - .expect("Invalid tile ID"); - rows = vec![]; - } else if line.is_empty() { - result.push(Tile { - id, - pixels: rows.to_owned(), - }); - } else { - rows.push(line.chars().collect::>()); - } - } - result -} - -#[derive(Clone)] -pub struct TileArrangement<'t> { - arrangement: Vec>, - - /// The number of _tiles_ on each edge of the arrangement - edge_length: usize, -} - -impl<'t> FromIterator<&'t Tile> for TileArrangement<'t> { - fn from_iter>(iter: T) -> Self { - let tiles = iter.into_iter().collect::>(); - let edge_length = (tiles.len() as f32).sqrt() as usize; - TileArrangement { - arrangement: tiles - .iter() - .map(|tile| OrientedTile { - tile, - transformations: vec![], - }) - .collect(), - edge_length, - } - } -} - -impl<'t> TileArrangement<'t> { - pub fn top_left_corner(&self) -> Option<&OrientedTile<'t>> { - self.arrangement.get(0) - } - - pub fn top_right_corner(&self) -> Option<&OrientedTile<'t>> { - self.arrangement.get(self.edge_length - 1) - } - - pub fn bottom_left_corner(&self) -> Option<&OrientedTile<'t>> { - self.arrangement - .get(self.arrangement.len() - self.edge_length) - } - - pub fn bottom_right_corner(&self) -> Option<&OrientedTile<'t>> { - self.arrangement.last() - } - - fn tile_above(&self, index: usize) -> Option<&OrientedTile<'t>> { - if index < self.edge_length { - None - } else { - self.arrangement.get(index - self.edge_length) - } - } - - fn tile_to_left(&self, index: usize) -> Option<&OrientedTile<'t>> { - if index % self.edge_length == 0 { - None - } else { - self.arrangement.get(index - 1) - } - } - - fn fits(&self, candidate: &'t Tile) -> Option> { - let new_index = self.arrangement.len(); - let tile_above = self.tile_above(new_index); - let tile_to_left = self.tile_to_left(new_index); - for orientation in candidate.permutations() { - let top_fits = - tile_above.is_none() || tile_above.as_ref().unwrap().fits_above(&orientation); - let left_fits = tile_to_left.is_none() - || tile_to_left.as_ref().unwrap().fits_to_left_of(&orientation); - if top_fits && left_fits { - return Some(orientation); - } - } - None - } - - /// Combine a specific arrangement of tiles into one big tile - pub fn combine(&self) -> Tile { - assert_eq!( - self.arrangement.len(), - self.edge_length * self.edge_length, - "arrangement is incomplete" - ); - let mut grid: Vec> = Vec::with_capacity(self.edge_length); - for (index, tile) in self.arrangement.iter().enumerate() { - let tile_row = index / self.edge_length; - let tile_column = index % self.edge_length; - let pixels = tile.pixels(); - let row_offset = pixels.len() * tile_row; - let column_offset = pixels.len() * tile_column; - for (original_row, row) in pixels.iter().enumerate() { - let row_id = original_row + row_offset; - for (original_column, pixel) in row.iter().enumerate() { - let column_id = original_column + column_offset; - if column_id == 0 { - grid.push(Vec::with_capacity(self.edge_length)); - } - let row = &mut grid[row_id]; - row.push(*pixel); - } - } - } - Tile { - id: 0, - pixels: grid, - } - } -} - -/// Find valid permutations of tile orientations that yield an image. -/// -/// Parameters: -/// - `partial_arrangement` - A valid arrangement prefix -/// - `remaining_tiles` - All of the tiles not in `partial_arrangement` -/// - `edge_length` - the number of _tiles_ on each edge of the final arrangement -/// Returns: some permutations of some arrangements of tiles whose borders match -pub fn get_valid_arrangements<'t>( - partial_arrangement: TileArrangement<'t>, - remaining_tiles: Vec<&'t Tile>, - edge_length: usize, -) -> Vec> { - if remaining_tiles.is_empty() { - return vec![partial_arrangement]; - } else if partial_arrangement.arrangement.is_empty() { - // Find candidates for the top-left tile - for i in 0..remaining_tiles.len() { - let candidate = remaining_tiles[i]; // choose a candidate for the top left corner - for orientation in candidate.permutations() { - // choose an orientation for the candidate tile - let partial = TileArrangement { - arrangement: vec![orientation], - edge_length, - }; - let (left, _) = remaining_tiles.split_at(i); - let (_, right) = remaining_tiles.split_at(i + 1); - let mut remaining = vec![]; - remaining.extend_from_slice(left); - remaining.extend_from_slice(right); - - // get all the possible arrangements with this orientation as the first tile - let valid_arrangements = get_valid_arrangements(partial, remaining, edge_length); - if !valid_arrangements.is_empty() { - // There are more valid arrangements, but we only need one - return valid_arrangements; - } - } - } - return vec![]; - } - - // Find all possible suffixes given the partial, valid, arrangement - let mut prefixes = vec![]; - for i in 0..remaining_tiles.len() { - // choose a candidate for the next tile - let candidate = remaining_tiles[i]; - // check if it fits in some orientation - if let Some(candidate) = partial_arrangement.fits(candidate) { - let mut partial = partial_arrangement.clone(); - partial.arrangement.push(candidate); - - let (left, _) = remaining_tiles.split_at(i); - let (_, right) = remaining_tiles.split_at(i + 1); - let mut remaining = vec![]; - remaining.extend_from_slice(left); - remaining.extend_from_slice(right); - - let valid_arrangements = get_valid_arrangements(partial, remaining, edge_length); - prefixes.extend(valid_arrangements.iter().cloned()); - } - } - prefixes -} - -#[cfg(test)] -mod tests { - use crate::day20::{get_input, get_valid_arrangements, Tile, TileArrangement}; - - #[test] - fn part1() { - let tiles = get_input(); - let refs = tiles.iter().collect(); - let edge_length = (tiles.len() as f32).sqrt() as usize; - let empty = TileArrangement { - arrangement: vec![], - edge_length, - }; - let possible_arrangements = get_valid_arrangements(empty, refs, edge_length); - assert!(!possible_arrangements.is_empty()); - let arrangement = possible_arrangements.get(0).unwrap(); - let result: u64 = vec![ - arrangement.top_left_corner().unwrap(), - arrangement.top_right_corner().unwrap(), - arrangement.bottom_left_corner().unwrap(), - arrangement.bottom_right_corner().unwrap(), - ] - .iter() - .map(|corner| corner.id()) - .product(); - println!("Part 1: {}", result); - } - - #[test] - fn part2() { - let tiles = get_input(); - let refs = tiles.iter().collect(); - let edge_length = (tiles.len() as f32).sqrt() as usize; - let empty = TileArrangement { - arrangement: vec![], - edge_length, - }; - let possible_arrangements = get_valid_arrangements(empty, refs, edge_length); - assert!(!possible_arrangements.is_empty()); - let arrangement = &possible_arrangements[0]; - - let cropped = arrangement - .arrangement - .iter() - .map(|oriented| oriented.tile()) - .map(|tile| tile.crop_borders()) - .collect::>(); - let cropped = cropped.iter().collect::(); - let combined = cropped.combine(); - for permutation in combined.permutations() { - let (num_sea_monsters, highlighted) = permutation.highlight_seamonsters(); - if num_sea_monsters > 0 { - println!("Part 2: {}", highlighted.roughness()); - return; - } - } - unreachable!("None of the permutations had sea monsters") - } -} diff --git a/src/day21.rs b/src/day21.rs deleted file mode 100644 index d5e69be..0000000 --- a/src/day21.rs +++ /dev/null @@ -1,194 +0,0 @@ -// --- Day 21: Allergen Assessment --- -// https://adventofcode.com/2020/day/21 - -use crate::get_lines; -use std::collections::{BTreeSet, HashMap, HashSet}; - -/// A substance used in a food. It may contain 0 or 1 _Allergen_. -pub type Ingredient = String; - -/// A substance that may be harmful to some individuals. It is found in exactly one _Ingredient_. -pub type Allergen = String; - -/// A food item you are considering taking on your journey -#[derive(Eq, PartialEq, Hash)] -pub struct Food { - /// A comprehensive list of the ingredients used in this food, listed in a language you do not - /// understand. - ingredient_ids: Vec, - - /// Some or all of the allergens contained in this food, listed in a language you understand. - /// Some allergens may be omitted. - allergen_ids: Vec, -} - -/// Read the puzzle input -/// -/// Returns: -/// - the unique set of ingredients that can appear in any food -/// - all of the potential food items -pub fn get_input() -> (Vec, Vec, HashSet) { - let mut all_ingredients = BTreeSet::new(); - let mut all_allergens = BTreeSet::new(); - let mut foods = HashSet::new(); - - for line in get_lines("day-21-input.txt") { - let mut split = line.split(" (contains "); - let ingredient_list = split.next().expect("Missing ingredients"); - let allergen_list = split.next().expect("Missing allergens"); - if split.next().is_some() { - panic!("More components found"); - } - let mut ingredients = Vec::new(); - for ingredient in ingredient_list.split(' ').map(String::from) { - ingredients.push(ingredient.clone()); - all_ingredients.insert(ingredient); - } - let mut allergens = Vec::new(); - for allergen in allergen_list.replace(')', "").split(", ").map(String::from) { - allergens.push(allergen.clone()); - all_allergens.insert(allergen.clone()); - } - let food = (ingredients, allergens); - foods.insert(food); - } - let mut ingredient_map = HashMap::new(); - let mut allergen_map = HashMap::new(); - let mut ingredients = Vec::with_capacity(all_ingredients.len()); - let mut allergens = Vec::with_capacity(all_allergens.len()); - for (index, ingredient) in all_ingredients.iter().enumerate() { - ingredients.push(ingredient.clone()); // a "drain" operation would be ideal - ingredient_map.insert(ingredient, index); - } - for (index, allergen) in all_allergens.iter().enumerate() { - allergens.push(allergen.clone()); // a "drain" operation would be ideal - allergen_map.insert(allergen, index); - } - let foods = foods - .iter() - .map(|(ingredients, allergens)| -> Food { - Food { - ingredient_ids: ingredients - .iter() - .map(|ingredient| ingredient_map[ingredient]) - .collect(), - allergen_ids: allergens - .iter() - .map(|allergen| allergen_map[allergen]) - .collect(), - } - }) - .collect(); - - (ingredients, allergens, foods) -} - -#[cfg(test)] -mod tests { - use crate::day21::{get_input, Food, Ingredient}; - use std::collections::{BTreeMap, HashMap, HashSet}; - - #[test] - fn part1() { - let (ingredients, allergens, foods) = get_input(); - let mut allergen_to_food = (0..allergens.len()) - .map(|_| HashSet::new()) - .collect::>>(); - for food in &foods { - for allergen_id in &food.allergen_ids { - allergen_to_food[*allergen_id].insert(food); - } - } - // "determine which ingredients can't possibly contain any of the allergens in any food in your list" - let mut inert_ingredient_ids = (0..ingredients.len()).collect::>(); - - for foods_that_contain_allergen in allergen_to_food { - let mut ingredients_that_may_contain_allergen = - (0..ingredients.len()).collect::>(); - - for food in foods_that_contain_allergen { - ingredients_that_may_contain_allergen - .retain(|ingredient_id| food.ingredient_ids.contains(ingredient_id)); - } - inert_ingredient_ids.retain(|ingredient_id| { - !ingredients_that_may_contain_allergen.contains(ingredient_id) - }); - } - - // "How many times do any of those ingredients appear?" - let mut sum = 0usize; - for food in foods { - for ingredient_id in &inert_ingredient_ids { - if food.ingredient_ids.contains(ingredient_id) { - sum += 1; - } - } - } - println!("Part 1: {}", sum); - } - - #[test] - fn part2() { - let (ingredients, allergens, foods) = get_input(); - let mut allergen_to_food = (0..allergens.len()) - .map(|_| HashSet::new()) - .collect::>>(); - for food in &foods { - for allergen_id in &food.allergen_ids { - allergen_to_food[*allergen_id].insert(food); - } - } - // "determine which ingredients can't possibly contain any of the allergens in any food in your list" - let mut dangerous_ingredients = HashSet::new(); - let mut allergen_to_ingredient = (0..allergens.len()) - .map(|_| HashSet::new()) - .collect::>>(); - for (allergen_id, foods) in allergen_to_food.iter().enumerate() { - let mut ingredients_that_may_contain_allergen = - (0..ingredients.len()).collect::>(); - for food in foods { - ingredients_that_may_contain_allergen - .retain(|ingredient_id| food.ingredient_ids.contains(ingredient_id)); - } - for dangerous_ingredient in ingredients_that_may_contain_allergen.clone() { - dangerous_ingredients.insert(dangerous_ingredient); - } - allergen_to_ingredient[allergen_id] = ingredients_that_may_contain_allergen; - } - - let mut ingredient_to_allergen = HashMap::new(); - while !dangerous_ingredients.is_empty() { - let mut mapped_ingredients = HashSet::new(); - for dangerous_ingredient in dangerous_ingredients.clone() { - let mut mapped_allergen = None; - for (allergen_id, ingredients) in allergen_to_ingredient.iter().enumerate() { - if ingredients.len() == 1 && ingredients.contains(&dangerous_ingredient) { - // this is the only ingredient known to contain this allergen - ingredient_to_allergen.insert(dangerous_ingredient, allergen_id); - mapped_allergen = Some(allergen_id); - break; - } - } - if let Some(allergen_to_remove) = mapped_allergen { - allergen_to_ingredient[allergen_to_remove] = HashSet::with_capacity(0); - allergen_to_ingredient.iter_mut().for_each(|ingredients| { - ingredients.remove(&dangerous_ingredient); - }); - mapped_ingredients.insert(dangerous_ingredient); - } - } - for item in mapped_ingredients { - dangerous_ingredients.remove(&item); - } - } - let result = ingredient_to_allergen - .iter() - .map(|(ingredient_id, allergen_id)| (*allergen_id, &ingredients[*ingredient_id])) - .collect::>() - .iter() - .map(|(_, ingredient)| String::from(*ingredient)) - .collect::>() - .join(","); - println!("Part 2: {}", result); - } -} diff --git a/src/day22.rs b/src/day22.rs deleted file mode 100644 index aa9da7d..0000000 --- a/src/day22.rs +++ /dev/null @@ -1,192 +0,0 @@ -// --- Day 22: Crab Combat --- -// https://adventofcode.com/2020/day/22 - -use std::collections::HashSet; - -use crate::get_lines; - -/// A space card -type Card = u8; - -/// A small deck of space cards -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct Deck { - cards: Vec, // TODO try linked list -} - -impl Deck { - /// Remove the first card so it can be played in a Round - pub fn draw_top(&mut self) -> Card { - let result = self.cards[0]; - self.cards.remove(0); - result - } - - /// Call if the controlling player won a round - /// Parameters: - /// - `winning_card` - the first card to insert (will become the penultimate card in the deck) - /// - `losing_card` - the second card to insert (will become the bottom card in the deck) - pub fn insert(&mut self, winning_card: Card, losing_card: Card) { - self.cards.push(winning_card); - self.cards.push(losing_card); - } - - /// Calculate the current score of the deck - pub fn score(&self) -> usize { - let mut multiplier = self.cards.len(); - let mut score = 0usize; - for card in &self.cards { - score += multiplier * (*card as usize); - multiplier -= 1; - } - score - } - - pub fn is_empty(&self) -> bool { - self.cards.is_empty() - } - - /// Create a new deck based on the first _count_ space cards - pub fn clone_first(&self, count: usize) -> Deck { - let (first, _) = self.cards.split_at(count); - Deck { - cards: first.to_vec(), - } - } -} - -/// One of two players in a game of _Combat_ -#[derive(Eq)] -pub struct Player { - id: u8, - deck: Deck, -} - -impl Player { - pub fn draw_top(&mut self) -> Card { - self.deck.draw_top() - } - - pub fn insert(&mut self, winning_card: Card, losing_card: Card) { - self.deck.insert(winning_card, losing_card) - } - - pub fn has_cards(&self) -> bool { - !self.deck.is_empty() - } - - pub fn has_at_least(&self, card_count: u8) -> bool { - self.deck.cards.len() >= card_count as usize - } - - pub fn clone_first(&self, count: usize) -> Player { - Player { - id: self.id, - deck: self.deck.clone_first(count), - } - } - - pub fn score(&self) -> usize { - self.deck.score() - } -} - -impl PartialEq for Player { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -pub fn get_input() -> (Player, Player) { - let mut player_1 = Deck { cards: vec![] }; - let mut player_2 = Deck { cards: vec![] }; - let mut target = &mut player_1; - for line in get_lines("day-22-input.txt") { - if line.eq("Player 1:") { - target = &mut player_1; - } else if line.eq("Player 2:") { - target = &mut player_2; - } else if line.is_empty() { - continue; - } else { - target.cards.push(line.parse::().unwrap()) - } - } - ( - Player { - id: 1, - deck: player_1, - }, - Player { - id: 2, - deck: player_2, - }, - ) -} - -/// Play a game of _Recursive Combat_ -pub fn play(mut player1: Player, mut player2: Player) -> Player { - let mut previous_rounds: HashSet<(Deck, Deck)> = HashSet::new(); - while player1.has_cards() && player2.has_cards() { - if !previous_rounds.insert((player1.deck.clone(), player2.deck.clone())) { - return player1; - } - let x = player1.draw_top(); - let y = player2.draw_top(); - if player1.has_at_least(x) && player2.has_at_least(y) { - let winner = play( - player1.clone_first(x as usize), - player2.clone_first(y as usize), - ); - if winner == player1 { - player1.insert(x, y); - } else { - player2.insert(y, x); - } - } else if x > y { - player1.insert(x, y); - } else { - player2.insert(y, x); - } - } - - if player1.has_cards() { - player1 - } else { - player2 - } -} - -#[cfg(test)] -mod tests { - use crate::day22::{get_input, play}; - - #[test] - fn part1() { - let (mut player_1, mut player_2) = get_input(); - while player_1.has_cards() && player_2.has_cards() { - let x = player_1.draw_top(); - let y = player_2.draw_top(); - assert_ne!(x, y); - if x > y { - player_1.insert(x, y); - } else { - player_2.insert(y, x); - } - } - let winner = if player_1.has_cards() { - player_1 - } else { - player_2 - }; - let score = winner.score(); - println!("Part 1: {}", score); - } - - #[test] - fn part2() { - let (player_1, player_2) = get_input(); - let winner = play(player_1, player_2); - println!("Part 2: {}", winner.score()); - } -} diff --git a/src/day23.rs b/src/day23.rs deleted file mode 100644 index 4cc7ed6..0000000 --- a/src/day23.rs +++ /dev/null @@ -1,111 +0,0 @@ -// --- Day 23: Crab Cups --- -// https://adventofcode.com/2020/day/22 - -use crate::get_lines; - -type Cup = u8; - -pub struct Game { - circle: Vec, - lowest_label: Cup, - highest_label: Cup, - current_index: usize, -} - -impl Game { - fn take(&mut self) -> Cup { - let mut index = self.current_index + 1; - if index >= self.circle.len() { - index = 0; - } - let result = self.circle.remove(index); - if self.current_index >= self.circle.len() { - self.current_index = self.circle.len() - 1; - } - result - } - - fn get_index(&self, label: Cup) -> usize { - for (index, cup) in self.circle.iter().enumerate() { - if *cup == label { - return index; - } - } - panic!("Cannot find label: {}", label) - } - - pub fn perform_move(&mut self) { - let first = self.take(); - let second = self.take(); - let third = self.take(); - let current_cup = self.circle[self.current_index]; - let mut destination_label = current_cup - 1; - if destination_label < self.lowest_label { - destination_label = self.highest_label; - } - while destination_label == first - || destination_label == second - || destination_label == third - { - destination_label -= 1; - if destination_label < self.lowest_label { - destination_label = self.highest_label; - } - } - let mut destination = self.get_index(destination_label) + 1; - if destination >= self.circle.len() { - destination = 0; - } - self.circle.insert(destination, third); - self.circle.insert(destination, second); - self.circle.insert(destination, first); - let mut index = self.get_index(current_cup) + 1; - if index >= self.circle.len() { - index = 0; - } - self.current_index = index; - } - - pub fn get_cup_order(&self) -> Vec { - let start = self.get_index(1); - (1..self.circle.len()) - .map(|i| (i + start) % self.circle.len()) - .map(|index| self.circle[index]) - .collect() - } -} - -pub fn get_input() -> Vec { - let mut lines = get_lines("day-23-input.txt"); - let line = lines.next().unwrap(); - line.chars() - .map(|c| c.to_digit(10).unwrap() as Cup) - .collect() -} - -#[cfg(test)] -mod tests { - use crate::day23::{get_input, Game}; - - #[test] - fn part1() { - let cups = get_input(); - let mut min = u8::MAX; - let mut max = u8::MIN; - for cup in &cups { - min = min.min(*cup); - max = max.max(*cup); - } - let mut game = Game { - circle: cups, - lowest_label: min, - highest_label: max, - current_index: 0, - }; - for _ in 1..=100 { - game.perform_move(); - } - let order = game.get_cup_order(); - println!("Part 1: {:?}", order); - } -} diff --git a/src/lib.rs b/src/lib.rs index a935922..6d19d31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#[macro_use] +extern crate core; extern crate lazy_static; pub mod day01; @@ -14,16 +14,6 @@ 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; use crate::BufReadResult::{BufferingError, EndOfBlock, EndOfInput, PartialBlock}; use serde_derive::Deserialize; @@ -138,7 +128,7 @@ impl Iterator for Blocks { match &self.try_read(previous_byte) { EndOfInput => { if !bytes.is_empty() { - result = Some(String::from_utf8_lossy(&bytes).trim().to_string()); + result = Some(String::from_utf8_lossy(&bytes).to_string()); } complete = true; } @@ -148,7 +138,7 @@ impl Iterator for Blocks { } EndOfBlock(partial) => { bytes = [&bytes, *partial].concat(); - result = Some(String::from_utf8_lossy(&bytes).trim().to_string()); + result = Some(String::from_utf8_lossy(&bytes).to_string()); bytes_read = partial.len(); complete = true; }