🏡 index : ~doyle/aoc.git

author Jordan Doyle <jordan@doyle.la> 2023-12-30 20:21:43.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-12-30 20:21:43.0 +00:00:00
commit
8478a64dc0f5e1b5fdea627d2fea346d28ec84cb [patch]
tree
4b8b184f1f9eb913b9209c6fa6cee70c97c1da6f
parent
ca0effa3af21ddb37e730b42aa9aac406134e3f5
download
master.tar.gz

Fix sort order



Diff

 Cargo.toml                  |   4 ++--
 2022/03.clj                 |  50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2022/3.clj                  |  50 --------------------------------------------------
 2023/01.hs                  |  68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/02.hs                  |  84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/03.hs                  |  87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/04.hs                  |  81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/05.rs                  | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/06.hs                  |  46 ++++++++++++++++++++++++++++++++++++++++++++++
 2023/07.hs                  | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/08.hs                  |  83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/09.hs                  |  69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2023/1.hs                   |  68 --------------------------------------------------------------------
 2023/2.hs                   |  84 --------------------------------------------------------------------------------
 2023/3.hs                   |  87 --------------------------------------------------------------------------------
 2023/4.hs                   |  81 --------------------------------------------------------------------------------
 2023/5.rs                   | 246 --------------------------------------------------------------------------------
 2023/6.hs                   |  46 ----------------------------------------------
 2023/7.hs                   | 108 --------------------------------------------------------------------------------
 2023/8.hs                   |  83 --------------------------------------------------------------------------------
 2023/9.hs                   |  69 ---------------------------------------------------------------------
 2022/01/default.nix         |  17 +++++++++++++++++
 2022/01/main.f90            |  90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2022/02/default.nix         |  49 +++++++++++++++++++++++++++++++++++++++++++++++++
 2022/04/4.jsonnet           |  18 ++++++++++++++++++
 2022/04/default.nix         |  14 ++++++++++++++
 2022/05/.terraform.lock.hcl |  19 +++++++++++++++++++
 2022/05/default.nix         |  19 +++++++++++++++++++
 2022/05/main.tf.jinja2      |  54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2022/06/.gitignore          |   2 ++
 2022/06/apache2.conf        | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2022/06/read.sh             |   9 +++++++++
 2022/1/default.nix          |  17 -----------------
 2022/1/main.f90             |  90 --------------------------------------------------------------------------------
 2022/2/default.nix          |  49 -------------------------------------------------
 2022/4/4.jsonnet            |  18 ------------------
 2022/4/default.nix          |  14 --------------
 2022/5/.terraform.lock.hcl  |  19 -------------------
 2022/5/default.nix          |  19 -------------------
 2022/5/main.tf.jinja2       |  54 ------------------------------------------------------
 2022/6/.gitignore           |   2 --
 2022/6/apache2.conf         | 156 --------------------------------------------------------------------------------
 2022/6/read.sh              |   9 ---------
 2022/05/executor/main.tf    |  24 ++++++++++++++++++++++++
 2022/5/executor/main.tf     |  24 ------------------------
 45 files changed, 1395 insertions(+), 1395 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 4ac3eec..0f37151 100644
--- a/Cargo.toml
+++ a/Cargo.toml
@@ -23,8 +23,8 @@
path = "2023/12.rs"

[[bin]]
name = "aoc2023-5"
path = "2023/5.rs"
name = "aoc2023-05"
path = "2023/05.rs"

[profile.release]
overflow-checks = true
diff --git a/2022/03.clj b/2022/03.clj
new file mode 100755
index 0000000..bc478b2 100755
--- /dev/null
+++ a/2022/03.clj
@@ -1,0 +1,50 @@
#!/usr/bin/env nix-shell

#!nix-shell --pure -i bb -p babashka


