diff --git a/README.md b/README.md index 18ce939..1b4f4da 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The codes are structured in a way that discusses and walks through the solution - Day 6: Guard Gallivant [[link]](/src/2024/2024-12-06/README.md) - Day 7: Bridge Repair [[link]](/src/2024/2024-12-07/README.md) - Day 8: Resonant Collinearity [[link]](/src/2024/2024-12-08/README.md) +- Day 9: Disk Fragmenter [[link]](/src/2024/2024-12-09/README.md) diff --git a/src/2024/2024-12-09/README.md b/src/2024/2024-12-09/README.md new file mode 100644 index 0000000..52082a0 --- /dev/null +++ b/src/2024/2024-12-09/README.md @@ -0,0 +1,32 @@ +## Day 9: Disk Fragmenter + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/8
+**Status:** Complete ⭐⭐ + +## Code + +### `disk.ts` + +**`Disk`** class - Object that provides common disk-like utility processing methods and stores processed data with the following methods: + +- **`createCharacterMap()`** - Converts files and spaces disk text representation into a character map, storing it in the `this.map[]` string array +- **`calculateDiskChecksum()`** - Calculates the checksum of fragmented or defragmented disk files and spaces text map +- **`getGrid()`** - Joins the `this.map[]` disk and spaces character array into a string format if it is less than `this.maxArrayToPrintLength` +- **`getCompactGrid()`** - Joins the `this.compactMap[]` "compact" disk and spaces character array into a string format if it is less than `this.maxArrayToPrintLength` + + +### `compact.ts` + +**`CompactDisk`** class (extends the **`Disk`** class) - Object that compacts disk character symbols representing file and space blocks from the right to the left-most free disk spaces with the following methods: + +- **`defragmentation()`** - Moves file blocks one character at a time from the right to the left-most free disk spaces of a disk character map, Storing the result in the `this.compactMap[]` and returning the result. + +### `whole.ts` + +**WholeDisk** class - Object that compacts disk character symbols representing files and spaces by moving whole (groups) of file blocks into spaces that can accommodate them with the following methods: + +- **`findDiskBlocks()`** - Finds and initializes the array indices of file blocks and spaces, noting their lengths for tracking +- **`findFreeSpaceBlock()`** - Finds the starting index of a group of free space blocks that can contain the whole length of a `FileBlock` in the `this.map[]` array from the `this.spaceBlocks` block map +- **`defragmentation()`** - Moves whole file blocks from the right to the left-most free disk spaces that can contain the whole file blocks, Storing the result in the this.compactMap[] and returning the result. diff --git a/src/2024/2024-12-09/input.txt b/src/2024/2024-12-09/input.txt new file mode 100644 index 0000000..ae6fb74 --- /dev/null +++ b/src/2024/2024-12-09/input.txt @@ -0,0 +1 @@ +233313312141413 \ No newline at end of file diff --git a/src/2024/2024-12-09/lib/compact.ts b/src/2024/2024-12-09/lib/compact.ts new file mode 100644 index 0000000..3e1f6ce --- /dev/null +++ b/src/2024/2024-12-09/lib/compact.ts @@ -0,0 +1,66 @@ +import { Disk } from './disk.js' +import type { CompactParams } from './types.js' + +/** + * @class CompactDisk + * @extends Disk + * @inheritdoc + * @description Object that compacts disk character symbols representing file and space blocks from the right to the left-most free disk spaces + */ +export class CompactDisk extends Disk { + /** + * Creates an instance of the `CompactDisk` class + * @constructor + * @param {string} diskMapText Series of numbers representing an alternating file and disk space blocks + * @param {boolean} printLog Flag to display the step-by-step file movement on screen. Defaults to `false` + */ + constructor(diskMapText: string, printLog: boolean = false) { + super(diskMapText) + this.defragmentation({ printLog }) + } + + /** + * Moves file blocks one character at a time from the right to the left-most free disk spaces of + * a disk character map, storing the result in the `this.compactMap[]` and returning the result. + * @param {CompactParams} params - Parameters for input map string array. + * @returns {string[]} Array of defragmented files and spaces blocks + */ + defragmentation (params: CompactParams): string[] { + const map: string[] = [...(params?.charMap || this.map)] + + let charIndex = map.length - 1 + let logs = '' + + // Total number of file (non-space) blocks + const filesCount = map.reduce( + (sum, item) => Number(item) >= 0 + ? sum += 1 + : sum, + 0 + ) + + while (charIndex >= filesCount) { + const dotIndex = map.indexOf('.') + + if (params?.printLog) { + logs += this.getGrid(map) + } + + // Swap file and space locations one unit at a time + if (dotIndex !== -1) { + map[dotIndex] = map[charIndex] ?? '.' + map[charIndex] = '.' + } + + charIndex -= 1 + } + + if (params?.printLog) { + logs += this.getGrid(map) + console.log(logs) + } + + this.compactMap = map + return map + } +} diff --git a/src/2024/2024-12-09/lib/disk.ts b/src/2024/2024-12-09/lib/disk.ts new file mode 100644 index 0000000..a7336bf --- /dev/null +++ b/src/2024/2024-12-09/lib/disk.ts @@ -0,0 +1,85 @@ +/** + * @class Disk + * @description Object that provides common disk-like utility processing methods and stores processed data. + */ +export class Disk { + /** 1-dimensional character array conversion of a disk map string */ + protected map: string[] = [] + + /** Compacted disk character array */ + protected compactMap: string[] = [] + + /** Maximum number of array elements to allow printing as string in console.log() */ + maxArrayToPrintLength: number = 10000 + + /** + * Creates an instance of the `Disk` class + * @constructor + * @param {string} diskMapText Series of numbers representing an alternating file and disk space blocks + */ + constructor(diskMapText: string) { + this.createCharacterMap(diskMapText) + } + + /** + * Converts files and spaces disk text representation into a character map, storing it in the `this.map[]` string array + * @param {string} diskMapText Series of numbers representing an alternating file and disk space blocks + * @returns {string[]} Character array conversion of a disk map string + */ + createCharacterMap (diskMapText: string): string[] { + for (let i = 0; i < diskMapText.length; i += 1) { + if (i % 2 === 1) { + this.map.push( + ...Array(Number(diskMapText[i])).fill('.') + ) + } else { + const itemID = i / 2 + + this.map.push( + ...Array(Number(diskMapText[i])).fill(itemID.toString()) + ) + } + } + + return this.map + } + + /** + * Calculates the checksum of fragmented or defragmented disk files and spaces text map + * @param {string[]} [compactFileText] (Optional) Compacted disk map resulting from the `this.defragmentation()` function. Processes the local `this.compactMap[]` data if parameter is not provided. + * @returns {number} Check sum - sum of file block IDs multiplied with their positions + */ + calculateDiskChecksum (compactFileText?: string[]): number { + return (compactFileText || this.compactMap) + .reduce((sum, item, index) => { + if (item === '.') return sum + return sum + Number(item) * index + }, 0) + } + + /** + * Joins the `this.map[]` disk and spaces character array into a string format if it is less than `this.maxArrayToPrintLength` + * @param {string} [map] (Optional) Character string array similar to `this.map[]`. Uses the `this.map[]` array by default. + * @returns {string} linear string/text version of the `this.map[]` character array + */ + getGrid (map?: string[]): string { + if ((map?.length ?? this.map.length) <= this.maxArrayToPrintLength) { + return (map ?? this.map).join('') + '\n' + } + + return '' + } + + /** + * Joins the `this.compactMap[]` "compact" disk and spaces character array into a string format if it is less than `this.maxArrayToPrintLength` + * @param {string} [map] (Optional) Character string array similar to `this.map[]`. Uses the `this.compactMap[]` array by default. + * @returns {string} linear string/text version of the `this.compactMap[]` character array + */ + getCompactGrid (map?: string[]): string { + if ((map?.length ?? this.compactMap.length) <= this.maxArrayToPrintLength) { + return (map ?? this.compactMap).join('') + '\n' + } + + return '' + } +} diff --git a/src/2024/2024-12-09/lib/types.ts b/src/2024/2024-12-09/lib/types.ts new file mode 100644 index 0000000..d393e11 --- /dev/null +++ b/src/2024/2024-12-09/lib/types.ts @@ -0,0 +1,33 @@ +/** + * Parameters for input map string array + * @interface CompactParams + * @param {string} [charMap] - (Optional) Character mapping conversion of a disk map. Processes the local `this.map[]` if parameter is not provided. + * @param {boolean} [printLog] - (Optional) Flag to display the step-by-step file block movement on screen. + */ +export interface CompactParams { + charMap?: string[]; + printLog? : boolean; +} + +/** + * Properties of a system file block + * @interface FileBlock + * @param {number} length - Length of a file block - number of units that it occupies in the `this.map[]` array + * @param {number} index - Starting array index of a file block in the `this.map[]` array + */ +export interface FileBlock { + length: number; + index: number; +} + +/** + * Properties of a space (free space) block + * @interface SpaceBlock + * @param {number} occupied - Number of space block units occupied by a `FileBlock`. A `SpaceBlock` is full if its `occupied` + * and `length` properties are equal + */ +export interface SpaceBlock extends FileBlock { + occupied: number; +} + + diff --git a/src/2024/2024-12-09/lib/whole.ts b/src/2024/2024-12-09/lib/whole.ts new file mode 100644 index 0000000..95bc617 --- /dev/null +++ b/src/2024/2024-12-09/lib/whole.ts @@ -0,0 +1,143 @@ +import { Disk } from './disk.js' +import type { CompactParams, FileBlock, SpaceBlock } from './types.js' + +/** + * @class WholeDisk + * @extends Disk + * @inheritdoc + * @description Object that compacts disk character symbols representing files and spaces by moving whole (groups) of file blocks into spaces that can accommodate them. + */ +export class WholeDisk extends Disk { + /** Structure for tracking the indices, length and availability of `SpaceBlock` disk space blocks */ + spaceBlocks = new Map() + + /** Structure for tracking the indices and lengths of `FileBlock` file blocks */ + fileBlocks = new Map() + + /** + * Creates an instance of the `WholeDisk` class + * @constructor + * @param {string} diskMapText Series of numbers representing an alternating file and disk space files + */ + constructor(diskMapText: string, printLog: boolean = false) { + super(diskMapText) + this.defragmentation({ printLog }) + } + + /** + * Finds and initializes the array indices of file blocks and spaces, noting their lengths for tracking. + * @returns {void} + */ + findDiskBlocks (): void { + let spaceCount = 0 + + this.map.forEach((digit, index) => { + if (digit !== '.') { + if (!this.fileBlocks.has(digit)) { + this.fileBlocks.set(digit, { length: 1, index }) + } else { + this.fileBlocks.get(digit)!.length += 1 + } + + if (spaceCount > 0) { + this.spaceBlocks.set(String(index - spaceCount), { + length: spaceCount, + index: index - spaceCount, + occupied: 0 + }) + + spaceCount = 0 + } + } else { + spaceCount += 1 + } + }) + } + + /** + * Moves whole file blocks from the right to the left-most free disk spaces that can contain the whole file blocks, Storing the result in the `this.compactMap[]` and returning the result. + * @param {number} fileBlockLength Length of a `FileBlock` - number of spaces it occupies in the `this.map[]` array + * @param {number} fileStartIndex Starting index of a `FileBlock` in the `this.map[]` array + * @returns {number} Start array index in the `this.map[]` of a valid free space that can contain the whole length of a file block + */ + findFreeSpaceBlock (fileBlockLength: number, fileStartIndex: number): number { + let indexOfSpace = -1 + + for (const item of this.spaceBlocks.values()) { + if ( + // Space units already occupied with file blocks plus expected file block length is less than or equal + // the spaces (length) it can accommodate + item.occupied + fileBlockLength <= item.length && + // The max units (spaces) should be large enough to contain the file length + item.length >= fileBlockLength && + // Space block's index (position in the this.map[]) array is lower than the file's starting index to avoid repitition + item.index < fileStartIndex + ) { + indexOfSpace = item.index + break + } + } + + return indexOfSpace + } + + /** + * Moves whole file blocks from the right to the left-most free disk spaces that can contain the whole files blocks, + * Storing the result in the `this.compactMap[]` and returning the result. + * @typedef {CompactParams} params - Parameters for input map string array. + * @returns {string[]} Array of defragmented files and spaces blocks + */ + defragmentation (params: CompactParams): string[] { + const map: string[] = structuredClone(params.charMap ?? this.map) + let logs = '' + + // Find the locations and stats of files and spaces + this.findDiskBlocks() + + // Ascending order file IDs + const digitKeys = Array.from(this.fileBlocks.keys()) + + for (let i = digitKeys.length - 1; i >= 0; i -= 1) { + const key = digitKeys[i] + + // Full file block + const file = this.fileBlocks.get(String(key)) + + if (file === undefined) continue + + // Find the available free disk space location that can fit the full file block + const fileBlockLength = file.length + const spaceIndex = this.findFreeSpaceBlock(fileBlockLength, file.index) + + const space = this.spaceBlocks.get(String(spaceIndex)) + if (space === undefined) continue + + const start = space.index + space.occupied + const end = start + fileBlockLength + + // Move files to the free space + for (let j = start; j < end; j += 1) { + map[j] = String(key) + space.occupied += 1 + } + + // Remove file trace from the old location + for ( + let k = file.index; + k < file.index + fileBlockLength; + k += 1 + ) { + map[k] = '.' + } + + logs += this.getCompactGrid(map) + } + + if (params?.printLog) { + console.log(logs) + } + + this.compactMap = map + return map + } +} diff --git a/src/2024/2024-12-09/main.ts b/src/2024/2024-12-09/main.ts new file mode 100644 index 0000000..7a048d3 --- /dev/null +++ b/src/2024/2024-12-09/main.ts @@ -0,0 +1,33 @@ +import path from 'path' + +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { currentDirectory } from '@/utils/file.js' + +import { CompactDisk } from './lib/compact.js' +import { WholeDisk } from './lib/whole.js' + +const input = readAOCInputFile({ + filePath: path.join(currentDirectory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING +}) as string + +/** + * Part 1/2 of the 2024-12-09 quiz + * Counts the check sum of a compacted disk represented by strings of text + */ +export const quiz20241209_01 = () => { + const disk = new CompactDisk(input, true) + const sum = disk.calculateDiskChecksum() + + console.log('Compacted disk checksum:', sum) +} + +export const quiz20241209_02 = () => { + const disk = new WholeDisk(input, true) + const sum = disk.calculateDiskChecksum() + + console.log('Disk - whole file blocks checksum:', sum) +} + +quiz20241209_01() +quiz20241209_02() diff --git a/src/2024/2024-12-09/sample.test.ts b/src/2024/2024-12-09/sample.test.ts new file mode 100644 index 0000000..ca1d259 --- /dev/null +++ b/src/2024/2024-12-09/sample.test.ts @@ -0,0 +1,23 @@ +import path from 'path' +import { test, expect } from 'vitest' + +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { currentDirectory } from '@/utils/file.js' + +import { CompactDisk } from './lib/compact.js' +import { WholeDisk } from './lib/whole.js' + +const input = readAOCInputFile({ + filePath: path.join(currentDirectory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING +}) as string + +test('Defragmented disk checksum', () => { + const disk = new CompactDisk(input) + expect(disk.calculateDiskChecksum()).toBe(967) +}) + +test('Defragmented disk - (move whole file blocks) checksum', () => { + const disk = new WholeDisk(input) + expect(disk.calculateDiskChecksum()).toBe(1440) +})