Advent of Code 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.
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:
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 runor 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.
TLDR: Its cool paradigm but I don’t why I would actively choose to use it in real world applications
Solution
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.
TLDR: Nice and simple language
Solution
#!/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.
TLDR: Piping is lit. Everyone should know at least a little bit of bash or sh
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.
TLDR: Pretty decent language no fusses
Solution
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#.
TLDR: Functional C# 👍
Solution
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.
TLDR: Decent language. Just C but plus plus as well
Solution
#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.
TLDR: Java is fine as long as I’m not stuck in design pattern hell
Solution
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.
TLDR: TypeScript is just a linter
Solution
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.
TLDR: pay your respects to C
Solution
#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.
TLDR: I like JavaScript
Solution
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.
TLDR: Pretty nice to code in
Solution
// #!/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.
TLDR: 1v1 the compiler and fall in love
Solution
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
TLDR: Ruby is lovely to code in
Solution
#!/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.
TLDR: Make better docs and I’d like it a lot
Solution
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.
TLDR: Python awesome for productivity but brackets >> indents
Solution
#!/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.
TLDR: Worst language in terms of setup. Would only use if I had IntelliJ
Solution
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.
TLDR: Pretty enjoyable language no complaints
Solution
#!/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.
TLDR: Elixir was very joyful to code in. I wanna use it more in future
Solution
#!/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.
TLDR: .nut is funny suffix. Good language but too mysterious and
nonchalant for me
Solution
# 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.
TLDR: Nim is just better Python
Solution
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.
TLDR: Crystal is just better Ruby
Solution
#!/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.
TLDR: Gleam is less joyful Elixir
Solution
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.
TLDR: Raku pretty cool but too strays too far from English
Solution
#!/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.
TLDR: Odin is more painful Go
Solution
#!/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.
TLDR: Mojo is the worst language I’ve ever used
Solution
#!/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).

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 ~!