hero image

Advent of Code 2024

Dec 25, 2024

The challenge

Advent of Code is a 25 day challenge of language agnostic problems for the month of December in the lead up to Christmas. Being language agnostic, the problems can be solved by any means necessary including by hand, on LibreOffice Calc, with Scratch or with a programming language.

I decided that for 2024, I would try and have a crack trying a different language for each day:

You can find all of my solutions in my GitHub repo.

Visualizing language usage

GitHub will automatically generate the language distribution in a repository according to linguist to generate a HTML span like the one below.

  • Rust44.2%
  • Python9.2%
  • C8.6%
  • C++5.7%
  • Zig5.3%
  • Java4.3%
  • Other22.7%
  • Although this fulfils my intended purpose, when too many languages are used in a single repo, information is aggregated into an ‘Other’ category. If GitHub wasn’t going to flaunt all the different languages that I’m tooooootally proficient at, I was gonna have to do it myself.

    Initially, I wrote a nodejs script that would generate the HTML element on every commit with a GitHub workflow actions script. Using a JSON file that was a subset linguist’s yaml file worked as intended, however, GitHub’s README.md files do not render raw HTML.

    So, my next idea was to redraw an image that the README.md file would reference in order to automatically update the language distribution on each commit. I decided to use python and learn a little bit of matplotlib to generate the language distribution in a doughnut graph.

    This script was a perfectly acceptable however, the arbitrary ordering of the globing and thus language distribution did get on my nerves so much so that I had to cave into some scope creep; I decided also to sort the languages based on colour.

    Sorting the Languages by Colour

    Initially I tried to sort the colours by their hue

    let hue = Math.atan2(Math.sqrt(3) * (G - B), 2 * R - G - B);

    Hue

    This worked well except for colours that were achromatic (whereby r == g == b). The hue function would break down for these languages (Crystal and C) and so I needed another dimension for sorting.

    Two methods were considered for this extra dimension, luminance and value / lightness.

    let luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
    let value = Math.max(R, G, B);

    Luminance

    Value

    Looking at the two outputs, the vibes for luminance were just better because as a human, colour temperature is an important factor that value seems to not account for.

    Though I now had 2 dimensions to sort the colours, the language distribution graph was only in 1. So, I decided that I would just find the grays and blacks and chuck them next to the colour of lowest luminance. This produced the final image:

    language distribution

    If more robust colour sorting is wanted in the future, I think I would use both the hue and luminance in my calculation to get to a 2D plot and from there do some dimensionality reduction.

    The GitHub workflow script can be found here in the same aoc repo. If you know of a way to hide the commits of the github actions user please let me know!

    The Languages

    Most languages have their specific use cases where they flourish. I will be judging them based on ✨vibes✨ according to my experience solving the advent of code problems. Some criteria that I would like met when using a programming language include:

    • Easy to read and write (should be close to english or common language patterns)
    • Good documentation with examples (I will try avoid tutorials and head straight to the docs)
    • Quick installation and setup (if I can’t make it a simple shebangable script at least make it a simple cargo run or equivalent)
    • Helpful error messages (Code completion is nice as well)
    • I don’t want to be fighting the language or compiler, i want to be fighting the problem at hand
    • Other requirements: functional paradigm support, lambdas, destructuring, types or type hints

    Note: For most languages on this list I have little to no experience with so clearer opinions on them would require some more time marinating

    Day 01: Haskell

    I took a course at uni on Haskell with a terrible professor as my first introduction to pure functional programming. Although assignments were released weeks after they should have been, I still found this new way of thinking and expanding my mind to be satisfying (though I still don’t get what ‘A monad is a monoid in the category of endofunctors’ means). Using the IO monad and the simplicity of day01’s problem made this a pleasant experience. Additionally, resources like Hoogle were lovely to work with. Though I like functional programming, Haskell just feels a bit off beat and frankly, scary at times.

    Overall Rating: 5.7
    ******
    ****

    TLDR: Its cool paradigm but I don’t why I would actively choose to use it in real world applications

    Solution
    Open File on Github >
    module Main (main) where
    
    import System.IO
    import Data.List
    import Text.Read (readMaybe)
    
    main :: IO ()
    -- main = part1 "day01.txt"
    main = part2 "day01.txt"
    
    pathToUnzippedInts :: FilePath -> IO ([Integer], [Integer])
    pathToUnzippedInts inputFile = do
        content <- readFile inputFile
        let linesList = lines content
        let unzippedInts = unzip $
                map (\line ->
                    let [left_str, right_str] = words line
                    in (read left_str :: Integer, read right_str :: Integer)
                ) linesList
        return unzippedInts
    
    part1 :: FilePath -> IO ()
    part1 filePath = do
        (list1, list2) <- pathToUnzippedInts filePath
        let total_distance = sum $ map (\(left_n, right_n) -> abs (left_n - right_n)) (zip (sort list1) (sort list2))
        print total_distance
    
    part2 :: FilePath -> IO ()
    part2 filePath = do
        (list1, list2) <- pathToUnzippedInts filePath
        let total_similarity = sum $ map (\x -> x * fromIntegral (length $ filter (== x) list2)) list1
        print total_similarity

    Day 02: Lua

    There was a lot of hype around Lua (means moon 🌕). All the youngins be using it for Roblox and I also see it in neovim, obs and other random places. It was very easy to setup and get the hang of with only 23 keywords. The trouble with being so lightweight is the lack of niceties that come with languages that are more featureful meaning I had to manually implement some behaviour like a table.copy. I also don’t know how I feel about the fact that arrays can be indexed with any value and so are not always 1 indexed.

    Overall Rating: 7.7
    ********
    **

    TLDR: Nice and simple language

    Solution
    Open File on Github >
    #!/bin/lua
    
    function table.copy(t)
        local t2 = {}
        for k,v in pairs(t) do
            t2[k] = v
        end
        return t2
    end
    
    function line_to_report(line)
        report = {}
        for token in string.gmatch(line, "[^%s]+") do
            table.insert(report, tonumber(token))
        end
        return report
    end
    
    function is_in_bounds(arr, bound_lower, bound_upper)
        for i=1,#arr - 1 do
            difference = arr[i] - arr[i + 1]
            if not (difference >= bound_lower and difference <= bound_upper) then
                return false
            end
        end
        return true
    end
    
    function is_safe(report)
        return is_in_bounds(report, -3, -1) or is_in_bounds(report, 1, 3)
    end
    
    function part1()
        io.input("./day02.txt")
        n_safe = 0
        for line in io.lines() do
            if is_safe(line_to_report(line)) then
                n_safe = n_safe + 1
            end
        end
        print(n_safe)
    end
    
    function part2()
        io.input("./day02.txt")
        n_safe = 0
        for line in io.lines() do
            report = line_to_report(line)
    
            for i, num in ipairs(report) do
                report_temp = table.copy(report)
                table.remove(report_temp, i)
                if is_safe(report_temp) then
                    n_safe = n_safe + 1
                    break
                end
            end
        end
        print(n_safe)
    end
    
    part1()
    part2()

    Day 03: Bash

    I use Debian which comes with bash pre-installed. I took a uni course that dipped my toes into bash scripts which I found awesome for simple tasks in the terminal and regexing my life away. I learnt the art of the ‘|’ pipe operator and have fallen in love since. As such, I was able to do this day’s problem in just 2 lines of code after a bit of regex101.

    #!/bin/bash
    
    # part1
    cat day03.txt | grep -Eo 'mul\([[:digit:]]+,[[:digit:]]+\)' | sed -r 's/mul\((.+),(.+)\)/\1*\2/g' | xargs | sed -r 's/ /+/g' | bc
    
    # part2
    echo $(cat day03.txt) | perl -pe "s/don\'t\(\).*?do\(\)|(don't\(\).*?$)//g" | grep -Eo 'mul\([[:digit:]]+,[[:digit:]]+\)' | sed -r 's/mul\((.+),(.+)\)/\1*\2/g' | xargs | sed -r 's/ /+/g' | bc

    Additionally, just copying your code from the script straight into the terminal is a fun way to verify correctness.

    Overall Rating: 7
    *******
    ***

    TLDR: Piping is lit. Everyone should know at least a little bit of bash or sh

    Open Solution on GitHub

    Day 04: Go

    Go felt like C but with some features to make life a little easier. My solution for this day’s challenge mainly just worked with jumping around array indices and so I wasn’t forced to lean into some more Go specific idiomatic syntax.

    Some things like the lack of an All or Any function just lead me to write it myself.

    Overall Rating: 6.6
    *******
    ***

    TLDR: Pretty decent language no fusses

    Solution
    Open File on Github >
    package main
    
    import (
    	"fmt"
    	"os"
    	"strings"
    )
    
    func main() {
    	dat, err := os.ReadFile("day04.txt")
    	if err != nil {
    		panic(err)
    	}
    	word_search := strings.Split(string(dat), "\n")
    
    	total_xmas_appears := 0
    	total_x_mas_appears := 0
    	for i, row := range word_search {
    		for j, letter := range row {
    			total_xmas_appears += xmas_appears(i, j, letter, word_search)
    			total_x_mas_appears += x_mas_appears(i, j, letter, word_search)
    		}
    	}
    	fmt.Println("part1: ", total_xmas_appears)
    	fmt.Println("part2: ", total_x_mas_appears)
    }
    
    func All(chars []rune, f func(index int, char rune) bool) bool {
    	for index, char := range chars {
    		if !f(index, char) {
    			return false
    		}
    	}
    	return true
    }
    
    func xmas_appears(row int, col int, letter rune, word_search []string) int {
    	matches := 0
    	if letter != rune('X') {
    		return matches
    	}
    
    	row_len := len(word_search)
    	col_len := len(word_search[0])
    
    	chars := []rune{'M', 'A', 'S'}
    
    	//                       down    up       right   left   quad_4   quad_1   quad_2    quad_3
    	directions := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, -1}, {-1, 1}}
    
    	for _, direction := range directions {
    		if All(chars, func(index int, char rune) bool {
    			i := row + direction[0]*(index+1)
    			j := col + direction[1]*(index+1)
    			return i >= 0 && i < row_len &&
    				j >= 0 && j < col_len &&
    				char == rune(word_search[i][j])
    		}) {
    			matches += 1
    		}
    	}
    
    	return matches
    }
    
    func x_mas_appears(row int, col int, letter rune, word_search []string) int {
    	matches := 0
    	if letter != rune('A') {
    		return matches
    	}
    
    	row_len := len(word_search)
    	col_len := len(word_search[0])
    
    	chars := []rune{'S', 'S', 'M', 'M'}
    
    	// slide diagonals around directions and check for SSMM
    	directions := [][2]int{{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}
    	for i := 0; i < len(directions); i++ {
    		is_matching := true
    		for j := 0; j < len(chars); j++ {
    			row_i := row + (directions[(i+j)%len(directions)][0])
    			col_i := col + (directions[(i+j)%len(directions)][1])
    			if row_i < 0 || row_i >= row_len ||
    				col_i < 0 || col_i >= col_len ||
    				rune(word_search[row_i][col_i]) != chars[j] {
    				is_matching = false
    				break
    			}
    		}
    
    		if is_matching {
    			matches += 1
    			break
    		}
    	}
    
    	return matches
    }

    Day 05: C#

    I’ve heard that C# is just a slightly better Java. To me it just seemed like the language you use for game development. The functional language features were easy to use and worked nicely for this day’s problem making the experience pleasant. Maybe I’ll make a game in Godot in the future using C#.

    Overall Rating: 7.3
    *******
    ***

    TLDR: Functional C# 👍

    Solution
    Open File on Github >
    class Day05
    {
        public record Ordering(List<int> before);
        public record Update_Pages(int[] pages, int middle);
    
        static void Main(string[] args)
        {
            var parts = File.ReadAllText("./day05.txt").Split("\n\n", count: 2);
            var ordering_rules = string_to_rules(parts[0]);
            var updates = string_to_updates(parts[1]);
    
            Console.WriteLine("Part 1: {0}", part1(ordering_rules, updates));
            Console.WriteLine("Part 2: {0}", part2(ordering_rules, updates));
        }
    
        static Dictionary<int, Ordering> string_to_rules(string text)
        {
            Dictionary<int, Ordering> ordering_rules = new Dictionary<int, Ordering>();
    
            foreach (var line in text.Split('\n'))
            {
                var rules = line.Split('|').Select((str, _) => int.Parse(str)).ToArray();
                var before = rules[0];
                var after = rules[1];
    
                if (ordering_rules.TryGetValue(after, out var ordering_after))
                {
                    ordering_after.before.Add(before);
                }
                else
                {
                    ordering_rules.Add(after, new([before]));
                }
            }
    
            return ordering_rules;
        }
    
        static Update_Pages[] string_to_updates(string text)
        {
            return text
                .Split("\n")
                .Select((line, index) =>
                {
                    var update_pages = line.Split(',').Select((str, _) => int.Parse(str)).ToArray();
    
                    var middle = update_pages[update_pages.Length / 2];
                    return new Update_Pages(update_pages, middle);
                })
                .ToArray();
        }
    
        static int part1(Dictionary<int, Ordering> rules, Update_Pages[] updates)
        {
            return updates.Aggregate(0, (count, update) =>
            {
                var isCorrectOrder = update.pages
                    .Zip(update.pages.Skip(1), (current, next) => is_correct_order(current, [next], rules))
                    .All(result => result);
    
                return isCorrectOrder ? count + update.middle : count;
            });
        }
    
        static bool is_correct_order(int head, int[] tail, Dictionary<int, Ordering> rules)
        {
            if (rules.TryGetValue(head, out var head_ordering))
            {
                return !tail.All(head_ordering.before.Contains);
            }
    
            return true;
        }
    
        static int part2(Dictionary<int, Ordering> rules, Update_Pages[] updates)
        {
            return updates.Aggregate(0, (acc, update) =>
            {
                var isCorrectOrder = update.pages
                    .Zip(update.pages.Skip(1), (current, next) => is_correct_order(current, [next], rules))
                    .All(result => result);
    
                return !isCorrectOrder ? acc + sorted_middle(update.pages, rules) : acc;
            });
        }
    
        static int sorted_middle(int[] pages, Dictionary<int, Ordering> rules)
        {
            var sorted_list = pages.ToList();
            sorted_list.Sort((int a, int b) =>
            {
                if (rules.TryGetValue(a, out var a_ord) && a_ord.before.Contains(b)) { return 1; }
                if (rules.TryGetValue(b, out var b_ord) && b_ord.before.Contains(a)) { return -1; }
                return 0;
            });
            return sorted_list[pages.Length / 2];
        }
    }

    Day 06: C++

    C++ initially seemed like a language to code in mainly to say ‘I use c++ btw’. It includes some features that do make the experience feel better than C like destructuring, vectors and dictionaries. This day’s problem may have been one of the slowest with the speed of checking for cycles in a graph however, the thread and atomic libraries were easy to work with.

    Similar to Go, there didn’t seem to be anything I hated with the language.

    Overall Rating: 6.9
    *******
    ***

    TLDR: Decent language. Just C but plus plus as well

    Solution
    Open File on Github >
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    #include <tuple>
    #include <algorithm>
    #include <thread>
    #include <atomic>
    #include <vector>
    #include <functional>
    #include <iostream>
    
    using namespace std;
    
    tuple<vector<string>, tuple<int, int, tuple<int, int>>> file_to_vector(const char *file_path);
    
    int part1(tuple<int, int, tuple<int, int>> pos, vector<string> &lines);
    int part2(tuple<int, int, tuple<int, int>> &pos, vector<string> &lines);
    
    int main()
    {
        auto [lines, pos] = file_to_vector("./day06.txt");
    
        cout << "part1: " << part1(pos, lines) << endl;
        cout << "part2: " << part2(pos, lines) << endl;
    
        return 0;
    }
    
    tuple<vector<string>, tuple<int, int, tuple<int, int>>> file_to_vector(const char *file_path)
    {
        ifstream file(file_path);
        vector<string> lines;
        string s;
        tuple<int, int, tuple<int, int>> pos;
    
        for (int row = 0; getline(file, s); row++)
        {
            int col = s.find('^');
            if (col != string::npos)
            {
                pos = {row, col, {-1, 0}};
            }
    
            lines.push_back(s);
        }
    
        return {lines, pos};
    }
    
    void move_forward(tuple<int, int, tuple<int, int>> &pos)
    {
        get<0>(pos) += get<0>(get<2>(pos));
        get<1>(pos) += get<1>(get<2>(pos));
    }
    
    void rotate_clockwise(tuple<int, int, tuple<int, int>> &pos)
    {
        int d_row = get<0>(get<2>(pos));
        int d_col = get<1>(get<2>(pos));
        get<2>(pos) = make_tuple(d_col, -d_row);
    }
    
    bool move_guard(tuple<int, int, tuple<int, int>> &pos, vector<string> &map)
    {
        auto [row, col, direction] = pos;
        auto [d_row, d_col] = direction;
    
        if (row + d_row < 0 || row + d_row >= map.size() ||
            col + d_col < 0 || col + d_col >= map.front().size())
        {
            map[row][col] = 'X';
            return false;
        }
    
        if (map[row + d_row][col + d_col] == '#')
        {
            rotate_clockwise(pos);
        }
        else
        {
            map[row][col] = 'X';
            move_forward(pos);
        }
    
        return true;
    }
    
    int part1(tuple<int, int, tuple<int, int>> pos, vector<string> &lines)
    {
        while (move_guard(pos, lines))
        {
        }
    
        size_t distinct_moves = 0;
        for (const auto &line : lines)
        {
            distinct_moves += count_if(line.begin(), line.end(), [](char c)
                                       { return c == 'X'; });
        }
    
        return distinct_moves;
    }
    
    tuple<bool, bool> move_guard2(tuple<int, int, tuple<int, int>> &pos, vector<string> &map, vector<tuple<int, int, tuple<int, int>>> &visited)
    {
        auto [row, col, direction] = pos;
        auto [d_row, d_col] = direction;
    
        if (find(visited.begin(), visited.end(), pos) != visited.end())
        {
            return {false, true};
        }
    
        if (row + d_row < 0 || row + d_row >= map.size() ||
            col + d_col < 0 || col + d_col >= map.front().size())
        {
            map[row][col] = 'X';
            return {true, false};
        }
    
        if (map[row + d_row][col + d_col] == '#' || map[row + d_row][col + d_col] == 'O')
        {
            rotate_clockwise(pos);
        }
        else
        {
            move_forward(pos);
            map[row][col] = 'X';
            visited.push_back(make_tuple(row, col, make_tuple(d_row, d_col)));
        }
    
        return {false, false};
    }
    
    bool creates_cycle(tuple<int, int> obstacle_pos, tuple<int, int, tuple<int, int>> pos, vector<string> lines)
    {
        auto [o_row, o_col] = obstacle_pos;
        if (lines[o_row][o_col] == '#' || lines[o_row][o_col] == '^')
        {
            return false;
        }
    
        lines[o_row][o_col] = 'O';
    
        vector<tuple<int, int, tuple<int, int>>> visited;
    
        for (;;)
        {
            auto [left_map, cycle] = move_guard2(pos, lines, visited);
            if (left_map)
            {
                return false;
            }
            if (cycle)
            {
                return true;
            }
        }
    
        return true;
    }
    
    int part2(tuple<int, int, tuple<int, int>> &pos, vector<string> &lines)
    {
        atomic<int> obstructions{0};
        int num_threads = thread::hardware_concurrency();
        vector<thread> threads;
    
        int rows_per_thread = lines.size() / num_threads;
        for (int t = 0; t < num_threads; t++)
        {
            int start_row = t * rows_per_thread;
            int end_row = (t == num_threads - 1) ? lines.size() : start_row + rows_per_thread;
    
            threads.emplace_back([&](int start, int end)
                                 {
                for (int i = start; i < end; i++)
                {
                    for (int j = 0; j < lines[i].size(); j++)
                    {
                        if (lines[i][j] == 'X')
                        {
                            // cout << "trying obstacle at: " << i << ", " << j << " obstructions: " << obstructions << endl;
                            if (creates_cycle({i, j}, pos, lines))
                            {
                                obstructions++;
                            }
                        }
                    }
                } }, start_row, end_row);
        }
    
        for (auto &thread : threads)
        {
            thread.join();
        }
    
        return obstructions.load();
    }

    Day 07: Java

    Java was my first introduction to OOP in uni. Though the language was perfectly fine with the ability to use streams and a lot of nice features you can just import, for my uni course, the idea that SOLID principles had the final say and not performance or some objective measurement didn’t sit with me right (I even showed my tutor how an assignment could be significantly ‘optimised’ just by switching from a strategy pattern to enumeration with a switch statement). Overtime, I have come to the understanding that “code doesn’t need to be refactored to perfection” as requirements constantly change anyways.

    Overall Rating: 7.2
    *******
    ***

    TLDR: Java is fine as long as I’m not stuck in design pattern hell

    Solution
    Open File on Github >
    import java.io.File;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.util.*;
    import java.util.stream.*;
    
    class Day07 {
        public static void main(String[] args) {
            List<Map.Entry<Long, Long[]>> entryList = fileToList("day07.txt");
    
            System.out.println("Part 1: " + entryList
                    .stream()
                    .mapToLong(entry -> calibration(entry.getKey(), entry.getValue(),
                            new Operation[] { Operation.Add, Operation.Mul }))
                    .sum());
    
            System.out.println("Part 2: " + entryList
                    .stream()
                    .mapToLong(entry -> calibration(entry.getKey(), entry.getValue(),
                            new Operation[] { Operation.Add, Operation.Mul, Operation.Cat }))
                    .sum());
        }
    
        static List<Map.Entry<Long, Long[]>> fileToList(String filePath) {
            try {
                return Files.readAllLines(new File(filePath).toPath())
                        .stream()
                        .map(line -> {
                            String[] parts = line.split(": ");
                            Long answer = Long.parseLong(parts[0]);
                            Long[] nums = Arrays.stream(parts[1].split(" "))
                                    .map(Long::parseLong)
                                    .toArray(Long[]::new);
                            return new AbstractMap.SimpleEntry<>(answer, nums);
                        })
                        .collect(Collectors.toList());
            } catch (IOException e) {
                return Collections.emptyList();
            }
        }
    
        enum Operation {
            Add,
            Mul,
            Cat
        }
    
        static Long apply(Operation op, Long a, Long b) {
            switch (op) {
                case Add:
                    return a + b;
                case Mul:
                    return a * b;
                case Cat:
                    return Long.parseLong(a.toString() + b.toString());
                default:
                    return 0L;
            }
        }
    
        static Operation[][] operationPermutations(Operation[] operations, Operation[] operation_options) {
            List<Operation[]> permutations = new ArrayList<>();
            generatePermutations(operations, 0, permutations, operation_options);
            return permutations.toArray(new Operation[0][]);
        }
    
        private static void generatePermutations(Operation[] operations, int index, List<Operation[]> result,
                Operation[] operation_options) {
            if (index == operations.length) {
                result.add(operations.clone());
                return;
            }
    
            for (Operation operation_option : operation_options) {
                operations[index] = operation_option;
                generatePermutations(operations, index + 1, result, operation_options);
            }
        }
    
        static Long calibration(Long target, Long[] nums, Operation[] operation_options) {
            if (nums.length == 0)
                return 0L;
    
            Operation[] operations = new Operation[nums.length - 1];
            Arrays.fill(operations, Operation.Add);
    
            for (int l = 0; l < operations.length; l++) {
                boolean addAnotherMul = false;
    
                for (Operation[] perm : operationPermutations(operations, operation_options)) {
                    Long result = nums[0];
                    for (int i = 0; i < perm.length; i++) {
                        result = apply(perm[i], result, nums[i + 1]);
                    }
    
                    if (result.equals(target)) {
                        return target;
                    } else if (result < target) {
                        addAnotherMul = true;
                    }
                }
    
                if (!addAnotherMul) {
                    return 0L;
                }
            }
    
            return 0L;
        }
    }

    Day 08: TypeScript

    TypeScript / JavaScript are probably my most used languages. Because of my comfortability in them, I was able to just focus on the problem at hand and not try to learn the language at the same time. This made it a very lovely experience however, I am interested in trying JSDoc as an alternative.

    Overall Rating: 8
    ********
    **

    TLDR: TypeScript is just a linter

    Solution
    Open File on Github >
    import fs from "fs";
    
    type Antennas = {
      [key: string]: [number, number][];
    };
    
    function file_to_antennas(file_path: string): [Antennas, [number, number]] {
      let antennas: Antennas = {};
    
      const lines = fs.readFileSync(file_path, "utf-8").split("\n");
    
      lines.forEach((line, row) => {
        line.split("").forEach((char, col) => {
          if (char == "." || char == "#") {
            return;
          }
    
          if (!antennas[char]) {
            antennas[char] = [[row, col]];
          } else {
            antennas[char].push([row, col]);
          }
        });
      });
    
      return [antennas, [lines.length, lines[0].length]];
    }
    
    function is_inbounds(
      antinode: [number, number],
      bounds: [number, number]
    ): boolean {
      return (
        antinode[0] >= 0 &&
        antinode[0] < bounds[0] &&
        antinode[1] >= 0 &&
        antinode[1] < bounds[1]
      );
    }
    
    function get_resonant_locations1(
      antenna_a: [number, number],
      antenna_b: [number, number],
      bounds: [number, number]
    ): [number, number][] {
      const row_distance = antenna_a[0] - antenna_b[0];
      const col_distance = antenna_a[1] - antenna_b[1];
    
      const resonant_locations: [number, number][] = [
        [antenna_a[0] + row_distance, antenna_a[1] + col_distance],
        [antenna_b[0] - row_distance, antenna_b[1] - col_distance],
      ];
    
      return resonant_locations.filter((x) => is_inbounds(x, bounds));
    }
    
    function get_resonant_locations2(
      antenna_a: [number, number],
      antenna_b: [number, number],
      bounds: [number, number]
    ): [number, number][] {
      const row_distance = antenna_a[0] - antenna_b[0];
      const col_distance = antenna_a[1] - antenna_b[1];
    
      const resonant_locations: [number, number][] = [];
    
      let location = antenna_a;
      while (is_inbounds(location, bounds)) {
        resonant_locations.push(location);
        location = [location[0] + row_distance, location[1] + col_distance];
      }
    
      location = antenna_b;
      while (is_inbounds(location, bounds)) {
        resonant_locations.push(location);
        location = [location[0] - row_distance, location[1] - col_distance];
      }
    
      return resonant_locations;
    }
    
    function count_antinodes(
      antennas: Antennas,
      bounds: [number, number],
      get_resonant_locations: (
        a: [number, number],
        b: [number, number],
        bounds: [number, number]
      ) => [number, number][]
    ): number {
      const antinodes: Set<string> = new Set();
    
      for (const pos of Object.values(antennas)) {
        for (let i = 0; i < pos.length; i++) {
          for (let j = i + 1; j < pos.length; j++) {
            get_resonant_locations(pos[i], pos[j], bounds).forEach((x) =>
              antinodes.add(x.toString())
            );
          }
        }
      }
    
      return antinodes.size;
    }
    
    let [antennas, [rows, cols]] = file_to_antennas("./day08.txt");
    console.log(
      `part1: ${count_antinodes(antennas, [rows, cols], get_resonant_locations1)}`
    );
    console.log(
      `part2: ${count_antinodes(antennas, [rows, cols], get_resonant_locations2)}`
    );

    Day 09: C

    C is the first programming language I learnt in uni and you gotta respect how easy it is to mentally map the code to assembly instructions. Additionally, building my data structures and algorithms manually was helpful for getting a deeper understanding of how the computer was actually using them. Though I respect C in its position as the OG, I find that it can take away from solving the problem and force you into other tasks like making sure your program is memory safe.

    Overall Rating: 6.7
    *******
    ***

    TLDR: pay your respects to C

    Solution
    Open File on Github >
    #include <stdio.h>
    #include <string.h>
    
    #define FILE_SIZE 20000
    #define TRUE 1
    #define FALSE 0
    
    int file_to_expanded(const char *file, int *expanded);
    int defrag_part1(int *to_defrag, int size);
    int defrag_part2(int *to_defrag, int size);
    long sum_storage(int *arr, int size);
    
    int main(void) {
        int arr[FILE_SIZE * 10];
        int size;
    
        size = file_to_expanded("./day09.txt", arr);
        size = defrag_part1(arr, size);
        printf("part1: %ld\n", sum_storage(arr, size));
    
        size = file_to_expanded("./day09.txt", arr);
        size = defrag_part2(arr, size);
        printf("part2: %ld\n", sum_storage(arr, size));
    }
    
    long sum_storage(int *arr, int size) {
        long sum = 0;
        for (int i = 0; i < size; i++) {
            if (arr[i] != -1) {
                sum += arr[i] * i;
            }
        }
        return sum;
    }
    
    int file_to_expanded(const char *file, int *expanded) {
        FILE *fp;
        fp = fopen(file, "r");
        if (fp == NULL) {
            return 1;
        }
    
        char file_str[FILE_SIZE];
        if (!fscanf(fp, "%s", file_str)) {
            return 1;
        }
        fclose(fp);
    
        int file_len = strlen(file_str);
        int e_idx = 0;
        for (int i = 0; i < file_len; i++) {
            int id = file_str[i] - '0';
            for (int j = 0; j < id; j++) {
                if (i % 2 == 0) {
                    expanded[e_idx] = i / 2;
                } else {
                    expanded[e_idx] = -1;
                }
                e_idx++;
            }
        }
    
        return e_idx;
    }
    
    int defrag_part1(int *arr, int size) {
        int j = size - 1;
        for (int i = 0; i < size; i++) {
            if (arr[i] == -1) {
                while (arr[j] == -1) {
                    j--;
                }
                if (i >= j) {
                    break;
                }
    
                arr[i] = arr[j];
                j--;
            }
        }
    
        return j + 1;
    }
    
    int defrag_part2(int *arr, int size) {
        int j;
        int file_id;
        int file_size;
        int file_end;
    
        int gap_size;
        for (j = size - 1; j >= 0; j--) {
            if (arr[j] != -1) {
                file_end = j;
                for (file_id = arr[j], file_size = 1; arr[j - 1] != -1 && file_id == arr[j - 1]; j--) {
                    file_size++;
                }
    
                for (int i = 0; i <= size; i++) {
                    if (arr[i] == -1) {
                        gap_size++;
                    } else {
                        gap_size = 0;
                    }
    
                    if (gap_size >= file_size && i < j) {
                        for (int k = i, l = j; l <= file_end && l >= 0; k--, l++) {
                            arr[k] = arr[l];
                            arr[l] = -1;
                        }
                        break;
                    }
                }
            }
        }
    
        return size;
    }

    Day 10: JavaScript

    Solving this days’ problem in the airport was lovely. JavaScript is one my favourite languages as someone that dabbles in web development. When first learning about lambdas in python, I was so confused, but, using arrow functions in js felt way more intuitive. JavaScript’s implementation of the functional paradigm might just be my favourite with optional indexing and dot notation. Though it doesn’t have a sum function, it is really easy to extend Array.prototype,

    Array.prototype.sum = function () {
      return this.reduce((acc, x) => acc + x, 0);
    };

    I know that JavaScript can get real weird at times but if you want to connect to the world, I feel like it is the best language to learn.

    Overall Rating: 9.1
    *********
    *

    TLDR: I like JavaScript

    Solution
    Open File on Github >
    import fs from "fs";
    
    const directions = [
      [-1, 0],
      [0, 1],
      [1, 0],
      [0, -1],
    ];
    
    Array.prototype.sum = function () {
      return this.reduce((acc, x) => acc + x, 0);
    };
    
    function file_to_map_and_start_pos(file_path) {
      const start_positions = [];
    
      const map = fs
        .readFileSync(file_path, "ascii")
        .split("\n")
        .map((x, row) =>
          x.split("").map((x, col) => {
            const num = parseInt(x);
            if (num == 0) {
              start_positions.push([row, col]);
            }
            return num;
          })
        );
    
      return [map, start_positions];
    }
    
    function calculate_score(map, pos, visited, height) {
      if (height == 9) {
        visited.add(pos.toString());
        return 1;
      }
    
      return directions
        .filter(([d_row, d_col]) => {
          let [row, col] = [pos[0] + d_row, pos[1] + d_col];
          return (
            row >= 0 &&
            row < map.length &&
            col >= 0 &&
            col < map[0].length &&
            !visited.has([row, col].toString()) &&
            map[row][col] === height + 1
          );
        })
        .map(([d_row, d_col]) => {
          const pos_new = [pos[0] + d_row, pos[1] + d_col];
          visited.add(pos.toString());
          return calculate_score(map, pos_new, visited, height + 1);
        })
        .sum();
    }
    
    function calculate_rating(map, pos, height) {
      if (height == 9) {
        return 1;
      }
    
      return directions
        .filter(([d_row, d_col]) => {
          let [row, col] = [pos[0] + d_row, pos[1] + d_col];
          return (
            row >= 0 &&
            row < map.length &&
            col >= 0 &&
            col < map[0].length &&
            map[row][col] === height + 1
          );
        })
        .map(([d_row, d_col]) => {
          const pos_new = [pos[0] + d_row, pos[1] + d_col];
          return calculate_rating(map, pos_new, height + 1);
        })
        .sum();
    }
    
    const [map, start_positions] = file_to_map_and_start_pos("day10.txt");
    
    const score = start_positions
      .map((x) => calculate_score(map, x, new Set(), 0))
      .sum();
    console.log(`part1: ${score}`);
    
    const rating = start_positions
      .map((x) => calculate_rating(map, x, 0))
      .sum();
    console.log(`part2: ${rating}`);

    Day 11: Scala

    I initially wrote this day’s problem in python and then translated it to scala. This process was pretty easy considering the simplistic nature of today’s problem coupled with Scala’s delightful language features like functional support and its built-in map data structure. Scala let me focus on the problem at hand basically as well as python did.

    Overall Rating: 7.5
    ********
    **

    TLDR: Pretty nice to code in

    Solution
    Open File on Github >
    // #!/usr/bin/env scala
    //> using scala 3.6.2
    //> using toolkit default
    
    import scala.io.Source
    import scala.collection.mutable
    
    def get_stones(file_path: String): Array[Int] =
      Source.fromFile(file_path).getLines().next().split(" ").map(_.toInt)
    
    var cache = mutable.Map[(BigInt, Int), BigInt]()
    def get_length(stone: BigInt, iteration: Int): BigInt =
      val key = (stone, iteration)
    
      if iteration == 0 then
        cache.update(key, 1)
        return 1
      
      cache.getOrElseUpdate(key, {
        if stone == 0 then
          get_length(1, iteration - 1)
        else if stone.toString.length % 2 == 0 then
          val stoneStr = stone.toString
          val mid = stoneStr.length / 2
          val left = stoneStr.substring(0, mid).toInt
          val right = stoneStr.substring(mid).toInt
          get_length(left, iteration - 1) + get_length(right, iteration - 1)
        else
          get_length(stone * 2024, iteration - 1)
      })
    
    @main
    def day11(): Unit =
      val stones = get_stones("day11.txt")
    
      val part1 = stones.map(get_length(_, 25)).sum
      val part2 = stones.map(get_length(_, 75)).sum
    
      println(s"part 1: $part1")
      println(s"part 2: $part2")

    Day 12: Rust

    Rust my beloved. One of my favourite uni electives is the rust course and I tried some of Advent of Code 2023 purely in rust. The support around the language like the documentation, compiler error messages, cargo package manager, rust analyzer and the beautiful rust book make the entire experience feel so seamless. Plus the language itself with the functional paradigm support, type inference, pattern matching; absolutely splendid.

    Realizing that the number of corners in a polygon is always equal to the number of sides was an elating revelation that, coupled with the beautiful language, made this a very fun day.

    Though sometimes it can be a pain to fight the compiler when all you wanna do is focus on the problem at hand, overall: one of the best language experiences.

    Overall Rating: 9.3
    *********
    *

    TLDR: 1v1 the compiler and fall in love

    Solution
    Open File on Github >
    use std::{collections::HashSet, fs};
    
    fn main() {
        let connected_components = get_connected_components("./day12.txt");
        println!(
            "part1: {}",
            connected_components
                .iter()
                .map(|x| x.len() as i32 * perimeter(x))
                .sum::<i32>()
        );
    
        println!(
            "part2 : {}",
            connected_components
                .iter()
                .map(|x| x.len() as i32 * num_corners(x))
                .sum::<i32>()
        );
    }
    
    fn get_connected_components(file_path: &str) -> Vec<HashSet<(i32, i32)>> {
        let farm: Vec<String> = fs::read_to_string(file_path)
            .expect("file_path not found")
            .split("\n")
            .into_iter()
            .map(|x| x.to_string())
            .collect();
    
        let mut connected_components = Vec::new();
    
        farm.iter().enumerate().for_each(|(i, row)| {
            row.chars().enumerate().for_each(|(j, plant)| {
                if !connected_components
                    .iter()
                    .any(|component: &HashSet<_>| component.contains(&(i as i32, j as i32)))
                {
                    let mut visited = HashSet::new();
                    dfs(&farm, &mut visited, (i as i32, j as i32), plant);
                    connected_components.push(visited);
                }
            });
        });
    
        connected_components
    }
    
    static DIRECTIONS: [(i32, i32); 4] = [(1, 0), (0, -1), (-1, 0), (0, 1)];
    fn dfs(farm: &Vec<String>, visited: &mut HashSet<(i32, i32)>, pos: (i32, i32), plant: char) {
        visited.insert(pos);
    
        DIRECTIONS.iter().for_each(|(d_row, d_col)| {
            let row = pos.0 + d_row;
            let col = pos.1 + d_col;
            if row >= 0
                && row < farm.len() as i32
                && col >= 0
                && col < farm[0].len() as i32
                && !visited.contains(&(pos.0 + d_row, pos.1 + d_col))
                && farm[row as usize].chars().nth(col as usize).expect("💀") == plant
            {
                dfs(farm, visited, (pos.0 + d_row, pos.1 + d_col), plant);
            }
        });
    }
    
    fn perimeter(component: &HashSet<(i32, i32)>) -> i32 {
        component
            .iter()
            .map(|(row, col)| {
                DIRECTIONS
                    .iter()
                    .map(
                        |(d_row, d_col)| match component.contains(&(row + d_row, col + d_col)) {
                            true => 0,
                            false => 1,
                        },
                    )
                    .sum::<i32>()
            })
            .sum()
    }
    
    static DIAGONALS: [(i32, i32); 4] = [(1, 1), (-1, -1), (-1, 1), (1, -1)];
    fn num_corners(component: &HashSet<(i32, i32)>) -> i32 {
        component
            .iter()
            .map(|(row, col)| {
                DIAGONALS
                    .iter()
                    .map(|(d_row, d_col)| {
                        let diagonal = (row + d_row, col + d_col);
                        let vertical = (row + d_row, *col);
                        let horizontal = (*row, col + d_col);
    
                        // if 45degress to either side of the diagonal both are empty or both are in the component (xor)
                        // then theres a corner at that diagonal
                        match (!component.contains(&diagonal)
                            && component.contains(&vertical) == component.contains(&horizontal))
                            || (component.contains(&diagonal)
                                && component.contains(&vertical) == false
                                && component.contains(&horizontal) == false)
                        {
                            true => 1,
                            false => 0,
                        }
                    })
                    .sum::<i32>()
            })
            .sum::<i32>()
    }

    Day 13: Ruby

    There was something charming about ruby’s syntax and how productive I could be like with python. Additionally, when using matrix to solve the system of linear equations, ruby stored the numbers as fractions. This meant I could easily check if the solution produced whole numbers as required by the problem.

    #!/usr/bin/env ruby
    require 'matrix'
    coefficients, results = Matrix[[2, 3], [5, 7]],  Matrix[[10], [19]]
    a_presses, b_presses = (coefficients.inverse * results).to_a.flatten
    # a_presses: -13/1
    # b_presses: 12/1

    On the other hand if I used a language like python, it would produce floating point errors forcing me to account for some epsilon.

    #!/usr/bin/env python3
    import numpy as np
    coefficients, results = np.array([[2, 3], [5, 7]]), np.array([[10], [19]])
    a_presses, b_presses = np.linalg.inv(coefficients).dot(results).flatten()
    # a_presses: -13.000000000000028
    # b_resses: 12.000000000000021
    Overall Rating: 8.4
    ********
    **

    TLDR: Ruby is lovely to code in

    Solution
    Open File on Github >
    #!/usr/bin/env ruby
    
    require 'matrix'
    
    def section_to_machine(section_lines)
      button_a, button_b, prize = section_lines.split("\n")
    
      a_vec_components = button_a.match(/X\+(\d+), Y\+(\d+)/).captures.map(&:to_i)
      b_vec_components = button_b.match(/X\+(\d+), Y\+(\d+)/).captures.map(&:to_i)
      prize_pos = prize.match(/X=(\d+), Y=(\d+)/).captures.map(&:to_i)
    
      {
        a: a_vec_components,
        b: b_vec_components,
        prize: prize_pos
      }
    end
    
    A_COST = 3
    B_COST = 1
    def cheapest_win(machine, max_presses)
      # assume there is only one solution to the system of 2 linear equations
      # machine[:a][0] * a_presses + machine[:b][0] * b_presses = machine[:prize][0]
      # machine[:a][1] * a_presses + machine[:b][1] * b_presses = machine[:prize][1]
    
      button_coefficients = Matrix[[machine[:a][0], machine[:b][0]], [machine[:a][1], machine[:b][1]]]
      prize = Matrix[[machine[:prize][0]], [machine[:prize][1]]]
    
      a_presses, b_presses = (button_coefficients.inverse * prize).to_a.flatten
    
      if a_presses % 1 == 0 && b_presses % 1 == 0
        return (a_presses * A_COST + b_presses * B_COST).to_i
      end
    
      return 0
    end
    
    sections = File.read('./day13.txt')
    
    part1 = 0
    sections.split("\n\n").each do |section|
      machine = section_to_machine(section)
      part1 += cheapest_win(machine, 100)
    end
    puts "part1: #{part1}"
    
    part2 = 0
    sections.split("\n\n").each do |section|
      machine = section_to_machine(section)
      machine[:prize][0] += 10000000000000
      machine[:prize][1] += 10000000000000
      part2 += cheapest_win(machine, Float::INFINITY)
    end
    puts "part2: #{part2}"

    Day 14: Zig

    I was excited to try Zig hearing the hype around it. Although IntelliSense wasn’t working on vscode, all was good because Zed seemed to work with the click of a button.

    I found the syntax to be the steepest learning curve of all the languages here. Reading the docs was frequently unhelpful and along with the language being too verbose for my liking, it was only natural I was put into a bad mood. Though this day’s problem was hilarious in that it was like ‘just find the christmas tree bro’, I had to sleep on it for a day. When my second day of Zig came around, I noticed a shift in my mood; I was actually enjoying the language as I came to grasps with the syntax.

    Overall Rating: 6.9
    *******
    ***

    TLDR: Make better docs and I’d like it a lot

    Solution
    Open File on Github >
    const std = @import("std");
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator();
    const ArrayList = std.ArrayList;
    
    const WIDTH = 101;
    const HEIGHT = 103;
    
    const Vec2 = struct { x: i32, y: i32 };
    
    const Robot = struct {
        pos: Vec2,
        vel: Vec2,
        pub fn simulate(self: *Robot, moves: i32) void {
            self.pos.x = @mod((self.pos.x + self.vel.x * moves), WIDTH);
            self.pos.y = @mod((self.pos.y + self.vel.y * moves), HEIGHT);
        }
    };
    
    pub fn main() !void {
        std.debug.print("part1: {d}\n", .{try part1()});
        std.debug.print("part2: {d}\n", .{try part2()});
    }
    
    fn file_to_robots(file_path: []const u8) !ArrayList(Robot) {
        var robots = ArrayList(Robot).init(allocator);
        const file = try std.fs.cwd().openFile(file_path, .{});
        defer file.close();
    
        var buf_reader = std.io.bufferedReader(file.reader());
        var in_stream = buf_reader.reader();
    
        var buf: [1024]u8 = undefined;
        while (try in_stream.readUntilDelimiterOrEof(&buf, '\n')) |line| {
            var it = std.mem.splitSequence(u8, line[2..], " v=");
            var pos_split = std.mem.splitSequence(u8, it.next().?, ",");
            var vel_split = std.mem.splitSequence(u8, it.next().?, ",");
    
            const pos_x = try std.fmt.parseInt(i32, pos_split.next().?, 10);
            const pos_y = try std.fmt.parseInt(i32, pos_split.next().?, 10);
    
            const vel_x = try std.fmt.parseInt(i32, vel_split.next().?, 10);
            const vel_y = try std.fmt.parseInt(i32, vel_split.next().?, 10);
    
            try robots.append(Robot{
                .pos = Vec2{ .x = pos_x, .y = pos_y },
                .vel = Vec2{ .x = vel_x, .y = vel_y },
            });
        }
    
        return robots;
    }
    
    fn part1() !i32 {
        var robots = try file_to_robots("day14.txt");
        defer robots.deinit();
    
        for (robots.items) |*robot| {
            robot.simulate(100);
        }
    
        return product_quadrants(robots.items);
    }
    
    pub fn product_quadrants(robots: []Robot) i32 {
        var quad_1: i32 = 0;
        var quad_2: i32 = 0;
        var quad_3: i32 = 0;
        var quad_4: i32 = 0;
    
        for (robots) |robot| {
            if (robot.pos.y < HEIGHT / 2 and robot.pos.x < WIDTH / 2) {
                quad_1 += 1;
            } else if (robot.pos.y < HEIGHT / 2 and robot.pos.x > WIDTH / 2) {
                quad_2 += 1;
            } else if (robot.pos.y > HEIGHT / 2 and robot.pos.x < WIDTH / 2) {
                quad_3 += 1;
            } else if (robot.pos.y > HEIGHT / 2 and robot.pos.x > WIDTH / 2) {
                quad_4 += 1;
            }
        }
        return quad_1 * quad_2 * quad_3 * quad_4;
    }
    
    fn part2() !i32 {
        var robots = try file_to_robots("day14.txt");
        defer robots.deinit();
    
        var seconds: i32 = 1;
        while (true) {
            for (robots.items) |*robot| {
                robot.simulate(1);
            }
    
            if (!try has_overlaps(robots.items)) {
                break;
            }
            seconds += 1;
        }
    
        debug_robots(robots.items);
        return seconds;
    }
    
    pub fn debug_robots(robots: []Robot) void {
        var map = [_][WIDTH]i32{[_]i32{0} ** WIDTH} ** HEIGHT;
        for (robots) |robot| {
            map[@intCast(robot.pos.y)][@intCast(robot.pos.x)] += 1;
        }
    
        for (map) |row| {
            for (row) |num_aliens| {
                if (num_aliens == 0) {
                    std.debug.print(".", .{});
                } else {
                    std.debug.print("{d}", .{num_aliens});
                }
            }
            std.debug.print("\n", .{});
        }
    }
    
    fn has_overlaps(robots: []Robot) !bool {
        var map = std.AutoHashMap(Vec2, bool).init(allocator);
        for (robots) |robot| {
            if (!map.contains(robot.pos)) {
                try map.put(robot.pos, true);
            } else {
                return true;
            }
        }
        return false;
    }

    Day 15: Python

    While Zig had me demotivated for a day, I thought that I would fall back to a nice cushy language; Python. The Zen of Python is a great philosophy in language design making both writing and reading python a blissful experience. While it does have lovely list comprehensions, if the functional support used dot notation instead of normal functions, the language might have been goated.

    Overall Rating: 8.6
    *********
    *

    TLDR: Python awesome for productivity but brackets >> indents

    Solution
    Open File on Github >
    #!/usr/bin/env python3
    
    def get_map_moves(file_path: str):
        map, moves = open(file_path, 'r').read().split("\n\n")
    
        map = [list(row) for row in map.split('\n')]
        start_position = next((row, col) for row, row_data in enumerate(map) for col, cell in enumerate(row_data) if cell == '@')
    
        return start_position, map, moves
    
    def get_connected_components(map, pos, direction):
        connected_componets = set()
    
        stack = [pos]
        while stack:
            curr = stack.pop()
            if curr in stack or (curr[0], curr[1], map[curr[0]][curr[1]]) in connected_componets:
                continue
    
            if map[curr[0]][curr[1]] == '.':
                continue
            elif map[curr[0]][curr[1]] == '#':
                return []
            elif map[curr[0]][curr[1]] == 'O':
                connected_componets.add((curr[0], curr[1], map[curr[0]][curr[1]]))
                stack.append((curr[0] + direction[0], curr[1] + direction[1]))
            elif map[curr[0]][curr[1]] in ['[', ']'] and direction[0] == 0:
                # horizontal movements, skip the matching box end
                connected_componets.add((curr[0], curr[1], map[curr[0]][curr[1]]))
                connected_componets.add((curr[0], curr[1] + direction[1], map[curr[0]][curr[1] + direction[1]]))
                stack.append((curr[0], curr[1] + direction[1] * 2))
            elif map[curr[0]][curr[1]] in ['[', ']'] and direction[1] == 0:
                # vertical movements, append both sides of the box
                connected_componets.add((curr[0], curr[1], map[curr[0]][curr[1]]))
                stack.append((curr[0] + direction[0], curr[1]))
                if map[curr[0]][curr[1]] == '[':
                    stack.append((curr[0], curr[1] + 1))
                elif map[curr[0]][curr[1]] == ']':
                    stack.append((curr[0], curr[1] - 1))
    
        return connected_componets
    
    def move_robot(map, pos, direction):
        new_pos = (pos[0] + direction[0], pos[1] + direction[1])
        if map[new_pos[0]][new_pos[1]] == '#':
            return pos
        elif map[new_pos[0]][new_pos[1]] == '.':
            map[pos[0]][pos[1]] = '.'
            map[new_pos[0]][new_pos[1]] = '@'
            return new_pos
        elif map[new_pos[0]][new_pos[1]] in ['O', '[', ']']:
            boxes_to_move = get_connected_components(map, new_pos, direction)
            if boxes_to_move:
                # remove all boxes from prev location
                for box in boxes_to_move:
                    map[box[0]][box[1]] = '.'
                # move all boxes to new location
                for box in boxes_to_move:
                    map[box[0] + direction[0]][box[1] + direction[1]] = box[2]
    
                map[pos[0]][pos[1]] = '.'
                map[new_pos[0]][new_pos[1]] = '@'
                return new_pos
    
        return pos
    
    def sum_coordinates(pos, map, moves):
        move_table = {'^': (-1, 0), '>': (0, 1), 'v': (1, 0), '<': (0, -1)}
        for move in moves:
            if move != '\n':
                pos = move_robot(map, pos, move_table[move])
    
        return sum(100 * row + col for row, row_data in enumerate(map) for col, cell in enumerate(row_data) if cell in ['O', '['])
    
    if __name__ == "__main__":
    
        start_pos, map, moves = get_map_moves("day15.txt")
        print(f'part1: {sum_coordinates(start_pos, map, moves)}')
    
        start_pos, map, moves = get_map_moves("day15.txt")
        start_pos = (start_pos[0], start_pos[1] * 2)
        translation = {'O': '[]', '@': '@.', '.': '..', '#': '##'}
        for i, row in enumerate(map):
            for j, col in enumerate(row):
                map[i][j] = translation[col]
            map[i] = [a for x in map[i] for a in x] # flatten
    
        print(f'part2: {sum_coordinates(start_pos, map, moves)}')

    Day 16: Kotlin

    Setting up Kotlin was the most painful experience out of the ones listed here. I had to go out of my way to find the binaries and avoid the website pushing the IntelliJ IDE onto my broke ass. I also installed the native linux binary instead of the JVM one which was annoying to realise.

    Though the installation sucked, actually writing code in it was quite nice with the Kotlin docs and the ability to use the Java libraries even though vscode had no IntelliSense. Though the compilation times were comically slow, the syntax was nice.

    Overall Rating: 3.2
    ***
    *******

    TLDR: Worst language in terms of setup. Would only use if I had IntelliJ

    Solution
    Open File on Github >
    import java.io.File
    import java.util.PriorityQueue
    
    data class Node(
        val pos: Pair<Int, Int>,
        val direction: Pair<Int, Int>,
        val score: Int
    )
    
    data class StepNode(
        val pos: Pair<Int, Int>,
        val direction: Pair<Int, Int>,
        val score: Int,
        var steps: MutableSet<Pair<Int, Int>>
    )
    
    fun get_graph(file_path: String) : Pair<MutableMap<Pair<Int, Int>, Int>, Pair<Pair<Int, Int>, Pair<Int, Int>>> {
        val graph: MutableMap<Pair<Int, Int>, Int> = mutableMapOf()
        var start: Pair<Int, Int> = Pair(-1, -1)
        var finish: Pair<Int, Int> = Pair(-1, -1)
    
        val text = File(file_path).readText().lines()
        for ((row, line) in text.withIndex()) {
            for ((col, char) in line.withIndex()) {
                when (char) {
                    '.' -> graph[Pair(row, col)] = Int.MAX_VALUE
                    'S' -> start = Pair(row, col)
                    'E' -> {
                        finish = Pair(row, col)
                        graph[Pair(row, col)] = Int.MAX_VALUE
                    }
                }
            }
        }
    
        return Pair(graph, Pair(start, finish))
    }
    
    fun dijkstra(graph: MutableMap<Pair<Int, Int>, Int>, start: Pair<Int, Int>, finish: Pair<Int, Int>): Int {
        var res = 0;
        val queue = PriorityQueue<Node>(compareBy { it.score })
        var visited: MutableSet<Pair<Int,Int>> = mutableSetOf()
        
        queue.add(Node(start, Pair(0, 1), 0)) // start facing east (0,1) & score 0
    
        while (queue.isNotEmpty()) {
            val curr = queue.poll()
    
            if (curr.pos == finish) {
                res = curr.score
            }
    
            val neighbour_directions = listOf(
                Pair(curr.direction, 1),
                Pair(Pair(curr.direction.second, -curr.direction.first), 1001),
                Pair(Pair(-curr.direction.second, curr.direction.first), 1001)  
            )
    
            for ((direction, score) in neighbour_directions) {
                val next_pos = Pair(curr.pos.first + direction.first, curr.pos.second + direction.second)
                val next_score = curr.score + score
                if (graph.containsKey(next_pos) && next_score < graph[next_pos]!!) {
                    queue.add(Node(next_pos, direction, next_score))
                    visited.add(next_pos)
                    graph[next_pos] = next_score
                }
            }
        }
    
        return res;
    }
    
    fun backwards_bfs(graph: MutableMap<Pair<Int, Int>, Int>, finish: Pair<Int, Int>, min_score: Int): Int {
        var nodes = 2
        var queue: ArrayDeque<Node> = ArrayDeque(listOf(Node(finish, Pair(1, 0), min_score), Node(finish, Pair(0, -1), min_score)))
        var visited: MutableSet<Pair<Int,Int>> = mutableSetOf()
    
        while (queue.isNotEmpty()) {
            val curr = queue.removeFirst()
    
            val neighbour_directions = listOf(
                Pair(curr.direction, 1),
                Pair(Pair(curr.direction.second, -curr.direction.first), 1001),
                Pair(Pair(-curr.direction.second, curr.direction.first), 1001)  
            )
    
            for ((direction, score) in neighbour_directions) {
                val next_pos = Pair(curr.pos.first + direction.first, curr.pos.second + direction.second)
                val next_score = curr.score - score
                if (graph.containsKey(next_pos) && !visited.contains(next_pos) && (next_score == graph[next_pos]!! || next_score - 1000 == graph[next_pos]!!)) {
                    queue.addLast(Node(next_pos, direction, next_score))
                    visited.add(next_pos)
                    nodes += 1
                }
            }
        }
    
        return nodes
    }
    
    fun main() {
        val (graph, positions) = get_graph("day16.txt")
        val (start, finish) = positions
    
        val min_score = dijkstra(graph, start, finish)
        println("part1: $min_score")
    
        val part2 = backwards_bfs(graph, finish, min_score)
        println("part2: $part2")
    }

    Day 17: Dart

    For some reason Dart’s installation using apt-get requires 4 steps. Being 3 steps too many, I just downloaded the binaries only to find that analytics was opt-out. Why is this open source language trying to steal my data? Typical Google I guess.

    Overall, the language was pretty nice with things like being shebangable and having destructuring despite this day’s problem being very difficult.

    Overall Rating: 7.5
    ********
    **

    TLDR: Pretty enjoyable language no complaints

    Solution
    Open File on Github >
    #!/usr/bin/env dart
    
    import 'dart:convert';
    import 'dart:io';
    
    void main() {
      var (registers, program) = get_registers_and_program("day17.txt");
    
      print("part1: " + run(registers, program).join(","));
    
      var A = 0;
      for (int i = program.length - 1; i >= 0; i--) {
        A <<= 3;
        registers['A'] = A;
        while (!eq(run(registers, program), program.sublist(i))) {
          A += 1;
          registers['A'] = A;
        }
      }
    
      print("part2: ${A}");
    }
    
    bool eq(List<int> a, List<int> b) {
      if (a.length != b.length) {
        return false;
      }
      for (int i = 0; i < a.length; i++) {
        if (a[i] != b[i]) {
          return false;
        }
      }
      return true;
    }
    
    List<int> run(registers, program) {
      var output = [];
      for (int i = 0; i < program.length - 1; i += 2) {
        var opcode = program[i];
        var operand = program[i + 1];
        switch (opcode) {
          case 0:
            registers['A'] >>= combo(registers, operand);
          case 1:
            registers['B'] ^= operand;
          case 2:
            registers['B'] = combo(registers, operand) % 8;
          case 3:
            if (registers['A'] != 0) {
              i = operand - 2;
            }
          case 4:
            registers['B'] ^= registers['C'];
          case 5:
            output.add(combo(registers, operand) % 8);
          case 6:
            registers['B'] = registers['A'] >> combo(registers, operand);
          case 7:
            registers['C'] = registers['A'] >> combo(registers, operand);
        }
      }
    
      return new List<int>.from(output);
    }
    
    int combo(registers, int operand) {
      switch (operand) {
        case 4:
          return registers['A'];
        case 5:
          return registers['B'];
        case 6:
          return registers['C'];
        default:
          return operand;
      }
    }
    
    (Map<dynamic, dynamic>, List<int>) get_registers_and_program(file_path) {
      var [registers_str, program_str] = File(file_path)
          .readAsStringSync(encoding: ascii)
          .toString()
          .split("\n\n");
    
      var regs = Map.fromIterable(
          registers_str.split("\n").map((e) {
            var [reg, num] = e.substring(9).split(": ");
            return [reg, int.parse(num)];
          }),
          key: (x) => x[0],
          value: (x) => x[1]);
    
      var prog = program_str.trim().substring(9).split(",").map(int.parse).toList();
    
      return (regs, prog);
    }

    Day 18: Elixir

    Elixir was easy to set up and came with iex which was lovely to work with. Being shebangable, having readable syntax that supports piping with |>, great documentation and being functional made this a transcendent experience; the best new language I learnt in this list.

    For this days solution, I wanted use bfs and instead of doing it with normal pattern matching recursively, I tried to do it using Stream.unfold/2 which worked with great success after playing around with in iex.

    Overall Rating: 9.2
    *********
    *

    TLDR: Elixir was very joyful to code in. I wanna use it more in future

    Solution
    Open File on Github >
    #!/usr/bin/env elixir
    
    defmodule Parse do
      def get_inverted_graph(file_path, bytes) do
        get_inverted_graph(file_path)
        |> Enum.take(bytes)
      end
    
      def get_inverted_graph(file_path) do
        {:ok, content} = File.read(file_path)
    
        content
        |> String.split("\n", trim: true)
        |> Enum.map(fn line ->
          line
          |> String.split(",")
          |> Enum.map(&String.to_integer/1)
          |> List.to_tuple()
        end)
      end
    end
    
    defmodule Graph do
      @start {0, 0}
      @finish {70, 70}
    
      def min_steps(inverted_graph) do
        bfs(inverted_graph |> MapSet.new())
        |> Enum.take(-1)
        |> List.last()
        |> elem(2)
        |> Map.get(@finish)
      end
    
      def get_neighbours({row, col}, visited, inverted_graph, steps) do
        neighbours =
          [{row + 1, col}, {row - 1, col}, {row, col + 1}, {row, col - 1}]
          |> Enum.filter(fn {r, c} ->
            r >= 0 and r <= elem(@finish, 0) and
              c >= 0 and c <= elem(@finish, 1) and
              not MapSet.member?(inverted_graph, {r, c}) and
              not MapSet.member?(visited, {r, c})
          end)
    
        updated_steps =
          neighbours
          |> List.foldl(steps, fn x, steps ->
            steps |> Map.put(x, steps[{row, col}] + 1)
          end)
    
        {neighbours, updated_steps}
      end
    
      def bfs(inverted_graph) do
        queue = [@start]
        visited = MapSet.new([@start])
        steps = %{@start => 0}
    
        Stream.unfold({queue, visited, steps}, fn
          {[], _visited, _steps} ->
            nil
    
          {[head | tail], visited, steps} when head == @finish ->
            {
              {[head | tail], visited, steps},
              {[], MapSet.new(), %{@finish => steps[@finish]}}
              # 1 more emission and then terminate
            }
    
          {[head | tail], visited, steps} ->
            {neighbours, updated_steps} = get_neighbours(head, visited, inverted_graph, steps)
    
            updated_visited = Enum.reduce(neighbours, visited, fn x, acc -> MapSet.put(acc, x) end)
    
            {
              {[head | tail], visited, steps},
              {tail ++ neighbours, updated_visited, updated_steps}
            }
        end)
      end
    end
    
    defmodule Day18 do
      def part1(file_path, bytes) do
        inverted_graph = Parse.get_inverted_graph(file_path, bytes)
        Graph.min_steps(inverted_graph)
      end
    
      def part2(file_path, bytes) do
        inverted_graph = Parse.get_inverted_graph(file_path, bytes)
    
        case Graph.min_steps(inverted_graph) do
          nil ->
            {r, c} = inverted_graph |> Enum.at(bytes - 1)
            "#{r},#{c}"
    
          _ ->
            Day18.part2(file_path, bytes + 1)
        end
      end
    end
    
    bytes = 1024
    file_path = "day18.txt"
    IO.puts("part1: #{Day18.part1(file_path, bytes)}")
    IO.puts("part2: #{Day18.part2(file_path, bytes)}")

    Day 19: Squirrel

    Squirrel was definitely the most obscure language of this list not even having shiki syntax highlighting or seti icon support. It has found some adoption for scripting in games like Portal 2 but the way I found out about it was just hearing a random person mention it in a discord I was lurking in.

    Actually setting up the language and coding in it was quite nice being dynamic like lua and the documentation was surprisingly good despite not hearing anybody ever talking about this language.

    Overall Rating: 6.3
    ******
    ****

    TLDR: .nut is funny suffix. Good language but too mysterious and nonchalant for me

    Solution
    Open File on Github >
    # squirrel 3.2
    
    function get_patterns_and_designs(file_path) {
        local my_file = file(file_path, "r");
    
        local patterns_str = "";
        while (!my_file.eos()) {
            local c = my_file.readn('b').tochar();
            if (c == "\n") {
                break;
            }
            patterns_str += c;
        }
    
        local designs_str = "";
        while (!my_file.eos()) {
            local c = my_file.readn('b').tochar();
            designs_str += c;
        }
    
        my_file.close();
    
        return [split(patterns_str, " ,", true), split(designs_str, "\n", true)];
    }
    
    local cache = {};
    function num_design_ways(design, patterns) {
        if (design == "") {
            return 1;
        }
    
        if (design in cache) {
            return cache[design];
        }
    
        local design_ways = 0;
        foreach (pat in patterns) {
            if (startswith(design, pat)) {
                local remaining = design.slice(pat.len());
                local res = num_design_ways(remaining, patterns);
                design_ways += res;
            }
        }
    
        cache[design] <- design_ways;
        return design_ways;
    }
    
    local res = get_patterns_and_designs("day19.txt");
    local patterns = res[0];
    local designs = res[1];
    
    local part1 = 0;
    local part2 = 0;
    foreach (design in designs) {
        local ways = num_design_ways(design, patterns);
        if (ways > 0) {
            part1 += 1;
        }
        part2 += ways;
    }
    
    print("part1: " + part1 + "\n");
    print("part2: " + part2 + "\n");

    Day 20: Nim

    Nim felt just like Python but better. It felt like it gave the same amount of productivity while also being statically typed and compiled for some free performance benefits. Nothing much to say other than it was a pleasant experience. Despite this, I’m not too sure I’m going to be in a position where I need to solve a problem / build a product and will choose Nim. I guess if I want some performant code that is also easy to write, it would have to be a toss up between this and Crystal.

    Overall Rating: 9
    *********
    *

    TLDR: Nim is just better Python

    Solution
    Open File on Github >
    import strutils, deques, sets, tables, sequtils, algorithm
    
    proc get_racetrack(file_path: string): (seq[seq[char]], (int, int), (int, int)) =
        let racetrack = readFile(file_path).split("\n")
    
        var
            start_pos = (0, 0)
            end_pos = (0, 0)
    
        for row, line in racetrack:
            for col, c in line:
                if c == 'S':
                    start_pos = (row, col)
                if c == 'E':
                    end_pos = (row, col)
    
        return (map(racetrack, proc(x: string): seq[char] = @x), start_pos, end_pos);
    
    proc bfs(racetrack: seq[seq[char]], start_pos: (int, int), end_pos: (int, int)): (seq[(int, int)], Table[(int, int), int]) =
        var
            visited = initHashSet[(int, int)]()
            prev = initTable[(int, int), (int, int)]()
            queue = [start_pos].toDeque
            shortest_path = newSeq[(int, int)]()
            node_weights = initTable[(int, int), int]()
            picosecs = 0
    
        while queue.len > 0:
            let curr = queue.popFirst
            visited.incl(curr)
            node_weights[curr] = picosecs
            picosecs += 1;
    
            if curr == end_pos:
                shortest_path.add(end_pos)
                var backtrack = prev[curr]
                while backtrack != start_pos:
                    shortest_path.add(backtrack)
                    backtrack = prev[backtrack]
                shortest_path.add(start_pos)
                return (reversed(shortest_path), node_weights)
    
            for (d_row, d_col) in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                let neighbour = (curr[0] + d_row, curr[1] + d_col)
                if neighbour[0] >= 0 and neighbour[0] < racetrack.len and
                    neighbour[1] >= 0 and neighbour[1] < racetrack[0].len and
                    not visited.contains(neighbour) and
                    racetrack[neighbour[0]][neighbour[1]] in ['.', 'E', '1', '2']:
                    prev[neighbour] = curr
                    queue.addLast(neighbour)
    
        return (reversed(shortest_path), node_weights)
    
    var (racetrack, s_pos, e_pos) = get_racetrack("day20.txt")
    let (shortest_path, node_weights) = bfs(racetrack, s_pos, e_pos)
    
    proc num_cheats(max_taxicab_dist: int, min_saved_picos: int): int =
        var cheats = 0
        for i in 0..<shortest_path.len:
            let cheat_start = shortest_path[i]
            for j in (i + 1)..<shortest_path.len:
                let cheat_end = shortest_path[j]
                let taxicab_dist = abs(cheat_end[0] - cheat_start[0]) + abs(cheat_end[1] - cheat_start[1])
                let saved_picos = node_weights[cheat_end] - node_weights[cheat_start] - taxicab_dist
                if cheat_end != cheat_start and taxicab_dist <= max_taxicab_dist and saved_picos >= min_saved_picos:
                    cheats += 1
    
        return cheats
    
    echo "part1: ", num_cheats(2, 100)
    echo "part2: ", num_cheats(20, 100)

    Day 21: Crystal

    Crystal to Ruby feels very much like Nim to Python; a statically typed more performant alternative that is joyful to code in. She has good documentation, functional features and the niceties of ruby like implicit returns and stuff. I really enjoyed using it.

    Overall Rating: 9
    *********
    *

    TLDR: Crystal is just better Ruby

    Solution
    Open File on Github >
    #!/usr/bin/env crystal
    
    def get_codes(file_path)
      content = File.read(file_path).split("\n")
      content.map { |code| {code, code[0, 3].to_i} }
    end
    
    class Pad
      NUM = {
        '7' => {0, 0}, '8' => {0, 1}, '9' => {0, 2},
        '4' => {1, 0}, '5' => {1, 1}, '6' => {1, 2},
        '1' => {2, 0}, '2' => {2, 1}, '3' => {2, 2},
                       '0' => {3, 1}, 'A' => {3, 2},
      }
    
      KEY = {
                       '^' => {0, 1}, 'A' => {0, 2},
        '<' => {1, 0}, 'v' => {1, 1}, '>' => {1, 2},
      }
    end
    
    class Cache
      SEQ = Hash(Tuple(String, Int32), Int64).new
      DIST = Hash(Tuple(Char, Char, Int32), Int64).new
    end
    
    def move_opt(a : Tuple(Int, Int), b : Tuple(Int, Int)) : String
      seq = ""
      if a[1] - b[1] > 0
        seq += "<" * (a[1] - b[1])
      else
        seq += ">" * (b[1] - a[1])
      end
    
      if a[0] - b[0] > 0
        seq += "^" * (a[0] - b[0])
      else
        seq += "v" * (b[0] - a[0])
      end
    
      seq
    end
    
    def sequences(start : Char, finish : Char, pad = Pad::KEY) : Set
      if Pad::NUM.has_key?(start) && Pad::NUM.has_key?(finish)
        pad = Pad::NUM
      end
    
      [{pad[finish][0], pad[start][1]}, {pad[start][0], pad[finish][1]}]
        .reject { |x| !pad.invert.has_key?(x) }
        .map { |x| move_opt(pad[start], x) + move_opt(x, pad[finish]) + "A" }
        .to_set
    end
    
    def shortest_sequence(seq : String, depth : Int, length : Int64 = Int64.new(0)) : Int64
      if depth == 0
        return seq.size.to_i64
      end
    
      length += Cache::SEQ.fetch({seq, depth}) {
        Cache::SEQ[{seq, depth}] = (0..seq.size - 1).map { |i|
          Cache::DIST.fetch({seq.char_at(i - 1), seq.char_at(i), depth}) {
            Cache::DIST[{seq.char_at(i - 1), seq.char_at(i), depth}] = sequences(seq.char_at(i - 1), seq.char_at(i))
            .map { |x| shortest_sequence(x, depth - 1) }
            .min
          }
        }.sum
      }
    
      length
    end
    
    codes = get_codes("day21.txt")
    
    part1 = codes
      .map { |(code, numeric)| (shortest_sequence(code, 3) * numeric) }
      .sum
    
    puts "part1: #{part1}"
    
    part2 = codes
      .map { |(code, numeric)| (shortest_sequence(code, 26) * numeric) }
      .sum
    
    puts "part2: #{part2}"

    Day 22: Gleam

    Gleam, like Elixir, is an Erlang based language that has the lovely |> pipe operator and great documentation. I didn’t read up anything about the language but just liked the colour and icon only to realise it was functional later when solving the problem which was pretty funny. I didn’t like how there was no standard file system to read the input forcing me to import simplifile. Also, it forces you to handle all the errors which was kinda annoying when I would prefer to just crash if certain functions errored. Though I can understand why this is important for building more robust software, it just made it feel like a less enjoyable elixir substitute.

    Overall Rating: 5.8
    ******
    ****

    TLDR: Gleam is less joyful Elixir

    Solution
    Open File on Github >
    import gleam/dict
    import gleam/int
    import gleam/io
    import gleam/list
    import gleam/option
    import gleam/result
    import gleam/set
    import gleam/string
    import simplifile
    
    pub fn main() {
      let init_secrets = get_initial_secrets("day22.txt")
      let secret_map =
        init_secrets
        |> list.map(fn(x) { secret_map(x, 2000) })
    
      let part1 =
        secret_map
        |> list.fold(0, fn(acc, x) { acc + x.0 })
    
      io.println("part1: " <> int.to_string(part1))
    
      let part2 =
        list.fold(secret_map, dict.new(), fn(sequence_dict, x) {
          list.window_by_2(x.1)
          |> list.map(fn(x) { #(x.1 % 10, x.1 % 10 - x.0 % 10) })
          |> list.window(4)
          |> list.fold(#(set.new(), sequence_dict), fn(collects, changes) {
            let seen_sequences = collects.0
            let sequence_dict = collects.1
            let sequence = list.map(changes, fn(x) { x.1 })
            case set.contains(seen_sequences, sequence) {
              True -> collects
              False -> {
                let price = result.unwrap(list.last(changes), #(0, 0)).0
                let updated_dict =
                  dict.upsert(sequence_dict, sequence, fn(res) {
                    case res {
                      option.Some(val) -> val + price
                      option.None -> price
                    }
                  })
                #(set.insert(seen_sequences, sequence), updated_dict)
              }
            }
          })
          |> fn(x) { x.1 }
        })
        |> dict.to_list()
        |> list.map(fn(x) { x.1 })
        |> list.max(int.compare)
        |> result.unwrap(0)
    
      io.println("part2: " <> int.to_string(part2))
    }
    
    fn get_initial_secrets(file_path: String) -> List(Int) {
      case simplifile.read(file_path) {
        Ok(contents) ->
          contents
          |> string.split("\n")
          |> list.map(fn(line) {
            int.parse(line)
            |> result.unwrap(0)
          })
        Error(_) -> []
      }
    }
    
    fn secret_map(secret: Int, n: Int) -> #(Int, List(Int)) {
      list.map_fold(list.range(1, n), secret, fn(acc, _) {
        let sec =
          acc
          |> int.bitwise_exclusive_or(acc * 64)
          |> int.modulo(16_777_216)
          |> result.unwrap(0)
          |> fn(step1) {
            step1
            |> int.divide(32)
            |> result.unwrap(0)
            |> int.bitwise_exclusive_or(step1)
            |> int.modulo(16_777_216)
            |> result.unwrap(0)
          }
          |> fn(step2) {
            step2
            |> int.bitwise_exclusive_or(step2 * 2048)
            |> int.modulo(16_777_216)
            |> result.unwrap(0)
          }
        #(sec, sec)
      })
    }

    Day 23: Raku

    Apparently Raku was meant to be Perl 6. I have zero knowledge of Perl (apart from the one time I used it instead of sed because it supported lazy quantifiers in Day03 within a pipe). Raku was easy to install and the documentation was decent with the added bonus of weeb anime references.

    There are a ton of operators to learn and some of them aren’t even ascii. I mean just look at this cursed condition using with && and and, !(cont) and , and >>.& which is a hyper method call operator with a metaoperator to invoke the lambda subroutine. What!?

    my $condition = $visited !(cont) $neighbour &&
      @curr_arr ∌ $neighbour and
      all @curr_arr>>.&{ %graph{$_} (cont) $neighbour };

    In the Zen of Python, you’ll see that one of their philosophies is “There should be one— and preferably only one —obvious way to do it.” On the contrary following Perl’s philosophy of “There’s more than one way to do it”(TMTOWTDI) made writing Raku fun but the initial understanding and reading of it quite agonising.

    Overall Rating: 5.2
    *****
    *****

    TLDR: Raku pretty cool but too strays too far from English

    Solution
    Open File on Github >
    #!/usr/bin/env raku
    
    sub get_graph($file_path) {
        my $file = open $file_path;
        my %graph;
        for $file.lines -> $line {
            my ($l, $r) = $line.split('-');
            %graph{$l}.push($r);
            %graph{$r}.push($l);
        }
        return %graph;
    }
    
    sub interconnected($set, $depth, $curr, @curr_arr, %graph, $visited) {
        if $depth == 1 {
            for %graph{$curr}.values -> $neighbour {
                if $visited !(cont) $neighbour and @curr_arr ∌ $neighbour and all @curr_arr>>.&{ %graph{$_} (cont) $neighbour } {
                    my $inter_connected = sort(@curr_arr.clone.push($neighbour));
                    $set{ $inter_connected.join('-') } = True;
                }
            }
            return;
        }
    
        for %graph{$curr}.values -> $neighbour {
            if $visited !(cont) $neighbour and @curr_arr ∌ $neighbour and all @curr_arr>>.&{ %graph{$_} (cont) $neighbour } {
                my @inter_connected = @curr_arr.clone.push($neighbour);
                interconnected($set, $depth - 1, $neighbour, @inter_connected, %graph, $visited);
            }
        }
    }
    
    sub largest_cluster($start, %graph, $visited) {
        my $curr_visited = SetHash.new($start);
    
        my @inter_connected = [$start];
        for %graph{$start}.values -> $neighbour {
            if $visited !(cont) $neighbour and $curr_visited ∌ $neighbour and all @inter_connected>>.&{ %graph{$_} (cont) $neighbour } {
                @inter_connected.push($neighbour);
            }
            $curr_visited{$neighbour} = True;
        }
    
        for @inter_connected -> $curr {
            for %graph{$curr}.values -> $neighbour {
                if $visited !(cont) $neighbour and $curr_visited ∌ $neighbour and all @inter_connected>>.&{ %graph{$_} (cont) $neighbour } {
                    @inter_connected.push($neighbour);
                }
                $curr_visited{$neighbour} = True;
            }
        }
    
        return sort(@inter_connected).join(',');
    }
    
    sub part1(%graph) {
        my $set = SetHash.new();
        my $visited = SetHash.new();
        for %graph.keys -> $node {
            interconnected($set, 2, $node, [$node], %graph, $visited);
            $visited{$node} = True;
        }
    
        return $set.keys.grep({ /<(^t|\-t)>/ }).elems;
    }
    
    sub part2(%graph) {
        my @clusters = [];
        my $visited = SetHash.new();
        for %graph.keys -> $node {
            @clusters.push(largest_cluster($node, %graph, $visited));
            $visited{$node} = True;
        }
    
        return @clusters.max(:by({ $_.chars }));
    }
    
    my %graph = get_graph("day23.txt");
    say "part1: &part1(%graph)";
    say "part2: &part2(%graph)";

    Day 24: Odin

    Odin feels very much like Go syntactically but with manual memory management. The documentation is decent but definitely could be better due to the limited examples. Additionally, Odin has non-capturing lambda produres so some weird solutions using the context had to be implemented to work around this.

    Overall Rating: 5.1
    *****
    *****

    TLDR: Odin is more painful Go

    Solution
    Open File on Github >
    #!/usr/bin/env -S odin run "day24.odin" -file
    
    package main
    
    import "core:fmt"
    import "core:os"
    import "core:slice"
    import "core:strconv"
    import "core:strings"
    
    Wire :: struct {
    	op:     string,
    	args:   [2]string,
    	result: string,
    }
    
    ResultWire :: struct {
    	val:  int,
    	wire: string,
    }
    
    main :: proc() {
    	wires, gates := get_wires_and_gates("day24.txt")
    
    	fmt.printf("part1: %d\n", part1(wires, &gates))
    	fmt.printf("part2: %s\n", part2(wires, &gates, "z45"))
    }
    
    get_wires_and_gates :: proc(file_path: string) -> (map[string]Wire, map[string]int) {
    	file_content, ok := os.read_entire_file(file_path)
    	parts := strings.split(string(file_content), "\n\n")
    
    	wires := map[string]int{}
    	context.user_ptr = &wires
    	for wire_line in strings.split(parts[0], "\n") {
    		wire_parts := strings.split(wire_line, ": ")
    		key := strings.clone(wire_parts[0])
    		wires[key] = strconv.atoi(wire_parts[1])
    	}
    
    	gates := map[string]Wire{}
    	for gate_line in strings.split(parts[1], "\n") {
    		gate_parts := strings.split(gate_line, " -> ")
    		operation := strings.split(gate_parts[0], " ")
    		gate := Wire {
    			op     = strings.clone(operation[1]),
    			args   = [2]string{strings.clone(operation[2]), strings.clone(operation[0])},
    			result = strings.clone(gate_parts[1]),
    		}
    		gates[strings.clone(gate_parts[1])] = gate
    	}
    
    	return gates, wires
    }
    
    part1 :: proc(wires: map[string]Wire, gates: ^map[string]int) -> int {
    	results := [dynamic]ResultWire{}
    	for key, value in wires {
    		append(&results, get_wire_values(value, wires, gates))
    	}
    
    	res := slice.filter(results[:], proc(x: ResultWire) -> bool {
    		return strings.starts_with(x.wire, "z")
    	})
    
    	slice.sort_by(res, proc(i: ResultWire, j: ResultWire) -> bool {
    		return i.wire > j.wire
    	})
    
    	bits := slice.mapper(res, proc(x: ResultWire) -> string {return fmt.aprint(x.val)})
    	bit_string := strings.concatenate(bits)
    
    	part1, ok := strconv.parse_int(bit_string, base = 2)
    	if !ok {os.exit(1)}
    
    	return part1
    }
    
    get_wire_values :: proc(wire: Wire, wires: map[string]Wire, gates: ^map[string]int) -> ResultWire {
    	l := gates[wire.args[0]]
    	if !(wire.args[0] in gates) {
    		a := get_wire_values(wires[wire.args[0]], wires, gates)
    		l = a.val
    	}
    
    	r := gates[wire.args[1]]
    	if !(wire.args[1] in gates) {
    		a := get_wire_values(wires[wire.args[1]], wires, gates)
    		r = a.val
    	}
    
    	val := 0
    	switch wire.op {
    	case "AND":
    		val = l & r
    	case "OR":
    		val = l | r
    	case "XOR":
    		val = l ~ r
    	case:
    		fmt.printf("wtf?: %v", wire)
    		os.exit(1)
    	}
    
    	gates[strings.clone(wire.result)] = val
    	return ResultWire{val = val, wire = wire.result}
    }
    
    part2 :: proc(wires: map[string]Wire, gates: ^map[string]int, zMAX: string) -> string {
    	incorrect := [dynamic]string{}
    	for key, wire in wires {
    		if key[0] == 'z' && wire.op != "XOR" && key != zMAX {
    			append(&incorrect, key)
    		} else if (wire.op == "XOR" &&
    			   !(strings.contains_rune("xyz", rune(key[0]))) &&
    			   !(strings.contains_rune("xyz", rune(wire.args[0][0]))) &&
    			   !(strings.contains_rune("xyz", rune(wire.args[1][0])))) {
    			append(&incorrect, key)
    		} else if (wire.op == "AND" && wire.args[0] != "x00" && wire.args[1] != "x00") {
    			for sub_key, sub_wire in wires {
    				if ((key == sub_wire.args[0] || key == sub_wire.args[1]) && sub_wire.op != "OR") {
    					append(&incorrect, key)
    					break;
    				}
    			}
    		} else if (wire.op == "XOR") {
    			for sub_key, sub_wire in wires {
    				if ((key == sub_wire.args[0] || key == sub_wire.args[1]) && sub_wire.op == "OR") {
    					append(&incorrect, key)
    					break;
    				}
    			}
    		}
    	}
    
    	slice.sort(incorrect[:]);
    	return strings.join(incorrect[:], ",")
    }

    Day 25: Mojo

    Initially, I wrote a lovely solution with list comprehensions to the problem in python thinking I would be able to change the suffix of the file to conclude the challenge. Turns out that Mojo doesn’t support list comprehensions well, zip or filter functions, sets don’t work with strings, String.replace just doesn’t work and you have to dereference pointers with a [] suffix. Why does Mojo make the promise of ‘easy transition from Python’ when it is quite the opposite. I would much rather translate python to any other language on this list.

    The language experience combined with the generative ai art slop on its website and the bootleg MDN logo allows me to confidently say it is the worst language I’ve ever used and will never use it again even after a 1.0 release.

    Overall Rating: 1
    *
    *********

    TLDR: Mojo is the worst language I’ve ever used

    Solution
    Open File on Github >
    #!/usr/bin/env mojo
    
    def get_keys_locks(file_path: String) -> (List[List[Int]], List[List[Int]]):
        var keys = List[List[Int]]()
        var locks = List[List[Int]]()
        
        var content = open(file_path, "r").read()
        var schematics = content.split("\n\n")
        
        for schematic in schematics:
    
            var is_key = False
            if schematic[].startswith("#####"):
                is_key = True
    
            var lines = schematic[].split("\n")
            var num_columns = len(lines[0])
            var transposed = List[List[String]]()
            for _ in range(num_columns):
                transposed.append(List[String]())
            
            for line in lines:
                for idx in range(len(line[])):
                    transposed[idx].append(line[][idx])
        
            var c = "."
            if is_key:
                c = '#'
    
            var pin_lengths = List[Int]()
            for pin in transposed:
        
                var pin_length = 0
                for char in pin[]:
                    if char[] == c:
                        pin_length += 1
                    else:
                        break
                if is_key:
                    pin_lengths.append(pin_length - 1)
                else:
                    pin_lengths.append(5 - (pin_length - 1))
                    
            if is_key:
                keys.append(pin_lengths)
            else:
                locks.append(pin_lengths)
        
        return (keys, locks)
    
    def main():
        keys, locks = get_keys_locks("day25.txt")
    
        var part1 = 0
        for lock in locks:
            for key in keys:
                var valid = True
                for i in range(len(key[])):
                    if key[][i] + lock[][i] > 5:
                        valid = False
                        break
                if valid:
                    part1 += 1
        
        print("part1: ", part1)
    
    # Wrote python program first thinking I would be able to 
    # easily translate it to mojo. Turns out mojo is too immature
    # and takes 3 times the amount of lines 😠
    
    #!/usr/bin/env python3
    
    # def get_keys_locks(file_path: str) -> tuple[list[int], list[int]]:
    #     keys = []
    #     locks = []
    #     [
    #         (keys if schematic.startswith("#####") else locks).append(
    #             [len("".join(pin).replace('#' if pin[0] == "#####" else '.', '')) - 1 for pin in list(zip(*schematic.split("\n")))]
    #         )
    #         for schematic in open(file_path, "r").read().split("\n\n")
    #     ]
    
    #     return keys, locks
    
    
    # if __name__ == "__main__":
    #     keys, locks = get_keys_locks("day25.txt")
    
    #     part1 = sum(
    #         [
    #             1 if all([key_pin + lock_hole <= 5 for key_pin, lock_hole in zip(key, lock)]) else 0 
    #             for lock in locks 
    #             for key in keys
    #         ]
    #     )
        
    #     print(f"part1: {part1}")

    Conclusion

    Lets rank these languages based on nothing but vibes

    • 0-3: actively avoiding
    • 4-6: fine language for its purpose but wouldn’t reach for them unless specifically asked to
    • 7-8: good language that I like and would reach for if appropriate
    • 9-10: I love these languages and will definitely be trying to them use in future

    The take away from all of this in regards to languages is that most of them are good enough if they have just existed for long enough to have good documentation and a solid ecosystem (more popular ~= better).

    Gleam
    HaskellLua
    BashGoC#
    C++JavaTypeScriptC
    JavaScriptScalaRust
    RubyZigPythonKotlinDartElixir
    SquirrelNimCrystalRakuOdin
    Mojo

    Not sure if I would encourage this kind of challenge because as they say, “A Jack of all trades is a master of none” but, personally, I’m happy that I completed it.

    Anyways ggs and Merry Christmas everyone ~!