(require '[clojure.java.io :as io])
(require '[clojure.set :as set])

; a-z -> 1->26, A-Z -> 27->52

(defn char-to-number [char]
  (cond
    (Character/isLowerCase char) (- (int char) 96)
    (Character/isUpperCase char) (- (int char) 38)))

; splits a string into two sets of characters

(defn split-half [s]
  (let [idx (quot (count s) 2)
        [left right] (split-at idx s)]
    [(set (apply str left)) (set (apply str right))]))

; splits a string in half, stores it in a set and finds the intersection

(defn part1-process-line [line]
  (->> (split-half line)
       (apply set/intersection)))

; processes each line and figures out the "priority" of the intersection

(defn part1 [lines]
  (->> lines
       (map part1-process-line)
       (mapcat identity)
       (map char-to-number)
       (reduce +)))

; processes each line and figures out which characters are shared between

; 3 lines and sums their "priority"

(defn part2-process-lines [group]
  (let [[g1 g2 g3] group]
    (->> (set/intersection (set g1) (set g2) (set g3))
         (map char-to-number)
         (reduce +))))

; splits the input up into 3 lines, processes the "priority" and sums

(defn part2 [lines]
  (->> (partition 3 lines)
       (map part2-process-lines)
       (reduce +)))

; read the entirety of stdin

(def lines (with-open [rdr (io/reader *in*)] (vec (->> (line-seq rdr)))))

(println (part1 lines))
(println (part2 lines))
diff --git a/2022/3.clj b/2022/3.clj
deleted file mode 100755
index bc478b2..0000000 100755
--- a/2022/3.clj
+++ /dev/null
@@ -1,50 +1,0 @@
#!/usr/bin/env nix-shell

#!nix-shell --pure -i bb -p babashka


(require '[clojure.java.io :as io])
(require '[clojure.set :as set])

; a-z -> 1->26, A-Z -> 27->52

(defn char-to-number [char]
  (cond
    (Character/isLowerCase char) (- (int char) 96)
    (Character/isUpperCase char) (- (int char) 38)))

; splits a string into two sets of characters

(defn split-half [s]
  (let [idx (quot (count s) 2)
        [left right] (split-at idx s)]
    [(set (apply str left)) (set (apply str right))]))

; splits a string in half, stores it in a set and finds the intersection

(defn part1-process-line [line]
  (->> (split-half line)
       (apply set/intersection)))

; processes each line and figures out the "priority" of the intersection

(defn part1 [lines]
  (->> lines
       (map part1-process-line)
       (mapcat identity)
       (map char-to-number)
       (reduce +)))

; processes each line and figures out which characters are shared between

; 3 lines and sums their "priority"

(defn part2-process-lines [group]
  (let [[g1 g2 g3] group]
    (->> (set/intersection (set g1) (set g2) (set g3))
         (map char-to-number)
         (reduce +))))

; splits the input up into 3 lines, processes the "priority" and sums

(defn part2 [lines]
  (->> (partition 3 lines)
       (map part2-process-lines)
       (reduce +)))

; read the entirety of stdin

(def lines (with-open [rdr (io/reader *in*)] (vec (->> (line-seq rdr)))))

(println (part1 lines))
(println (part2 lines))
diff --git a/2023/01.hs b/2023/01.hs
new file mode 100755
index 0000000..1676ab4 100755
--- /dev/null
+++ a/2023/01.hs
@@ -1,0 +1,68 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"
import Data.List (find, isInfixOf)
import Data.Maybe (catMaybes)

{- https://adventofcode.com/2023/day/1 -}

main = print =<< run 0

-- recursively read each line from stdin, concatenating first and last digits and folding the result into a sum
run :: Int -> IO Int
run acc = do
  line <- getLine
  if null line
    then return acc
    else do
      let x = concatFirstLastDigitsInString line
      run $ acc + x

-- read first and last digit in a string and concatenate the two together
concatFirstLastDigitsInString :: String -> Int
concatFirstLastDigitsInString s =

  case catMaybes [findDigitFromLeft "" s, findDigitFromRight "" s] of
    [x, y] -> x * 10 + y
    [x] -> x * 11
    _ -> 0

-- find the first digit in the string, searching from the left hand side
findDigitFromLeft :: String -> String -> Maybe Int
findDigitFromLeft acc "" = findDigit acc
findDigitFromLeft acc (x : xs) = case findDigit acc of
  Just v -> Just v
  Nothing -> findDigitFromLeft (acc ++ [x]) xs

-- find the last digit in the string, searching from the right hand side
findDigitFromRight :: String -> String -> Maybe Int
findDigitFromRight acc "" = findDigit acc
findDigitFromRight acc xs = case findDigit acc of
  Just v -> Just v
  Nothing -> findDigitFromRight (last xs : acc) (init xs)

-- finds a digit in either textual or numeric form and returns it as an int
findDigit :: String -> Maybe Int
findDigit s = case find (`isInfixOf` s) digitAsText of
  Just textual -> lookup textual digitMap
  Nothing -> Nothing
  where
    digitMap =

      [ ("eight", 8),
        ("seven", 7),
        ("three", 3),
        ("nine", 9),
        ("four", 4),
        ("five", 5),
        ("two", 2),
        ("one", 1),
        ("six", 6),
        ("1", 1),
        ("2", 2),
        ("3", 3),
        ("4", 4),
        ("5", 5),
        ("6", 6),
        ("7", 7),
        ("8", 8),
        ("9", 9)
      ]
    digitAsText = map fst digitMap
diff --git a/2023/02.hs b/2023/02.hs
new file mode 100755
index 0000000..68ba85b 100755
--- /dev/null
+++ a/2023/02.hs
@@ -1,0 +1,84 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Control.Applicative ((<*))
import Data.Map (Map, elems, fromListWith)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/2 -}

main = do
  input <- getContents
  case parseString input of
    Left err -> print err
    Right games -> do
      part1PrintValidGamesMaxCubes games
      part2PrintMinimumRequiredCubes games

-- print the sum of game ids that can be played with `cubesAllowed` cubes
part1PrintValidGamesMaxCubes :: [Game] -> IO ()
part1PrintValidGamesMaxCubes games = do
  print $ sum $ map gameId (filter checkGameIsValid games)

-- print the sum of the "power" required to play each game (which is just the product of max(amount) per colour)
part2PrintMinimumRequiredCubes :: [Game] -> IO ()
part2PrintMinimumRequiredCubes games = do
  print $ sum $ map (product . elems . getMinimumCubesRequiredForGame) games

-- fold every round in a game into map<string, int> where string is a colour and int is the max cubes for the colour
getMinimumCubesRequiredForGame :: Game -> Map String Int
getMinimumCubesRequiredForGame game = fromListWith max $ concat (rounds game)

-- check if every colourset pulled within a game is within the bounds of `cubesAllowed`
checkGameIsValid :: Game -> Bool
checkGameIsValid game = all (all isCubeAmountAllowed) (rounds game)

-- check if the given colour, amount tuple is within the allowed range
isCubeAmountAllowed :: (String, Int) -> Bool
isCubeAmountAllowed (colour, amount) = amount <= cubesAllowed colour

-- consts set by the task
cubesAllowed "red" = 12
cubesAllowed "green" = 13
cubesAllowed "blue" = 14
cubesAllowed _ = 0

data Game = Game
  { gameId :: Int,
    rounds :: [[(String, Int)]]
  }
  deriving (Show)

-- parse `Game [n]: [n] [colour], [n] [colour], ...; [n] [colour]; Game [n]...`
parseString :: String -> Either ParseError [Game]
parseString = parse fullParser ""

fullParser :: Parser [Game]
fullParser = gameParser `sepBy` char '\n'

-- parse a single game
gameParser :: Parser Game
gameParser = do
  _ <- string "Game "
  gameId <- many1 digit <* char ':' <* spaces
  rounds <- roundParser `sepBy` (char ';' <* spaces)

  return
    Game
      { gameId = read gameId,
        rounds
      }

-- parse all the colour, count tuples in a given round
roundParser :: Parser [(String, Int)]
roundParser = cubeNumberParser `sepBy` (char ',' <* spaces)

-- parse a single colour, count tuple
cubeNumberParser :: Parser (String, Int)
cubeNumberParser = do
  amount <- many1 digit <* spaces
  colour <- many1 letter
  return (colour, read amount)
diff --git a/2023/03.hs b/2023/03.hs
new file mode 100755
index 0000000..b3a1d06 100755
--- /dev/null
+++ a/2023/03.hs
@@ -1,0 +1,87 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"
import Data.Char (isNumber)
import Data.List (findIndices)

{- https://adventofcode.com/2023/day/3 -}

main = do
  input <- getContents
  let (schematics, symbols) = takeSymbols input
  print $ sum $ part1FindNumbersAdjacentToSchematic schematics symbols
  print $ sum $ map product $ part2FindGearPartNumbers schematics symbols

-- find all the part numbers (schematics that are adjacent to a symbol)
part1FindNumbersAdjacentToSchematic :: [Schematic] -> [Symbol] -> [Int]
part1FindNumbersAdjacentToSchematic schematics symbols = map partNumber $ filter (isSchematicAdjacentToAnySymbol symbols) schematics

-- find all part numbers for gears (schematics that are adjacent to exactly two symbols)
part2FindGearPartNumbers :: [Schematic] -> [Symbol] -> [[Int]]
part2FindGearPartNumbers schematics symbols = map (map partNumber) $ filter (\inner -> length inner == 2) $ map (findSchematicsAdjacentToSymbol schematics) symbols

-- returns all schematics that are adjacent to the given symbol
findSchematicsAdjacentToSymbol :: [Schematic] -> Symbol -> [Schematic]
findSchematicsAdjacentToSymbol schematics symbol = filter (`isSchematicAdjacent` symbol) schematics

-- returns true, if the given schematic is adjacent to any of the given symbols
isSchematicAdjacentToAnySymbol :: [Symbol] -> Schematic -> Bool
isSchematicAdjacentToAnySymbol symbol schematic = any (isSchematicAdjacent schematic) symbol

-- returns true, if the given schematic is directly adjacent to the given symbol
isSchematicAdjacent :: Schematic -> Symbol -> Bool
isSchematicAdjacent sc sy = isAdjacent (symbolCoords sy) (schematicCoords sc)

-- returns true, grid position (px, py) is adjacent to single height multi column grid position (rx, ry, width)
isAdjacent :: (Int, Int) -> (Int, Int, Int) -> Bool
isAdjacent (px, py) (rx, ry, width) =

  let leftX = rx
      rightX = rx + width - 1
      upperY = ry + 1
      lowerY = ry - 1
   in (px >= leftX - 1 && px <= rightX + 1 && py == ry) -- adjacent horizontally
        || (py == upperY || py == lowerY) && (px >= leftX && px <= rightX) -- adjacent vertically
        || (px == leftX - 1 || px == rightX + 1) && (py == upperY || py == lowerY) -- adjacent diagonally

data Schematic = Schematic
  { partNumber :: Int,
    schematicCoords :: (Int, Int, Int)
  }
  deriving (Show)

data Symbol = Symbol
  { symbolCoords :: (Int, Int),
    symbolType :: Char
  }
  deriving (Show)

-- parse the entire input
takeSymbols :: String -> ([Schematic], [Symbol])
takeSymbols input = takeSymbol input Nothing (0, 0) [] []

-- recursively parses the input character by character
takeSymbol :: String -> Maybe Schematic -> (Int, Int) -> [Schematic] -> [Symbol] -> ([Schematic], [Symbol])
takeSymbol "" Nothing _ schematicAcc symbolAcc = (schematicAcc, symbolAcc)
takeSymbol "" (Just inProgressSchematic) _ schematicAcc symbolAcc = (schematicAcc ++ [inProgressSchematic], symbolAcc)
takeSymbol (x : xs) inProgressSchematic (posX, posY) schematicAcc symbolAcc =

  case x of
    _ | isNumber x -> takeSymbol xs (Just $ appendToSchematic inProgressSchematic (posX, posY) (read [x])) (posX + 1, posY) schematicAcc symbolAcc
    '.' -> takeSymbol xs Nothing (posX + 1, posY) (maybeAppend schematicAcc inProgressSchematic) symbolAcc
    '\n' -> takeSymbol xs Nothing (0, posY + 1) (maybeAppend schematicAcc inProgressSchematic) symbolAcc
    _ -> takeSymbol xs Nothing (posX + 1, posY) (maybeAppend schematicAcc inProgressSchematic) (symbolAcc ++ [buildSymbol posX posY x])

-- appends a character to the schematic, creating a new schematic if one isn't already instantiated
appendToSchematic :: Maybe Schematic -> (Int, Int) -> Int -> Schematic
appendToSchematic (Just schematic) _ c =

  let (x, y, n) = schematicCoords schematic
      currPartNumber = partNumber schematic
   in schematic {partNumber = currPartNumber * 10 + c, schematicCoords = (x, y, n + 1)}
appendToSchematic Nothing (x, y) c = Schematic {partNumber = c, schematicCoords = (x, y, 1)}

-- append a Maybe Schematic to [Schematic]
maybeAppend :: [Schematic] -> Maybe Schematic -> [Schematic]
maybeAppend out (Just new) = out ++ [new]
maybeAppend out Nothing = out

-- easy constructor for a Symbol
buildSymbol :: Int -> Int -> Char -> Symbol
buildSymbol x y symbolType = Symbol {symbolCoords = (x, y), symbolType}
diff --git a/2023/04.hs b/2023/04.hs
new file mode 100755
index 0000000..28339ac 100755
--- /dev/null
+++ a/2023/04.hs
@@ -1,0 +1,81 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Data.List (intersect)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/4 -}

main = do
  cards <- readAndParseStdin []
  print $ part1 cards
  print $ part2 cards

-- sum up amount of winning numbers using the part1Score formula
part1 :: [Card] -> Int
part1 cards = sum $ map (score . length . getWinningNumbers) cards
  where
    score 0 = 0
    score n = 2 ^ (n - 1)

-- calculates number of cards won in part 2 of the task
part2 :: [Card] -> Int
part2 cards = sum $ calculateCardCopies $ map (length . getWinningNumbers) cards

-- starts with a base array of [1; n] and replicates Ns right for each `getWinningNumbers`
-- where N is the amount of card replicas
calculateCardCopies :: [Int] -> [Int]
calculateCardCopies xs = foldl replicateSingleCardWinnings (replicate (length xs) 1) (zip [0 ..] xs)

-- helper function for calling `copyCards` within `foldl`
replicateSingleCardWinnings :: [Int] -> (Int, Int) -> [Int]
replicateSingleCardWinnings cardReplicas (idx, winningNumbers) = copyCards cardReplicas (idx + 1) idx winningNumbers

-- copies N cards to `winningNumbers` elements right of `winningCardIdx` where N is `cardReplicas[winningCardIdx]`
copyCards :: [Int] -> Int -> Int -> Int -> [Int]
copyCards cardReplicas currIdx winningCardIdx winningNumbers
  | currIdx <= length cardReplicas && winningNumbers > 0 =

      let incrementedList = incrementAtIndex cardReplicas currIdx (cardReplicas !! winningCardIdx)
       in copyCards incrementedList (currIdx + 1) winningCardIdx (winningNumbers - 1)
  | otherwise = cardReplicas

-- takes a list, an index and an amount to increment by
incrementAtIndex :: [Int] -> Int -> Int -> [Int]
incrementAtIndex xs idx amount = take idx xs ++ [(xs !! idx) + amount] ++ drop (idx + 1) xs

-- gets the intersection of winning numbers and player numbers
getWinningNumbers :: Card -> [Int]
getWinningNumbers card = myNumbers card `intersect` winningNumbers card

data Card = Card
  { winningNumbers :: [Int],
    myNumbers :: [Int]
  }
  deriving (Show)

-- reads entirety of stdin and parses each line
readAndParseStdin :: [Card] -> IO [Card]
readAndParseStdin acc = do
  line <- getLine
  if null line
    then return acc
    else case parse cardParser "" line of
      Left parseError -> error $ show parseError
      Right card -> readAndParseStdin $ acc ++ [card]

-- parses a `Card [i]: [n1] [n2] [n3] | [n4] [n5] [n6]` line
cardParser :: Parser Card
cardParser = do
  _ <- string "Card" <* spaces <* many1 digit <* char ':' <* spaces
  winningNumbers <- numberParser
  _ <- char '|' <* spaces
  myNumbers <- numberParser

  return Card {winningNumbers, myNumbers}

-- reads a single number delimited by spaces
numberParser :: Parser [Int]
numberParser = map read <$> many1 digit `endBy` spaces
diff --git a/2023/05.rs b/2023/05.rs
new file mode 100755
index 0000000..a9327c9 100755
--- /dev/null
+++ a/2023/05.rs
@@ -1,0 +1,246 @@
use std::{collections::HashMap, io::Read, ops::Range, str::FromStr, time::Instant};

use itertools::Itertools;
use nom::IResult;
use rangemap::RangeMap;

const TRANSLATION_PATH: &[MapKind] = &[
    MapKind::Soil,
    MapKind::Fertilizer,
    MapKind::Water,
    MapKind::Light,
    MapKind::Temperature,
    MapKind::Humidity,
    MapKind::Location,
];

fn main() {
    let mut input = Vec::new();
    std::io::stdin().lock().read_to_end(&mut input).unwrap();
    let input = std::str::from_utf8(&input).unwrap();

    let (rest, input) = parse_input(input).unwrap();
    assert!(rest.is_empty());

    let i = Instant::now();
    let answer = part1(&input);
    eprintln!("part 1: {answer} ({:?})", i.elapsed());

    let i = Instant::now();
    let answer = part2(&input);
    eprintln!("part 2: {answer} ({:?})", i.elapsed());
}

fn part1(input: &Input) -> u64 {
    let mut lowest_location = u64::MAX;

    for seed in &input.seeds {
        let mut source = *seed;
        let mut from = MapKind::Seed;

        for to in TRANSLATION_PATH {
            let Some(translation) = input.maps.get(&Translation { from, to: *to }) else {
                panic!("invalid path {from:?} to {to:?}");
            };

            if let Some((source_range, destination_base)) = translation.get_key_value(&source) {
                source = destination_base + (source - source_range.start);
            }

            from = *to;
        }

        assert_eq!(from, MapKind::Location);
        lowest_location = lowest_location.min(source);
    }

    lowest_location
}

fn part2(input: &Input) -> u64 {
    let seed_ranges: Vec<_> = input
        .seeds
        .iter()
        .tuples()
        .map(|(start, len)| (*start)..(*start) + len)
        .collect();

    let mut lowest_bound_seen = u64::MAX;

    for seed_range in seed_ranges {
        let lowest_for_seed = traverse_path(input, TRANSLATION_PATH, MapKind::Seed, seed_range);
        lowest_bound_seen = lowest_bound_seen.min(lowest_for_seed);
    }

    lowest_bound_seen
}

fn traverse_path(input: &Input, path: &[MapKind], from: MapKind, source_range: Range<u64>) -> u64 {
    let mut lowest_bound_seen = u64::MAX;

    let Some((next_path, rest)) = path.split_first() else {
        return source_range.start;
    };

    let Some(translation) = input.maps.get(&Translation {
        from,
        to: *next_path,
    }) else {
        panic!("invalid path {from:?} to {next_path:?}");
    };

    for (new_source_range, destination_base) in translation.overlapping(&source_range) {
        // determine intersection between the source range and destination range
        let start = source_range.start.max(new_source_range.start);
        let end = source_range.end.min(new_source_range.end);
        let offset = start.saturating_sub(new_source_range.start);
        let length = end.saturating_sub(start);

        let destination_range = (*destination_base + offset)..(*destination_base + offset + length);

        let lowest_in_tree = traverse_path(input, rest, *next_path, destination_range);

        lowest_bound_seen = lowest_bound_seen.min(lowest_in_tree);
    }

    // traverse any uncovered sources, which the spec allows us to use our
    // destination number directly for
    for uncovered_range in split_range(
        source_range.clone(),
        translation
            .overlapping(&source_range)
            .map(|v| v.0.clone())
            .collect(),
    ) {
        let current_range = traverse_path(input, rest, *next_path, uncovered_range);
        lowest_bound_seen = lowest_bound_seen.min(current_range);
    }

    lowest_bound_seen
}

/// Splits `main_range` into multiple ranges not covered by `ranges`.

fn split_range(main_range: Range<u64>, mut ranges: Vec<Range<u64>>) -> Vec<Range<u64>> {
    let mut non_intersecting_ranges = Vec::new();
    let mut current_start = main_range.start;

    ranges.sort_by_key(|r| r.start);

    for range in ranges {
        if range.start > current_start {
            non_intersecting_ranges.push(current_start..range.start);
        }

        if range.end > current_start {
            current_start = range.end;
        }
    }

    if current_start < main_range.end {
        non_intersecting_ranges.push(current_start..main_range.end);
    }

    non_intersecting_ranges
}

#[derive(strum::EnumString, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[strum(serialize_all = "kebab-case")]
enum MapKind {
    Seed,
    Soil,
    Fertilizer,
    Water,
    Light,
    Temperature,
    Humidity,
    Location,
}

#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
struct Translation {
    from: MapKind,
    to: MapKind,
}

impl From<(MapKind, MapKind)> for Translation {
    fn from((from, to): (MapKind, MapKind)) -> Self {
        Self { from, to }
    }
}

#[derive(Debug)]
struct Input {
    seeds: Vec<u64>,
    maps: HashMap<Translation, RangeMap<u64, u64>>,
}

/// parse entire input

fn parse_input(rest: &str) -> IResult<&str, Input> {
    use nom::{
        bytes::complete::tag, character::complete::digit1, combinator::map_res,
        multi::separated_list1, sequence::delimited,
    };

    let (rest, seeds) = delimited(
        tag("seeds: "),
        separated_list1(tag(" "), map_res(digit1, u64::from_str)),
        tag("\n\n"),
    )(rest)?;
    let (rest, maps) = separated_list1(tag("\n"), parse_single_map)(rest)?;

    Ok((
        rest,
        Input {
            seeds,
            maps: maps.into_iter().collect(),
        },
    ))
}

/// parse header along with each map line

fn parse_single_map(rest: &str) -> IResult<&str, (Translation, RangeMap<u64, u64>)> {
    use nom::multi::many1;

    let (rest, header) = parse_header(rest)?;
    let (rest, lines) = many1(parse_map_line)(rest)?;

    Ok((rest, (header, lines.into_iter().collect())))
}

/// parse `803774611 641364296 1132421037` line

fn parse_map_line(rest: &str) -> IResult<&str, (Range<u64>, u64)> {
    use nom::{
        branch::alt,
        bytes::complete::tag,
        character::complete::digit1,
        combinator::{eof, map_res},
        sequence::terminated,
    };

    let (rest, destination) = terminated(map_res(digit1, u64::from_str), tag(" "))(rest)?;
    let (rest, source) = terminated(map_res(digit1, u64::from_str), tag(" "))(rest)?;
    let (rest, size) = terminated(map_res(digit1, u64::from_str), alt((tag("\n"), eof)))(rest)?;

    Ok((rest, (source..source + size, destination)))
}

/// parse `seed-to-soil map:` line

fn parse_header(rest: &str) -> IResult<&str, Translation> {
    use nom::{
        bytes::complete::{tag, take_until},
        combinator::{map, map_res},
        sequence::{separated_pair, terminated},
    };

    map(
        terminated(
            separated_pair(
                map_res(take_until("-"), MapKind::from_str),
                tag("-to-"),
                map_res(take_until(" "), MapKind::from_str),
            ),
            tag(" map:\n"),
        ),
        Translation::from,
    )(rest)
}
diff --git a/2023/06.hs b/2023/06.hs
new file mode 100755
index 0000000..5836634 100755
--- /dev/null
+++ a/2023/06.hs
@@ -1,0 +1,46 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Control.Applicative ((<*))
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/6 -}

main = do
  game <- readAndParseStdin gameParser
  print $ part1 game
  print $ part2 game

-- returns the product of how many winning times there are per game
part1 :: [(Int, Int)] -> Int
part1 = product . map (length . getWinningTimes)
  where
    getWinningTimes (time, winCond) = filter (> winCond) $ map (`distance` time) [1 .. time - 1]

-- concatenates all games into one big game and returns how many winning times
-- there are in the big game
part2 :: [(Int, Int)] -> Int
part2 game = part1 [foldl concatenate (0, 0) game]
  where
    concatenate (tAcc, dAcc) (t, d) = (tAcc * scale t + t, dAcc * scale d + d)

-- calculates the "scale" of a number + 1 and returns the magnitude ie. 8 -> 10, 23 -> 100, 694 -> 1000
scale :: Int -> Int
scale n
  | n < 10 = 10
  | otherwise = 10 * scale (n `div` 10)

-- calculates distance travelled in a game based on velocity * time minus "button pressing time"
distance :: Int -> Int -> Int
distance v t = v * (t - v)

-- parses `Time: [n1] [n2] [n3]\nDistance:[n4] [n5] [n6]` and returns `[(n1, n4), (n2, n5), (n3, n6)]`
gameParser :: Parser [(Int, Int)]
gameParser = do
  time <- map read <$> (string "Time:" *> spaces *> many1 digit `endBy` spaces)
  distance <- map read <$> (string "Distance:" *> spaces *> many1 digit `endBy` spaces)
  return $ zip time distance
diff --git a/2023/07.hs b/2023/07.hs
new file mode 100755
index 0000000..564ce30 100755
--- /dev/null
+++ a/2023/07.hs
@@ -1,0 +1,108 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Data.List
import Data.Ord
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/7 -}

main :: IO ()
main = do
  games <- readAndParseStdin parseAllGames
  print $ part1 games
  print $ part2 games

-- sums the score of each game
part1 :: [Game] -> Int
part1 games = calcScore $ sortBy gameComparator games
  where
    gameComparator = comparing (Down . getHandStrength . groupCards . cards) <> comparing cards

-- maps jacks to jokers (lowest scoring card, which can transmorph itself to the highest scoring group)
-- and sums the score of each game
part2 :: [Game] -> Int
part2 games = calcScore $ sortBy gameComparator $ mapAllJacksToJokers games
  where
    gameComparator = comparing (Down . getHandStrength . transmorphJokers . cards) <> comparing cards

-- transforms every jack in a game to a joker
mapAllJacksToJokers :: [Game] -> [Game]
mapAllJacksToJokers = map (\game -> game {cards = map mapJackToJoker $ cards game})
  where
    mapJackToJoker Jack = Joker
    mapJackToJoker a = a

-- transmorphs jokers into whatever the highest scoring rank was
transmorphJokers :: [Rank] -> [Int]
transmorphJokers [Joker, Joker, Joker, Joker, Joker] = groupCards [Joker, Joker, Joker, Joker, Joker]
transmorphJokers cards = (head grouped + jokerCount) : tail grouped
  where
    cardsWithoutJokers = filter (/= Joker) cards
    jokerCount = length cards - length cardsWithoutJokers
    grouped = groupCards cardsWithoutJokers

-- calculates the final score of the game
calcScore :: [Game] -> Int
calcScore game = sum $ zipWith (curry formula) [1 ..] game
  where
    formula (idx, game) = baseScore game * idx

-- determines the strength of the hand based on cards in hand
getHandStrength :: [Int] -> HandStrength
getHandStrength sortedCardCount = case sortedCardCount of
  [5] -> FiveOfAKind
  [4, 1] -> FourOfAKind
  [3, 2] -> FullHouse
  (3 : _) -> ThreeOfAKind
  (2 : 2 : _) -> TwoPair
  (2 : _) -> OnePair
  _ -> HighCard

-- groups any ranks together, returning the amount of items per group. ie. [J, J, A, A, A, 1] would return [3, 2, 1]
groupCards :: [Rank] -> [Int]
groupCards = sortOn Down . map length . group . sort

-- parses each game delimited by a newline
parseAllGames :: Parser [Game]
parseAllGames = parseGame `endBy` char '\n'

-- parses games in the format `[C][C][C] 123`
parseGame :: Parser Game
parseGame = do
  cards <- many1 parseRank <* spaces
  baseScore <- read <$> many1 digit
  return Game {cards, baseScore}

-- parses a single card rank
parseRank :: Parser Rank
parseRank = do
  c <- oneOf "23456789TJQKA"
  return $ case c of
    '2' -> Two
    '3' -> Three
    '4' -> Four
    '5' -> Five
    '6' -> Six
    '7' -> Seven
    '8' -> Eight
    '9' -> Nine
    'T' -> Ten
    'J' -> Jack
    'Q' -> Queen
    'K' -> King
    'A' -> Ace

data Game = Game
  { cards :: [Rank],
    baseScore :: Int
  }
  deriving (Show)

data Rank = Joker | Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Ace deriving (Eq, Ord, Enum, Show)

data HandStrength = FiveOfAKind | FourOfAKind | FullHouse | ThreeOfAKind | TwoOfAKind | TwoPair | OnePair | HighCard deriving (Eq, Ord, Enum, Show)
diff --git a/2023/08.hs b/2023/08.hs
new file mode 100755
index 0000000..87781ec 100755
--- /dev/null
+++ a/2023/08.hs
@@ -1,0 +1,83 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Data.List
import qualified Data.Map as Map
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/8 -}

main = do
  document <- readAndParseStdin parseDocument
  print $ part1 document
  print $ part2 document

-- find the path from AAAA -> ZZZZ
part1 :: Document -> Int
part1 doc = traverseTreeUntilD doc (== "ZZZ") "AAA"

-- find the length of each startingPosition loop, find least common multiple between all
part2 :: Document -> Int
part2 doc = foldl1 lcm . map (traverseTreeUntilD doc ("Z" `isSuffixOf`)) $ startingPositions
  where
    startingPositions = filter ("A" `isSuffixOf`) $ (Map.keys . docMap) doc

-- helper function to call traverseTreeUntil with a document
traverseTreeUntilD :: Document -> (String -> Bool) -> String -> Int
traverseTreeUntilD doc = traverseTreeUntil (cycle $ directions doc) (docMap doc) 0

-- traverse the tree until the predicate is matched
traverseTreeUntil :: [Direction] -> Map.Map String (String, String) -> Int -> (String -> Bool) -> String -> Int
traverseTreeUntil (x : xs) m n predicate elem
  | predicate elem = n
  | otherwise = traverseTreeUntil xs m (n + 1) predicate (readNext getNode x)
  where
    getNode = case Map.lookup elem m of
      Just a -> a
      Nothing -> error (show elem)
    readNext (n, _) DirectionLeft = n
    readNext (_, n) DirectionRight = n

-- parse entire document
parseDocument :: Parser Document
parseDocument = do
  directions <- many1 parseDirection
  _ <- string "\n\n"
  nodes <- parseNode `endBy` char '\n'
  return
    Document
      { directions,
        docMap = Map.fromList nodes
      }

-- parse a single node `AAA = (BBB, CCC)`
parseNode :: Parser (String, (String, String))
parseNode = do
  node <- many1 alphaNum
  _ <- spaces <* char '=' <* spaces
  _ <- char '('
  left <- many1 alphaNum
  _ <- char ',' <* spaces
  right <- many1 alphaNum
  _ <- char ')'
  return (node, (left, right))

-- parse the direction string `LRLLLR`
parseDirection :: Parser Direction
parseDirection = do
  c <- oneOf "LR"
  return $ case c of
    'L' -> DirectionLeft
    'R' -> DirectionRight

data Document = Document
  { directions :: [Direction],
    docMap :: Map.Map String (String, String)
  }
  deriving (Show)

data Direction = DirectionLeft | DirectionRight deriving (Enum, Show)
diff --git a/2023/09.hs b/2023/09.hs
new file mode 100755
index 0000000..9e9dc8d 100755
--- /dev/null
+++ a/2023/09.hs
@@ -1,0 +1,69 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Control.Monad (guard)
import Data.List (unfoldr)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

main = do
  input <- readAndParseStdin parseInput
  print $ part1 input
  print $ part2 input

-- interpolate the next value on every input and sum them
part1 :: [[Int]] -> Int
part1 = sum . map (round . interpolateNext)

-- interpolate the previous value on every input and sum them
part2 :: [[Int]] -> Int
part2 = sum . map (round . interpolatePrevious)

-- helper function to call interpolatePolynomial for the next value
interpolateNext :: [Int] -> Double
interpolateNext i = interpolatePolynomial (length i) i

-- helper function to call interpolatePolynomial for the previous value
interpolatePrevious :: [Int] -> Double
interpolatePrevious = interpolatePolynomial (-1)

-- given an nth term and a sequence, calculate newton's polynomial and
-- interpolate the nth value for the sequence
interpolatePolynomial :: Int -> [Int] -> Double
interpolatePolynomial nth seq =

  let divDiff = (dividedDifference . buildDifferenceTable) seq
      initialValue = (1, 0)
      (_, val) = foldl foldFunction initialValue $ zip [0 ..] (tail divDiff)
   in head divDiff + val
  where
    foldFunction (productAcc, valueAcc) (idx, val) =

      let prod = productAcc * fromIntegral (nth - idx)
       in (prod, valueAcc + (val * prod))

-- calculate the divided differences from our table for newton's
-- polynomial
dividedDifference :: [[Int]] -> [Double]
dividedDifference table = [fromIntegral (head row) / fromIntegral (fac i) | (i, row) <- zip [0 ..] table]
  where
    fac i = product [1 .. i]

-- build the difference table of each input
buildDifferenceTable :: [Int] -> [[Int]]
buildDifferenceTable input = input : unfoldr buildRow input
  where
    zipPairs list = zip list $ tail list
    diffPairs = map $ uncurry subtract
    buildRow lst =

      let row = diffPairs $ zipPairs lst
       in guard (not $ null row) >> Just (row, row)

-- parse each input line
parseInput :: Parser [[Int]]
parseInput = parseSequence `sepBy` char '\n'

-- parse sequence of numbers
parseSequence :: Parser [Int]
parseSequence = map read <$> many1 (digit <|> char '-') `sepBy` char ' '
diff --git a/2023/1.hs b/2023/1.hs
deleted file mode 100755
index 1676ab4..0000000 100755
--- a/2023/1.hs
+++ /dev/null
@@ -1,68 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"
import Data.List (find, isInfixOf)
import Data.Maybe (catMaybes)

{- https://adventofcode.com/2023/day/1 -}

main = print =<< run 0

-- recursively read each line from stdin, concatenating first and last digits and folding the result into a sum
run :: Int -> IO Int
run acc = do
  line <- getLine
  if null line
    then return acc
    else do
      let x = concatFirstLastDigitsInString line
      run $ acc + x

-- read first and last digit in a string and concatenate the two together
concatFirstLastDigitsInString :: String -> Int
concatFirstLastDigitsInString s =

  case catMaybes [findDigitFromLeft "" s, findDigitFromRight "" s] of
    [x, y] -> x * 10 + y
    [x] -> x * 11
    _ -> 0

-- find the first digit in the string, searching from the left hand side
findDigitFromLeft :: String -> String -> Maybe Int
findDigitFromLeft acc "" = findDigit acc
findDigitFromLeft acc (x : xs) = case findDigit acc of
  Just v -> Just v
  Nothing -> findDigitFromLeft (acc ++ [x]) xs

-- find the last digit in the string, searching from the right hand side
findDigitFromRight :: String -> String -> Maybe Int
findDigitFromRight acc "" = findDigit acc
findDigitFromRight acc xs = case findDigit acc of
  Just v -> Just v
  Nothing -> findDigitFromRight (last xs : acc) (init xs)

-- finds a digit in either textual or numeric form and returns it as an int
findDigit :: String -> Maybe Int
findDigit s = case find (`isInfixOf` s) digitAsText of
  Just textual -> lookup textual digitMap
  Nothing -> Nothing
  where
    digitMap =

      [ ("eight", 8),
        ("seven", 7),
        ("three", 3),
        ("nine", 9),
        ("four", 4),
        ("five", 5),
        ("two", 2),
        ("one", 1),
        ("six", 6),
        ("1", 1),
        ("2", 2),
        ("3", 3),
        ("4", 4),
        ("5", 5),
        ("6", 6),
        ("7", 7),
        ("8", 8),
        ("9", 9)
      ]
    digitAsText = map fst digitMap
diff --git a/2023/2.hs b/2023/2.hs
deleted file mode 100755
index 68ba85b..0000000 100755
--- a/2023/2.hs
+++ /dev/null
@@ -1,84 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Control.Applicative ((<*))
import Data.Map (Map, elems, fromListWith)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/2 -}

main = do
  input <- getContents
  case parseString input of
    Left err -> print err
    Right games -> do
      part1PrintValidGamesMaxCubes games
      part2PrintMinimumRequiredCubes games

-- print the sum of game ids that can be played with `cubesAllowed` cubes
part1PrintValidGamesMaxCubes :: [Game] -> IO ()
part1PrintValidGamesMaxCubes games = do
  print $ sum $ map gameId (filter checkGameIsValid games)

-- print the sum of the "power" required to play each game (which is just the product of max(amount) per colour)
part2PrintMinimumRequiredCubes :: [Game] -> IO ()
part2PrintMinimumRequiredCubes games = do
  print $ sum $ map (product . elems . getMinimumCubesRequiredForGame) games

-- fold every round in a game into map<string, int> where string is a colour and int is the max cubes for the colour
getMinimumCubesRequiredForGame :: Game -> Map String Int
getMinimumCubesRequiredForGame game = fromListWith max $ concat (rounds game)

-- check if every colourset pulled within a game is within the bounds of `cubesAllowed`
checkGameIsValid :: Game -> Bool
checkGameIsValid game = all (all isCubeAmountAllowed) (rounds game)

-- check if the given colour, amount tuple is within the allowed range
isCubeAmountAllowed :: (String, Int) -> Bool
isCubeAmountAllowed (colour, amount) = amount <= cubesAllowed colour

-- consts set by the task
cubesAllowed "red" = 12
cubesAllowed "green" = 13
cubesAllowed "blue" = 14
cubesAllowed _ = 0

data Game = Game
  { gameId :: Int,
    rounds :: [[(String, Int)]]
  }
  deriving (Show)

-- parse `Game [n]: [n] [colour], [n] [colour], ...; [n] [colour]; Game [n]...`
parseString :: String -> Either ParseError [Game]
parseString = parse fullParser ""

fullParser :: Parser [Game]
fullParser = gameParser `sepBy` char '\n'

-- parse a single game
gameParser :: Parser Game
gameParser = do
  _ <- string "Game "
  gameId <- many1 digit <* char ':' <* spaces
  rounds <- roundParser `sepBy` (char ';' <* spaces)

  return
    Game
      { gameId = read gameId,
        rounds
      }

-- parse all the colour, count tuples in a given round
roundParser :: Parser [(String, Int)]
roundParser = cubeNumberParser `sepBy` (char ',' <* spaces)

-- parse a single colour, count tuple
cubeNumberParser :: Parser (String, Int)
cubeNumberParser = do
  amount <- many1 digit <* spaces
  colour <- many1 letter
  return (colour, read amount)
diff --git a/2023/3.hs b/2023/3.hs
deleted file mode 100755
index b3a1d06..0000000 100755
--- a/2023/3.hs
+++ /dev/null
@@ -1,87 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"
import Data.Char (isNumber)
import Data.List (findIndices)

{- https://adventofcode.com/2023/day/3 -}

main = do
  input <- getContents
  let (schematics, symbols) = takeSymbols input
  print $ sum $ part1FindNumbersAdjacentToSchematic schematics symbols
  print $ sum $ map product $ part2FindGearPartNumbers schematics symbols

-- find all the part numbers (schematics that are adjacent to a symbol)
part1FindNumbersAdjacentToSchematic :: [Schematic] -> [Symbol] -> [Int]
part1FindNumbersAdjacentToSchematic schematics symbols = map partNumber $ filter (isSchematicAdjacentToAnySymbol symbols) schematics

-- find all part numbers for gears (schematics that are adjacent to exactly two symbols)
part2FindGearPartNumbers :: [Schematic] -> [Symbol] -> [[Int]]
part2FindGearPartNumbers schematics symbols = map (map partNumber) $ filter (\inner -> length inner == 2) $ map (findSchematicsAdjacentToSymbol schematics) symbols

-- returns all schematics that are adjacent to the given symbol
findSchematicsAdjacentToSymbol :: [Schematic] -> Symbol -> [Schematic]
findSchematicsAdjacentToSymbol schematics symbol = filter (`isSchematicAdjacent` symbol) schematics

-- returns true, if the given schematic is adjacent to any of the given symbols
isSchematicAdjacentToAnySymbol :: [Symbol] -> Schematic -> Bool
isSchematicAdjacentToAnySymbol symbol schematic = any (isSchematicAdjacent schematic) symbol

-- returns true, if the given schematic is directly adjacent to the given symbol
isSchematicAdjacent :: Schematic -> Symbol -> Bool
isSchematicAdjacent sc sy = isAdjacent (symbolCoords sy) (schematicCoords sc)

-- returns true, grid position (px, py) is adjacent to single height multi column grid position (rx, ry, width)
isAdjacent :: (Int, Int) -> (Int, Int, Int) -> Bool
isAdjacent (px, py) (rx, ry, width) =

  let leftX = rx
      rightX = rx + width - 1
      upperY = ry + 1
      lowerY = ry - 1
   in (px >= leftX - 1 && px <= rightX + 1 && py == ry) -- adjacent horizontally
        || (py == upperY || py == lowerY) && (px >= leftX && px <= rightX) -- adjacent vertically
        || (px == leftX - 1 || px == rightX + 1) && (py == upperY || py == lowerY) -- adjacent diagonally

data Schematic = Schematic
  { partNumber :: Int,
    schematicCoords :: (Int, Int, Int)
  }
  deriving (Show)

data Symbol = Symbol
  { symbolCoords :: (Int, Int),
    symbolType :: Char
  }
  deriving (Show)

-- parse the entire input
takeSymbols :: String -> ([Schematic], [Symbol])
takeSymbols input = takeSymbol input Nothing (0, 0) [] []

-- recursively parses the input character by character
takeSymbol :: String -> Maybe Schematic -> (Int, Int) -> [Schematic] -> [Symbol] -> ([Schematic], [Symbol])
takeSymbol "" Nothing _ schematicAcc symbolAcc = (schematicAcc, symbolAcc)
takeSymbol "" (Just inProgressSchematic) _ schematicAcc symbolAcc = (schematicAcc ++ [inProgressSchematic], symbolAcc)
takeSymbol (x : xs) inProgressSchematic (posX, posY) schematicAcc symbolAcc =

  case x of
    _ | isNumber x -> takeSymbol xs (Just $ appendToSchematic inProgressSchematic (posX, posY) (read [x])) (posX + 1, posY) schematicAcc symbolAcc
    '.' -> takeSymbol xs Nothing (posX + 1, posY) (maybeAppend schematicAcc inProgressSchematic) symbolAcc
    '\n' -> takeSymbol xs Nothing (0, posY + 1) (maybeAppend schematicAcc inProgressSchematic) symbolAcc
    _ -> takeSymbol xs Nothing (posX + 1, posY) (maybeAppend schematicAcc inProgressSchematic) (symbolAcc ++ [buildSymbol posX posY x])

-- appends a character to the schematic, creating a new schematic if one isn't already instantiated
appendToSchematic :: Maybe Schematic -> (Int, Int) -> Int -> Schematic
appendToSchematic (Just schematic) _ c =

  let (x, y, n) = schematicCoords schematic
      currPartNumber = partNumber schematic
   in schematic {partNumber = currPartNumber * 10 + c, schematicCoords = (x, y, n + 1)}
appendToSchematic Nothing (x, y) c = Schematic {partNumber = c, schematicCoords = (x, y, 1)}

-- append a Maybe Schematic to [Schematic]
maybeAppend :: [Schematic] -> Maybe Schematic -> [Schematic]
maybeAppend out (Just new) = out ++ [new]
maybeAppend out Nothing = out

-- easy constructor for a Symbol
buildSymbol :: Int -> Int -> Char -> Symbol
buildSymbol x y symbolType = Symbol {symbolCoords = (x, y), symbolType}
diff --git a/2023/4.hs b/2023/4.hs
deleted file mode 100755
index 28339ac..0000000 100755
--- a/2023/4.hs
+++ /dev/null
@@ -1,81 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i runghc -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Data.List (intersect)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/4 -}

main = do
  cards <- readAndParseStdin []
  print $ part1 cards
  print $ part2 cards

-- sum up amount of winning numbers using the part1Score formula
part1 :: [Card] -> Int
part1 cards = sum $ map (score . length . getWinningNumbers) cards
  where
    score 0 = 0
    score n = 2 ^ (n - 1)

-- calculates number of cards won in part 2 of the task
part2 :: [Card] -> Int
part2 cards = sum $ calculateCardCopies $ map (length . getWinningNumbers) cards

-- starts with a base array of [1; n] and replicates Ns right for each `getWinningNumbers`
-- where N is the amount of card replicas
calculateCardCopies :: [Int] -> [Int]
calculateCardCopies xs = foldl replicateSingleCardWinnings (replicate (length xs) 1) (zip [0 ..] xs)

-- helper function for calling `copyCards` within `foldl`
replicateSingleCardWinnings :: [Int] -> (Int, Int) -> [Int]
replicateSingleCardWinnings cardReplicas (idx, winningNumbers) = copyCards cardReplicas (idx + 1) idx winningNumbers

-- copies N cards to `winningNumbers` elements right of `winningCardIdx` where N is `cardReplicas[winningCardIdx]`
copyCards :: [Int] -> Int -> Int -> Int -> [Int]
copyCards cardReplicas currIdx winningCardIdx winningNumbers
  | currIdx <= length cardReplicas && winningNumbers > 0 =

      let incrementedList = incrementAtIndex cardReplicas currIdx (cardReplicas !! winningCardIdx)
       in copyCards incrementedList (currIdx + 1) winningCardIdx (winningNumbers - 1)
  | otherwise = cardReplicas

-- takes a list, an index and an amount to increment by
incrementAtIndex :: [Int] -> Int -> Int -> [Int]
incrementAtIndex xs idx amount = take idx xs ++ [(xs !! idx) + amount] ++ drop (idx + 1) xs

-- gets the intersection of winning numbers and player numbers
getWinningNumbers :: Card -> [Int]
getWinningNumbers card = myNumbers card `intersect` winningNumbers card

data Card = Card
  { winningNumbers :: [Int],
    myNumbers :: [Int]
  }
  deriving (Show)

-- reads entirety of stdin and parses each line
readAndParseStdin :: [Card] -> IO [Card]
readAndParseStdin acc = do
  line <- getLine
  if null line
    then return acc
    else case parse cardParser "" line of
      Left parseError -> error $ show parseError
      Right card -> readAndParseStdin $ acc ++ [card]

-- parses a `Card [i]: [n1] [n2] [n3] | [n4] [n5] [n6]` line
cardParser :: Parser Card
cardParser = do
  _ <- string "Card" <* spaces <* many1 digit <* char ':' <* spaces
  winningNumbers <- numberParser
  _ <- char '|' <* spaces
  myNumbers <- numberParser

  return Card {winningNumbers, myNumbers}

-- reads a single number delimited by spaces
numberParser :: Parser [Int]
numberParser = map read <$> many1 digit `endBy` spaces
diff --git a/2023/5.rs b/2023/5.rs
deleted file mode 100755
index a9327c9..0000000 100755
--- a/2023/5.rs
+++ /dev/null
@@ -1,246 +1,0 @@
use std::{collections::HashMap, io::Read, ops::Range, str::FromStr, time::Instant};

use itertools::Itertools;
use nom::IResult;
use rangemap::RangeMap;

const TRANSLATION_PATH: &[MapKind] = &[
    MapKind::Soil,
    MapKind::Fertilizer,
    MapKind::Water,
    MapKind::Light,
    MapKind::Temperature,
    MapKind::Humidity,
    MapKind::Location,
];

fn main() {
    let mut input = Vec::new();
    std::io::stdin().lock().read_to_end(&mut input).unwrap();
    let input = std::str::from_utf8(&input).unwrap();

    let (rest, input) = parse_input(input).unwrap();
    assert!(rest.is_empty());

    let i = Instant::now();
    let answer = part1(&input);
    eprintln!("part 1: {answer} ({:?})", i.elapsed());

    let i = Instant::now();
    let answer = part2(&input);
    eprintln!("part 2: {answer} ({:?})", i.elapsed());
}

fn part1(input: &Input) -> u64 {
    let mut lowest_location = u64::MAX;

    for seed in &input.seeds {
        let mut source = *seed;
        let mut from = MapKind::Seed;

        for to in TRANSLATION_PATH {
            let Some(translation) = input.maps.get(&Translation { from, to: *to }) else {
                panic!("invalid path {from:?} to {to:?}");
            };

            if let Some((source_range, destination_base)) = translation.get_key_value(&source) {
                source = destination_base + (source - source_range.start);
            }

            from = *to;
        }

        assert_eq!(from, MapKind::Location);
        lowest_location = lowest_location.min(source);
    }

    lowest_location
}

fn part2(input: &Input) -> u64 {
    let seed_ranges: Vec<_> = input
        .seeds
        .iter()
        .tuples()
        .map(|(start, len)| (*start)..(*start) + len)
        .collect();

    let mut lowest_bound_seen = u64::MAX;

    for seed_range in seed_ranges {
        let lowest_for_seed = traverse_path(input, TRANSLATION_PATH, MapKind::Seed, seed_range);
        lowest_bound_seen = lowest_bound_seen.min(lowest_for_seed);
    }

    lowest_bound_seen
}

fn traverse_path(input: &Input, path: &[MapKind], from: MapKind, source_range: Range<u64>) -> u64 {
    let mut lowest_bound_seen = u64::MAX;

    let Some((next_path, rest)) = path.split_first() else {
        return source_range.start;
    };

    let Some(translation) = input.maps.get(&Translation {
        from,
        to: *next_path,
    }) else {
        panic!("invalid path {from:?} to {next_path:?}");
    };

    for (new_source_range, destination_base) in translation.overlapping(&source_range) {
        // determine intersection between the source range and destination range
        let start = source_range.start.max(new_source_range.start);
        let end = source_range.end.min(new_source_range.end);
        let offset = start.saturating_sub(new_source_range.start);
        let length = end.saturating_sub(start);

        let destination_range = (*destination_base + offset)..(*destination_base + offset + length);

        let lowest_in_tree = traverse_path(input, rest, *next_path, destination_range);

        lowest_bound_seen = lowest_bound_seen.min(lowest_in_tree);
    }

    // traverse any uncovered sources, which the spec allows us to use our
    // destination number directly for
    for uncovered_range in split_range(
        source_range.clone(),
        translation
            .overlapping(&source_range)
            .map(|v| v.0.clone())
            .collect(),
    ) {
        let current_range = traverse_path(input, rest, *next_path, uncovered_range);
        lowest_bound_seen = lowest_bound_seen.min(current_range);
    }

    lowest_bound_seen
}

/// Splits `main_range` into multiple ranges not covered by `ranges`.

fn split_range(main_range: Range<u64>, mut ranges: Vec<Range<u64>>) -> Vec<Range<u64>> {
    let mut non_intersecting_ranges = Vec::new();
    let mut current_start = main_range.start;

    ranges.sort_by_key(|r| r.start);

    for range in ranges {
        if range.start > current_start {
            non_intersecting_ranges.push(current_start..range.start);
        }

        if range.end > current_start {
            current_start = range.end;
        }
    }

    if current_start < main_range.end {
        non_intersecting_ranges.push(current_start..main_range.end);
    }

    non_intersecting_ranges
}

#[derive(strum::EnumString, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[strum(serialize_all = "kebab-case")]
enum MapKind {
    Seed,
    Soil,
    Fertilizer,
    Water,
    Light,
    Temperature,
    Humidity,
    Location,
}

#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)]
struct Translation {
    from: MapKind,
    to: MapKind,
}

impl From<(MapKind, MapKind)> for Translation {
    fn from((from, to): (MapKind, MapKind)) -> Self {
        Self { from, to }
    }
}

#[derive(Debug)]
struct Input {
    seeds: Vec<u64>,
    maps: HashMap<Translation, RangeMap<u64, u64>>,
}

/// parse entire input

fn parse_input(rest: &str) -> IResult<&str, Input> {
    use nom::{
        bytes::complete::tag, character::complete::digit1, combinator::map_res,
        multi::separated_list1, sequence::delimited,
    };

    let (rest, seeds) = delimited(
        tag("seeds: "),
        separated_list1(tag(" "), map_res(digit1, u64::from_str)),
        tag("\n\n"),
    )(rest)?;
    let (rest, maps) = separated_list1(tag("\n"), parse_single_map)(rest)?;

    Ok((
        rest,
        Input {
            seeds,
            maps: maps.into_iter().collect(),
        },
    ))
}

/// parse header along with each map line

fn parse_single_map(rest: &str) -> IResult<&str, (Translation, RangeMap<u64, u64>)> {
    use nom::multi::many1;

    let (rest, header) = parse_header(rest)?;
    let (rest, lines) = many1(parse_map_line)(rest)?;

    Ok((rest, (header, lines.into_iter().collect())))
}

/// parse `803774611 641364296 1132421037` line

fn parse_map_line(rest: &str) -> IResult<&str, (Range<u64>, u64)> {
    use nom::{
        branch::alt,
        bytes::complete::tag,
        character::complete::digit1,
        combinator::{eof, map_res},
        sequence::terminated,
    };

    let (rest, destination) = terminated(map_res(digit1, u64::from_str), tag(" "))(rest)?;
    let (rest, source) = terminated(map_res(digit1, u64::from_str), tag(" "))(rest)?;
    let (rest, size) = terminated(map_res(digit1, u64::from_str), alt((tag("\n"), eof)))(rest)?;

    Ok((rest, (source..source + size, destination)))
}

/// parse `seed-to-soil map:` line

fn parse_header(rest: &str) -> IResult<&str, Translation> {
    use nom::{
        bytes::complete::{tag, take_until},
        combinator::{map, map_res},
        sequence::{separated_pair, terminated},
    };

    map(
        terminated(
            separated_pair(
                map_res(take_until("-"), MapKind::from_str),
                tag("-to-"),
                map_res(take_until(" "), MapKind::from_str),
            ),
            tag(" map:\n"),
        ),
        Translation::from,
    )(rest)
}
diff --git a/2023/6.hs b/2023/6.hs
deleted file mode 100755
index 5836634..0000000 100755
--- a/2023/6.hs
+++ /dev/null
@@ -1,46 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Control.Applicative ((<*))
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/6 -}

