This one was a bit of a surprise! I didn’t expect to need to break out the parser. I did enjoy it, though! And, after solving it, I realized there’s some parsing shortcuts I could take and some info which doesn’t actually require tracking.
Ugliest code so far this year for me. Not totally happy with the strategy I used and with my use of Typescript itself. But it has been a busy day and tiring day, so I’ll probably call it good enough for now and revise this solution in later days.
Discussion about strategy used (spoilers)
Representing the filesystem as a hashmap is a good idea. In the end, I took advantage of the fact the input is basically traversing directories depth-first, which meant that my code would only need to keep track of a stack and when a cd .. came, I would only need to pop from the stack and update the size of the parent directory, doing the computations for part 1 when that pop happens. My idea in doing this was to keep only in memory what I absolutely needed. Then part 2 kinda threw all of that under the bus by encouraging to keep track of every directory and by making me dealing with with values leftover in the stack.
Typescript solution
type Dir = [name: string, size: number];
const FILESYSTEM_TOTAL_SPACE = 70000000;
const TARGET_UNUSED_SPACE = 30000000;
export function parse(input: string): State {
const lines = nonEmptyLines(input).map((l) => l.split(" "));
const stack = new Array<Dir>();
stack.push(["/", 0]);
let total_used = 0;
const dirs = new Array<Dir>();
let sumUnder100k = 0;
for (const line of lines) {
const fst = line[0];
switch (fst) {
// deno-lint-ignore no-case-declarations
case "$":
const [cmd, arg] = line.slice(1);
if (cmd == "cd" && arg == "/") {
// ignore root
continue;
} else if (cmd == "cd" && arg == "..") {
// we know we are done with this directory
const [name, size] = stack.pop()!;
dirs.push([name, size]);
// update parent size with the size of
// the directory we just visited
stack[stack.length - 1][1] += size;
if (size <= 100000) {
sumUnder100k += size;
}
continue;
} else if (cmd == "cd") {
stack.push([arg, 0]);
continue;
}
break;
case "dir":
continue;
// deno-lint-ignore no-case-declarations
default:
// line representing a file in the current directory
const size = parseInt(line[0], 10);
stack[stack.length - 1][1] += size;
total_used += size;
}
}
// deal with items still in the stack
let rest = 0;
for (const [name, size] of stack.reverse()) {
dirs.push([name, size + rest]);
rest += size;
}
const actual_unused = FILESYSTEM_TOTAL_SPACE - total_used;
const space_to_free = TARGET_UNUSED_SPACE - actual_unused;
const minDir = dirs
.filter(([_, size]) => size >= space_to_free)
.sort((a, b) => a[1] - b[1]);
return { part1: sumUnder100k, part2: minDir[0][1] };
}