aboutsummaryrefslogtreecommitdiff
path: root/aoc2023-gleam
diff options
context:
space:
mode:
Diffstat (limited to 'aoc2023-gleam')
-rw-r--r--aoc2023-gleam/.DS_Storebin0 -> 6148 bytes
-rw-r--r--aoc2023-gleam/.github/workflows/test.yml23
-rw-r--r--aoc2023-gleam/.gitignore6
-rw-r--r--aoc2023-gleam/README.md28
-rw-r--r--aoc2023-gleam/gleam.toml22
-rw-r--r--aoc2023-gleam/manifest.toml29
-rw-r--r--aoc2023-gleam/src/.gitignore1
-rw-r--r--aoc2023-gleam/src/day1/.gitignore1
-rw-r--r--aoc2023-gleam/src/day1/solve.gleam57
-rw-r--r--aoc2023-gleam/src/day10/.gitignore1
-rw-r--r--aoc2023-gleam/src/day10/solve.gleam177
-rw-r--r--aoc2023-gleam/src/day11/.gitignore1
-rw-r--r--aoc2023-gleam/src/day11/solve.gleam84
-rw-r--r--aoc2023-gleam/src/day12/.gitignore1
-rw-r--r--aoc2023-gleam/src/day12/solve.gleam90
-rw-r--r--aoc2023-gleam/src/day13/.gitignore1
-rw-r--r--aoc2023-gleam/src/day13/solve.gleam87
-rw-r--r--aoc2023-gleam/src/day14/.gitignore1
-rw-r--r--aoc2023-gleam/src/day14/solve.gleam94
-rw-r--r--aoc2023-gleam/src/day15/.gitignore1
-rw-r--r--aoc2023-gleam/src/day15/solve.gleam104
-rw-r--r--aoc2023-gleam/src/day16/.gitignore1
-rw-r--r--aoc2023-gleam/src/day16/solve.gleam119
-rw-r--r--aoc2023-gleam/src/day17/.gitignore1
-rw-r--r--aoc2023-gleam/src/day17/solve.gleam143
-rw-r--r--aoc2023-gleam/src/day18/.gitignore1
-rw-r--r--aoc2023-gleam/src/day18/solve.gleam113
-rw-r--r--aoc2023-gleam/src/day19/.gitignore1
-rw-r--r--aoc2023-gleam/src/day19/solve.gleam255
-rw-r--r--aoc2023-gleam/src/day2/.gitignore1
-rw-r--r--aoc2023-gleam/src/day2/solve.gleam66
-rw-r--r--aoc2023-gleam/src/day20/.gitignore1
-rw-r--r--aoc2023-gleam/src/day20/solve.gleam251
-rw-r--r--aoc2023-gleam/src/day21/.gitignore1
-rw-r--r--aoc2023-gleam/src/day21/solve.gleam25
-rw-r--r--aoc2023-gleam/src/day22/.gitignore1
-rw-r--r--aoc2023-gleam/src/day22/solve.gleam199
-rw-r--r--aoc2023-gleam/src/day23/.gitignore1
-rw-r--r--aoc2023-gleam/src/day23/solve.gleam194
-rw-r--r--aoc2023-gleam/src/day3/.gitignore1
-rw-r--r--aoc2023-gleam/src/day3/solve.gleam180
-rw-r--r--aoc2023-gleam/src/day4/.gitignore1
-rw-r--r--aoc2023-gleam/src/day4/solve.gleam98
-rw-r--r--aoc2023-gleam/src/day5/.gitignore1
-rw-r--r--aoc2023-gleam/src/day5/solve.gleam162
-rw-r--r--aoc2023-gleam/src/day6/.gitignore1
-rw-r--r--aoc2023-gleam/src/day6/solve.gleam85
-rw-r--r--aoc2023-gleam/src/day7/.gitignore1
-rw-r--r--aoc2023-gleam/src/day7/solve.gleam140
-rw-r--r--aoc2023-gleam/src/day8/.gitignore1
-rw-r--r--aoc2023-gleam/src/day8/solve.gleam91
-rw-r--r--aoc2023-gleam/src/day9/.gitignore1
-rw-r--r--aoc2023-gleam/src/day9/solve.gleam70
-rw-r--r--aoc2023-gleam/src/utilities/array2d.gleam74
-rw-r--r--aoc2023-gleam/src/utilities/memo.gleam57
-rw-r--r--aoc2023-gleam/src/utilities/prioqueue.gleam64
-rw-r--r--aoc2023-gleam/test/aoc2023_test.gleam5
-rw-r--r--aoc2023-gleam/test/day1/day1_test.gleam57
-rw-r--r--aoc2023-gleam/test/day10/day10_test.gleam60
-rw-r--r--aoc2023-gleam/test/day11/day11_test.gleam66
-rw-r--r--aoc2023-gleam/test/day12/day12_test.gleam48
-rw-r--r--aoc2023-gleam/test/day13/day13_test.gleam76
-rw-r--r--aoc2023-gleam/test/day14/day14_test.gleam66
-rw-r--r--aoc2023-gleam/test/day15/day15_test.gleam42
-rw-r--r--aoc2023-gleam/test/day16/day16_test.gleam66
-rw-r--r--aoc2023-gleam/test/day17/day17_test.gleam54
-rw-r--r--aoc2023-gleam/test/day18/day18_test.gleam74
-rw-r--r--aoc2023-gleam/test/day19/day19_test.gleam80
-rw-r--r--aoc2023-gleam/test/day2/day2_test.gleam57
-rw-r--r--aoc2023-gleam/test/day20/day20_test.gleam55
-rw-r--r--aoc2023-gleam/test/day21/day21_test.gleam38
-rw-r--r--aoc2023-gleam/test/day22/day22_test.gleam60
-rw-r--r--aoc2023-gleam/test/day23/day23_test.gleam92
-rw-r--r--aoc2023-gleam/test/day3/day3_test.gleam66
-rw-r--r--aoc2023-gleam/test/day4/day4_test.gleam58
-rw-r--r--aoc2023-gleam/test/day5/day5_test.gleam112
-rw-r--r--aoc2023-gleam/test/day6/day6_test.gleam50
-rw-r--r--aoc2023-gleam/test/day7/day7_test.gleam56
-rw-r--r--aoc2023-gleam/test/day8/day8_test.gleam61
-rw-r--r--aoc2023-gleam/test/day9/day9_test.gleam52
80 files changed, 4662 insertions, 0 deletions
diff --git a/aoc2023-gleam/.DS_Store b/aoc2023-gleam/.DS_Store
new file mode 100644
index 0000000..5172429
--- /dev/null
+++ b/aoc2023-gleam/.DS_Store
Binary files differ
diff --git a/aoc2023-gleam/.github/workflows/test.yml b/aoc2023-gleam/.github/workflows/test.yml
new file mode 100644
index 0000000..cf2096e
--- /dev/null
+++ b/aoc2023-gleam/.github/workflows/test.yml
@@ -0,0 +1,23 @@
+name: test
+
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: erlef/setup-beam@v1
+ with:
+ otp-version: "26.0.2"
+ gleam-version: "0.32.4"
+ rebar3-version: "3"
+ # elixir-version: "1.15.4"
+ - run: gleam deps download
+ - run: gleam test
+ - run: gleam format --check src test
diff --git a/aoc2023-gleam/.gitignore b/aoc2023-gleam/.gitignore
new file mode 100644
index 0000000..8248306
--- /dev/null
+++ b/aoc2023-gleam/.gitignore
@@ -0,0 +1,6 @@
+*.beam
+*.ez
+build
+erl_crash.dump
+
+aoc.toml \ No newline at end of file
diff --git a/aoc2023-gleam/README.md b/aoc2023-gleam/README.md
new file mode 100644
index 0000000..3f534e8
--- /dev/null
+++ b/aoc2023-gleam/README.md
@@ -0,0 +1,28 @@
+# aoc2023
+
+[![Package Version](https://img.shields.io/hexpm/v/aoc2023)](https://hex.pm/packages/aoc2023)
+[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/aoc2023/)
+
+## Quick start
+
+```sh
+gleam run # Run the project
+gleam test # Run the tests
+gleam shell # Run an Erlang shell
+```
+
+## Installation
+
+If available on Hex this package can be added to your Gleam project:
+
+```sh
+gleam add aoc2023
+```
+
+and its documentation can be found at <https://hexdocs.pm/aoc2023>.
+
+## Use
+
+* Set up a solution: `gleam run -m adglent/day <n>`
+* Check against examples: `gleam test -- --modules=day<n>/day<n>_test`
+* Get final answer: `gleam run -m day<n>/solve <p>` \ No newline at end of file
diff --git a/aoc2023-gleam/gleam.toml b/aoc2023-gleam/gleam.toml
new file mode 100644
index 0000000..8190aef
--- /dev/null
+++ b/aoc2023-gleam/gleam.toml
@@ -0,0 +1,22 @@
+name = "aoc2023"
+version = "0.1.0"
+gleam = ">= 0.33.0"
+
+# Fill out these fields if you intend to generate HTML documentation or publish
+# your project to the Hex package manager.
+#
+# description = ""
+# licences = ["Apache-2.0"]
+# repository = { type = "github", user = "username", repo = "project" }
+# links = [{ title = "Website", href = "https://gleam.run" }]
+
+[dependencies]
+gleam_stdlib = "~> 0.33"
+simplifile = "~> 1.0"
+gleam_erlang = "~> 0.23"
+gleam_community_maths = "~> 1.0"
+gleam_otp = "~> 0.8"
+pqueue = "~> 2.0"
+
+[dev-dependencies]
+adglent = "~> 1.2"
diff --git a/aoc2023-gleam/manifest.toml b/aoc2023-gleam/manifest.toml
new file mode 100644
index 0000000..416a155
--- /dev/null
+++ b/aoc2023-gleam/manifest.toml
@@ -0,0 +1,29 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "adglent", version = "1.2.0", build_tools = ["gleam"], requirements = ["gap", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_otp", "gleam_stdlib", "glint", "simplifile", "snag", "tom"], otp_app = "adglent", source = "hex", outer_checksum = "A20D35001061F8AD602E3B92FB3AC0E1E4EEC642AD2AAE0ACEAD3A85F37DA7F0" },
+ { name = "gap", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib"], otp_app = "gap", source = "hex", outer_checksum = "2EE1B0A17E85CF73A0C1D29DA315A2699117A8F549C8E8D89FA8261BE41EDEB1" },
+ { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
+ { name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
+ { name = "gleam_community_maths", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_maths", source = "hex", outer_checksum = "E30C61A75051DAF7CFD77C4FBAA04140FDA0B5D831955E7A74521E5576E2780D" },
+ { name = "gleam_erlang", version = "0.23.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "C21CFB816C114784E669FFF4BBF433535EEA9960FA2F216209B8691E87156B96" },
+ { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
+ { name = "gleam_httpc", version = "2.1.2", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "ACD05CA3BAC7780DF5FFAE334621FD199D1B490FAF6ECDFF74316CAA61CE88E6" },
+ { name = "gleam_otp", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "18EF8242A5E54BA92F717C7222F03B3228AEE00D1F286D4C56C3E8C18AA2588E" },
+ { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
+ { name = "glint", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "46E56049CD370D61F720D319D0AB970408C9336EEB918F08B5DCB1DCE9845FA3" },
+ { name = "pqueue", version = "2.0.7", build_tools = ["rebar3"], requirements = [], otp_app = "pqueue", source = "hex", outer_checksum = "8B0204BB202335890E4E7F9B99A8EC0B84DDB8513EE298EB180EE9B3BCB4C859" },
+ { name = "simplifile", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "EB9AA8E65E5C1E3E0FDCFC81BC363FD433CB122D7D062750FFDF24DE4AC40116" },
+ { name = "snag", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "8FD70D8FB3728E08AC425283BB509BB0F012BE1AE218424A597CDE001B0EE589" },
+ { name = "tom", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "5C5A9B8586C547F1F39542B1A3BBD9FEE17AFEAB51CE53B32B13D0D46B421249" },
+]
+
+[requirements]
+adglent = { version = "~> 1.2" }
+gleam_community_maths = { version = "~> 1.0" }
+gleam_erlang = { version = "~> 0.23" }
+gleam_otp = { version = "~> 0.8" }
+gleam_stdlib = { version = "~> 0.33" }
+pqueue = { version = "~> 2.0" }
+simplifile = { version = "~> 1.0" }
diff --git a/aoc2023-gleam/src/.gitignore b/aoc2023-gleam/src/.gitignore
new file mode 100644
index 0000000..bc13a69
--- /dev/null
+++ b/aoc2023-gleam/src/.gitignore
@@ -0,0 +1 @@
+aoc2023.gleam \ No newline at end of file
diff --git a/aoc2023-gleam/src/day1/.gitignore b/aoc2023-gleam/src/day1/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day1/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day1/solve.gleam b/aoc2023-gleam/src/day1/solve.gleam
new file mode 100644
index 0000000..37a19d2
--- /dev/null
+++ b/aoc2023-gleam/src/day1/solve.gleam
@@ -0,0 +1,57 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/list
+import gleam/string
+import gleam/regex.{type Match, Match}
+import gleam/int
+
+pub fn part1(input: String) {
+ let assert Ok(re) = regex.from_string("[1-9]")
+
+ input
+ |> string.split("\n")
+ |> list.fold(0, fn(acc, s) {
+ let matches = regex.scan(s, with: re)
+
+ let assert Ok(Match(content: first, ..)) = list.first(matches)
+ let assert Ok(Match(content: last, ..)) = list.last(matches)
+ let assert Ok(i) = int.parse(first <> last)
+ acc + i
+ })
+ |> string.inspect
+}
+
+const substitutions = [
+ #("one", "o1e"),
+ #("two", "t2o"),
+ #("three", "t3e"),
+ #("four", "4"),
+ #("five", "5e"),
+ #("six", "6"),
+ #("seven", "7n"),
+ #("eight", "e8t"),
+ #("nine", "n9e"),
+]
+
+pub fn part2(input: String) {
+ list.fold(over: substitutions, from: input, with: fn(acc, sub) {
+ let #(from, to) = sub
+ string.replace(in: acc, each: from, with: to)
+ })
+ |> part1
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("1")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day10/.gitignore b/aoc2023-gleam/src/day10/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day10/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day10/solve.gleam b/aoc2023-gleam/src/day10/solve.gleam
new file mode 100644
index 0000000..c33634d
--- /dev/null
+++ b/aoc2023-gleam/src/day10/solve.gleam
@@ -0,0 +1,177 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/list
+import gleam/int
+import gleam/io
+import gleam/set.{type Set}
+import gleam/string
+
+type Posn {
+ Posn(row: Int, col: Int)
+}
+
+fn add_posns(p1: Posn, p2: Posn) -> Posn {
+ Posn(p1.row + p2.row, p1.col + p2.col)
+}
+
+type PipeGrid =
+ Dict(Posn, String)
+
+const north = Posn(-1, 0)
+
+const south = Posn(1, 0)
+
+const east = Posn(0, 1)
+
+const west = Posn(0, -1)
+
+const initial_directions = [
+ #(north, ["|", "7", "F"]),
+ #(south, ["|", "J", "L"]),
+ #(east, ["-", "J", "7"]),
+ #(west, ["-", "F", "L"]),
+]
+
+fn pipe_neighbors(pipe: String) -> List(Posn) {
+ case pipe {
+ "|" -> [north, south]
+ "-" -> [east, west]
+ "L" -> [north, east]
+ "F" -> [south, east]
+ "7" -> [south, west]
+ "J" -> [north, west]
+ _ -> panic as "bad pipe"
+ }
+}
+
+fn make_grid(input: String) -> PipeGrid {
+ {
+ use r, row <- list.index_map(string.split(input, "\n"))
+ use c, col <- list.index_map(string.to_graphemes(row))
+ #(Posn(r, c), col)
+ }
+ |> list.flatten
+ |> dict.from_list
+}
+
+fn valid_start_direction(grid: PipeGrid, s: Posn) {
+ let assert [dir, ..] = {
+ use d <- list.filter_map(initial_directions)
+ let #(delta, valids) = d
+ let neighbor = add_posns(s, delta)
+ case dict.get(grid, neighbor) {
+ Ok(pipe) ->
+ case list.contains(valids, pipe) {
+ True -> Ok(neighbor)
+ False -> Error(Nil)
+ }
+ Error(_) -> Error(Nil)
+ }
+ }
+ dir
+}
+
+fn to_next_pipe(current: Posn, grid: PipeGrid, acc: List(Posn)) {
+ let assert [prev, ..] = acc
+ let assert Ok(pipe) = dict.get(grid, current)
+ use <- bool.guard(pipe == "S", [current, ..acc])
+ let assert [next] = {
+ pipe
+ |> pipe_neighbors
+ |> list.filter_map(fn(p) {
+ case add_posns(p, current) {
+ neighbor if neighbor == prev -> Error(Nil)
+ neighbor -> Ok(neighbor)
+ }
+ })
+ }
+ to_next_pipe(next, grid, [current, ..acc])
+}
+
+pub fn part1(input: String) {
+ let grid =
+ input
+ |> make_grid
+
+ let assert Ok(s) =
+ grid
+ |> dict.filter(fn(_, v) { v == "S" })
+ |> dict.keys
+ |> list.first
+
+ grid
+ |> valid_start_direction(s)
+ |> to_next_pipe(grid, [s])
+ |> list.length
+ |> fn(i) { { { i - 1 } / 2 } }
+ |> string.inspect
+}
+
+fn trace_ray(p: Posn, loop: Set(Posn), grid: PipeGrid) -> Bool {
+ use <- bool.guard(set.contains(loop, p), False)
+ int.is_odd(count_crossings(p, loop, grid, 0, ""))
+}
+
+fn count_crossings(
+ p: Posn,
+ loop: Set(Posn),
+ grid: PipeGrid,
+ acc: Int,
+ corner: String,
+) {
+ let maybe_cell = dict.get(grid, p)
+ use <- bool.guard(maybe_cell == Error(Nil), acc)
+ let assert Ok(cell) = maybe_cell
+ let next = add_posns(p, east)
+ case set.contains(loop, p) {
+ False -> count_crossings(next, loop, grid, acc, corner)
+ True ->
+ case corner, cell {
+ _, "|" -> count_crossings(next, loop, grid, acc + 1, corner)
+ _, "F" | _, "L" -> count_crossings(next, loop, grid, acc, cell)
+ "F", "J" | "L", "7" -> count_crossings(next, loop, grid, acc + 1, "")
+ "F", "7" | "L", "J" -> count_crossings(next, loop, grid, acc, "")
+ _, _ -> count_crossings(next, loop, grid, acc, corner)
+ }
+ }
+}
+
+pub fn part2(input: String) {
+ let grid =
+ input
+ |> make_grid
+
+ let assert Ok(s) =
+ grid
+ |> dict.filter(fn(_, v) { v == "S" })
+ |> dict.keys
+ |> list.first
+
+ let loop_pipes =
+ grid
+ |> valid_start_direction(s)
+ |> to_next_pipe(grid, [s])
+ |> set.from_list
+
+ grid
+ |> dict.keys
+ |> list.filter(trace_ray(_, loop_pipes, grid))
+ |> list.length()
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("10")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day11/.gitignore b/aoc2023-gleam/src/day11/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day11/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day11/solve.gleam b/aoc2023-gleam/src/day11/solve.gleam
new file mode 100644
index 0000000..35464a1
--- /dev/null
+++ b/aoc2023-gleam/src/day11/solve.gleam
@@ -0,0 +1,84 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/int
+import gleam/string
+import gleam/list
+
+type Posn {
+ Posn(x: Int, y: Int)
+}
+
+fn find_empty(grid: List(List(String))) {
+ use acc, row, r <- list.index_fold(grid, [])
+ case list.unique(row) {
+ ["."] -> [r, ..acc]
+ _ -> acc
+ }
+}
+
+fn count_prior_empty_ranks(rank: Int, empty_ranks: List(Int)) -> Int {
+ empty_ranks
+ |> list.drop_while(fn(r_empty) { r_empty > rank })
+ |> list.length
+}
+
+fn parse_with_expansion(input: String, expansion: Int) -> List(Posn) {
+ let add = expansion - 1
+ let grid =
+ input
+ |> string.split("\n")
+ |> list.map(string.to_graphemes)
+
+ let empty_row_list = find_empty(grid)
+ let empty_col_list = find_empty(list.transpose(grid))
+
+ {
+ use r, row <- list.index_map(grid)
+ use acc, cell, c <- list.index_fold(over: row, from: [])
+
+ let p = Posn(r, c)
+ let empty_r = count_prior_empty_ranks(r, empty_row_list)
+ let empty_c = count_prior_empty_ranks(c, empty_col_list)
+ case cell {
+ "#" -> [Posn(p.x + empty_r * add, p.y + empty_c * add), ..acc]
+ _empty -> acc
+ }
+ }
+ |> list.flatten()
+}
+
+fn all_distances(stars: List(Posn)) -> Int {
+ use acc, pair <- list.fold(list.combination_pairs(stars), 0)
+ let #(s1, s2) = pair
+ acc + int.absolute_value(s1.x - s2.x) + int.absolute_value(s1.y - s2.y)
+}
+
+fn find_distances(input: String, expand_by: Int) -> String {
+ input
+ |> parse_with_expansion(expand_by)
+ |> all_distances
+ |> string.inspect
+}
+
+pub fn part1(input: String) {
+ find_distances(input, 2)
+}
+
+pub fn part2(input: String) {
+ find_distances(input, 1_000_000)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("11")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day12/.gitignore b/aoc2023-gleam/src/day12/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day12/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day12/solve.gleam b/aoc2023-gleam/src/day12/solve.gleam
new file mode 100644
index 0000000..893b83c
--- /dev/null
+++ b/aoc2023-gleam/src/day12/solve.gleam
@@ -0,0 +1,90 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/string
+import gleam/list
+import gleam/int
+import gleam/result
+import utilities/memo.{type Cache}
+
+type ParserState =
+ #(String, List(Int), Int, Bool)
+
+fn parse_folds(input: String, folds: Int) {
+ let records = string.split(input, "\n")
+ use record <- list.map(records)
+ let assert Ok(#(template, sets_str)) = string.split_once(record, " ")
+
+ let template =
+ template
+ |> list.repeat(folds)
+ |> list.intersperse("?")
+ |> string.concat
+ let sets =
+ sets_str
+ |> string.split(",")
+ |> list.map(int.parse)
+ |> result.values
+ |> list.repeat(folds)
+ |> list.flatten()
+
+ #(template, sets)
+}
+
+fn do_count(
+ template: String,
+ groups: List(Int),
+ left: Int,
+ gap: Bool,
+ cache: Cache(ParserState, Int),
+) -> Int {
+ use <- memo.memoize(cache, #(template, groups, left, gap))
+ case template, groups, left, gap {
+ "", [], 0, _ -> 1
+ "?" <> t_rest, [g, ..g_rest], 0, False ->
+ do_count(t_rest, g_rest, g - 1, g == 1, cache)
+ + do_count(t_rest, groups, 0, False, cache)
+ "?" <> t_rest, [], 0, False
+ | "?" <> t_rest, _, 0, True
+ | "." <> t_rest, _, 0, _ -> do_count(t_rest, groups, 0, False, cache)
+ "#" <> t_rest, [g, ..g_rest], 0, False ->
+ do_count(t_rest, g_rest, g - 1, g == 1, cache)
+ "?" <> t_rest, gs, l, False | "#" <> t_rest, gs, l, False ->
+ do_count(t_rest, gs, l - 1, l == 1, cache)
+ _, _, _, _ -> 0
+ }
+}
+
+fn count_solutions(acc: Int, condition: #(String, List(Int))) -> Int {
+ use cache: Cache(ParserState, Int) <- memo.create()
+ let #(template, groups) = condition
+ acc + do_count(template, groups, 0, False, cache)
+}
+
+pub fn part1(input: String) {
+ input
+ |> parse_folds(1)
+ |> list.fold(0, count_solutions)
+ |> string.inspect
+}
+
+pub fn part2(input: String) {
+ input
+ |> parse_folds(5)
+ |> list.fold(0, count_solutions)
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("12")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day13/.gitignore b/aoc2023-gleam/src/day13/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day13/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day13/solve.gleam b/aoc2023-gleam/src/day13/solve.gleam
new file mode 100644
index 0000000..6f9b9a0
--- /dev/null
+++ b/aoc2023-gleam/src/day13/solve.gleam
@@ -0,0 +1,87 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/list
+import gleam/string
+import gleam/bool
+
+type SymmetryType {
+ Horizontal(Int)
+ Vertical(Int)
+}
+
+fn is_symmetric(xss: List(List(a)), errs: Int) {
+ let assert [left, ..right] = xss
+ do_is_symmetric([left], right, errs)
+}
+
+fn do_is_symmetric(
+ left: List(List(a)),
+ right: List(List(a)),
+ errors: Int,
+) -> Result(Int, Nil) {
+ use <- bool.guard(list.is_empty(right), Error(Nil))
+ let assert [h, ..t] = right
+ let found_errors =
+ list.zip(list.flatten(left), list.flatten(right))
+ |> list.filter(fn(tup) { tup.1 != tup.0 })
+ |> list.length
+ case found_errors == errors {
+ True -> Ok(list.length(left))
+ False -> do_is_symmetric([h, ..left], t, errors)
+ }
+}
+
+fn get_symmetry_type(xss: List(List(String)), errors: Int) {
+ case is_symmetric(xss, errors) {
+ Ok(n) -> Horizontal(n)
+ _ -> {
+ let assert Ok(n) = is_symmetric(list.transpose(xss), errors)
+ Vertical(n)
+ }
+ }
+}
+
+fn summarize_notes(symmetries: List(SymmetryType)) {
+ use acc, note <- list.fold(symmetries, 0)
+ acc
+ + case note {
+ Horizontal(n) -> 100 * n
+ Vertical(n) -> n
+ }
+}
+
+fn solve(input: String, errors: Int) {
+ input
+ |> string.split("\n\n")
+ |> list.map(fn(strs) {
+ strs
+ |> string.split("\n")
+ |> list.map(string.to_graphemes)
+ |> get_symmetry_type(errors)
+ })
+ |> summarize_notes
+ |> string.inspect
+}
+
+pub fn part1(input: String) {
+ solve(input, 0)
+}
+
+pub fn part2(input: String) {
+ solve(input, 1)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("13")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day14/.gitignore b/aoc2023-gleam/src/day14/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day14/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day14/solve.gleam b/aoc2023-gleam/src/day14/solve.gleam
new file mode 100644
index 0000000..ecc5361
--- /dev/null
+++ b/aoc2023-gleam/src/day14/solve.gleam
@@ -0,0 +1,94 @@
+import adglent.{First, Second}
+import gleam/dict
+import gleam/int
+import gleam/io
+import gleam/list
+import gleam/order
+import gleam/string
+
+fn parse(input) {
+ input
+ |> string.split("\n")
+ |> list.map(string.to_graphemes)
+ |> list.transpose()
+}
+
+fn roll_boulders(strs: List(String)) {
+ {
+ use chunks <- list.map(list.chunk(strs, fn(c) { c == "O" || c == "." }))
+ list.sort(chunks, order.reverse(string.compare))
+ }
+ |> list.flatten
+}
+
+fn score(matrix) {
+ use acc, col <- list.fold(matrix, 0)
+ acc
+ + {
+ use col_acc, char, n <- list.index_fold(list.reverse(col), 0)
+ case char {
+ "O" -> col_acc + n + 1
+ _ -> col_acc
+ }
+ }
+}
+
+pub fn part1(input: String) {
+ input
+ |> parse
+ |> list.map(roll_boulders)
+ |> score()
+ |> string.inspect
+}
+
+fn rotate(matrix) {
+ matrix
+ |> list.map(list.reverse)
+ |> list.transpose
+}
+
+fn spin(matrix) {
+ use acc, _ <- list.fold(list.range(1, 4), matrix)
+ acc
+ |> list.map(roll_boulders)
+ |> rotate
+}
+
+fn spin_cycle(matrix) {
+ let cache = dict.new()
+ check_if_seen(matrix, cache, 1_000_000_000)
+}
+
+fn check_if_seen(matrix, cache, count) {
+ case dict.get(cache, matrix) {
+ Error(Nil) ->
+ check_if_seen(spin(matrix), dict.insert(cache, matrix, count), count - 1)
+ Ok(n) -> {
+ let assert Ok(extra) = int.modulo(count, n - count)
+ list.fold(list.range(1, extra), matrix, fn(acc, _) { spin(acc) })
+ |> score
+ }
+ }
+}
+
+pub fn part2(input: String) {
+ input
+ |> parse
+ |> spin_cycle
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("14")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day15/.gitignore b/aoc2023-gleam/src/day15/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day15/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day15/solve.gleam b/aoc2023-gleam/src/day15/solve.gleam
new file mode 100644
index 0000000..a7d250c
--- /dev/null
+++ b/aoc2023-gleam/src/day15/solve.gleam
@@ -0,0 +1,104 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/string
+import gleam/list
+import gleam/int
+import gleam/dict.{type Dict}
+import gleam/option.{None, Some}
+
+fn split(input: String) -> List(String) {
+ input
+ |> string.split(",")
+}
+
+fn hash_algorithm(str: String) -> Int {
+ let codepoints =
+ str
+ |> string.to_utf_codepoints()
+ |> list.map(string.utf_codepoint_to_int)
+ use acc, c <- list.fold(codepoints, 0)
+ let assert Ok(acc) = int.modulo({ acc + c } * 17, 256)
+ acc
+}
+
+pub fn part1(input: String) -> String {
+ input
+ |> split
+ |> list.fold(0, fn(acc, str) { acc + hash_algorithm(str) })
+ |> string.inspect
+}
+
+type Instruction {
+ Remove(label: String)
+ Insert(label: String, focal: Int)
+}
+
+fn read_instruction(str: String) -> Instruction {
+ case string.split(str, "=") {
+ [label, focal_str] -> {
+ let assert Ok(focal) = int.parse(focal_str)
+ Insert(label, focal)
+ }
+ _ -> Remove(string.drop_right(str, 1))
+ }
+}
+
+fn parse_instructions(insts: List(String)) -> Dict(Int, List(#(String, Int))) {
+ use acc, inst <- list.fold(insts, dict.new())
+ case read_instruction(inst) {
+ Remove(label) -> remove_lens(acc, label)
+ Insert(label, focal) -> insert_lens(acc, label, focal)
+ }
+}
+
+fn remove_lens(boxes, label) {
+ use v <- dict.update(boxes, hash_algorithm(label))
+ case v {
+ Some(lenses) ->
+ case list.key_pop(lenses, label) {
+ Ok(#(_, updated)) -> updated
+ Error(Nil) -> lenses
+ }
+ None -> []
+ }
+}
+
+fn insert_lens(boxes, label, focal) {
+ use v <- dict.update(boxes, hash_algorithm(label))
+ case v {
+ Some(lenses) -> list.key_set(lenses, label, focal)
+ None -> [#(label, focal)]
+ }
+}
+
+fn focusing_power(boxes: Dict(Int, List(#(String, Int)))) -> Int {
+ use acc, k, v <- dict.fold(boxes, 0)
+ let box_acc = {
+ use acc, lens, i <- list.index_fold(v, 0)
+ acc + lens.1 * { i + 1 }
+ }
+ acc + { k + 1 } * box_acc
+}
+
+pub fn part2(input: String) -> String {
+ input
+ |> split
+ |> parse_instructions
+ |> focusing_power
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("15")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day16/.gitignore b/aoc2023-gleam/src/day16/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day16/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day16/solve.gleam b/aoc2023-gleam/src/day16/solve.gleam
new file mode 100644
index 0000000..65ce36b
--- /dev/null
+++ b/aoc2023-gleam/src/day16/solve.gleam
@@ -0,0 +1,119 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/io
+import gleam/list
+import gleam/result
+import gleam/set.{type Set}
+import utilities/array2d.{type Posn, Posn}
+
+type Direction {
+ Up
+ Right
+ Down
+ Left
+}
+
+type Light {
+ Light(posn: Posn, dir: Direction)
+}
+
+fn move(l: Light) -> Light {
+ let Light(p, dir) = l
+ case dir {
+ Up -> Light(..l, posn: Posn(..p, r: p.r - 1))
+ Down -> Light(..l, posn: Posn(..p, r: p.r + 1))
+ Left -> Light(..l, posn: Posn(..p, c: p.c - 1))
+ Right -> Light(..l, posn: Posn(..p, c: p.c + 1))
+ }
+}
+
+fn transform(l: Light, cell: Result(String, Nil)) -> List(Light) {
+ use <- bool.guard(result.is_error(cell), [])
+ let assert Ok(c) = cell
+ let Light(p, dir) = l
+ case dir, c {
+ // no change
+ _, "." | Up, "|" | Down, "|" | Left, "-" | Right, "-" -> [l]
+ // diagonal mirrors
+ Left, "/" -> [Light(p, Down)]
+ Down, "/" -> [Light(p, Left)]
+ Right, "/" -> [Light(p, Up)]
+ Up, "/" -> [Light(p, Right)]
+ Left, "\\" -> [Light(p, Up)]
+ Up, "\\" -> [Light(p, Left)]
+ Right, "\\" -> [Light(p, Down)]
+ Down, "\\" -> [Light(p, Right)]
+ // splitters
+ Left, "|" | Right, "|" -> [Light(p, Up), Light(p, Down)]
+ Up, "-" | Down, "-" -> [Light(p, Left), Light(p, Right)]
+ _, _ -> panic as "unrecognized cell type"
+ }
+}
+
+fn energize(lights: List(Light), visited: Set(Light), grid: Dict(Posn, String)) {
+ let next_positions =
+ lights
+ |> list.flat_map(fn(l) {
+ let next = move(l)
+ transform(next, dict.get(grid, next.posn))
+ })
+ |> list.filter(fn(l) { !set.contains(visited, l) })
+ let all_visited = set.union(set.from_list(next_positions), visited)
+ case visited == all_visited {
+ True ->
+ set.fold(visited, set.new(), fn(acc, l) { set.insert(acc, l.posn) })
+ |> set.to_list
+ |> list.length
+ False -> energize(next_positions, all_visited, grid)
+ }
+}
+
+pub fn part1(input: String) {
+ let grid = array2d.parse_grid(input)
+
+ [Light(Posn(0, -1), Right)]
+ |> energize(set.new(), grid)
+}
+
+pub fn part2(input: String) {
+ let grid = array2d.parse_grid(input)
+
+ let Posn(rows, cols) = {
+ use acc, p <- list.fold(dict.keys(grid), Posn(0, 0))
+ case acc.r + acc.c > p.r + p.c {
+ True -> acc
+ False -> p
+ }
+ }
+
+ let all_starts =
+ list.concat([
+ list.map(list.range(0, rows), fn(r) { Light(Posn(r, -1), Right) }),
+ list.map(list.range(0, rows), fn(r) { Light(Posn(r, cols + 1), Left) }),
+ list.map(list.range(0, cols), fn(c) { Light(Posn(-1, c), Down) }),
+ list.map(list.range(0, cols), fn(c) { Light(Posn(rows + 1, c), Up) }),
+ ])
+
+ use acc, p <- list.fold(all_starts, 0)
+ let energized = energize([p], set.new(), grid)
+ case acc > energized {
+ True -> acc
+ False -> energized
+ }
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("16")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day17/.gitignore b/aoc2023-gleam/src/day17/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day17/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day17/solve.gleam b/aoc2023-gleam/src/day17/solve.gleam
new file mode 100644
index 0000000..7a01c4d
--- /dev/null
+++ b/aoc2023-gleam/src/day17/solve.gleam
@@ -0,0 +1,143 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/io
+import gleam/list
+import gleam/result
+import gleam/string
+import gleam/set.{type Set}
+import utilities/array2d.{type Posn, Posn}
+import utilities/prioqueue.{type PriorityQueue}
+
+type State {
+ State(posn: Posn, heatloss: Int, previous: Posn, history: List(Posn))
+}
+
+const deltas = [Posn(-1, 0), Posn(1, 0), Posn(0, -1), Posn(0, 1)]
+
+fn make_key(s: State) {
+ #(s.posn, same_dir(s))
+}
+
+fn same_dir(s: State) {
+ case s.history {
+ [] -> []
+ [first, ..] as deltas ->
+ list.take_while(deltas, fn(d) { d == first })
+ |> list.take(10)
+ }
+}
+
+fn is_goal(s: State, min_run: Int, goal: Posn) {
+ goal == s.posn && list.length(same_dir(s)) >= min_run
+}
+
+fn find_good_neighbors(max: Int, min: Int, s: State, grid) {
+ deltas
+ |> list.filter(eliminate_bad_neighbors(_, s, max, min, grid))
+ |> list.map(make_state(_, s, grid))
+}
+
+fn eliminate_bad_neighbors(d: Posn, s: State, max, min, grid) {
+ let neighbor = array2d.add_posns(d, s.posn)
+
+ use <- bool.guard(
+ neighbor == s.previous || !dict.has_key(grid, neighbor),
+ False,
+ )
+ case same_dir(s), list.length(same_dir(s)) {
+ [prev, ..], l if l == max -> d != prev
+ _, 0 -> True
+ [prev, ..], l if l < min -> d == prev
+ _, _ -> True
+ }
+}
+
+fn make_state(d: Posn, s: State, grid) {
+ let neighbor = array2d.add_posns(d, s.posn)
+ let assert Ok(heat_lost) = dict.get(grid, neighbor)
+ State(
+ posn: neighbor,
+ heatloss: s.heatloss + heat_lost,
+ previous: s.posn,
+ history: [d, ..s.history],
+ )
+}
+
+fn find_path(
+ grid: Dict(Posn, Int),
+ queue: PriorityQueue(State),
+ seen: Set(#(Posn, List(Posn))),
+ get_neighbors: fn(State) -> List(State),
+ is_goal: fn(State) -> Bool,
+) {
+ let assert Ok(#(state, rest)) = prioqueue.pop(queue)
+ let key =
+ make_key(
+ state
+ |> io.debug,
+ )
+ case set.contains(seen, key) {
+ True -> find_path(grid, rest, seen, get_neighbors, is_goal)
+ False -> {
+ let now_seen = set.insert(seen, key)
+ let neighbors = get_neighbors(state)
+ case list.find(neighbors, is_goal) {
+ Ok(final) -> final.heatloss
+ _err -> {
+ let now_queue =
+ list.fold(neighbors, rest, fn(acc, n) {
+ prioqueue.insert(acc, n, n.heatloss)
+ })
+ find_path(grid, now_queue, now_seen, get_neighbors, is_goal)
+ }
+ }
+ }
+ }
+}
+
+pub fn part1(input: String) {
+ let raw_grid =
+ input
+ |> array2d.to_list_of_lists
+
+ let grid = array2d.to_2d_intarray(raw_grid)
+
+ let rmax = list.length(raw_grid)
+ let assert Ok(cmax) =
+ raw_grid
+ |> list.first
+ |> result.map(list.length)
+
+ let start = State(Posn(0, 0), 0, Posn(0, 0), [])
+ let goal = Posn(rmax, cmax)
+
+ find_path(
+ grid,
+ prioqueue.insert(prioqueue.new(), start, 0),
+ set.new(),
+ find_good_neighbors(0, 3, _, grid),
+ is_goal(_, 1, goal),
+ )
+ |> string.inspect
+}
+
+pub fn part2(input: String) {
+ input
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("17")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day18/.gitignore b/aoc2023-gleam/src/day18/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day18/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day18/solve.gleam b/aoc2023-gleam/src/day18/solve.gleam
new file mode 100644
index 0000000..2c000f9
--- /dev/null
+++ b/aoc2023-gleam/src/day18/solve.gleam
@@ -0,0 +1,113 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/int
+import gleam/list
+import gleam/option.{Some}
+import gleam/regex.{type Match, Match}
+import gleam/string
+
+type Coord {
+ Coord(x: Int, y: Int)
+}
+
+type Direction {
+ Up
+ Right
+ Down
+ Left
+}
+
+type Dig {
+ Dig(dir: Direction, dist: Int)
+}
+
+fn to_direction(c: String) {
+ case c {
+ "R" | "0" -> Right
+ "D" | "1" -> Down
+ "L" | "2" -> Left
+ "U" | "3" -> Up
+ _ -> panic
+ }
+}
+
+fn parse_front(line: String) {
+ let assert Ok(re) = regex.from_string("(.) (.*) \\(.*\\)")
+ let assert [Match(submatches: [Some(dir), Some(dist)], ..)] =
+ regex.scan(with: re, content: line)
+ let assert Ok(n) = int.parse(dist)
+ Dig(to_direction(dir), n)
+}
+
+fn parse_hex(line: String) {
+ let assert Ok(re) = regex.from_string("\\(#(.....)(.)\\)")
+ let assert [Match(submatches: [Some(dist), Some(dir)], ..)] =
+ regex.scan(with: re, content: line)
+ let assert Ok(n) = int.base_parse(dist, 16)
+ Dig(to_direction(dir), n)
+}
+
+fn go(current: Coord, dig: Dig) {
+ case dig {
+ Dig(Up, n) -> Coord(current.x, current.y + n)
+ Dig(Right, n) -> Coord(current.x + n, current.y)
+ Dig(Down, n) -> Coord(current.x, current.y - n)
+ Dig(Left, n) -> Coord(current.x - n, current.y)
+ }
+}
+
+fn double_triangle(c1: Coord, c2: Coord) {
+ { c1.x * c2.y } - { c2.x * c1.y }
+}
+
+fn start_dig(digs: List(Dig)) {
+ do_next_dig(digs, Coord(0, 0), 0, 0)
+}
+
+fn do_next_dig(
+ digs: List(Dig),
+ current: Coord,
+ area: Int,
+ perimeter: Int,
+) -> Int {
+ case digs {
+ [] -> int.absolute_value(area) / 2 + { perimeter / 2 } + 1
+ [dig, ..rest] -> {
+ let next = go(current, dig)
+ let area = area + double_triangle(current, next)
+ let perimeter = perimeter + dig.dist
+ do_next_dig(rest, next, area, perimeter)
+ }
+ }
+}
+
+fn solve_with(input, f) {
+ input
+ |> string.split("\n")
+ |> list.map(f)
+ |> start_dig
+ |> string.inspect
+}
+
+pub fn part1(input: String) {
+ solve_with(input, parse_front)
+}
+
+pub fn part2(input: String) {
+ solve_with(input, parse_hex)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("18")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day19/.gitignore b/aoc2023-gleam/src/day19/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day19/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day19/solve.gleam b/aoc2023-gleam/src/day19/solve.gleam
new file mode 100644
index 0000000..186e783
--- /dev/null
+++ b/aoc2023-gleam/src/day19/solve.gleam
@@ -0,0 +1,255 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/string
+import gleam/dict.{type Dict}
+import gleam/order.{type Order, Gt, Lt}
+import gleam/regex.{type Match, Match}
+import gleam/list
+import gleam/option.{Some}
+import gleam/int
+
+type Rating {
+ XtremelyCool
+ Musical
+ Aerodynamic
+ Shiny
+}
+
+type Part {
+ Part(x: Int, m: Int, a: Int, s: Int)
+}
+
+type Action {
+ Accept
+ Reject
+ SendTo(String)
+}
+
+type Rule {
+ If(rating: Rating, comparison: Order, threshold: Int, do: Action)
+ Just(do: Action)
+}
+
+type Workflow =
+ Dict(String, List(Rule))
+
+type Interval {
+ Interval(min: Int, max: Int)
+}
+
+type PartRange {
+ PartRange(x: Interval, m: Interval, a: Interval, s: Interval)
+}
+
+fn parse_workflow(input: String) -> Workflow {
+ let assert Ok(re) = regex.from_string("(.*){(.*)}")
+
+ use acc, line <- list.fold(string.split(input, "\n"), dict.new())
+ let assert [Match(submatches: [Some(name), Some(all_rules)], ..)] =
+ regex.scan(re, line)
+ let rules =
+ string.split(all_rules, ",")
+ |> parse_rules
+ dict.insert(acc, name, rules)
+}
+
+fn parse_rules(rules: List(String)) -> List(Rule) {
+ let assert Ok(re_rule) = regex.from_string("(.*)(>|<)(.*):(.*)")
+ use rule <- list.map(rules)
+ case regex.scan(re_rule, rule) {
+ [Match(submatches: [Some(r), Some(c), Some(t), Some(i)], ..)] ->
+ If(to_rating(r), to_comp(c), to_val(t), to_instruction(i))
+ _nomatch -> Just(to_instruction(rule))
+ }
+}
+
+fn to_instruction(rule: String) {
+ case rule {
+ "A" -> Accept
+ "R" -> Reject
+ name -> SendTo(name)
+ }
+}
+
+fn to_rating(rating: String) {
+ case rating {
+ "x" -> XtremelyCool
+ "m" -> Musical
+ "a" -> Aerodynamic
+ _s -> Shiny
+ }
+}
+
+fn get_rating(part: Part, rating: Rating) -> Int {
+ case rating {
+ XtremelyCool -> part.x
+ Musical -> part.m
+ Aerodynamic -> part.a
+ Shiny -> part.s
+ }
+}
+
+fn to_comp(comp: String) {
+ case comp {
+ "<" -> Lt
+ _gt -> Gt
+ }
+}
+
+fn to_val(val: String) {
+ let assert Ok(n) = int.parse(val)
+ n
+}
+
+fn parse_parts(input: String) -> List(Part) {
+ let assert Ok(re) = regex.from_string("{x=(.*),m=(.*),a=(.*),s=(.*)}")
+
+ use part <- list.map(string.split(input, "\n"))
+ let assert [Match(submatches: [Some(x), Some(m), Some(a), Some(s)], ..)] =
+ regex.scan(re, part)
+ Part(to_val(x), to_val(m), to_val(a), to_val(s))
+}
+
+fn start_evaluating_workflow(part: Part, workflow: Workflow) -> Int {
+ evaluate_workflow(part, "in", workflow)
+}
+
+fn evaluate_workflow(part: Part, name: String, workflow: Workflow) -> Int {
+ let assert Ok(rules) = dict.get(workflow, name)
+ case evaluate_rules(part, rules) {
+ Accept -> part.x + part.m + part.a + part.s
+ Reject -> 0
+ SendTo(name) -> evaluate_workflow(part, name, workflow)
+ }
+}
+
+fn evaluate_rules(part: Part, rules: List(Rule)) -> Action {
+ case rules {
+ [] -> panic
+ [Just(do), ..] -> do
+ [If(rating, comparison, threshold, do), ..rest] ->
+ case int.compare(get_rating(part, rating), threshold) == comparison {
+ True -> do
+ False -> evaluate_rules(part, rest)
+ }
+ }
+}
+
+pub fn part1(input: String) {
+ let assert Ok(#(workflows_str, parts_str)) = string.split_once(input, "\n\n")
+
+ let workflows = parse_workflow(workflows_str)
+ let parts = parse_parts(parts_str)
+
+ list.map(parts, start_evaluating_workflow(_, workflows))
+ |> int.sum
+ |> string.inspect
+}
+
+fn size(interval: Interval) {
+ interval.max - interval.min + 1
+}
+
+fn all_in_range(pr: PartRange) {
+ size(pr.x) * size(pr.m) * size(pr.a) * size(pr.s)
+}
+
+fn get_partrange(pr: PartRange, rating: Rating) -> Interval {
+ case rating {
+ XtremelyCool -> pr.x
+ Musical -> pr.m
+ Aerodynamic -> pr.a
+ Shiny -> pr.s
+ }
+}
+
+fn update_partrange(pr: PartRange, rating: Rating, i: Interval) -> PartRange {
+ case rating {
+ XtremelyCool -> PartRange(..pr, x: i)
+ Musical -> PartRange(..pr, m: i)
+ Aerodynamic -> PartRange(..pr, a: i)
+ Shiny -> PartRange(..pr, s: i)
+ }
+}
+
+pub fn part2(input: String) {
+ let assert Ok(#(workflows_str, _)) = string.split_once(input, "\n\n")
+
+ let workflow = parse_workflow(workflows_str)
+ let start = Interval(1, 4000)
+
+ PartRange(start, start, start, start)
+ |> evaluate_workflow_on_range("in", workflow)
+ |> string.inspect
+}
+
+fn evaluate_workflow_on_range(
+ pr: PartRange,
+ name: String,
+ workflow: Workflow,
+) -> Int {
+ let assert Ok(rules) = dict.get(workflow, name)
+ evaluate_rules_on_range(pr, rules, workflow)
+}
+
+fn evaluate_rules_on_range(
+ pr: PartRange,
+ rules: List(Rule),
+ workflow: Workflow,
+) -> Int {
+ case rules {
+ [Just(Accept), ..] -> all_in_range(pr)
+ [Just(Reject), ..] -> 0
+ [Just(SendTo(name)), ..] -> evaluate_workflow_on_range(pr, name, workflow)
+ [If(rating, comparison, t, action), ..rest] -> {
+ let mod_i = get_partrange(pr, rating)
+ case comparison {
+ Lt ->
+ split_range(
+ keep: update_partrange(pr, rating, Interval(mod_i.min, t - 1)),
+ and_do: action,
+ pass: update_partrange(pr, rating, Interval(t, mod_i.max)),
+ and_eval: rest,
+ with: workflow,
+ )
+ _gt ->
+ split_range(
+ keep: update_partrange(pr, rating, Interval(t + 1, mod_i.max)),
+ and_do: action,
+ pass: update_partrange(pr, rating, Interval(mod_i.min, t)),
+ and_eval: rest,
+ with: workflow,
+ )
+ }
+ }
+ [] -> panic
+ }
+}
+
+fn split_range(
+ keep keep: PartRange,
+ and_do action: Action,
+ pass pass: PartRange,
+ and_eval rest: List(Rule),
+ with workflow: Workflow,
+) -> Int {
+ int.add(
+ evaluate_rules_on_range(keep, [Just(action)], workflow),
+ evaluate_rules_on_range(pass, rest, workflow),
+ )
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("19")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day2/.gitignore b/aoc2023-gleam/src/day2/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day2/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day2/solve.gleam b/aoc2023-gleam/src/day2/solve.gleam
new file mode 100644
index 0000000..608955f
--- /dev/null
+++ b/aoc2023-gleam/src/day2/solve.gleam
@@ -0,0 +1,66 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/int
+import gleam/string
+import gleam/list
+
+pub type Game {
+ Game(red: Int, blue: Int, green: Int)
+}
+
+fn parse(input: String) -> List(List(Game)) {
+ use line <- list.map(string.split(input, "\n"))
+ let assert [_, rounds] = string.split(line, on: ": ")
+ use match <- list.map(string.split(rounds, on: "; "))
+ use acc, draw <- list.fold(
+ over: string.split(match, on: ", "),
+ from: Game(0, 0, 0),
+ )
+ let assert Ok(#(n_str, color)) = string.split_once(draw, " ")
+ let assert Ok(n) = int.parse(n_str)
+ case color {
+ "red" -> Game(..acc, red: n)
+ "blue" -> Game(..acc, blue: n)
+ "green" -> Game(..acc, green: n)
+ _ -> panic as "unrecognized color"
+ }
+}
+
+pub fn part1(input: String) {
+ use acc, game, i <- list.index_fold(parse(input), 0)
+ case list.any(game, fn(m) { m.red > 12 || m.green > 13 || m.blue > 14 }) {
+ False -> acc + i + 1
+ True -> acc
+ }
+}
+
+pub fn part2(input: String) {
+ {
+ use game <- list.map(parse(input))
+ use acc, match <- list.fold(game, Game(0, 0, 0))
+ let Game(red: red, green: green, blue: blue) = match
+ Game(
+ red: int.max(red, acc.red),
+ blue: int.max(blue, acc.blue),
+ green: int.max(green, acc.green),
+ )
+ }
+ |> list.fold(from: 0, with: fn(acc, g: Game) {
+ acc + g.red * g.blue * g.green
+ })
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("2")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day20/.gitignore b/aoc2023-gleam/src/day20/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day20/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day20/solve.gleam b/aoc2023-gleam/src/day20/solve.gleam
new file mode 100644
index 0000000..9192dac
--- /dev/null
+++ b/aoc2023-gleam/src/day20/solve.gleam
@@ -0,0 +1,251 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/io
+import gleam/iterator.{type Iterator, type Step, Next}
+import gleam/list
+import gleam/queue.{type Queue}
+import gleam/set
+import gleam/string
+
+type Node {
+ Broadcaster(children: List(String))
+ Flipflop(children: List(String), state: Power)
+ Conjunction(children: List(String), state: Dict(String, TonePitch))
+ Ground
+}
+
+type Tone {
+ Tone(from: String, to: String, pitch: TonePitch)
+}
+
+type Power {
+ On
+ Off
+}
+
+type TonePitch {
+ Low
+ High
+}
+
+type State {
+ State(
+ nodes: Dict(String, Node),
+ low: Int,
+ high: Int,
+ cycle: Int,
+ sentry_nodes: Dict(String, Int),
+ )
+}
+
+fn flip_power(p: Power) -> Power {
+ case p {
+ On -> Off
+ Off -> On
+ }
+}
+
+fn flip_flop_pitch(p: Power) -> TonePitch {
+ case p {
+ Off -> High
+ On -> Low
+ }
+}
+
+fn combinator_pitch(state) {
+ case list.unique(dict.values(state)) {
+ [High] -> Low
+ _ -> High
+ }
+}
+
+fn get_children(node) {
+ case node {
+ Flipflop(children: cs, ..) -> cs
+ Conjunction(children: cs, ..) -> cs
+ Broadcaster(children: cs) -> cs
+ Ground -> []
+ }
+}
+
+fn parse_node(input: String) -> #(String, Node) {
+ let assert [full_name, children_str] = string.split(input, on: " -> ")
+ let children = string.split(children_str, on: ", ")
+
+ case full_name {
+ "%" <> name -> #(name, Flipflop(children: children, state: Off))
+ "&" <> name -> #(name, Conjunction(children: children, state: dict.new()))
+ "broadcaster" -> #("broadcaster", Broadcaster(children: children))
+ name -> #(name, Ground)
+ }
+}
+
+fn to_initial_state(nodes: List(#(String, Node))) -> Dict(String, Node) {
+ let node_dict = dict.from_list(nodes)
+ let node_names = dict.keys(node_dict)
+
+ let node_dict =
+ node_dict
+ |> dict.values
+ |> list.map(get_children)
+ |> list.concat
+ |> set.from_list
+ |> set.drop(dict.keys(node_dict))
+ |> set.to_list
+ |> list.fold(node_dict, fn(acc, n) { dict.insert(acc, n, Ground) })
+
+ use name, node <- dict.map_values(node_dict)
+ case node {
+ Conjunction(state: _, children: chs) ->
+ node_names
+ |> list.filter(fn(n) {
+ let assert Ok(node) = dict.get(node_dict, n)
+ list.contains(get_children(node), any: name)
+ })
+ |> list.map(fn(n) { #(n, Low) })
+ |> dict.from_list()
+ |> fn(dict) { Conjunction(state: dict, children: chs) }
+ other -> other
+ }
+}
+
+fn add_to_queue(from, children, pitch, queue) {
+ use acc, c <- list.fold(children, queue)
+ queue.push_back(acc, Tone(from: from, to: c, pitch: pitch))
+}
+
+fn add_tones(state: State, nodes, pitch, n) {
+ case pitch {
+ Low ->
+ State(..state, nodes: nodes, low: state.low + n, cycle: state.cycle + 1)
+ High ->
+ State(..state, nodes: nodes, high: state.high + n, cycle: state.cycle + 1)
+ }
+}
+
+fn press_button_once(initial: State, queue: Queue(Tone)) {
+ let State(nodes: nodes, ..) = initial
+
+ use <- bool.guard(queue.is_empty(queue), initial)
+ let assert Ok(#(Tone(from_name, to_name, pitch), rest)) =
+ queue.pop_front(queue)
+
+ let assert Ok(to_node) = dict.get(nodes, to_name)
+ case to_node {
+ Broadcaster(children) -> {
+ let new_state =
+ add_tones(initial, nodes, pitch, list.length(children) + 1)
+
+ let new_queue = add_to_queue(to_name, children, pitch, rest)
+ press_button_once(new_state, new_queue)
+ }
+
+ Conjunction(state: state, children: children) -> {
+ let new_state =
+ state
+ |> dict.insert(from_name, pitch)
+
+ let updated_nodes =
+ Conjunction(state: new_state, children: children)
+ |> dict.insert(nodes, to_name, _)
+
+ let pitch_out = combinator_pitch(new_state)
+
+ let new_state =
+ add_tones(initial, updated_nodes, pitch_out, list.length(children))
+ |> check_for_interesting_node(from_name, pitch_out)
+
+ add_to_queue(to_name, children, pitch_out, rest)
+ |> press_button_once(new_state, _)
+ }
+
+ Flipflop(..) if pitch == High ->
+ press_button_once(State(..initial, cycle: initial.cycle + 1), rest)
+
+ Flipflop(state: state, children: children) -> {
+ let updated_nodes =
+ Flipflop(state: flip_power(state), children: children)
+ |> dict.insert(nodes, to_name, _)
+
+ let pitch_out = flip_flop_pitch(state)
+ let new_state =
+ add_tones(initial, updated_nodes, pitch_out, list.length(children))
+
+ add_to_queue(to_name, children, flip_flop_pitch(state), rest)
+ |> press_button_once(new_state, _)
+ }
+
+ Ground(..) ->
+ press_button_once(State(..initial, cycle: initial.cycle + 1), rest)
+ }
+}
+
+pub fn part1(input: String) {
+ let initial_state =
+ input
+ |> string.split(on: "\n")
+ |> list.map(parse_node)
+ |> to_initial_state()
+
+ iterator.iterate(
+ from: State(initial_state, 0, 0, 1, dict.new()),
+ with: press_button_once(_, queue.from_list([
+ Tone("button", "broadcaster", Low),
+ ])),
+ )
+ |> iterator.at(1000)
+ |> fn(s) {
+ let assert Ok(State(high: high, low: low, ..)) = s
+ high * low
+ }
+ |> string.inspect
+}
+
+fn check_for_interesting_node(state, name, pitch_out) {
+ case name, pitch_out {
+ "rk", High | "cd", High | "zf", High | "qx", High ->
+ State(
+ ..state,
+ sentry_nodes: dict.insert(state.sentry_nodes, name, state.cycle),
+ )
+ _, _ -> state
+ }
+}
+
+pub fn part2(input: String) {
+ let initial_state =
+ input
+ |> string.split(on: "\n")
+ |> list.map(parse_node)
+ |> to_initial_state()
+
+ iterator.iterate(
+ from: State(initial_state, 0, 0, 1, dict.new()),
+ with: press_button_once(_, queue.from_list([
+ Tone("button", "broadcaster", Low),
+ ])),
+ )
+ |> iterator.drop_while(fn(s) { dict.size(s.sentry_nodes) < 4 })
+ |> iterator.step
+ |> fn(s: Step(State, Iterator(State))) {
+ let assert Next(goal, _rest) = s
+ goal.sentry_nodes
+ }
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("20")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day21/.gitignore b/aoc2023-gleam/src/day21/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day21/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day21/solve.gleam b/aoc2023-gleam/src/day21/solve.gleam
new file mode 100644
index 0000000..4d5c246
--- /dev/null
+++ b/aoc2023-gleam/src/day21/solve.gleam
@@ -0,0 +1,25 @@
+import adglent.{First, Second}
+import gleam/io
+
+pub fn part1(input: String) {
+ todo as "Implement solution to part 1"
+}
+
+pub fn part2(input: String) {
+ todo as "Implement solution to part 2"
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("21")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day22/.gitignore b/aoc2023-gleam/src/day22/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day22/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day22/solve.gleam b/aoc2023-gleam/src/day22/solve.gleam
new file mode 100644
index 0000000..7bf2fb4
--- /dev/null
+++ b/aoc2023-gleam/src/day22/solve.gleam
@@ -0,0 +1,199 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/int
+import gleam/io
+import gleam/list
+import gleam/option.{None, Some}
+import gleam/regex
+import gleam/result
+import gleam/set.{type Set}
+import gleam/string
+
+type Point {
+ Point(x: Int, y: Int, z: Int)
+}
+
+fn down_one(p: Point) -> Point {
+ Point(..p, z: p.z - 1)
+}
+
+type Block {
+ Block(index: Int, from: Point, to: Point)
+}
+
+fn compare_blocks(b1: Block, b2: Block) {
+ int.compare(b1.to.z, b2.to.z)
+}
+
+type Space =
+ Dict(Point, Block)
+
+type AllBlocks =
+ Dict(Block, List(Point))
+
+type BlockTree =
+ Dict(Int, Set(Int))
+
+fn parse_block(index: Int, input: String) -> Block {
+ let assert Ok(re) = regex.from_string("(.*),(.*),(.*)~(.*),(.*),(.*)")
+
+ let assert [scan] = regex.scan(with: re, content: input)
+
+ let assert [x1, y1, z1, x2, y2, z2] =
+ scan.submatches
+ |> option.all
+ |> option.unwrap([])
+ |> list.map(int.parse)
+ |> result.values
+ Block(index: index, from: Point(x1, y1, z1), to: Point(x2, y2, z2))
+}
+
+fn cross_section_at_level(b: Block, z: Int) -> List(Point) {
+ use x <- list.flat_map(list.range(b.from.x, b.to.x))
+ use y <- list.map(list.range(b.from.y, b.to.y))
+ Point(x, y, z)
+}
+
+fn place_block(space: Space, b: Block, z: Int) -> Space {
+ let now_occupied = {
+ use x <- list.flat_map(list.range(b.from.x, b.to.x))
+ use y <- list.flat_map(list.range(b.from.y, b.to.y))
+ use z <- list.map(list.range(z, z + b.to.z - b.from.z))
+ #(Point(x, y, z), b)
+ }
+
+ dict.merge(space, dict.from_list(now_occupied))
+}
+
+fn find_lowest_level(space: Space, b: Block) -> Space {
+ do_find_lowest(space, b, b.from.z)
+}
+
+fn do_find_lowest(space: Space, b: Block, z: Int) -> Space {
+ let is_intersecting =
+ list.any(cross_section_at_level(b, z), dict.has_key(space, _))
+
+ case z, is_intersecting {
+ 0, _ -> place_block(space, b, 1)
+ _, True -> place_block(space, b, z + 1)
+ _, False -> do_find_lowest(space, b, z - 1)
+ }
+}
+
+fn to_block_positions(space: Space) -> AllBlocks {
+ use acc, point, index <- dict.fold(space, dict.new())
+ use points <- dict.update(acc, index)
+ case points {
+ Some(ps) -> [point, ..ps]
+ None -> [point]
+ }
+}
+
+fn above_blocks(blocks: AllBlocks) -> BlockTree {
+ use acc, block, points <- dict.fold(blocks, dict.new())
+ use _ <- dict.update(acc, block.index)
+ {
+ use above_block, above_points <- dict.filter(blocks)
+ above_block.index != block.index
+ && list.any(above_points, fn(p) { list.contains(points, down_one(p)) })
+ }
+ |> dict.keys
+ |> list.map(fn(b) { b.index })
+ |> set.from_list
+}
+
+fn below_blocks(blocktree: BlockTree) -> BlockTree {
+ use acc, block, _ <- dict.fold(blocktree, dict.new())
+ use _ <- dict.update(acc, block)
+ {
+ use _, aboves <- dict.filter(blocktree)
+ set.contains(aboves, block)
+ }
+ |> dict.keys
+ |> set.from_list
+}
+
+fn vulnerable_blocks(below_tree: BlockTree) -> List(Int) {
+ use block <- list.filter(dict.keys(below_tree))
+ use bs <- list.any(dict.values(below_tree))
+ !{ set.size(bs) == 0 } && { set.size(set.delete(bs, block)) == 0 }
+}
+
+pub fn part1(input: String) {
+ let settled_blocks =
+ input
+ |> string.split("\n")
+ |> list.index_map(parse_block)
+ |> list.sort(compare_blocks)
+ |> list.fold(dict.new(), find_lowest_level)
+
+ let block_positions = to_block_positions(settled_blocks)
+ let above_blocks = above_blocks(block_positions)
+ let below_blocks = below_blocks(above_blocks)
+
+ let vulnerable_blocks = vulnerable_blocks(below_blocks)
+
+ list.length(dict.keys(block_positions)) - list.length(vulnerable_blocks)
+}
+
+fn all_falling_blocks(n: Int, above: BlockTree, below: BlockTree) {
+ let starting_set = set.insert(set.new(), n)
+ do_falling_blocks(starting_set, starting_set, above, below)
+}
+
+fn do_falling_blocks(
+ fallen: Set(Int),
+ blocks: Set(Int),
+ above: BlockTree,
+ below: BlockTree,
+) -> Int {
+ use <- bool.guard(set.size(blocks) == 0, set.size(fallen) - 1)
+
+ let blocks_above =
+ {
+ use block <- list.flat_map(set.to_list(blocks))
+ let assert Ok(supports) = dict.get(above, block)
+ use support <- list.filter(set.to_list(supports))
+ let assert Ok(supportings) = dict.get(below, support)
+ use supporting <- list.all(set.to_list(supportings))
+ set.contains(fallen, supporting)
+ }
+ |> set.from_list()
+
+ set.union(fallen, blocks_above)
+ |> do_falling_blocks(blocks_above, above, below)
+}
+
+pub fn part2(input: String) {
+ let settled_blocks =
+ input
+ |> string.split("\n")
+ |> list.index_map(parse_block)
+ |> list.sort(compare_blocks)
+ |> list.fold(dict.new(), find_lowest_level)
+
+ let block_positions = to_block_positions(settled_blocks)
+ let above_blocks = above_blocks(block_positions)
+ let below_blocks = below_blocks(above_blocks)
+
+ let vulnerable_blocks = vulnerable_blocks(below_blocks)
+
+ use acc, b <- list.fold(vulnerable_blocks, 0)
+ acc + all_falling_blocks(b, above_blocks, below_blocks)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("22")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day23/.gitignore b/aoc2023-gleam/src/day23/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day23/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day23/solve.gleam b/aoc2023-gleam/src/day23/solve.gleam
new file mode 100644
index 0000000..e1fe638
--- /dev/null
+++ b/aoc2023-gleam/src/day23/solve.gleam
@@ -0,0 +1,194 @@
+import adglent.{First, Second}
+import gleam/int
+import gleam/io
+import gleam/dict.{type Dict}
+import gleam/list
+import gleam/option.{type Option, None, Some}
+import gleam/string
+import gleam/set.{type Set}
+import gleam/bool
+import utilities/array2d.{type Array2D, type Posn, Posn}
+
+type Path {
+ Unknown
+ Straight
+ Junction
+}
+
+type Route {
+ Route(to: Posn, distance: Int)
+}
+
+fn append_to_key(v: Option(List(a)), new: a) -> List(a) {
+ case v {
+ None -> [new]
+ Some(xs) -> [new, ..xs]
+ }
+}
+
+fn first_parse_path(c: String) -> Result(Path, Nil) {
+ case c {
+ "#" -> Error(Nil)
+ _ -> Ok(Unknown)
+ }
+}
+
+fn junction_neighbors(p: Posn) -> List(Posn) {
+ [Posn(..p, r: p.r + 1), Posn(..p, c: p.c + 1)]
+}
+
+fn mark_junctions(trails: Array2D(Path)) -> Array2D(Path) {
+ use trail, _ <- dict.map_values(trails)
+
+ let valid_neighbors =
+ trail
+ |> array2d.ortho_neighbors
+ |> list.filter(dict.has_key(trails, _))
+
+ case list.length(valid_neighbors) {
+ 2 -> Straight
+ _ -> Junction
+ }
+}
+
+fn start_walking_to_next_junction(
+ start: Posn,
+ next: Posn,
+ trails: Array2D(Path),
+) {
+ let seen =
+ set.new()
+ |> set.insert(start)
+ |> set.insert(next)
+ walk_to_next_junction(start, next, 1, seen, trails)
+}
+
+fn walk_to_next_junction(
+ start: Posn,
+ current: Posn,
+ length: Int,
+ seen: Set(Posn),
+ trails: Array2D(Path),
+) -> #(Posn, Route) {
+ let assert [next] =
+ current
+ |> array2d.ortho_neighbors
+ |> list.filter(fn(n) { dict.has_key(trails, n) && !set.contains(seen, n) })
+
+ case dict.get(trails, next) {
+ Ok(Junction) -> #(start, Route(to: next, distance: length + 1))
+ _ -> {
+ let seen = set.insert(seen, current)
+ walk_to_next_junction(start, next, { length + 1 }, seen, trails)
+ }
+ }
+}
+
+fn find_routes(junctions, trails) {
+ use junction <- list.flat_map(junctions)
+ use neighbor <- list.filter_map(junction_neighbors(junction))
+ case dict.has_key(trails, neighbor) {
+ True -> Ok(start_walking_to_next_junction(junction, neighbor, trails))
+ False -> Error(Nil)
+ }
+}
+
+fn generate_routes(
+ junctions: List(Posn),
+ trails: Array2D(Path),
+) -> Dict(Posn, List(Route)) {
+ use acc, #(from, route) <- list.fold(
+ find_routes(junctions, trails),
+ dict.new(),
+ )
+ dict.update(acc, from, append_to_key(_, route))
+}
+
+fn generate_2way_routes(
+ junctions: List(Posn),
+ trails: Array2D(Path),
+) -> Dict(Posn, List(Route)) {
+ use acc, #(from, route) <- list.fold(
+ find_routes(junctions, trails),
+ dict.new(),
+ )
+ acc
+ |> dict.update(from, append_to_key(_, route))
+ |> dict.update(route.to, append_to_key(_, Route(from, route.distance)))
+}
+
+fn dfs(routes, from, to) {
+ let seen = set.insert(set.new(), from)
+ do_dfs(routes, from, to, 0, seen)
+}
+
+fn do_dfs(
+ routes: Dict(Posn, List(Route)),
+ from: Posn,
+ to: Posn,
+ acc: Int,
+ seen: Set(Posn),
+) -> Int {
+ use <- bool.guard(to == from, acc)
+
+ let assert Ok(all_routes) = dict.get(routes, from)
+ let neighbors = list.filter(all_routes, fn(r) { !set.contains(seen, r.to) })
+
+ case neighbors {
+ [] -> 0
+ neighbors ->
+ list.fold(neighbors, acc, fn(inner_acc, n) {
+ let score =
+ do_dfs(routes, n.to, to, acc + n.distance, set.insert(seen, n.to))
+ int.max(score, inner_acc)
+ })
+ }
+}
+
+fn solve_using(
+ input: String,
+ using: fn(List(Posn), Dict(Posn, Path)) -> Dict(Posn, List(Route)),
+) -> Int {
+ let min_row = 0
+ let max_row = list.length(string.split(input, "\n")) - 1
+
+ let trails =
+ input
+ |> array2d.parse_grid_using(first_parse_path)
+ |> mark_junctions
+
+ let junctions =
+ trails
+ |> dict.filter(fn(_, v) { v == Junction })
+ |> dict.keys
+
+ let assert Ok(start) = list.find(junctions, fn(j) { j.r == min_row })
+ let assert Ok(end) = list.find(junctions, fn(j) { j.r == max_row })
+
+ let routes = using(junctions, trails)
+
+ dfs(routes, start, end)
+}
+
+pub fn part1(input: String) {
+ solve_using(input, generate_routes)
+}
+
+pub fn part2(input: String) {
+ solve_using(input, generate_2way_routes)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("23")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day3/.gitignore b/aoc2023-gleam/src/day3/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day3/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day3/solve.gleam b/aoc2023-gleam/src/day3/solve.gleam
new file mode 100644
index 0000000..ad975aa
--- /dev/null
+++ b/aoc2023-gleam/src/day3/solve.gleam
@@ -0,0 +1,180 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/dict.{type Dict}
+import gleam/string
+import gleam/list
+import gleam/int
+import gleam/order.{type Order, Eq}
+
+type Coord {
+ Coord(x: Int, y: Int)
+}
+
+type SymbolKind {
+ Gear
+ SomethingElse
+}
+
+type Symbol {
+ Number(Int)
+ Symbol(SymbolKind)
+ Empty
+}
+
+type Board =
+ Dict(Coord, Symbol)
+
+type Cell {
+ Cell(coord: Coord, symbol: Symbol)
+}
+
+type Part {
+ Part(coords: List(Coord), part_number: Int)
+}
+
+fn to_symbol(c: String) -> Symbol {
+ case int.parse(c), c {
+ Ok(n), _ -> Number(n)
+ _, "." -> Empty
+ _, "*" -> Symbol(Gear)
+ _, _ -> Symbol(SomethingElse)
+ }
+}
+
+fn to_board(input: String) -> Board {
+ {
+ use y, r <- list.index_map(string.split(input, "\n"))
+ use x, c <- list.index_map(string.to_graphemes(r))
+ #(Coord(x, y), to_symbol(c))
+ }
+ |> list.flatten()
+ |> dict.from_list()
+}
+
+fn cell_compare(a: Cell, b: Cell) -> Order {
+ case int.compare(a.coord.y, b.coord.y) {
+ Eq -> int.compare(a.coord.x, b.coord.x)
+ other -> other
+ }
+}
+
+fn find_all_part_digits(b: Board) -> List(Cell) {
+ b
+ |> dict.filter(fn(_, v) {
+ case v {
+ Number(_) -> True
+ _ -> False
+ }
+ })
+ |> dict.to_list()
+ |> list.map(fn(tup) { Cell(tup.0, tup.1) })
+ |> list.sort(cell_compare)
+}
+
+fn to_parts(cells: List(Cell)) -> List(Part) {
+ do_parts(cells, [])
+}
+
+fn do_parts(cells: List(Cell), parts: List(Part)) -> List(Part) {
+ case cells {
+ [] -> parts
+ [Cell(next, Number(n)), ..t] -> {
+ case parts {
+ [] -> do_parts(t, [Part([next], n), ..parts])
+ [Part([prev, ..] as coords, n0), ..rest_parts] ->
+ case { next.x - prev.x }, { next.y - prev.y } {
+ 1, 0 ->
+ do_parts(t, [Part([next, ..coords], n0 * 10 + n), ..rest_parts])
+ _, _ -> do_parts(t, [Part([next], n), ..parts])
+ }
+ _ -> panic
+ }
+ }
+ _ -> panic
+ }
+}
+
+fn all_neighbors(c: Coord) -> List(Coord) {
+ use dx <- list.flat_map([-1, 0, 1])
+ use dy <- list.filter_map([-1, 0, 1])
+ case dx, dy {
+ 0, 0 -> Error(Nil)
+ _, _ -> Ok(Coord(c.x + dx, c.y + dy))
+ }
+}
+
+fn sum_valid_parts(acc: Int, part: Part, board: Board) -> Int {
+ let neighbors =
+ part.coords
+ |> list.flat_map(all_neighbors)
+ |> list.unique()
+
+ let sym = [Ok(Symbol(Gear)), Ok(Symbol(SomethingElse))]
+ case list.any(neighbors, fn(c) { list.contains(sym, dict.get(board, c)) }) {
+ True -> acc + part.part_number
+ False -> acc
+ }
+}
+
+pub fn part1(input: String) -> Int {
+ let board = to_board(input)
+
+ board
+ |> find_all_part_digits
+ |> to_parts
+ |> list.fold(0, fn(acc, p) { sum_valid_parts(acc, p, board) })
+}
+
+fn to_part_with_neighbors(part: Part) -> Part {
+ part.coords
+ |> list.flat_map(all_neighbors)
+ |> list.unique
+ |> Part(part.part_number)
+}
+
+fn find_part_numbers_near_gear(gear: Coord, parts: List(Part)) -> List(Int) {
+ use part <- list.filter_map(parts)
+ case list.contains(part.coords, gear) {
+ True -> Ok(part.part_number)
+ False -> Error(Nil)
+ }
+}
+
+fn to_sum_of_gear_ratios(adjacent_parts: List(List(Int))) -> Int {
+ use acc, ps <- list.fold(adjacent_parts, 0)
+ case ps {
+ [p1, p2] -> acc + p1 * p2
+ _ -> acc
+ }
+}
+
+pub fn part2(input: String) -> Int {
+ let board = to_board(input)
+
+ let parts =
+ board
+ |> find_all_part_digits
+ |> to_parts
+ |> list.map(to_part_with_neighbors)
+
+ board
+ |> dict.filter(fn(_, v) { v == Symbol(Gear) })
+ |> dict.keys
+ |> list.map(find_part_numbers_near_gear(_, parts))
+ |> to_sum_of_gear_ratios
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("3")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day4/.gitignore b/aoc2023-gleam/src/day4/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day4/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day4/solve.gleam b/aoc2023-gleam/src/day4/solve.gleam
new file mode 100644
index 0000000..34d6098
--- /dev/null
+++ b/aoc2023-gleam/src/day4/solve.gleam
@@ -0,0 +1,98 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/int
+import gleam/io
+import gleam/list
+import gleam/option.{None, Some}
+import gleam/result
+import gleam/set.{type Set}
+import gleam/string
+
+type Card {
+ Card(number: Int, winners: Int)
+}
+
+fn numbers_to_set(str: String) -> Set(Int) {
+ str
+ |> string.split(" ")
+ |> list.map(int.parse)
+ |> result.values()
+ |> set.from_list()
+}
+
+fn parse_card(card: String) -> Card {
+ let assert Ok(#("Card" <> n_str, rest)) = string.split_once(card, ": ")
+ let assert Ok(#(winning_str, has_str)) = string.split_once(rest, " | ")
+ let assert Ok(n) = int.parse(string.trim(n_str))
+
+ let winning = numbers_to_set(winning_str)
+ let has = numbers_to_set(has_str)
+ let winners = set.size(set.intersection(winning, has))
+
+ Card(number: n, winners: winners)
+}
+
+fn win_points(n: Int) {
+ bool.guard(n < 2, n, fn() { 2 * win_points(n - 1) })
+}
+
+pub fn part1(input: String) {
+ use acc, c <- list.fold(string.split(input, "\n"), 0)
+ c
+ |> parse_card
+ |> fn(c: Card) { win_points(c.winners) }
+ |> int.add(acc)
+}
+
+fn win_more_cards(cards: List(String), count: Dict(Int, Int)) {
+ case cards {
+ [] ->
+ count
+ |> dict.values
+ |> int.sum
+ [raw_card, ..rest] -> {
+ let card = parse_card(raw_card)
+ case card.winners {
+ 0 -> win_more_cards(rest, count)
+ n -> win_more_cards(rest, update_counts(n, card, count))
+ }
+ }
+ }
+}
+
+fn update_counts(n: Int, card: Card, count: Dict(Int, Int)) -> Dict(Int, Int) {
+ let assert Ok(bonus) = dict.get(count, card.number)
+ use acc, n <- list.fold(list.range(card.number + 1, card.number + n), count)
+ use c <- dict.update(acc, n)
+ case c {
+ Some(i) -> i + bonus
+ None -> panic as "won a card that doesn't exist in the card pile"
+ }
+}
+
+pub fn part2(input: String) {
+ let cards = string.split(input, "\n")
+
+ let count =
+ list.range(1, list.length(cards))
+ |> list.map(fn(n) { #(n, 1) })
+ |> dict.from_list()
+
+ win_more_cards(cards, count)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("4")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day5/.gitignore b/aoc2023-gleam/src/day5/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day5/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day5/solve.gleam b/aoc2023-gleam/src/day5/solve.gleam
new file mode 100644
index 0000000..7c05310
--- /dev/null
+++ b/aoc2023-gleam/src/day5/solve.gleam
@@ -0,0 +1,162 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/string
+import gleam/result
+import gleam/list.{Continue, Stop}
+import gleam/int
+import gleam/function
+
+// Types -------------------------------------------------------------------------------------------
+
+pub type Almanac {
+ Almanac(seeds: List(Int), mappers: List(Mapper))
+}
+
+pub type MappingRange {
+ MRange(start: Int, end: Int, offset: Int)
+}
+
+pub type SeedRange {
+ SRange(start: Int, end: Int)
+}
+
+type Mapper =
+ List(MappingRange)
+
+// Parsing -----------------------------------------------------------------------------------------
+
+fn parse_input(input: String) {
+ let assert ["seeds: " <> raw_seeds, ..raw_mappers] =
+ string.split(input, on: "\n\n")
+
+ let seeds = string_to_int_list(raw_seeds)
+ let mappers =
+ list.map(
+ raw_mappers,
+ function.compose(string.split(_, on: "\n"), parse_mapper),
+ )
+ Almanac(seeds, mappers)
+}
+
+fn string_to_int_list(str: String) {
+ str
+ |> string.split(on: " ")
+ |> list.map(int.parse)
+ |> result.values
+}
+
+fn parse_mapper(strs: List(String)) -> Mapper {
+ let assert [_, ..raw_ranges] = strs
+ list.map(raw_ranges, parse_mrange)
+ |> list.sort(fn(a, b) { int.compare(a.start, b.start) })
+}
+
+fn parse_mrange(str: String) -> MappingRange {
+ let assert [destination, source, range_width] = string_to_int_list(str)
+ MRange(source, source + range_width - 1, destination - source)
+}
+
+// Part 1 ------------------------------------------------------------------------------------------
+
+pub fn part1(input: String) {
+ let Almanac(seeds, mappers) = parse_input(input)
+
+ list.map(seeds, list.fold(over: mappers, from: _, with: correspond))
+ |> list.reduce(int.min)
+ |> result.unwrap(0)
+ |> string.inspect
+}
+
+fn correspond(n: Int, mapper: Mapper) {
+ use acc, mrange <- list.fold_until(over: mapper, from: n)
+ case mrange.start <= acc && acc <= mrange.end {
+ True -> Stop(acc + mrange.offset)
+ False -> Continue(acc)
+ }
+}
+
+// Part 2 ------------------------------------------------------------------------------------------
+
+pub fn part2(input: String) {
+ let Almanac(seeds, mappers) = parse_input(input)
+
+ let assert [SRange(answer, _), ..] =
+ seeds
+ |> list.sized_chunk(into: 2)
+ |> list.map(fn(chunk) {
+ let assert [start, length] = chunk
+ [SRange(start, start + length - 1)]
+ |> remap_all_seed_ranges(mappers)
+ })
+ |> list.flatten()
+ |> list.sort(fn(a, b) { int.compare(a.start, b.start) })
+
+ string.inspect(answer)
+}
+
+fn remap_all_seed_ranges(srs: List(SeedRange), mappers: List(Mapper)) {
+ case mappers {
+ [] -> srs
+ [mapper, ..rest] ->
+ list.flat_map(srs, remap_range(_, mapper))
+ |> remap_all_seed_ranges(rest)
+ }
+}
+
+fn remap_range(r: SeedRange, mapper: Mapper) -> List(SeedRange) {
+ do_remap_range(r, mapper, [])
+}
+
+fn transform_range(r: SeedRange, mapper: MappingRange) -> SeedRange {
+ SRange(r.start + mapper.offset, r.end + mapper.offset)
+}
+
+fn do_remap_range(r: SeedRange, mapper: Mapper, acc: List(SeedRange)) {
+ case mapper {
+ // no more mappings -> no mapping covers this range
+ [] -> [r, ..acc]
+ // range is to the left of current mapping -> no mapping covers this range
+ [m, ..] if r.end < m.start -> [r, ..acc]
+ // range is to the right of current mapping -> move to next mapping
+ [m, ..ms] if r.start > m.end -> do_remap_range(r, ms, acc)
+ // range is fully inside mapping -> range is transformed
+ [m, ..] if r.start >= m.start && r.end <= m.end -> [
+ transform_range(r, m),
+ ..acc
+ ]
+ // range overlaps start but not end -> left side not transformed, right side transformed
+ [m, ..] if r.start < m.start && r.end <= m.end -> [
+ SRange(r.start, m.start - 1),
+ transform_range(SRange(m.start, r.end), m),
+ ..acc
+ ]
+ // range overlaps end but not start -> left side transformed, right side moves to next mapping
+ [m, ..ms] if r.start >= m.start && r.end > m.end ->
+ do_remap_range(SRange(m.end + 1, r.end), ms, [
+ transform_range(SRange(r.start, m.end), m),
+ ..acc
+ ])
+ // mapping is fully inside range -> left not transformed, middle transformed, right to next
+ [m, ..ms] ->
+ do_remap_range(SRange(m.end + 1, r.end), ms, [
+ SRange(r.start, m.start - 1),
+ transform_range(SRange(m.start, m.end), m),
+ ..acc
+ ])
+ }
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("5")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day6/.gitignore b/aoc2023-gleam/src/day6/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day6/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day6/solve.gleam b/aoc2023-gleam/src/day6/solve.gleam
new file mode 100644
index 0000000..88044c4
--- /dev/null
+++ b/aoc2023-gleam/src/day6/solve.gleam
@@ -0,0 +1,85 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/string
+import gleam/int
+import gleam/list
+import gleam/result
+
+type Race {
+ Race(time: Int, distance: Int)
+}
+
+fn parse_with_bad_kerning(input: String) {
+ input
+ |> string.split("\n")
+ |> list.map(fn(str) {
+ str
+ |> string.split(" ")
+ |> list.map(int.parse)
+ |> result.values
+ })
+ |> list.transpose
+ |> list.map(fn(ns) {
+ let assert [t, d] = ns
+ Race(t, d)
+ })
+}
+
+fn find_bound(race: Race, button_time: Int, step: Int) {
+ let travel_time = race.time - button_time
+ case button_time * travel_time > race.distance {
+ True -> button_time
+ False -> find_bound(race, button_time + step, step)
+ }
+}
+
+fn lower_bound(race: Race) {
+ find_bound(race, 1, 1)
+}
+
+fn upper_bound(race: Race) {
+ find_bound(race, race.time, -1)
+}
+
+pub fn part1(input: String) {
+ {
+ use acc, race <- list.fold(parse_with_bad_kerning(input), 1)
+ acc * { upper_bound(race) - lower_bound(race) + 1 }
+ }
+ |> string.inspect
+}
+
+fn parse_properly(input: String) {
+ input
+ |> string.replace(" ", "")
+ |> string.split("\n")
+ |> list.flat_map(string.split(_, ":"))
+ |> list.map(int.parse)
+ |> result.values
+}
+
+pub fn part2(input: String) {
+ let assert [time, distance] =
+ input
+ |> parse_properly
+
+ let race = Race(time, distance)
+
+ upper_bound(race) - lower_bound(race) + 1
+ |> string.inspect
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("6")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day7/.gitignore b/aoc2023-gleam/src/day7/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day7/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day7/solve.gleam b/aoc2023-gleam/src/day7/solve.gleam
new file mode 100644
index 0000000..4454883
--- /dev/null
+++ b/aoc2023-gleam/src/day7/solve.gleam
@@ -0,0 +1,140 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/function
+import gleam/int
+import gleam/io
+import gleam/list
+import gleam/order.{type Order, Eq, Lt}
+import gleam/string
+
+// Types -------------------------------------------------------------------------------------------
+
+type Hand {
+ Hand(cards: List(Int), wager: Int)
+}
+
+// Common functions --------------------------------------------------------------------------------
+
+fn parse_hand(str: String) -> Hand {
+ let assert [cards, wager] = string.split(str, " ")
+ let cards =
+ string.to_graphemes(cards)
+ |> list.map(card_rank)
+ let assert Ok(wager) = int.parse(wager)
+
+ Hand(cards, wager)
+}
+
+fn classify_hand(hand: Hand) -> Int {
+ case list.length(list.unique(hand.cards)), card_counts(hand) {
+ 1, _ -> 8
+ 2, [1, 4] -> 7
+ 2, [2, 3] -> 6
+ 3, [1, 1, 3] -> 5
+ 3, [1, 2, 2] -> 4
+ 4, _ -> 3
+ 5, _ -> 2
+ _, _ -> 1
+ }
+}
+
+fn card_counts(hand: Hand) {
+ hand.cards
+ |> list.sort(int.compare)
+ |> list.chunk(function.identity)
+ |> list.map(list.length)
+ |> list.sort(int.compare)
+}
+
+fn card_rank(card: String) -> Int {
+ case int.parse(card), card {
+ Ok(n), _ -> n
+ _, "A" -> 14
+ _, "K" -> 13
+ _, "Q" -> 12
+ _, "J" -> 11
+ _, "T" -> 10
+ _, _ -> 1
+ }
+}
+
+fn compare_hands(hand1: Hand, hand2: Hand, using: fn(Hand) -> Int) -> Order {
+ case int.compare(using(hand1), using(hand2)) {
+ Eq -> compare_top_card(hand1.cards, hand2.cards)
+ other -> other
+ }
+}
+
+fn compare_top_card(cards1: List(Int), cards2: List(Int)) -> Order {
+ use <- bool.guard(cards1 == [] || cards2 == [], Eq)
+ let assert [c1, ..rest1] = cards1
+ let assert [c2, ..rest2] = cards2
+ case int.compare(c1, c2) {
+ Eq -> compare_top_card(rest1, rest2)
+ other -> other
+ }
+}
+
+fn part(input: String, comparator: fn(Hand, Hand) -> Order) {
+ input
+ |> string.split("\n")
+ |> list.map(parse_hand)
+ |> list.sort(comparator)
+ |> list.index_map(fn(i, h) { { i + 1 } * h.wager })
+ |> int.sum
+ |> string.inspect
+}
+
+// Part 1 ------------------------------------------------------------------------------------------
+
+pub fn part1(input: String) {
+ part(input, compare_without_wilds)
+}
+
+fn compare_without_wilds(hand1: Hand, hand2: Hand) {
+ compare_hands(hand1, hand2, classify_hand)
+}
+
+// Part 2 ------------------------------------------------------------------------------------------
+
+pub fn part2(input: String) {
+ part(string.replace(input, "J", "*"), compare_hands_considering_jokers)
+}
+
+fn find_best_joker_substitution(hand: Hand) {
+ use acc, card <- list.fold(list.range(2, 14), Hand([], 0))
+ let subbed_cards = {
+ use c <- list.map(hand.cards)
+ case c {
+ 1 -> card
+ other -> other
+ }
+ }
+ let subbed_hand = Hand(..hand, cards: subbed_cards)
+ case compare_hands(acc, subbed_hand, classify_hand) {
+ Lt -> subbed_hand
+ _ -> acc
+ }
+}
+
+fn compare_hands_considering_jokers(hand1: Hand, hand2: Hand) -> Order {
+ use hand <- compare_hands(hand1, hand2)
+ hand
+ |> find_best_joker_substitution
+ |> classify_hand
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("7")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day8/.gitignore b/aoc2023-gleam/src/day8/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day8/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day8/solve.gleam b/aoc2023-gleam/src/day8/solve.gleam
new file mode 100644
index 0000000..6b36e2d
--- /dev/null
+++ b/aoc2023-gleam/src/day8/solve.gleam
@@ -0,0 +1,91 @@
+import adglent.{First, Second}
+import gleam/bool
+import gleam/dict.{type Dict}
+import gleam/io
+import gleam/iterator.{type Iterator, Next}
+import gleam/list
+import gleam/option.{Some}
+import gleam/string
+import gleam/regex.{type Match, Match}
+import gleam_community/maths/arithmetics
+
+type Paths {
+ Paths(to_left: String, to_right: String)
+}
+
+type Maze =
+ Dict(String, Paths)
+
+fn parse(input: String) -> #(Iterator(String), Dict(String, Paths)) {
+ let assert [directions_str, maze_str] = string.split(input, "\n\n")
+
+ let directions =
+ directions_str
+ |> string.to_graphemes()
+ |> iterator.from_list
+ |> iterator.cycle
+
+ let assert Ok(re) = regex.from_string("(...) = \\((...), (...)\\)")
+ let maze =
+ maze_str
+ |> string.split("\n")
+ |> list.map(fn(str) {
+ let assert [Match(submatches: [Some(name), Some(left), Some(right)], ..)] =
+ regex.scan(re, str)
+ #(name, Paths(left, right))
+ })
+ |> dict.from_list
+
+ #(directions, maze)
+}
+
+fn to_next_step(
+ current: String,
+ stop_at: String,
+ count: Int,
+ directions: Iterator(String),
+ maze: Maze,
+) -> Int {
+ use <- bool.guard(string.ends_with(current, stop_at), count)
+ let assert Next(next_direction, rest_directions) = iterator.step(directions)
+ let assert Ok(paths) = dict.get(maze, current)
+ case next_direction {
+ "L" -> paths.to_left
+ "R" -> paths.to_right
+ _ -> panic as "bad direction"
+ }
+ |> to_next_step(stop_at, count + 1, rest_directions, maze)
+}
+
+pub fn part1(input: String) -> Int {
+ let #(directions, maze) = parse(input)
+
+ to_next_step("AAA", "ZZZ", 0, directions, maze)
+}
+
+pub fn part2(input: String) -> Int {
+ let #(directions, maze) = parse(input)
+
+ use acc, name <- list.fold(dict.keys(maze), 1)
+ case string.ends_with(name, "A") {
+ False -> acc
+ True ->
+ to_next_step(name, "Z", 0, directions, maze)
+ |> arithmetics.lcm(acc)
+ }
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("8")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/day9/.gitignore b/aoc2023-gleam/src/day9/.gitignore
new file mode 100644
index 0000000..ae40cea
--- /dev/null
+++ b/aoc2023-gleam/src/day9/.gitignore
@@ -0,0 +1 @@
+input.txt \ No newline at end of file
diff --git a/aoc2023-gleam/src/day9/solve.gleam b/aoc2023-gleam/src/day9/solve.gleam
new file mode 100644
index 0000000..a2cc7ae
--- /dev/null
+++ b/aoc2023-gleam/src/day9/solve.gleam
@@ -0,0 +1,70 @@
+import adglent.{First, Second}
+import gleam/io
+import gleam/list
+import gleam/string
+import gleam/int
+
+fn parse(input: String, backwards backwards: Bool) -> List(List(Int)) {
+ use line <- list.map(string.split(input, "\n"))
+ use n_str <- list.map(maybe_backwards(string.split(line, " "), backwards))
+ let assert Ok(n) = int.parse(n_str)
+ n
+}
+
+fn maybe_backwards(xs: List(a), backwards: Bool) -> List(a) {
+ case backwards {
+ False -> list.reverse(xs)
+ True -> xs
+ }
+}
+
+fn is_constant(ns: List(Int)) -> Bool {
+ case list.unique(ns) {
+ [_] -> True
+ _ -> False
+ }
+}
+
+fn take_derivative(ns: List(Int)) -> List(Int) {
+ ns
+ |> list.window_by_2
+ |> list.map(fn(tup) { tup.0 - tup.1 })
+}
+
+fn extrapolate(ns: List(Int)) {
+ case is_constant(ns), ns {
+ True, [n, ..] -> n
+ False, [n, ..] -> n + extrapolate(take_derivative(ns))
+ _, _ -> panic as "list empty when it shouldn't be"
+ }
+}
+
+fn part(input: String, backwards backwards: Bool) {
+ input
+ |> parse(backwards: backwards)
+ |> list.fold(0, fn(acc, ns) { extrapolate(ns) + acc })
+ |> string.inspect
+}
+
+pub fn part1(input: String) {
+ part(input, backwards: False)
+}
+
+pub fn part2(input: String) {
+ part(input, backwards: True)
+}
+
+pub fn main() {
+ let assert Ok(part) = adglent.get_part()
+ let assert Ok(input) = adglent.get_input("9")
+ case part {
+ First ->
+ part1(input)
+ |> adglent.inspect
+ |> io.println
+ Second ->
+ part2(input)
+ |> adglent.inspect
+ |> io.println
+ }
+}
diff --git a/aoc2023-gleam/src/utilities/array2d.gleam b/aoc2023-gleam/src/utilities/array2d.gleam
new file mode 100644
index 0000000..8538129
--- /dev/null
+++ b/aoc2023-gleam/src/utilities/array2d.gleam
@@ -0,0 +1,74 @@
+import gleam/list
+import gleam/dict.{type Dict}
+import gleam/string
+import gleam/int
+import gleam/result
+
+pub type Posn {
+ Posn(r: Int, c: Int)
+}
+
+pub type Array2D(a) =
+ Dict(Posn, a)
+
+pub fn add_posns(p1: Posn, p2: Posn) -> Posn {
+ case p1, p2 {
+ Posn(r1, c1), Posn(r2, c2) -> Posn(r1 + r2, c1 + c2)
+ }
+}
+
+pub fn ortho_neighbors(p: Posn) -> List(Posn) {
+ let Posn(r, c) = p
+ [Posn(r + 1, c), Posn(r - 1, c), Posn(r, c + 1), Posn(r, c - 1)]
+}
+
+pub fn to_2d_array(xss: List(List(a))) -> Array2D(a) {
+ to_2d_array_using(xss, fn(x) { Ok(x) })
+}
+
+pub fn to_2d_array_using(
+ xss: List(List(a)),
+ f: fn(a) -> Result(b, Nil),
+) -> Array2D(b) {
+ {
+ use r, row <- list.index_map(xss)
+ use c, cell <- list.index_map(row)
+ case f(cell) {
+ Ok(contents) -> Ok(#(Posn(r, c), contents))
+ Error(Nil) -> Error(Nil)
+ }
+ }
+ |> list.flatten
+ |> result.values
+ |> dict.from_list
+}
+
+pub fn to_2d_intarray(xss: List(List(String))) -> Array2D(Int) {
+ {
+ use r, row <- list.index_map(xss)
+ use c, cell <- list.index_map(row)
+ let assert Ok(n) = int.parse(cell)
+ #(Posn(r, c), n)
+ }
+ |> list.flatten
+ |> dict.from_list
+}
+
+pub fn to_list_of_lists(str: String) -> List(List(String)) {
+ str
+ |> string.split("\n")
+ |> list.map(string.to_graphemes)
+}
+
+pub fn parse_grid(str: String) -> Array2D(String) {
+ parse_grid_using(str, fn(x) { Ok(x) })
+}
+
+pub fn parse_grid_using(
+ str: String,
+ f: fn(String) -> Result(a, Nil),
+) -> Array2D(a) {
+ str
+ |> to_list_of_lists
+ |> to_2d_array_using(f)
+}
diff --git a/aoc2023-gleam/src/utilities/memo.gleam b/aoc2023-gleam/src/utilities/memo.gleam
new file mode 100644
index 0000000..b06d8fd
--- /dev/null
+++ b/aoc2023-gleam/src/utilities/memo.gleam
@@ -0,0 +1,57 @@
+import gleam/dict.{type Dict}
+import gleam/otp/actor.{type Next, Continue, Stop}
+import gleam/erlang/process.{type Subject, Normal}
+import gleam/option.{None}
+
+const timeout = 1000
+
+type Message(k, v) {
+ Shutdown
+ Get(key: k, client: Subject(Result(v, Nil)))
+ Set(key: k, value: v)
+}
+
+type Server(k, v) =
+ Subject(Message(k, v))
+
+pub opaque type Cache(k, v) {
+ Cache(server: Server(k, v))
+}
+
+fn handle_message(
+ message: Message(k, v),
+ dict: Dict(k, v),
+) -> Next(Message(k, v), Dict(k, v)) {
+ case message {
+ Shutdown -> Stop(Normal)
+ Get(key, client) -> {
+ process.send(client, dict.get(dict, key))
+ Continue(dict, None)
+ }
+ Set(key, value) -> Continue(dict.insert(dict, key, value), None)
+ }
+}
+
+pub fn create(apply fun: fn(Cache(k, v)) -> t) -> t {
+ let assert Ok(server) = actor.start(dict.new(), handle_message)
+ let result = fun(Cache(server))
+ process.send(server, Shutdown)
+ result
+}
+
+pub fn set(in cache: Cache(k, v), for key: k, insert value: v) -> Nil {
+ process.send(cache.server, Set(key, value))
+}
+
+pub fn get(from cache: Cache(k, v), fetch key: k) -> Result(v, Nil) {
+ process.call(cache.server, fn(c) { Get(key, c) }, timeout)
+}
+
+pub fn memoize(with cache: Cache(k, v), this key: k, apply fun: fn() -> v) -> v {
+ let result = case get(from: cache, fetch: key) {
+ Ok(value) -> value
+ Error(Nil) -> fun()
+ }
+ set(in: cache, for: key, insert: result)
+ result
+}
diff --git a/aoc2023-gleam/src/utilities/prioqueue.gleam b/aoc2023-gleam/src/utilities/prioqueue.gleam
new file mode 100644
index 0000000..abf21b9
--- /dev/null
+++ b/aoc2023-gleam/src/utilities/prioqueue.gleam
@@ -0,0 +1,64 @@
+//adapted from https://github.com/byronanderson/adventofcode2021/blob/main/gleam_advent/src/priority_queue.gleam
+
+import gleam/dict.{type Dict}
+
+type Ref
+
+@external(erlang, "erlang", "make_ref")
+fn make_ref() -> Ref
+
+type PQueue(a)
+
+pub opaque type PriorityQueue(a) {
+ PriorityQueue(queue: PQueue(#(a, Ref)), refs: Dict(a, Ref))
+}
+
+type OutResult(a) {
+ Empty
+ Value(a, Int)
+}
+
+@external(erlang, "pqueue2", "new")
+fn new_() -> PQueue(a)
+
+@external(erlang, "pqueue2", "in")
+fn insert_(item: a, prio: Int, queue: PQueue(a)) -> PQueue(a)
+
+@external(erlang, "pqueue2", "pout")
+fn pop_(queue: PQueue(a)) -> #(OutResult(a), PQueue(a))
+
+pub fn new() -> PriorityQueue(a) {
+ PriorityQueue(queue: new_(), refs: dict.new())
+}
+
+pub fn insert(
+ queue: PriorityQueue(a),
+ value: a,
+ priority: Int,
+) -> PriorityQueue(a) {
+ let ref = make_ref()
+
+ let refs =
+ queue.refs
+ |> dict.insert(value, ref)
+
+ PriorityQueue(
+ refs: refs,
+ queue: insert_(#(value, ref), priority, queue.queue),
+ )
+}
+
+pub fn pop(queue: PriorityQueue(a)) -> Result(#(a, PriorityQueue(a)), Nil) {
+ case pop_(queue.queue) {
+ #(Value(#(value, ref), _priority), pqueue) -> {
+ let assert Ok(recently_enqueued_ref) = dict.get(queue.refs, value)
+ case recently_enqueued_ref == ref {
+ True -> Ok(#(value, PriorityQueue(refs: queue.refs, queue: pqueue)))
+ False -> pop(PriorityQueue(refs: queue.refs, queue: pqueue))
+ }
+ }
+ #(Empty, _pqueue) -> {
+ Error(Nil)
+ }
+ }
+}
diff --git a/aoc2023-gleam/test/aoc2023_test.gleam b/aoc2023-gleam/test/aoc2023_test.gleam
new file mode 100644
index 0000000..2b696a4
--- /dev/null
+++ b/aoc2023-gleam/test/aoc2023_test.gleam
@@ -0,0 +1,5 @@
+import showtime
+
+pub fn main() {
+ showtime.main()
+}
diff --git a/aoc2023-gleam/test/day1/day1_test.gleam b/aoc2023-gleam/test/day1/day1_test.gleam
new file mode 100644
index 0000000..374653c
--- /dev/null
+++ b/aoc2023-gleam/test/day1/day1_test.gleam
@@ -0,0 +1,57 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day1/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "1abc2
+pqr3stu8vwx
+a1b2c3d4e5f
+treb7uchet",
+ "142",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "two1nine
+eightwothree
+abcone2threexyz
+xtwone3four
+4nineeightseven2
+zoneight234
+7pqrstsixteen",
+ "281",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day10/day10_test.gleam b/aoc2023-gleam/test/day10/day10_test.gleam
new file mode 100644
index 0000000..be9d82e
--- /dev/null
+++ b/aoc2023-gleam/test/day10/day10_test.gleam
@@ -0,0 +1,60 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day10/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "7-F7-
+.FJ|7
+SJLL7
+|F--J
+LJ.LJ",
+ "8",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "...........
+.S-------7.
+.|F-----7|.
+.||OOOOO||.
+.||OOOOO||.
+.|L-7OF-J|.
+.|II|O|II|.
+.L--JOL--J.
+.....O.....",
+ "4",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day11/day11_test.gleam b/aoc2023-gleam/test/day11/day11_test.gleam
new file mode 100644
index 0000000..8bb8c06
--- /dev/null
+++ b/aoc2023-gleam/test/day11/day11_test.gleam
@@ -0,0 +1,66 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day11/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "...#......
+.......#..
+#.........
+..........
+......#...
+.#........
+.........#
+..........
+.......#..
+#...#.....",
+ "374",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "...#......
+.......#..
+#.........
+..........
+......#...
+.#........
+.........#
+..........
+.......#..
+#...#.....",
+ "8410",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day12/day12_test.gleam b/aoc2023-gleam/test/day12/day12_test.gleam
new file mode 100644
index 0000000..3daf0e9
--- /dev/null
+++ b/aoc2023-gleam/test/day12/day12_test.gleam
@@ -0,0 +1,48 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day12/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "???.### 1,1,3
+.??..??...?##. 1,1,3
+?#?#?#?#?#?#?#? 1,3,1,6
+????.#...#... 4,1,1
+????.######..#####. 1,6,5
+?###???????? 3,2,1",
+ "21",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = []
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day13/day13_test.gleam b/aoc2023-gleam/test/day13/day13_test.gleam
new file mode 100644
index 0000000..7c65bed
--- /dev/null
+++ b/aoc2023-gleam/test/day13/day13_test.gleam
@@ -0,0 +1,76 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day13/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "#.##..##.
+..#.##.#.
+##......#
+##......#
+..#.##.#.
+..##..##.
+#.#.##.#.
+
+#...##..#
+#....#..#
+..##..###
+#####.##.
+#####.##.
+..##..###
+#....#..#",
+ "405",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "#.##..##.
+..#.##.#.
+##......#
+##......#
+..#.##.#.
+..##..##.
+#.#.##.#.
+
+#...##..#
+#....#..#
+..##..###
+#####.##.
+#####.##.
+..##..###
+#....#..#",
+ "400",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day14/day14_test.gleam b/aoc2023-gleam/test/day14/day14_test.gleam
new file mode 100644
index 0000000..8efa74e
--- /dev/null
+++ b/aoc2023-gleam/test/day14/day14_test.gleam
@@ -0,0 +1,66 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day14/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "O....#....
+O.OO#....#
+.....##...
+OO.#O....O
+.O.....O#.
+O.#..O.#.#
+..O..#O..O
+.......O..
+#....###..
+#OO..#....",
+ "136",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "O....#....
+O.OO#....#
+.....##...
+OO.#O....O
+.O.....O#.
+O.#..O.#.#
+..O..#O..O
+.......O..
+#....###..
+#OO..#....",
+ "64",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day15/day15_test.gleam b/aoc2023-gleam/test/day15/day15_test.gleam
new file mode 100644
index 0000000..0ecaecc
--- /dev/null
+++ b/aoc2023-gleam/test/day15/day15_test.gleam
@@ -0,0 +1,42 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day15/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example("rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7", "1320"),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example("rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7", "145"),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day16/day16_test.gleam b/aoc2023-gleam/test/day16/day16_test.gleam
new file mode 100644
index 0000000..036504e
--- /dev/null
+++ b/aoc2023-gleam/test/day16/day16_test.gleam
@@ -0,0 +1,66 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day16/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ ".|...\\....
+|.-.\\.....
+.....|-...
+........|.
+..........
+.........\\
+..../.\\\\..
+.-.-/..|..
+.|....-|.\\
+..//.|....",
+ 46,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ ".|...\\....
+|.-.\\.....
+.....|-...
+........|.
+..........
+.........\\
+..../.\\\\..
+.-.-/..|..
+.|....-|.\\
+..//.|....",
+ 51,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day17/day17_test.gleam b/aoc2023-gleam/test/day17/day17_test.gleam
new file mode 100644
index 0000000..2ce48e2
--- /dev/null
+++ b/aoc2023-gleam/test/day17/day17_test.gleam
@@ -0,0 +1,54 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day17/solve
+
+type Problem1AnswerType =
+ String
+
+// type Problem2AnswerType =
+// String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "2413432311323
+3215453535623
+3255245654254
+3446585845452
+4546657867536
+1438598798454
+4457876987766
+3637877979653
+4654967986887
+4564679986453
+1224686865563
+2546548887735
+4322674655533",
+ "102",
+ ),
+]
+
+// /// ```
+// const part2_examples: List(Example(Problem2AnswerType)) = []
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+// pub fn part2_test() {
+// part2_examples
+// |> should.not_equal([])
+// use example <- list.map(part2_examples)
+// solve.part2(example.input)
+// |> should.equal(example.answer)
+// }
diff --git a/aoc2023-gleam/test/day18/day18_test.gleam b/aoc2023-gleam/test/day18/day18_test.gleam
new file mode 100644
index 0000000..7b510c8
--- /dev/null
+++ b/aoc2023-gleam/test/day18/day18_test.gleam
@@ -0,0 +1,74 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day18/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "R 6 (#70c710)
+D 5 (#0dc571)
+L 2 (#5713f0)
+D 2 (#d2c081)
+R 2 (#59c680)
+D 2 (#411b91)
+L 5 (#8ceee2)
+U 2 (#caa173)
+L 1 (#1b58a2)
+U 2 (#caa171)
+R 2 (#7807d2)
+U 3 (#a77fa3)
+L 2 (#015232)
+U 2 (#7a21e3)",
+ "62",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "R 6 (#70c710)
+D 5 (#0dc571)
+L 2 (#5713f0)
+D 2 (#d2c081)
+R 2 (#59c680)
+D 2 (#411b91)
+L 5 (#8ceee2)
+U 2 (#caa173)
+L 1 (#1b58a2)
+U 2 (#caa171)
+R 2 (#7807d2)
+U 3 (#a77fa3)
+L 2 (#015232)
+U 2 (#7a21e3)",
+ "952408144115",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day19/day19_test.gleam b/aoc2023-gleam/test/day19/day19_test.gleam
new file mode 100644
index 0000000..c911de5
--- /dev/null
+++ b/aoc2023-gleam/test/day19/day19_test.gleam
@@ -0,0 +1,80 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day19/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "px{a<2006:qkq,m>2090:A,rfg}
+pv{a>1716:R,A}
+lnx{m>1548:A,A}
+rfg{s<537:gd,x>2440:R,A}
+qs{s>3448:A,lnx}
+qkq{x<1416:A,crn}
+crn{x>2662:A,R}
+in{s<1351:px,qqz}
+qqz{s>2770:qs,m<1801:hdj,R}
+gd{a>3333:R,R}
+hdj{m>838:A,pv}
+
+{x=787,m=2655,a=1222,s=2876}
+{x=1679,m=44,a=2067,s=496}
+{x=2036,m=264,a=79,s=2244}
+{x=2461,m=1339,a=466,s=291}
+{x=2127,m=1623,a=2188,s=1013}",
+ "19114",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "px{a<2006:qkq,m>2090:A,rfg}
+pv{a>1716:R,A}
+lnx{m>1548:A,A}
+rfg{s<537:gd,x>2440:R,A}
+qs{s>3448:A,lnx}
+qkq{x<1416:A,crn}
+crn{x>2662:A,R}
+in{s<1351:px,qqz}
+qqz{s>2770:qs,m<1801:hdj,R}
+gd{a>3333:R,R}
+hdj{m>838:A,pv}
+
+{x=787,m=2655,a=1222,s=2876}
+{x=1679,m=44,a=2067,s=496}
+{x=2036,m=264,a=79,s=2244}
+{x=2461,m=1339,a=466,s=291}
+{x=2127,m=1623,a=2188,s=1013}",
+ "167409079868000",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day2/day2_test.gleam b/aoc2023-gleam/test/day2/day2_test.gleam
new file mode 100644
index 0000000..28a65da
--- /dev/null
+++ b/aoc2023-gleam/test/day2/day2_test.gleam
@@ -0,0 +1,57 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day2/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
+ 8,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
+ 2286,
+ ),
+ Example("Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green", 48),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day20/day20_test.gleam b/aoc2023-gleam/test/day20/day20_test.gleam
new file mode 100644
index 0000000..9b79b05
--- /dev/null
+++ b/aoc2023-gleam/test/day20/day20_test.gleam
@@ -0,0 +1,55 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day20/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "broadcaster -> a, b, c
+%a -> b
+%b -> c
+%c -> inv
+&inv -> a",
+ "32000000",
+ ),
+ Example(
+ "broadcaster -> a
+%a -> inv, con
+&inv -> b
+%b -> con
+&con -> output
+output -> ",
+ "11687500",
+ ),
+]
+
+// const part2_examples: List(Example(Problem2AnswerType)) = []
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+// pub fn part2_test() {
+// part2_examples
+// |> should.not_equal([])
+// use example <- list.map(part2_examples)
+// solve.part2(example.input)
+// |> should.equal(example.answer)
+// }
diff --git a/aoc2023-gleam/test/day21/day21_test.gleam b/aoc2023-gleam/test/day21/day21_test.gleam
new file mode 100644
index 0000000..5f46808
--- /dev/null
+++ b/aoc2023-gleam/test/day21/day21_test.gleam
@@ -0,0 +1,38 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day21/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = []
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = []
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day22/day22_test.gleam b/aoc2023-gleam/test/day22/day22_test.gleam
new file mode 100644
index 0000000..3f8c0ca
--- /dev/null
+++ b/aoc2023-gleam/test/day22/day22_test.gleam
@@ -0,0 +1,60 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day22/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "1,0,1~1,2,1
+0,0,2~2,0,2
+0,2,3~2,2,3
+0,0,4~0,2,4
+2,0,5~2,2,5
+0,1,6~2,1,6
+1,1,8~1,1,9",
+ 5,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "1,0,1~1,2,1
+0,0,2~2,0,2
+0,2,3~2,2,3
+0,0,4~0,2,4
+2,0,5~2,2,5
+0,1,6~2,1,6
+1,1,8~1,1,9",
+ 7,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day23/day23_test.gleam b/aoc2023-gleam/test/day23/day23_test.gleam
new file mode 100644
index 0000000..206571c
--- /dev/null
+++ b/aoc2023-gleam/test/day23/day23_test.gleam
@@ -0,0 +1,92 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day23/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "#.#####################
+#.......#########...###
+#######.#########.#.###
+###.....#.>.>.###.#.###
+###v#####.#v#.###.#.###
+###.>...#.#.#.....#...#
+###v###.#.#.#########.#
+###...#.#.#.......#...#
+#####.#.#.#######.#.###
+#.....#.#.#.......#...#
+#.#####.#.#.#########v#
+#.#...#...#...###...>.#
+#.#.#v#######v###.###v#
+#...#.>.#...>.>.#.###.#
+#####v#.#.###v#.#.###.#
+#.....#...#...#.#.#...#
+#.#########.###.#.#.###
+#...###...#...#...#.###
+###.###.#.###v#####v###
+#...#...#.#.>.>.#.>.###
+#.###.###.#.###.#.#v###
+#.....###...###...#...#
+#####################.#",
+ 94,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "#.#####################
+#.......#########...###
+#######.#########.#.###
+###.....#.>.>.###.#.###
+###v#####.#v#.###.#.###
+###.>...#.#.#.....#...#
+###v###.#.#.#########.#
+###...#.#.#.......#...#
+#####.#.#.#######.#.###
+#.....#.#.#.......#...#
+#.#####.#.#.#########v#
+#.#...#...#...###...>.#
+#.#.#v#######v###.###v#
+#...#.>.#...>.>.#.###.#
+#####v#.#.###v#.#.###.#
+#.....#...#...#.#.#...#
+#.#########.###.#.#.###
+#...###...#...#...#.###
+###.###.#.###v#####v###
+#...#...#.#.>.>.#.>.###
+#.###.###.#.###.#.#v###
+#.....###...###...#...#
+#####################.#",
+ 154,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day3/day3_test.gleam b/aoc2023-gleam/test/day3/day3_test.gleam
new file mode 100644
index 0000000..30e17a9
--- /dev/null
+++ b/aoc2023-gleam/test/day3/day3_test.gleam
@@ -0,0 +1,66 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day3/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..",
+ 4361,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..",
+ 467_835,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day4/day4_test.gleam b/aoc2023-gleam/test/day4/day4_test.gleam
new file mode 100644
index 0000000..324fe36
--- /dev/null
+++ b/aoc2023-gleam/test/day4/day4_test.gleam
@@ -0,0 +1,58 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day4/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
+Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
+Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
+Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
+Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
+Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11",
+ 13,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
+Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
+Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
+Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
+Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
+Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11",
+ 30,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day5/day5_test.gleam b/aoc2023-gleam/test/day5/day5_test.gleam
new file mode 100644
index 0000000..86a8692
--- /dev/null
+++ b/aoc2023-gleam/test/day5/day5_test.gleam
@@ -0,0 +1,112 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day5/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "seeds: 79 14 55 13
+
+seed-to-soil map:
+50 98 2
+52 50 48
+
+soil-to-fertilizer map:
+0 15 37
+37 52 2
+39 0 15
+
+fertilizer-to-water map:
+49 53 8
+0 11 42
+42 0 7
+57 7 4
+
+water-to-light map:
+88 18 7
+18 25 70
+
+light-to-temperature map:
+45 77 23
+81 45 19
+68 64 13
+
+temperature-to-humidity map:
+0 69 1
+1 0 69
+
+humidity-to-location map:
+60 56 37
+56 93 4",
+ "35",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "seeds: 79 14 55 13
+
+seed-to-soil map:
+50 98 2
+52 50 48
+
+soil-to-fertilizer map:
+0 15 37
+37 52 2
+39 0 15
+
+fertilizer-to-water map:
+49 53 8
+0 11 42
+42 0 7
+57 7 4
+
+water-to-light map:
+88 18 7
+18 25 70
+
+light-to-temperature map:
+45 77 23
+81 45 19
+68 64 13
+
+temperature-to-humidity map:
+0 69 1
+1 0 69
+
+humidity-to-location map:
+60 56 37
+56 93 4",
+ "46",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day6/day6_test.gleam b/aoc2023-gleam/test/day6/day6_test.gleam
new file mode 100644
index 0000000..c551993
--- /dev/null
+++ b/aoc2023-gleam/test/day6/day6_test.gleam
@@ -0,0 +1,50 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day6/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "Time: 7 15 30
+Distance: 9 40 200",
+ "288",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "Time: 7 15 30
+Distance: 9 40 200",
+ "71503",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day7/day7_test.gleam b/aoc2023-gleam/test/day7/day7_test.gleam
new file mode 100644
index 0000000..f7f8454
--- /dev/null
+++ b/aoc2023-gleam/test/day7/day7_test.gleam
@@ -0,0 +1,56 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day7/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "32T3K 765
+T55J5 684
+KK677 28
+KTJJT 220
+QQQJA 483",
+ "6440",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "32T3K 765
+T55J5 684
+KK677 28
+KTJJT 220
+QQQJA 483",
+ "5905",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day8/day8_test.gleam b/aoc2023-gleam/test/day8/day8_test.gleam
new file mode 100644
index 0000000..2cd499a
--- /dev/null
+++ b/aoc2023-gleam/test/day8/day8_test.gleam
@@ -0,0 +1,61 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day8/solve
+
+type Problem1AnswerType =
+ Int
+
+type Problem2AnswerType =
+ Int
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "LLR
+
+AAA = (BBB, BBB)
+BBB = (AAA, ZZZ)
+ZZZ = (ZZZ, ZZZ)",
+ 6,
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "LR
+
+11A = (11B, XXX)
+11B = (XXX, 11Z)
+11Z = (11B, XXX)
+22A = (22B, XXX)
+22B = (22C, 22C)
+22C = (22Z, 22Z)
+22Z = (22B, 22B)
+XXX = (XXX, XXX)",
+ 6,
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}
diff --git a/aoc2023-gleam/test/day9/day9_test.gleam b/aoc2023-gleam/test/day9/day9_test.gleam
new file mode 100644
index 0000000..84fd3ba
--- /dev/null
+++ b/aoc2023-gleam/test/day9/day9_test.gleam
@@ -0,0 +1,52 @@
+import gleam/list
+import showtime/tests/should
+import adglent.{type Example, Example}
+import day9/solve
+
+type Problem1AnswerType =
+ String
+
+type Problem2AnswerType =
+ String
+
+/// Add examples for part 1 here:
+/// ```gleam
+///const part1_examples: List(Example(Problem1AnswerType)) = [Example("some input", "")]
+/// ```
+const part1_examples: List(Example(Problem1AnswerType)) = [
+ Example(
+ "0 3 6 9 12 15
+1 3 6 10 15 21
+10 13 16 21 30 45",
+ "114",
+ ),
+]
+
+/// Add examples for part 2 here:
+/// ```gleam
+///const part2_examples: List(Example(Problem2AnswerType)) = [Example("some input", "")]
+/// ```
+const part2_examples: List(Example(Problem2AnswerType)) = [
+ Example(
+ "0 3 6 9 12 15
+1 3 6 10 15 21
+10 13 16 21 30 45",
+ "2",
+ ),
+]
+
+pub fn part1_test() {
+ part1_examples
+ |> should.not_equal([])
+ use example <- list.map(part1_examples)
+ solve.part1(example.input)
+ |> should.equal(example.answer)
+}
+
+pub fn part2_test() {
+ part2_examples
+ |> should.not_equal([])
+ use example <- list.map(part2_examples)
+ solve.part2(example.input)
+ |> should.equal(example.answer)
+}