Advent of Code 2024

Day 09 Disk Fragmenter

Description

Another push of the button leaves you in the familiar hallways of some friendly amphipods! Good thing you each somehow got your own personal mini submarine. The Historians jet away in search of the Chief, mostly by driving directly into walls.

While The Historians quickly figure out how to pilot these things, you notice an amphipod in the corner struggling with his computer. He's trying to make more contiguous free space by compacting all of the files, but his program isn't working; you offer to help.

He shows you the disk map (your puzzle input) he's already generated. For example:

2333133121414131402

The disk map uses a dense format to represent the layout of files and free space on the disk. The digits alternate between indicating the length of a file and the length of free space.

So, a disk map like 12345 would represent a one-block file, two blocks of free space, a three-block file, four blocks of free space, and then a five-block file. A disk map like 90909 would represent three nine-block files in a row (with no free space between them).

Each file on disk also has an ID number based on the order of the files as they appear before they are rearranged, starting with ID 0. So, the disk map 12345 has three files: a one-block file with ID 0, a three-block file with ID 1, and a five-block file with ID 2. Using one character for each block where digits are the file ID and . is free space, the disk map 12345 represents these individual blocks:

0..111....22222

The first example above, 2333133121414131402, represents these individual blocks:

00...111...2...333.44.5555.6666.777.888899

The amphipod would like to move file blocks one at a time from the end of the disk to the leftmost free space block (until there are no gaps remaining between file blocks). For the disk map 12345, the process looks like this:

0..111....22222
02.111....2222.
022111....222..
0221112...22...
02211122..2....
022111222......

The first example requires a few more steps:

00...111...2...333.44.5555.6666.777.888899
009..111...2...333.44.5555.6666.777.88889.
0099.111...2...333.44.5555.6666.777.8888..
00998111...2...333.44.5555.6666.777.888...
009981118..2...333.44.5555.6666.777.88....
0099811188.2...333.44.5555.6666.777.8.....
009981118882...333.44.5555.6666.777.......
0099811188827..333.44.5555.6666.77........
00998111888277.333.44.5555.6666.7.........
009981118882777333.44.5555.6666...........
009981118882777333644.5555.666............
00998111888277733364465555.66.............
0099811188827773336446555566..............

The final step of this file-compacting process is to update the filesystem checksum. To calculate the checksum, add up the result of multiplying each of these blocks' position with the file ID number it contains. The leftmost block is in position 0. If a block contains free space, skip it instead.

Continuing the first example, the first few blocks' position multiplied by its file ID number are 0 * 0 = 0, 1 * 0 = 0, 2 * 9 = 18, 3 * 9 = 27, 4 * 8 = 32, and so on. In this example, the checksum is the sum of these, 1928.

Compact the amphipod's hard drive using the process he requested. What is the resulting filesystem checksum? (Be careful copy/pasting the input for this puzzle; it is a single, very long line.)

main.ts

import { readFileSync } from "fs";

function solve<T>(inputFile: string, solution: (input: string) => T): T {
    const data = readFileSync(inputFile, "utf-8").trim()
    const solved = solution(data)
    return solved
}


type Freespace = {
    type: 'freespace',
    length: number
}

type File = {
    type: 'file',
    length: number,
    id: number
}

type DiskMapItem = File | Freespace;
type DiskMap = DiskMapItem[];

function parseDiskMap(input: string): DiskMap {
    let isFile = true
    let map: DiskMap = [];
    let fileId = 0;

    for (const item of input.split("")) {
        const length = parseInt(item, 10)
        if (isFile) {
            map.push({
                type: 'file',
                length,
                id: fileId
            })
            fileId += 1
        } else {
            map.push({
                type: 'freespace',
                length
            })
        }
        isFile = !isFile
    }
    return map
}

function repeat(s: string, length: number): string {
    let result = "";
    for (let i = 0; i < length; i++) {
        result += s
    }
    return result
}

function visualizeFreespace(i: Freespace): string {
    return repeat(".", i.length)
}

function visualizeFile(i: File): string {
    return repeat(i.id.toString(), i.length)
}

function visualizeMapItem(item: DiskMapItem): string {
    if (item.type === "file") {
        return visualizeFile(item)
    } else if (item.type === "freespace") {
        return visualizeFreespace(item)
    }

    throw new Error("unknown type")
}

function visualizeFileMap(map: DiskMap): string {
    return map.map(visualizeMapItem).join("")
}

function findLastIndex<T>(items: T[], predicate: (item: T) => boolean): number {
    for (let i = items.length - 1; i > 0; i--) {
        if (predicate(items[i])) {
            return i
        }
    }
    return -1
}