main = do
  game <- readAndParseStdin gameParser
  print $ part1 game
  print $ part2 game

-- returns the product of how many winning times there are per game
part1 :: [(Int, Int)] -> Int
part1 = product . map (length . getWinningTimes)
  where
    getWinningTimes (time, winCond) = filter (> winCond) $ map (`distance` time) [1 .. time - 1]

-- concatenates all games into one big game and returns how many winning times
-- there are in the big game
part2 :: [(Int, Int)] -> Int
part2 game = part1 [foldl concatenate (0, 0) game]
  where
    concatenate (tAcc, dAcc) (t, d) = (tAcc * scale t + t, dAcc * scale d + d)

-- calculates the "scale" of a number + 1 and returns the magnitude ie. 8 -> 10, 23 -> 100, 694 -> 1000
scale :: Int -> Int
scale n
  | n < 10 = 10
  | otherwise = 10 * scale (n `div` 10)

-- calculates distance travelled in a game based on velocity * time minus "button pressing time"
distance :: Int -> Int -> Int
distance v t = v * (t - v)

-- parses `Time: [n1] [n2] [n3]\nDistance:[n4] [n5] [n6]` and returns `[(n1, n4), (n2, n5), (n3, n6)]`
gameParser :: Parser [(Int, Int)]
gameParser = do
  time <- map read <$> (string "Time:" *> spaces *> many1 digit `endBy` spaces)
  distance <- map read <$> (string "Distance:" *> spaces *> many1 digit `endBy` spaces)
  return $ zip time distance