function findFirstIndex<T>(items: T[], predicate: (item: T) => boolean): number {
    return items.findIndex(predicate)
}

function isFreeSpace(s: DiskMapItem): boolean {
    return s.type === "freespace"
}

function isCompactedByBit(map: DiskMap): boolean {
    return findFirstIndex(map, isFreeSpace) === findLastIndex(map, isFreeSpace)
}

function compactByBit(map: DiskMap): DiskMap {
    let nextMap = [...map]

    const freeSpotIndex = findFirstIndex(map, s => s.type === "freespace" && s.length > 0)
    const freeSpot = map[freeSpotIndex]

    const fileToMoveIndex = findLastIndex(map, s => s.type === "file" && s.length > 0)
    const fileToMove = map[fileToMoveIndex]

    nextMap[freeSpotIndex] = {
        ...freeSpot,
        length: freeSpot.length - 1
    }
    nextMap[fileToMoveIndex] = {
        ...fileToMove,
        length: fileToMove.length - 1
    }
    nextMap.splice(freeSpotIndex, 0, {
        ...fileToMove,
        length: 1
    })
    nextMap.push({
        type: "freespace",
        length: 1
    })


    // Cleanup
    // - Remove empty blank spots / empty data
    // - Join neighboring data types
    nextMap = removeEmptySpots(nextMap)
    nextMap = joinNeighbors(nextMap)
    return nextMap
}

function isJoinable(a: DiskMapItem, b: DiskMapItem): boolean {
    if (a.type === "freespace" && b.type === "freespace") {
        return true
    }
    if (a.type === "file" && b.type == "file" && a.id == b.id) {
        return true
    }
    return false
}

function joinNeighbors(map: DiskMap): DiskMap {
    let nextMap = [...map]
    for (let i = 1; i < nextMap.length; i++) {
        const a = nextMap[i - 1]
        const b = nextMap[i]
        if (isJoinable(a, b)) {
            nextMap[i] = {
                ...a,
                length: a.length + b.length
            }
            nextMap.splice(i - 1, 1)
            // Step back to make sure we compare this with the next one
            i--;
        }
    }
    return nextMap
}

function removeEmptySpots(map: DiskMap): DiskMap {
    return map.filter(s => s.length > 0)
}

function checksumItem(item: DiskMapItem, index: number): number {
    if (item.type === "freespace") {
        return -1;
    }
    let total = 0;
    for (let i = 0; i < item.length; i++) {
        const componentChecksum = (index + i) * item.id;
        total += componentChecksum
    }
    return total;
}

function checksumDisk(disk: DiskMap): number {
    let index = 0;
    let total = 0;
    for (const item of disk) {
        const itemChecksum = checksumItem(item, index)
        if (itemChecksum > 0) {
            total += itemChecksum
        }
        index += item.length
    }

    return total
}

function compactByFile(map: DiskMap): DiskMap {
    return map
}

function isCompactedByFile(map: DiskMap): boolean {
    return true
}

function sum(total: number, entry: number): number {
    return total + entry
}

function untilDone<T>(
    transformFn: (map: T) => T,
    isDone: (map: T) => boolean
): (diskmap: T) => T {
    return (a: T) => {
        let current = a

        while (!isDone(current)) {
            current = transformFn(current)
        }

        return current
    }
}

const compactUntilDoneByBit = untilDone(compactByBit, isCompactedByBit)
const compactUntilDoneByFile = untilDone(compactByFile, isCompactedByFile)

class Pipeline<T> {
    constructor(private _value: T) { }

    pipe<R>(fn: (value: T) => R): Pipeline<R> {
        return new Pipeline(fn(this.value))
    }

    tap(fn: (value: T) => void): Pipeline<T> {
        fn(this.value)
        return this
    }

    get value(): T {
        return this._value
    }
}


function part1(input: string): number {
    return new Pipeline(input)
        .pipe(parseDiskMap)
        .pipe(compactUntilDoneByBit)
        .tap(diskMap => console.log(visualizeFileMap(diskMap)))
        .pipe(checksumDisk)
        .value
}

function part2(input: string): number {
    const diskmap = parseDiskMap(input)
    const compactedDiskMap = compactUntilDoneByFile(diskmap)

    console.log(visualizeFileMap(compactedDiskMap))
    return checksumDisk(compactedDiskMap)
}

// console.log("sample")
console.log(`Part 1: ${solve("src/day-09/sample-input.txt", part1)}`) //1928
console.log(`Part 2: ${solve("src/day-09/sample-input.txt", part2)}`)

// console.log("final")
//console.log(`Part 1: ${solve("src/day-09/input.txt", part1)}`) //6299243228569
// console.log(`Part 2: ${solve("src/day-XX/input.txt", part2)}`)


// Helpers