diff --git a/2023/7.hs b/2023/7.hs
deleted file mode 100755
index 564ce30..0000000 100755
--- a/2023/7.hs
+++ /dev/null
@@ -1,108 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Data.List
import Data.Ord
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/7 -}

main :: IO ()
main = do
  games <- readAndParseStdin parseAllGames
  print $ part1 games
  print $ part2 games

-- sums the score of each game
part1 :: [Game] -> Int
part1 games = calcScore $ sortBy gameComparator games
  where
    gameComparator = comparing (Down . getHandStrength . groupCards . cards) <> comparing cards

-- maps jacks to jokers (lowest scoring card, which can transmorph itself to the highest scoring group)
-- and sums the score of each game
part2 :: [Game] -> Int
part2 games = calcScore $ sortBy gameComparator $ mapAllJacksToJokers games
  where
    gameComparator = comparing (Down . getHandStrength . transmorphJokers . cards) <> comparing cards

-- transforms every jack in a game to a joker
mapAllJacksToJokers :: [Game] -> [Game]
mapAllJacksToJokers = map (\game -> game {cards = map mapJackToJoker $ cards game})
  where
    mapJackToJoker Jack = Joker
    mapJackToJoker a = a

-- transmorphs jokers into whatever the highest scoring rank was
transmorphJokers :: [Rank] -> [Int]
transmorphJokers [Joker, Joker, Joker, Joker, Joker] = groupCards [Joker, Joker, Joker, Joker, Joker]
transmorphJokers cards = (head grouped + jokerCount) : tail grouped
  where
    cardsWithoutJokers = filter (/= Joker) cards
    jokerCount = length cards - length cardsWithoutJokers
    grouped = groupCards cardsWithoutJokers

-- calculates the final score of the game
calcScore :: [Game] -> Int
calcScore game = sum $ zipWith (curry formula) [1 ..] game
  where
    formula (idx, game) = baseScore game * idx

-- determines the strength of the hand based on cards in hand
getHandStrength :: [Int] -> HandStrength
getHandStrength sortedCardCount = case sortedCardCount of
  [5] -> FiveOfAKind
  [4, 1] -> FourOfAKind
  [3, 2] -> FullHouse
  (3 : _) -> ThreeOfAKind
  (2 : 2 : _) -> TwoPair
  (2 : _) -> OnePair
  _ -> HighCard

-- groups any ranks together, returning the amount of items per group. ie. [J, J, A, A, A, 1] would return [3, 2, 1]
groupCards :: [Rank] -> [Int]
groupCards = sortOn Down . map length . group . sort

-- parses each game delimited by a newline
parseAllGames :: Parser [Game]
parseAllGames = parseGame `endBy` char '\n'

-- parses games in the format `[C][C][C] 123`
parseGame :: Parser Game
parseGame = do
  cards <- many1 parseRank <* spaces
  baseScore <- read <$> many1 digit
  return Game {cards, baseScore}

-- parses a single card rank
parseRank :: Parser Rank
parseRank = do
  c <- oneOf "23456789TJQKA"
  return $ case c of
    '2' -> Two
    '3' -> Three
    '4' -> Four
    '5' -> Five
    '6' -> Six
    '7' -> Seven
    '8' -> Eight
    '9' -> Nine
    'T' -> Ten
    'J' -> Jack
    'Q' -> Queen
    'K' -> King
    'A' -> Ace

data Game = Game
  { cards :: [Rank],
    baseScore :: Int
  }
  deriving (Show)

data Rank = Joker | Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King | Ace deriving (Eq, Ord, Enum, Show)

data HandStrength = FiveOfAKind | FourOfAKind | FullHouse | ThreeOfAKind | TwoOfAKind | TwoPair | OnePair | HighCard deriving (Eq, Ord, Enum, Show)
diff --git a/2023/8.hs b/2023/8.hs
deleted file mode 100755
index 87781ec..0000000 100755
--- a/2023/8.hs
+++ /dev/null
@@ -1,83 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Data.List
import qualified Data.Map as Map
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

{- https://adventofcode.com/2023/day/8 -}

main = do
  document <- readAndParseStdin parseDocument
  print $ part1 document
  print $ part2 document

-- find the path from AAAA -> ZZZZ
part1 :: Document -> Int
part1 doc = traverseTreeUntilD doc (== "ZZZ") "AAA"

-- find the length of each startingPosition loop, find least common multiple between all
part2 :: Document -> Int
part2 doc = foldl1 lcm . map (traverseTreeUntilD doc ("Z" `isSuffixOf`)) $ startingPositions
  where
    startingPositions = filter ("A" `isSuffixOf`) $ (Map.keys . docMap) doc

-- helper function to call traverseTreeUntil with a document
traverseTreeUntilD :: Document -> (String -> Bool) -> String -> Int
traverseTreeUntilD doc = traverseTreeUntil (cycle $ directions doc) (docMap doc) 0

-- traverse the tree until the predicate is matched
traverseTreeUntil :: [Direction] -> Map.Map String (String, String) -> Int -> (String -> Bool) -> String -> Int
traverseTreeUntil (x : xs) m n predicate elem
  | predicate elem = n
  | otherwise = traverseTreeUntil xs m (n + 1) predicate (readNext getNode x)
  where
    getNode = case Map.lookup elem m of
      Just a -> a
      Nothing -> error (show elem)
    readNext (n, _) DirectionLeft = n
    readNext (_, n) DirectionRight = n

-- parse entire document
parseDocument :: Parser Document
parseDocument = do
  directions <- many1 parseDirection
  _ <- string "\n\n"
  nodes <- parseNode `endBy` char '\n'
  return
    Document
      { directions,
        docMap = Map.fromList nodes
      }

-- parse a single node `AAA = (BBB, CCC)`
parseNode :: Parser (String, (String, String))
parseNode = do
  node <- many1 alphaNum
  _ <- spaces <* char '=' <* spaces
  _ <- char '('
  left <- many1 alphaNum
  _ <- char ',' <* spaces
  right <- many1 alphaNum
  _ <- char ')'
  return (node, (left, right))

-- parse the direction string `LRLLLR`
parseDirection :: Parser Direction
parseDirection = do
  c <- oneOf "LR"
  return $ case c of
    'L' -> DirectionLeft
    'R' -> DirectionRight

data Document = Document
  { directions :: [Direction],
    docMap :: Map.Map String (String, String)
  }
  deriving (Show)

data Direction = DirectionLeft | DirectionRight deriving (Enum, Show)
diff --git a/2023/9.hs b/2023/9.hs
deleted file mode 100755
index 9e9dc8d..0000000 100755
--- a/2023/9.hs
+++ /dev/null
@@ -1,69 +1,0 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i "runghc -- -i../" -p "haskellPackages.ghcWithPackages (pkgs: with pkgs; [ ])"

import Aoc (readAndParseStdin)
import Control.Monad (guard)
import Data.List (unfoldr)
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String (Parser)

main = do
  input <- readAndParseStdin parseInput
  print $ part1 input
  print $ part2 input

-- interpolate the next value on every input and sum them
part1 :: [[Int]] -> Int
part1 = sum . map (round . interpolateNext)

-- interpolate the previous value on every input and sum them
part2 :: [[Int]] -> Int
part2 = sum . map (round . interpolatePrevious)

-- helper function to call interpolatePolynomial for the next value
interpolateNext :: [Int] -> Double
interpolateNext i = interpolatePolynomial (length i) i

-- helper function to call interpolatePolynomial for the previous value
interpolatePrevious :: [Int] -> Double
interpolatePrevious = interpolatePolynomial (-1)

-- given an nth term and a sequence, calculate newton's polynomial and
-- interpolate the nth value for the sequence
interpolatePolynomial :: Int -> [Int] -> Double
interpolatePolynomial nth seq =

  let divDiff = (dividedDifference . buildDifferenceTable) seq
      initialValue = (1, 0)
      (_, val) = foldl foldFunction initialValue $ zip [0 ..] (tail divDiff)
   in head divDiff + val
  where
    foldFunction (productAcc, valueAcc) (idx, val) =

      let prod = productAcc * fromIntegral (nth - idx)
       in (prod, valueAcc + (val * prod))

-- calculate the divided differences from our table for newton's
-- polynomial
dividedDifference :: [[Int]] -> [Double]
dividedDifference table = [fromIntegral (head row) / fromIntegral (fac i) | (i, row) <- zip [0 ..] table]
  where
    fac i = product [1 .. i]

-- build the difference table of each input
buildDifferenceTable :: [Int] -> [[Int]]
buildDifferenceTable input = input : unfoldr buildRow input
  where
    zipPairs list = zip list $ tail list
    diffPairs = map $ uncurry subtract
    buildRow lst =

      let row = diffPairs $ zipPairs lst
       in guard (not $ null row) >> Just (row, row)

-- parse each input line
parseInput :: Parser [[Int]]
parseInput = parseSequence `sepBy` char '\n'

-- parse sequence of numbers
parseSequence :: Parser [Int]
parseSequence = map read <$> many1 (digit <|> char '-') `sepBy` char ' '
diff --git a/2022/01/default.nix b/2022/01/default.nix
new file mode 100644
index 0000000..41973b3 100644
--- /dev/null
+++ a/2022/01/default.nix
@@ -1,0 +1,17 @@
{ pkgs ? import <nixpkgs> { } }:

pkgs.stdenv.mkDerivation {
  name = "aoc-2022-1";
  buildInputs = [ pkgs.gfortran ];

  src = ./.;

  buildPhase = ''
    gfortran -o aoc-2022-1 main.f90
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp aoc-2022-1 $out/bin/
  '';
}
diff --git a/2022/01/main.f90 b/2022/01/main.f90
new file mode 100644
index 0000000..7b415e5 100644
--- /dev/null
+++ a/2022/01/main.f90
@@ -1,0 +1,90 @@
program day_1
   implicit none
   integer, dimension(300, 20) :: result
   integer, dimension(300) :: summed
   integer :: eof, i, out, size
   integer, external :: top3

   eof = 0
   result = 0
   i = 0

   ! read every block of ints from stdin
   do
      i = i + 1
      if (i > 300) then
         print *, 'Main read overflow: more than 300 entries read.'
         exit
      end if

      call read_block(result(i, :), eof)
      if (eof /= 0) exit
   end do

   ! sum results
   summed = sum(result, dim=2)

   ! print results
   print *, 'Part 1: ', maxval(summed)
   print *, 'Part 2: ', top3(summed)
end program day_1

! loops over entire input and returns the top 3 values from it
function top3(input) result(retval)
   implicit none
   integer, dimension(300), intent(in) :: input
   integer, dimension(3) :: topValues
   integer :: i, j, k, retval

   retval = 0
   topValues = 0

   do i = 1, 300
      do j = 1, 3
         if (input(i) > topValues(j)) then
            topValues(j) = input(i)
            exit
         end if
      end do
   end do

   retval = sum(topValues)
end function top3

! reads a single block of integers delimited by an empty line and returns
subroutine read_block(result, eof)
   implicit none
   integer, dimension(20), intent(out) :: result
   integer, intent(out) :: eof
   integer :: iostatus, n, parsedCalories
   character(len=10) :: line

   result = 0
   n = 0
   eof = 0
   parsedCalories = 0

   do
      read (*, '(A)', iostat=iostatus) line
      if (iostatus /= 0) then
         eof = iostatus
         exit
      else if (trim(line) == '') then
         exit
      end if

      read (line, '(I8)', iostat=iostatus) parsedCalories
      if (iostatus /= 0) then
         print *, 'Conversion error with iostat = ', iostatus
         exit
      end if

      n = n + 1
      if (n > 20) then
         print *, 'Read block overflow: more than 20 entries read.'
         exit
      end if

      result(n) = parsedCalories
   end do
end subroutine read_block
diff --git a/2022/02/default.nix b/2022/02/default.nix
new file mode 100644
index 0000000..bb0e383 100644
--- /dev/null
+++ a/2022/02/default.nix
@@ -1,0 +1,49 @@
{ pkgs ? import <nixpkgs> { } }:
with builtins;
let
  inherit (pkgs) lib;
  input = builtins.readFile ./input;
  # cipher for the opponent's hand
  leftCipher = { "A" = "Rock"; "B" = "Paper"; "C" = "Scissors"; };
  # opponent's choice required for a win
  winConditions = { "Rock" = "Scissors"; "Scissors" = "Paper"; "Paper" = "Rock"; };
  # inverse of win connections
  loseConditions = builtins.listToAttrs (map (pair: lib.nameValuePair pair.value pair.name) (lib.attrsToList winConditions));
  # determines the score for the game from both shape and outcome score
  determineScore = game:
    let
      shapeScore = { "Rock" = 1; "Paper" = 2; "Scissors" = 3; };
      outcomeScore = { "L" = 0; "D" = 3; "W" = 6; };
      gameOutcome = if winConditions.${elemAt game 0} == elemAt game 1 then "W" else if elemAt game 0 == elemAt game 1 then "D" else "L";
    in
    outcomeScore.${gameOutcome} + shapeScore.${elemAt game 0};
  # map X to rock, Y to paper and Z to scissors
  splitAndDecipher = x:
    let
      rightCipher = { "X" = "Rock"; "Y" = "Paper"; "Z" = "Scissors"; };
      split = lib.splitString " " x;
      us = rightCipher.${elemAt split 1};
      them = leftCipher.${elemAt split 0};
    in
    [ us them ];
  # map X to a loss, Z to a win and Y to a draw
  splitAndMapToResult = x:
    let
      split = lib.splitString " " x;
      them = leftCipher.${elemAt split 0};
      desiredOutcome = elemAt split 1;
      us = if desiredOutcome == "X" then winConditions.${them} else if desiredOutcome == "Z" then loseConditions.${them} else them;
    in
    [ us them ];
  # split each individual game
  games = lib.splitString "\n" input;
  # plays game with the given mapper and returns the score
  playGame = f: lib.foldl (x: y: x + y) 0 (map determineScore (map f games));
  # plays using part 1 rules with both sides ciphered
  part1 = playGame splitAndDecipher;
  # plays using aprt 2 rules with our side mapped to final outcome
  part2 = playGame splitAndMapToResult;
  # build json output
  out = builtins.toJSON { inherit part1; inherit part2; };
in
pkgs.writeText "out" out
diff --git a/2022/04/4.jsonnet b/2022/04/4.jsonnet
new file mode 100644
index 0000000..586a07d 100644
--- /dev/null
+++ a/2022/04/4.jsonnet
@@ -1,0 +1,18 @@
local input = importstr './input';

local parsedInput = [[[std.parseInt(z) for z in std.split(y, '-')] for y in std.split(x, ',')] for x in std.split(input, '\n')];

local part1 = std.sum([
  if ((x[0][0] >= x[1][0] && x[0][1] <= x[1][1]) || (x[1][0] >= x[0][0] && x[1][1] <= x[0][1])) then 1 else 0
  for x in parsedInput
]);

local part2 = std.sum([
  if (x[0][0] <= x[1][1] && x[0][1] >= x[1][0]) then 1 else 0
  for x in parsedInput
]);

{
  part1: part1,
  part2: part2,
}
diff --git a/2022/04/default.nix b/2022/04/default.nix
new file mode 100644
index 0000000..00c0efe 100644
--- /dev/null
+++ a/2022/04/default.nix
@@ -1,0 +1,14 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation rec {
  name = "aoc-2022-4";
  src = ./.;

  phases = [ "buildPhase" ];
  doCheck = true;

  buildInputs = with pkgs; [ go-jsonnet ];

  buildPhase = ''
    jsonnet $src/4.jsonnet > $out
  '';
}
diff --git a/2022/05/.terraform.lock.hcl b/2022/05/.terraform.lock.hcl
new file mode 100644
index 0000000..b65dc1c 100644
--- /dev/null
+++ a/2022/05/.terraform.lock.hcl
@@ -1,0 +1,19 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.

provider "registry.opentofu.org/hashicorp/local" {

  version = "2.4.0"
  hashes = [

    "h1:cZKMbwSvmjK4bgXY+1gSBd5nodgkhRnw90lYvB8j0hI=",
    "zh:184d6ec1f0e77713b37f0d9cf943b1371f2aa2f44c2c5a618978e897ce3dccab",
    "zh:2205a7955a4051c2c25e69646a60746d9416b73001491808ae5d10620f7b7ac1",
    "zh:256ddc56457f725819dc6be62f2d0bb3b9fee40a61771317bb32353df5b5c1a0",
    "zh:70146e603f540523f6fa2251dd52c225db5a92bda8c07fd198ed51ae2b50176b",
    "zh:8c3f9fe12ab8843e25ff7edabc26e01df4a0e8db204e432600a4c77a95ec0535",
    "zh:b003e421f643d14247d31dcb7f0f6470c46f772d0e15a175a555a525ce344bf2",
    "zh:b4c8ad7c5696aeb2a52adf6047d1e01943fafa57dc123d5192542527406ffd72",
    "zh:c3b6fbfa431f3c085621c74596ee63681a278fd433a4758f33c627e8936d5cb3",
    "zh:d2e57b19295b326d84ca5f39b797849d901170d5509aa7558f2a6545c9ce72a9",
    "zh:e2307421b0b380eb0e8fcee008e0af98ae30fccbfc9e9a1d24d952489e9b0df9",
  ]
}
diff --git a/2022/05/default.nix b/2022/05/default.nix
new file mode 100644
index 0000000..915e140 100644
--- /dev/null
+++ a/2022/05/default.nix
@@ -1,0 +1,19 @@
# must use --option sandbox false
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation rec {
  name = "aoc-2022-5";
  src = ./.;

  phases = [ "buildPhase" ];
  doCheck = true;

  buildInputs = with pkgs; [ opentofu jinja2-cli ];

  buildPhase = ''
    cp -r $src/* .
    jinja2 main.tf.jinja2 > main.tf

    tofu init
    tofu apply -auto-approve -lock=false -var=outpath=$out
  '';
}
diff --git a/2022/05/main.tf.jinja2 b/2022/05/main.tf.jinja2
new file mode 100644
index 0000000..efe983f 100644
--- /dev/null
+++ a/2022/05/main.tf.jinja2
@@ -1,0 +1,54 @@
{% set maxRecursion = 501 %}

locals {
  input         = file("input")
  inputParts    = split("\n\n", local.input)
  blocks        = split("\n", local.inputParts[0])
  parsedBlocks  = [for s in [for s in slice(local.blocks, 0, length(local.blocks) - 1) : split("", s)] : [for j in range(1, length(s), 4) : s[j]]]
  stackedBlocks = [for i in range(length(local.parsedBlocks[0])) : [for j in range(length(local.parsedBlocks)) : local.parsedBlocks[j][i] if local.parsedBlocks[j][i] != " "]]
  instructions  = split("\n", replace(replace(replace(local.inputParts[1], "move ", ""), " from ", ","), " to ", ","))
  output = {
    "part1" = join("", [for v in module.executor-{{ maxRecursion - 1 }}.finalBlocks : v[0]]),
    "part2" = join("", [for v in module.executor-non-reversed-{{ maxRecursion - 1 }}.finalBlocks : v[0]])
  }
}

# you'll have to forgive me for this blasphemy. hashicorp did everything in their power
# to prevent any forms of recursion, and i can't be bothered copy and pasting this block
# 1000 times.
{% for i in range(maxRecursion) %}
module "executor-{{ i }}" {
  source = "./executor"

  {% if i == 0 %}
    stackedBlocks = local.stackedBlocks
    instructions = local.instructions
  {% else %}
    stackedBlocks = module.executor-{{ i - 1 }}.finalBlocks
    instructions = module.executor-{{ i - 1 }}.remainingInstructions
  {% endif %}

  reverse = true
}

module "executor-non-reversed-{{ i }}" {
  source = "./executor"

  {% if i == 0 %}
    stackedBlocks = local.stackedBlocks
    instructions = local.instructions
  {% else %}
    stackedBlocks = module.executor-non-reversed-{{ i - 1 }}.finalBlocks
    instructions = module.executor-non-reversed-{{ i - 1 }}.remainingInstructions
  {% endif %}

  reverse = false
}
{% endfor %}

resource "local_file" "out" {
  filename = var.outpath
  content  = jsonencode(local.output)
}

variable "outpath" {}
diff --git a/2022/06/.gitignore b/2022/06/.gitignore
new file mode 100644
index 0000000..f5efcb4 100644
--- /dev/null
+++ a/2022/06/.gitignore
@@ -1,0 +1,2 @@
httpd.pid
logs
diff --git a/2022/06/apache2.conf b/2022/06/apache2.conf
new file mode 100644
index 0000000..fd4ad0e 100644
--- /dev/null
+++ a/2022/06/apache2.conf
@@ -1,0 +1,156 @@
LoadModule mpm_prefork_module libexec/apache2/mod_mpm_prefork.so
LoadModule alias_module libexec/apache2/mod_alias.so
LoadModule unixd_module libexec/apache2/mod_unixd.so
LoadModule rewrite_module libexec/apache2/mod_rewrite.so
LoadModule headers_module libexec/apache2/mod_headers.so

ServerRoot .
PidFile ./httpd.pid
RewriteEngine On

Listen 8888

<VirtualHost *:8888>
    LogLevel debug

    #########################
    #        PART 1         #
    #########################

    # entry point to part 1
    RedirectMatch 301 ^/part1/(.*)$ /part1-iter/$1/1

    # extract first 4 characters from string
    RewriteRule ^/part1-iter/(.)(.)(.)(.) - [E=C1:$1,E=C2:$2,E=C3:$3,E=C4:$4]

    # assert that all characters are distinct
    RewriteCond %{ENV:C1}#%{ENV:C2} !^([^#]+)#\1$
    RewriteCond %{ENV:C1}#%{ENV:C3} !^([^#]+)#\1$
    RewriteCond %{ENV:C1}#%{ENV:C4} !^([^#]+)#\1$
    RewriteCond %{ENV:C2}#%{ENV:C3} !^([^#]+)#\1$
    RewriteCond %{ENV:C2}#%{ENV:C4} !^([^#]+)#\1$
    RewriteCond %{ENV:C3}#%{ENV:C4} !^([^#]+)#\1$
    RewriteRule ^ - [E=ALL_PART1_CHARS_DISTINCT:1]

    # redirect to completion page if distinct
    RewriteCond %{ENV:ALL_PART1_CHARS_DISTINCT} =1
    RewriteRule ^/part1-iter/(.{4})[^/]*/(1+)$ /part1-complete/$1/111$2 [R=301,L]

    # remove the first character, increment the iterator and continue on
    RewriteCond %{ENV:ALL_PART1_CHARS_DISTINCT} !=1
    RewriteRule ^/part1-iter/.([^/]+)/(1+)$ /part1-iter/$1/1$2 [R=301,L]

    #########################
    #        PART 2         #
    #########################

    # entry point to part 2
    RedirectMatch 301 ^/part2/(.*)$ /part2-iter/$1/1

    # extract first 14 characters from string. this needs to be split into two capture
    # groups because apache reads $10 as $1 and a 0 literal. ask me how i know :)
    RewriteRule ^/part2-iter/(.)(.)(.)(.)(.)(.)(.)(.)(.) - [E=CC1:$1,E=CC2:$2,E=CC3:$3,E=CC4:$4,E=CC5:$5,E=CC6:$6,E=CC7:$7,E=CC8:$8,E=CC9:$9]
    RewriteRule ^/part2-iter/.{9}(.)(.)(.)(.)(.) - [E=CC10:$1,E=CC11:$2,E=CC12:$3,E=CC13:$4,E=CC14:$5]

    # assert that all characters are distinct
    RewriteCond %{ENV:CC1}#%{ENV:CC2} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC3} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC3} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC12}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC12}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC13}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteRule ^ - [E=ALL_PART2_CHARS_DISTINCT:1]

    # redirect to completion page if distinct
    RewriteCond %{ENV:ALL_PART2_CHARS_DISTINCT} =1
    RewriteRule ^/part2-iter/(.{14})[^/]*/(1+)$ /part2-complete/$1/1111111111111$2 [R=301,L]

    # remove the first character, increment the iterator and continue on
    RewriteCond %{ENV:ALL_PART2_CHARS_DISTINCT} !=1
    RewriteRule ^/part2-iter/.([^/]+)/(1+)$ /part2-iter/$1/1$2 [R=301,L]
</VirtualHost>
diff --git a/2022/06/read.sh b/2022/06/read.sh
new file mode 100755
index 0000000..68ca040 100755
--- /dev/null
+++ a/2022/06/read.sh
@@ -1,0 +1,9 @@
#!/usr/bin/env bash

STDIN=$(cat -)

PART1=$(curl --max-redirs 100000 -Ls "http://127.0.0.1:8888/part1/$STDIN" -o /dev/null -w %{url_effective})
echo $(echo $PART1 | gsed -E 's/.*\/([1]+)/\1/' | tr -d '\n' | wc -c)

PART2=$(curl --max-redirs 100000 -Ls "http://127.0.0.1:8888/part2/$STDIN" -o /dev/null -w %{url_effective})
echo $(echo $PART2 | gsed -E 's/.*\/([1]+)/\1/' | tr -d '\n' | wc -c)
diff --git a/2022/1/default.nix b/2022/1/default.nix
deleted file mode 100644
index 41973b3..0000000 100644
--- a/2022/1/default.nix
+++ /dev/null
@@ -1,17 +1,0 @@
{ pkgs ? import <nixpkgs> { } }:

pkgs.stdenv.mkDerivation {
  name = "aoc-2022-1";
  buildInputs = [ pkgs.gfortran ];

  src = ./.;

  buildPhase = ''
    gfortran -o aoc-2022-1 main.f90
  '';

  installPhase = ''
    mkdir -p $out/bin
    cp aoc-2022-1 $out/bin/
  '';
}
diff --git a/2022/1/main.f90 b/2022/1/main.f90
deleted file mode 100644
index 7b415e5..0000000 100644
--- a/2022/1/main.f90
+++ /dev/null
@@ -1,90 +1,0 @@
program day_1
   implicit none
   integer, dimension(300, 20) :: result
   integer, dimension(300) :: summed
   integer :: eof, i, out, size
   integer, external :: top3

   eof = 0
   result = 0
   i = 0

   ! read every block of ints from stdin
   do
      i = i + 1
      if (i > 300) then
         print *, 'Main read overflow: more than 300 entries read.'
         exit
      end if

      call read_block(result(i, :), eof)
      if (eof /= 0) exit
   end do

   ! sum results
   summed = sum(result, dim=2)

   ! print results
   print *, 'Part 1: ', maxval(summed)
   print *, 'Part 2: ', top3(summed)
end program day_1

! loops over entire input and returns the top 3 values from it
function top3(input) result(retval)
   implicit none
   integer, dimension(300), intent(in) :: input
   integer, dimension(3) :: topValues
   integer :: i, j, k, retval

   retval = 0
   topValues = 0

   do i = 1, 300
      do j = 1, 3
         if (input(i) > topValues(j)) then
            topValues(j) = input(i)
            exit
         end if
      end do
   end do

   retval = sum(topValues)
end function top3

! reads a single block of integers delimited by an empty line and returns
subroutine read_block(result, eof)
   implicit none
   integer, dimension(20), intent(out) :: result
   integer, intent(out) :: eof
   integer :: iostatus, n, parsedCalories
   character(len=10) :: line

   result = 0
   n = 0
   eof = 0
   parsedCalories = 0

   do
      read (*, '(A)', iostat=iostatus) line
      if (iostatus /= 0) then
         eof = iostatus
         exit
      else if (trim(line) == '') then
         exit
      end if

      read (line, '(I8)', iostat=iostatus) parsedCalories
      if (iostatus /= 0) then
         print *, 'Conversion error with iostat = ', iostatus
         exit
      end if

      n = n + 1
      if (n > 20) then
         print *, 'Read block overflow: more than 20 entries read.'
         exit
      end if

      result(n) = parsedCalories
   end do
end subroutine read_block
diff --git a/2022/2/default.nix b/2022/2/default.nix
deleted file mode 100644
index bb0e383..0000000 100644
--- a/2022/2/default.nix
+++ /dev/null
@@ -1,49 +1,0 @@
{ pkgs ? import <nixpkgs> { } }:
with builtins;
let
  inherit (pkgs) lib;
  input = builtins.readFile ./input;
  # cipher for the opponent's hand
  leftCipher = { "A" = "Rock"; "B" = "Paper"; "C" = "Scissors"; };
  # opponent's choice required for a win
  winConditions = { "Rock" = "Scissors"; "Scissors" = "Paper"; "Paper" = "Rock"; };
  # inverse of win connections
  loseConditions = builtins.listToAttrs (map (pair: lib.nameValuePair pair.value pair.name) (lib.attrsToList winConditions));
  # determines the score for the game from both shape and outcome score
  determineScore = game:
    let
      shapeScore = { "Rock" = 1; "Paper" = 2; "Scissors" = 3; };
      outcomeScore = { "L" = 0; "D" = 3; "W" = 6; };
      gameOutcome = if winConditions.${elemAt game 0} == elemAt game 1 then "W" else if elemAt game 0 == elemAt game 1 then "D" else "L";
    in
    outcomeScore.${gameOutcome} + shapeScore.${elemAt game 0};
  # map X to rock, Y to paper and Z to scissors
  splitAndDecipher = x:
    let
      rightCipher = { "X" = "Rock"; "Y" = "Paper"; "Z" = "Scissors"; };
      split = lib.splitString " " x;
      us = rightCipher.${elemAt split 1};
      them = leftCipher.${elemAt split 0};
    in
    [ us them ];
  # map X to a loss, Z to a win and Y to a draw
  splitAndMapToResult = x:
    let
      split = lib.splitString " " x;
      them = leftCipher.${elemAt split 0};
      desiredOutcome = elemAt split 1;
      us = if desiredOutcome == "X" then winConditions.${them} else if desiredOutcome == "Z" then loseConditions.${them} else them;
    in
    [ us them ];
  # split each individual game
  games = lib.splitString "\n" input;
  # plays game with the given mapper and returns the score
  playGame = f: lib.foldl (x: y: x + y) 0 (map determineScore (map f games));
  # plays using part 1 rules with both sides ciphered
  part1 = playGame splitAndDecipher;
  # plays using aprt 2 rules with our side mapped to final outcome
  part2 = playGame splitAndMapToResult;
  # build json output
  out = builtins.toJSON { inherit part1; inherit part2; };
in
pkgs.writeText "out" out
diff --git a/2022/4/4.jsonnet b/2022/4/4.jsonnet
deleted file mode 100644
index 586a07d..0000000 100644
--- a/2022/4/4.jsonnet
+++ /dev/null
@@ -1,18 +1,0 @@
local input = importstr './input';

local parsedInput = [[[std.parseInt(z) for z in std.split(y, '-')] for y in std.split(x, ',')] for x in std.split(input, '\n')];

local part1 = std.sum([
  if ((x[0][0] >= x[1][0] && x[0][1] <= x[1][1]) || (x[1][0] >= x[0][0] && x[1][1] <= x[0][1])) then 1 else 0
  for x in parsedInput
]);

local part2 = std.sum([
  if (x[0][0] <= x[1][1] && x[0][1] >= x[1][0]) then 1 else 0
  for x in parsedInput
]);

{
  part1: part1,
  part2: part2,
}
diff --git a/2022/4/default.nix b/2022/4/default.nix
deleted file mode 100644
index 00c0efe..0000000 100644
--- a/2022/4/default.nix
+++ /dev/null
@@ -1,14 +1,0 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation rec {
  name = "aoc-2022-4";
  src = ./.;

  phases = [ "buildPhase" ];
  doCheck = true;

  buildInputs = with pkgs; [ go-jsonnet ];

  buildPhase = ''
    jsonnet $src/4.jsonnet > $out
  '';
}
diff --git a/2022/5/.terraform.lock.hcl b/2022/5/.terraform.lock.hcl
deleted file mode 100644
index b65dc1c..0000000 100644
--- a/2022/5/.terraform.lock.hcl
+++ /dev/null
@@ -1,19 +1,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.

provider "registry.opentofu.org/hashicorp/local" {

  version = "2.4.0"
  hashes = [

    "h1:cZKMbwSvmjK4bgXY+1gSBd5nodgkhRnw90lYvB8j0hI=",
    "zh:184d6ec1f0e77713b37f0d9cf943b1371f2aa2f44c2c5a618978e897ce3dccab",
    "zh:2205a7955a4051c2c25e69646a60746d9416b73001491808ae5d10620f7b7ac1",
    "zh:256ddc56457f725819dc6be62f2d0bb3b9fee40a61771317bb32353df5b5c1a0",
    "zh:70146e603f540523f6fa2251dd52c225db5a92bda8c07fd198ed51ae2b50176b",
    "zh:8c3f9fe12ab8843e25ff7edabc26e01df4a0e8db204e432600a4c77a95ec0535",
    "zh:b003e421f643d14247d31dcb7f0f6470c46f772d0e15a175a555a525ce344bf2",
    "zh:b4c8ad7c5696aeb2a52adf6047d1e01943fafa57dc123d5192542527406ffd72",
    "zh:c3b6fbfa431f3c085621c74596ee63681a278fd433a4758f33c627e8936d5cb3",
    "zh:d2e57b19295b326d84ca5f39b797849d901170d5509aa7558f2a6545c9ce72a9",
    "zh:e2307421b0b380eb0e8fcee008e0af98ae30fccbfc9e9a1d24d952489e9b0df9",
  ]
}
diff --git a/2022/5/default.nix b/2022/5/default.nix
deleted file mode 100644
index 915e140..0000000 100644
--- a/2022/5/default.nix
+++ /dev/null
@@ -1,19 +1,0 @@
# must use --option sandbox false
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation rec {
  name = "aoc-2022-5";
  src = ./.;

  phases = [ "buildPhase" ];
  doCheck = true;

  buildInputs = with pkgs; [ opentofu jinja2-cli ];

  buildPhase = ''
    cp -r $src/* .
    jinja2 main.tf.jinja2 > main.tf

    tofu init
    tofu apply -auto-approve -lock=false -var=outpath=$out
  '';
}
diff --git a/2022/5/main.tf.jinja2 b/2022/5/main.tf.jinja2
deleted file mode 100644
index efe983f..0000000 100644
--- a/2022/5/main.tf.jinja2
+++ /dev/null
@@ -1,54 +1,0 @@
{% set maxRecursion = 501 %}

locals {
  input         = file("input")
  inputParts    = split("\n\n", local.input)
  blocks        = split("\n", local.inputParts[0])
  parsedBlocks  = [for s in [for s in slice(local.blocks, 0, length(local.blocks) - 1) : split("", s)] : [for j in range(1, length(s), 4) : s[j]]]
  stackedBlocks = [for i in range(length(local.parsedBlocks[0])) : [for j in range(length(local.parsedBlocks)) : local.parsedBlocks[j][i] if local.parsedBlocks[j][i] != " "]]
  instructions  = split("\n", replace(replace(replace(local.inputParts[1], "move ", ""), " from ", ","), " to ", ","))
  output = {
    "part1" = join("", [for v in module.executor-{{ maxRecursion - 1 }}.finalBlocks : v[0]]),
    "part2" = join("", [for v in module.executor-non-reversed-{{ maxRecursion - 1 }}.finalBlocks : v[0]])
  }
}

# you'll have to forgive me for this blasphemy. hashicorp did everything in their power
# to prevent any forms of recursion, and i can't be bothered copy and pasting this block
# 1000 times.
{% for i in range(maxRecursion) %}
module "executor-{{ i }}" {
  source = "./executor"

  {% if i == 0 %}
    stackedBlocks = local.stackedBlocks
    instructions = local.instructions
  {% else %}
    stackedBlocks = module.executor-{{ i - 1 }}.finalBlocks
    instructions = module.executor-{{ i - 1 }}.remainingInstructions
  {% endif %}

  reverse = true
}

module "executor-non-reversed-{{ i }}" {
  source = "./executor"

  {% if i == 0 %}
    stackedBlocks = local.stackedBlocks
    instructions = local.instructions
  {% else %}
    stackedBlocks = module.executor-non-reversed-{{ i - 1 }}.finalBlocks
    instructions = module.executor-non-reversed-{{ i - 1 }}.remainingInstructions
  {% endif %}

  reverse = false
}
{% endfor %}

resource "local_file" "out" {
  filename = var.outpath
  content  = jsonencode(local.output)
}

variable "outpath" {}
diff --git a/2022/6/.gitignore b/2022/6/.gitignore
deleted file mode 100644
index f5efcb4..0000000 100644
--- a/2022/6/.gitignore
+++ /dev/null
@@ -1,2 +1,0 @@
httpd.pid
logs
diff --git a/2022/6/apache2.conf b/2022/6/apache2.conf
deleted file mode 100644
index fd4ad0e..0000000 100644
--- a/2022/6/apache2.conf
+++ /dev/null
@@ -1,156 +1,0 @@
LoadModule mpm_prefork_module libexec/apache2/mod_mpm_prefork.so
LoadModule alias_module libexec/apache2/mod_alias.so
LoadModule unixd_module libexec/apache2/mod_unixd.so
LoadModule rewrite_module libexec/apache2/mod_rewrite.so
LoadModule headers_module libexec/apache2/mod_headers.so

ServerRoot .
PidFile ./httpd.pid
RewriteEngine On

Listen 8888

<VirtualHost *:8888>
    LogLevel debug

    #########################
    #        PART 1         #
    #########################

    # entry point to part 1
    RedirectMatch 301 ^/part1/(.*)$ /part1-iter/$1/1

    # extract first 4 characters from string
    RewriteRule ^/part1-iter/(.)(.)(.)(.) - [E=C1:$1,E=C2:$2,E=C3:$3,E=C4:$4]

    # assert that all characters are distinct
    RewriteCond %{ENV:C1}#%{ENV:C2} !^([^#]+)#\1$
    RewriteCond %{ENV:C1}#%{ENV:C3} !^([^#]+)#\1$
    RewriteCond %{ENV:C1}#%{ENV:C4} !^([^#]+)#\1$
    RewriteCond %{ENV:C2}#%{ENV:C3} !^([^#]+)#\1$
    RewriteCond %{ENV:C2}#%{ENV:C4} !^([^#]+)#\1$
    RewriteCond %{ENV:C3}#%{ENV:C4} !^([^#]+)#\1$
    RewriteRule ^ - [E=ALL_PART1_CHARS_DISTINCT:1]

    # redirect to completion page if distinct
    RewriteCond %{ENV:ALL_PART1_CHARS_DISTINCT} =1
    RewriteRule ^/part1-iter/(.{4})[^/]*/(1+)$ /part1-complete/$1/111$2 [R=301,L]

    # remove the first character, increment the iterator and continue on
    RewriteCond %{ENV:ALL_PART1_CHARS_DISTINCT} !=1
    RewriteRule ^/part1-iter/.([^/]+)/(1+)$ /part1-iter/$1/1$2 [R=301,L]

    #########################
    #        PART 2         #
    #########################

    # entry point to part 2
    RedirectMatch 301 ^/part2/(.*)$ /part2-iter/$1/1

    # extract first 14 characters from string. this needs to be split into two capture
    # groups because apache reads $10 as $1 and a 0 literal. ask me how i know :)
    RewriteRule ^/part2-iter/(.)(.)(.)(.)(.)(.)(.)(.)(.) - [E=CC1:$1,E=CC2:$2,E=CC3:$3,E=CC4:$4,E=CC5:$5,E=CC6:$6,E=CC7:$7,E=CC8:$8,E=CC9:$9]
    RewriteRule ^/part2-iter/.{9}(.)(.)(.)(.)(.) - [E=CC10:$1,E=CC11:$2,E=CC12:$3,E=CC13:$4,E=CC14:$5]

    # assert that all characters are distinct
    RewriteCond %{ENV:CC1}#%{ENV:CC2} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC3} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC1}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC3} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC2}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC4} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC3}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC5} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC4}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC6} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC5}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC7} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC6}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC8} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC7}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC9} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC8}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC10} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC9}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC11} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC10}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC12} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC11}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC12}#%{ENV:CC13} !^([^#]+)#\1$
    RewriteCond %{ENV:CC12}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteCond %{ENV:CC13}#%{ENV:CC14} !^([^#]+)#\1$
    RewriteRule ^ - [E=ALL_PART2_CHARS_DISTINCT:1]

    # redirect to completion page if distinct
    RewriteCond %{ENV:ALL_PART2_CHARS_DISTINCT} =1
    RewriteRule ^/part2-iter/(.{14})[^/]*/(1+)$ /part2-complete/$1/1111111111111$2 [R=301,L]

    # remove the first character, increment the iterator and continue on
    RewriteCond %{ENV:ALL_PART2_CHARS_DISTINCT} !=1
    RewriteRule ^/part2-iter/.([^/]+)/(1+)$ /part2-iter/$1/1$2 [R=301,L]
</VirtualHost>
diff --git a/2022/6/read.sh b/2022/6/read.sh
deleted file mode 100755
index 68ca040..0000000 100755
--- a/2022/6/read.sh
+++ /dev/null
@@ -1,9 +1,0 @@
#!/usr/bin/env bash

STDIN=$(cat -)

PART1=$(curl --max-redirs 100000 -Ls "http://127.0.0.1:8888/part1/$STDIN" -o /dev/null -w %{url_effective})
echo $(echo $PART1 | gsed -E 's/.*\/([1]+)/\1/' | tr -d '\n' | wc -c)

PART2=$(curl --max-redirs 100000 -Ls "http://127.0.0.1:8888/part2/$STDIN" -o /dev/null -w %{url_effective})
echo $(echo $PART2 | gsed -E 's/.*\/([1]+)/\1/' | tr -d '\n' | wc -c)
diff --git a/2022/05/executor/main.tf b/2022/05/executor/main.tf
new file mode 100644
index 0000000..3ed34a4 100644
--- /dev/null
+++ a/2022/05/executor/main.tf
@@ -1,0 +1,24 @@
locals {

  remainingInstructions = length(var.instructions) == 0 ? [] : slice(var.instructions, 1, length(var.instructions))
  currentInstruction = length(var.instructions) == 0 ? null : split(",", var.instructions[0])
  amount = local.currentInstruction == null ? null : tonumber(local.currentInstruction[0])
  fromStack = local.currentInstruction == null ? null : (tonumber(local.currentInstruction[1]) - 1)
  toStack = local.currentInstruction == null ? null : (tonumber(local.currentInstruction[2]) - 1)
  setFromBlocks = local.currentInstruction == null ? null : slice(var.stackedBlocks[local.fromStack], local.amount, length(var.stackedBlocks[local.fromStack]))
  newToBlocks = local.currentInstruction == null ? null : slice(var.stackedBlocks[local.fromStack], 0, local.amount)
  newToBlocksMaybeReversed = var.reverse ? reverse(local.newToBlocks) : local.newToBlocks
  setToBlocks = local.currentInstruction == null ? null : concat(local.newToBlocksMaybeReversed, var.stackedBlocks[local.toStack])
  finalBlocks = local.currentInstruction == null ? var.stackedBlocks : [for i in range(length(var.stackedBlocks)) : (i == local.fromStack ? local.setFromBlocks : (i == local.toStack ? local.setToBlocks : var.stackedBlocks[i]))]
}

variable "stackedBlocks" {}
variable "instructions" {}
variable "reverse" {}

output "finalBlocks" {

  value = local.finalBlocks
}

output "remainingInstructions" {

  value = local.remainingInstructions
}
diff --git a/2022/5/executor/main.tf b/2022/5/executor/main.tf
deleted file mode 100644
index 3ed34a4..0000000 100644
--- a/2022/5/executor/main.tf
+++ /dev/null
@@ -1,24 +1,0 @@
locals {

  remainingInstructions = length(var.instructions) == 0 ? [] : slice(var.instructions, 1, length(var.instructions))
  currentInstruction = length(var.instructions) == 0 ? null : split(",", var.instructions[0])
  amount = local.currentInstruction == null ? null : tonumber(local.currentInstruction[0])
  fromStack = local.currentInstruction == null ? null : (tonumber(local.currentInstruction[1]) - 1)
  toStack = local.currentInstruction == null ? null : (tonumber(local.currentInstruction[2]) - 1)
  setFromBlocks = local.currentInstruction == null ? null : slice(var.stackedBlocks[local.fromStack], local.amount, length(var.stackedBlocks[local.fromStack]))
  newToBlocks = local.currentInstruction == null ? null : slice(var.stackedBlocks[local.fromStack], 0, local.amount)
  newToBlocksMaybeReversed = var.reverse ? reverse(local.newToBlocks) : local.newToBlocks
  setToBlocks = local.currentInstruction == null ? null : concat(local.newToBlocksMaybeReversed, var.stackedBlocks[local.toStack])
  finalBlocks = local.currentInstruction == null ? var.stackedBlocks : [for i in range(length(var.stackedBlocks)) : (i == local.fromStack ? local.setFromBlocks : (i == local.toStack ? local.setToBlocks : var.stackedBlocks[i]))]
}

variable "stackedBlocks" {}
variable "instructions" {}
variable "reverse" {}

output "finalBlocks" {

  value = local.finalBlocks
}

output "remainingInstructions" {

  value = local.remainingInstructions
}