diff --git a/.gitignore b/.gitignore index d19df24..38e27fe 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ dist/ *.zip *.rar *.tgz +src/**/*.js + +**/*.txt +!src/**/*.txt diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..9075659 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.15.0 diff --git a/README.md b/README.md index a92f47c..5b1454a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ ## ✨ adventofcode -This repository contains solutions and a local development environment for the [Advent of Code](https://adventofcode.com/) event puzzles. +This repository contains solutions and a local development environment for the [Advent of Code](https://adventofcode.com/) event puzzles using TypeScript/JavaScript. + +The codes are structured in a way that discusses and walks through the solution steps for the AoC quizzes rather than focusing on AoC's competitive programming. ### 🎄 Advent of Code Quiz Information @@ -12,6 +14,11 @@ This repository contains solutions and a local development environment for the [ - Day 3: Mull It Over [[link]](/src/2024/2024-12-03/README.md) - Day 4: Ceres Search [[link]](/src/2024/2024-12-04/README.md) - Day 5: Print Queue [[link]](/src/2024/2024-12-05/README.md) +- 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) +- Day 10: Hoof It [[link]](/src/2024/2024-12-10/README.md) @@ -72,7 +79,7 @@ Each Advent of Code (AOC) event quiz has its folder under **`"/src////"` directories with actual AOC input. -2. Run a non-test TypeScript file inside the **/src** directory. For example: +2. Run a non-test TypeScript file inside the **/src** directory from the project's _**"root directory"**_. For example: ``` npx vite-node src/sample/sample.ts ``` @@ -108,7 +115,7 @@ Using Node npm run transpile node dist/sample/sample.js ``` -4. See the [Available Scripts](#available-scripts) section for more information. +4. See the [Available Scripts](#-available-scripts) section for more information. ## ⚡ Alternate Usage diff --git a/package-lock.json b/package-lock.json index 063cb99..85f4459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,8 @@ "vitest": "^2.1.8" }, "engines": { - "node": "20.15.0", - "npm": "10.7.0" + "node": ">=20.15.0", + "npm": ">=10.7.0" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/package.json b/package.json index 36368cb..270121f 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "dist/index.js", "type": "module", "engines": { - "node": "20.15.0", - "npm": "10.7.0" + "node": ">=20.15.0", + "npm": ">=10.7.0" }, "scripts": { "dev": "vitest", diff --git a/src/2024/2024-12-01/lib/fileReader.ts b/src/2024/2024-12-01/lib/fileReader.ts index f3aa32a..704e7b0 100644 --- a/src/2024/2024-12-01/lib/fileReader.ts +++ b/src/2024/2024-12-01/lib/fileReader.ts @@ -1,5 +1,5 @@ import path from 'path' -import { readFile, currentDirectory } from '@/utils/file.js' +import { readFile, directory } from '@/utils/file.js' export type arrayLists = { list1: string[]; @@ -12,8 +12,8 @@ export type arrayLists = { */ export const fileReader = (): arrayLists => { // Read quiz input file - const directory = currentDirectory(import.meta.url) - const file = readFile(path.join(directory, '..', 'input.txt')) + const dir = directory(import.meta.url) + const file = readFile(path.join(dir, '..', 'input.txt')) const pairs: string[] = file.split('\n') const list1: string[] = [] diff --git a/src/2024/2024-12-02/lib/fileReader.ts b/src/2024/2024-12-02/lib/fileReader.ts index 24c6b4e..ab2ae39 100644 --- a/src/2024/2024-12-02/lib/fileReader.ts +++ b/src/2024/2024-12-02/lib/fileReader.ts @@ -1,5 +1,5 @@ import path from 'path' -import { readFile, currentDirectory } from '@/utils/file.js' +import { readFile, directory } from '@/utils/file.js' /** * Reads the quiz's input file into two (2) string arrays @@ -7,8 +7,8 @@ import { readFile, currentDirectory } from '@/utils/file.js' */ export const fileReader = (): number[][] => { // Read quiz input file - const directory = currentDirectory(import.meta.url) - const file = readFile(path.join(directory, '..', 'input.txt')) + const dir = directory(import.meta.url) + const file = readFile(path.join(dir, '..', 'input.txt')) return file .split('\n') diff --git a/src/2024/2024-12-03/main.ts b/src/2024/2024-12-03/main.ts index 9f88c1d..6eae392 100644 --- a/src/2024/2024-12-03/main.ts +++ b/src/2024/2024-12-03/main.ts @@ -1,9 +1,9 @@ import path from 'path' -import { currentDirectory, readFile } from '@/utils/file.js' +import { directory, readFile } from '@/utils/file.js' import { extractMultiply, extractMultiplyCondition } from './lib/extractMultiply.js' -const directory = currentDirectory(import.meta.url) -const input = readFile(path.join(directory, 'input.txt')) +const dir = directory(import.meta.url) +const input = readFile(path.join(dir, 'input.txt')) /** * Part 1/2 of the 2024-12-03 quiz diff --git a/src/2024/2024-12-04/main.ts b/src/2024/2024-12-04/main.ts index 77a7bc3..d12deaf 100644 --- a/src/2024/2024-12-04/main.ts +++ b/src/2024/2024-12-04/main.ts @@ -1,11 +1,11 @@ import path from 'path' -import { currentDirectory, readFile } from '@/utils/file.js' +import { directory, readFile } from '@/utils/file.js' import { wordCount } from './lib/wordCount.js' import { countMASword } from './lib/xmasCount.js' -const directory = currentDirectory(import.meta.url) +const dir = directory(import.meta.url) -const data: string[][] = readFile(path.join(directory, 'input.txt')) +const data: string[][] = readFile(path.join(dir, 'input.txt')) .split('\n') .map(row => row.split('')) diff --git a/src/2024/2024-12-05/lib/fileReader.ts b/src/2024/2024-12-05/lib/fileReader.ts index 3ab063f..48b12c9 100644 --- a/src/2024/2024-12-05/lib/fileReader.ts +++ b/src/2024/2024-12-05/lib/fileReader.ts @@ -1,5 +1,5 @@ import path from 'path' -import { currentDirectory, readFile } from '@/utils/file.js' +import { directory, readFile } from '@/utils/file.js' import { uniformArrayElements } from '@/utils/arrays.js' export type Rules = Record @@ -15,8 +15,8 @@ export type QuizData = { * @returns {QuizData} Formatted data */ export const fileReader = (fileName: string): QuizData => { - const directory = currentDirectory(import.meta.url) - const file = readFile(path.join(directory, '..', fileName)) + const fir = directory(import.meta.url) + const file = readFile(path.join(fir, '..', fileName)) const segments = file.split('\n\n') diff --git a/src/2024/2024-12-05/lib/fixOrderingUpdates.ts b/src/2024/2024-12-05/lib/fixOrderingUpdates.ts index 98a6550..b7001eb 100644 --- a/src/2024/2024-12-05/lib/fixOrderingUpdates.ts +++ b/src/2024/2024-12-05/lib/fixOrderingUpdates.ts @@ -10,7 +10,7 @@ type CurrentItem = { } /** - * Finds the next appropriate array index to swap places with the incorrectly placed current item accoring to the "rules" + * Finds the next appropriate array index to swap places with the incorrectly placed current item according to the "rules" * @param rules {Rules} Object containing parsed and formatted rules and updates data * @param restItems {number[]} "updates" array items content * @param currentItem {CurrentItem} Object containing the "current" item data in focus: value and array index @@ -44,30 +44,32 @@ export const fixOrdering = (rules: Rules, unorderedItems: number[]): number[] => throw new Error('Invalid item/s') } - for (let i = 0; i < unorderedItems.length - 1; i += 1) { - let currentItem = unorderedItems[i] as number - const currentItemData = { value: currentItem, index: i } + let currentItem: number = -2 - // Swaps incorrectly placed items with target items in the array - const swapItems = () => { - const indexToSwap = nextHotSwapIndex( - rules, - unorderedItems, - currentItemData - ) + // Swaps incorrectly placed items with target items in the array + const swapItems = (activeItem: CurrentItem, activeIndex: number) => { + const indexToSwap = nextHotSwapIndex( + rules, + unorderedItems, + activeItem + ) - const temp = unorderedItems[indexToSwap] as number - unorderedItems[indexToSwap] = currentItem - unorderedItems[i] = temp - currentItem = temp + const temp = unorderedItems[indexToSwap] as number + unorderedItems[indexToSwap] = activeItem.value + unorderedItems[activeIndex] = temp + currentItem = temp - fixOrdering(rules, unorderedItems) - } + fixOrdering(rules, unorderedItems) + } + + for (let i = 0; i < unorderedItems.length - 1; i += 1) { + currentItem = unorderedItems[i] as number + const currentItemData = { value: currentItem, index: i } // Correct "update" item should have en entry in the "rules" object // Swap places with other items if its incorrect if (rules[currentItem] === undefined) { - swapItems() + swapItems(currentItemData, i) } // Get the rest of items after the current item for comparison @@ -76,7 +78,7 @@ export const fixOrdering = (rules: Rules, unorderedItems: number[]): number[] => // Correct item's "rule" should have the after-item entries // Swap places with other items if its incorrect if (!afterItems.every(item => rules[currentItem]?.includes(item))) { - swapItems() + swapItems(currentItemData, i) } } diff --git a/src/2024/2024-12-06/README.md b/src/2024/2024-12-06/README.md new file mode 100644 index 0000000..eb3a023 --- /dev/null +++ b/src/2024/2024-12-06/README.md @@ -0,0 +1,25 @@ +## Day 6: Guard Gallivant + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/6
+**Status:** Complete ⭐⭐ + +## Code + +1. **grid.ts** + - `Grid` - class that has a 2D array object containing paths and obstacles in which a `Guard` runs. + - Has functions and methods for managing a `Grid` object + +2. **guard.ts** + - `Guard` - class representing an object that can walk in the `Grid` map. + - Have methods for moving around in the `Grid`. + +3. **guardController.ts** + - `guardController()` - Runs the `Guard` on the `Grid`, counting distinct positions/steps. + +5. **guardControllerLoop.ts** + - `gridHasInfiniteLoop()` + - Runs the `Guard` on a `Grid` with obstacles, checking the placement of paths and obstacles that will make the `Guard` walk in an infinite loop + - `findObstructionPositions()` - Counts the number of positions in the `Grid` in which inserting one (1) obstruction symbol # will cause the `Guard` to walk in an infinite loop. + - > _**WARNING:** This is a very slow, unoptimized program execution that takes about ~3 mins to complete using the actual AoC input text._ diff --git a/src/2024/2024-12-06/input.txt b/src/2024/2024-12-06/input.txt new file mode 100644 index 0000000..6d7bbf6 --- /dev/null +++ b/src/2024/2024-12-06/input.txt @@ -0,0 +1,10 @@ +....#..... +.........# +.......... +.#........ +.......... +.......#.. +.#..^..... +.#......#. +....#..... +......#... \ No newline at end of file diff --git a/src/2024/2024-12-06/lib/grid.ts b/src/2024/2024-12-06/lib/grid.ts new file mode 100644 index 0000000..38f6019 --- /dev/null +++ b/src/2024/2024-12-06/lib/grid.ts @@ -0,0 +1,126 @@ +import { GuardStatus, GuardDirection } from './guard.types.js' +import type { GuardState } from './guard.types.js' + + +/** + * @class Grid + * @description Object containing a 2D array of strings and other data in which a `Guard` object runs. Each item represents an open path or obstacle. + */ +export class Grid { + /** 2D board string array */ + board: string[][] = [] + + /** Board length */ + length: number = 0 + + /** Board width */ + width: number = 0 + + /** Number of distinct positions that a `Guard` can traverse on the board */ + positionCount: number = 0 + + /** Initial (x,y) coordinate starting position of a `Guard` */ + start: { x: number; y: number; } + + /** Obstruction symbol. Guards turn clockwise if this symbol blocks their next step. */ + obstruction = '#' + + /** Path symbol. Guards proceed to the next (x,y) coordinate after on this symbol. */ + pathSymbol = '.' + + /** + * @constructor + * @param {string[][]} data 2D string array containing paths `"."`, obstacles `"#"` and the `Guard` object + * @param obstructionSymbol + */ + constructor (data: string[][], obstructionSymbol: string = '#') { + this.board = data + this.length = data.length + this.width = (data[0])?.length as number + this.obstruction = obstructionSymbol + this.start = { x: -1, y: -1 } + + this.findGuardStartPosition() + } + + /** + * Finds the initial position and state of the guard in a 2D board array + * @param data {string[][]} 2D string array of the guard board + * @returns {GuardState} Initial position and state of the guard + */ + findGuardStartPosition (): GuardState { + const GuardDirections: string[] = Object.values(GuardDirection) + + const initialState: GuardState = { + direction: null, + xPos: -1, + yPos: -1, + status: GuardStatus.IDLE + } + + for (let y = 0; y < this.board.length; y += 1) { + const row = this.board[y] + + if (row === undefined) { + throw new Error('Undefined row') + } + + for (let i = 0; i < GuardDirections.length; i += 1) { + const indexOfDirection = row.indexOf(GuardDirections[i] as string) + + if (indexOfDirection >= 0) { + initialState.direction = GuardDirections[i] as GuardDirection + initialState.xPos = indexOfDirection + initialState.yPos = y + initialState.status = GuardStatus.ACTIVE + break + } + } + + if (initialState.status === GuardStatus.ACTIVE) break + } + + this.start.x = initialState.xPos + this.start.y = initialState.yPos + + return initialState + } + + /** + * Checks if an (x,y) coordinate is outside the grid area + * @param {number} x array x coordinate + * @param {number} y array y coordinate + * @returns {boolean} Flag indicating if the guard is outside the grid + */ + isOutOfBounds (x: number, y: number): boolean { + const row = this.board[0] as string[] + + return x < 0 || + x >= row.length || + y < 0 || + y >= this.board.length + } + + /** + * Marks an (x,y) coordinate in the grid + * @param {number} x array x coordinate + * @param {number} y array y coordinate + * @returns {void} + */ + mark (x: number, y: number): void { + const row = this.board[y] + + if (row !== undefined && row[x] !== 'X') { + row[x] = 'X' + this.positionCount += 1 + } + } + + /** + * Displays the guard's distinct positions in the grid + */ + print () { + console.clear() + console.log(this.board.map(row => row.join(' '))) + } +} diff --git a/src/2024/2024-12-06/lib/grid.types.ts b/src/2024/2024-12-06/lib/grid.types.ts new file mode 100644 index 0000000..4c18219 --- /dev/null +++ b/src/2024/2024-12-06/lib/grid.types.ts @@ -0,0 +1,10 @@ +/** + * `Grid` dimensions properties + * + * @property {number} length - Length of the grid + * @property {number} wwidth - Width of the grid + */ +export type GridDimensions = { + length: number; + width: number; +} diff --git a/src/2024/2024-12-06/lib/guard.ts b/src/2024/2024-12-06/lib/guard.ts new file mode 100644 index 0000000..71e5d2a --- /dev/null +++ b/src/2024/2024-12-06/lib/guard.ts @@ -0,0 +1,65 @@ +import type { GuardState } from './guard.types.js' +import { GuardDirection, GuardStatus } from './guard.types.js' + +/** + * @class Guard + * @description Game object that can move around in a 2D grid array + */ +export class Guard { + direction: GuardDirection | null = null + xPos: number = 0 + yPos: number = 0 + status: GuardStatus = GuardStatus.IDLE + steps: number = 0 + + /** + * Creates an instance of the `Guard` class + * @constructor + * @param {GuardState} state - Initial state of the guard + */ + constructor (state: GuardState) { + this.direction = state.direction + this.xPos = state.xPos || 0 + this.yPos = state.yPos || 0 + this.status = state.status + } + + /** + * Turn the guard's direction clockwise by 90 degrees. + * If the direction is `null`, does nothing. + * @returns {void} + */ + turn (): void { + if (this.direction === null) return + + const directions = Object.values(GuardDirection) + const currentDirectionIndex = directions.indexOf(this.direction) + + const nextIndex = (currentDirectionIndex + 1) < directions.length + ? currentDirectionIndex + 1 + : 0 + + this.direction = directions[nextIndex] as GuardDirection + } + + /** + * Moves the guard by one (1) coordinate [x,y] position in the `Grid` + * @param xDirection {number} x coordinate + * @param yDirection {number} y coordinate + * @returns {void} + */ + walk (xDirection: number, yDirection: number): void { + if (xDirection === undefined || yDirection === undefined) return + + this.xPos += xDirection + this.yPos += yDirection + this.steps += 1 + } + + /** + * Set's the guard's status to "exited" + */ + exit (): void { + this.status = GuardStatus.EXIT + } +} diff --git a/src/2024/2024-12-06/lib/guard.types.ts b/src/2024/2024-12-06/lib/guard.types.ts new file mode 100644 index 0000000..4106c64 --- /dev/null +++ b/src/2024/2024-12-06/lib/guard.types.ts @@ -0,0 +1,62 @@ +/** + * Directions that a `Guard` can face in a `Grid` + * + * @enum {string} + * @property {string} UP - Upward direction `"^"` + * @property {string} RIGHT - Upward direction `">"` + * @property {string} DOWN - Upward direction `"v"` + * @property {string} LEFT - Upward direction `"<"` + */ +export enum GuardDirection { + UP = '^', + RIGHT = '>', + DOWN = 'v', + LEFT = '<' +} + +/** + * Mapping of `GuardDirections` to their corresponding numeric direction vectors. + * The vector indicates the direction of movement in the `Grid` + * + * @constant + * @type {Record} + * @property {string} [GuardDirection.UP] - Upwards movement direction along the y-axis + * @property {string} [GuardDirection.RIGHT] - Right-ise movement direction along the x-axis + * @property {string} [GuardDirection.DOWN] - Downwards movement direction along the y-axis + * @property {string} [GuardDirection.left] - Left-wise movement direction along the x-axis + */ +export const GuardDirectionVector: Record = { + [GuardDirection.UP]: -1, + [GuardDirection.RIGHT]: 1, + [GuardDirection.DOWN]: 1, + [GuardDirection.LEFT]: -1 +} + +/** + * `Guard` activity status in the `Grid` board + * + * @enum {string} + * @property {string} IDLE - Initial status + * @property {string} ACTIVE - Indicates active placement on the `Grid` + * @property {string} EXIT - Indicates off-Grid placement + */ +export enum GuardStatus { + IDLE = 'idle', + ACTIVE = 'active', + EXIT = 'exit' +} + +/** + * Properties identifying the current state/status of a `Guard` + * + * @property {GuardDirection | null} direction Current direction of a `Guard` in the `Grid` + * @property {number} xPos x-coordinate of the `Guard` in the `Grid` + * @property {number} yPos y-coordinate of the `Guard` in the `Grid` + * @property {GuardStatus} status `Guard` activity status + */ +export type GuardState = { + direction: GuardDirection | null; + xPos: number; + yPos: number; + status: GuardStatus; +} diff --git a/src/2024/2024-12-06/lib/guardController.ts b/src/2024/2024-12-06/lib/guardController.ts new file mode 100644 index 0000000..028474f --- /dev/null +++ b/src/2024/2024-12-06/lib/guardController.ts @@ -0,0 +1,64 @@ +import { GuardDirection, GuardStatus } from './guard.types.js' +import { Guard } from './guard.js' +import { Grid } from './grid.js' + +/** + * Runs the `Guard` on the grid, counting distinct positions/steps + * @param {string[][]} data 2D string array containing the guards grid paths + * @param {boolean} printTrail Flag to display the distinct guard positions in the grid + * @returns {number} Number of unique guard positions + */ +export const guardController = (data: string[][], printTrail: boolean = false): Grid => { + const grid = new Grid(structuredClone(data)) + const guard = new Guard(grid.findGuardStartPosition()) + + while ( + guard.status !== GuardStatus.EXIT && + !grid.isOutOfBounds(guard.xPos, guard.yPos) + ) { + let yDirection = 0 + let xDirection = 0 + + // Set the guard's direction in the grid + switch(guard.direction) { + case GuardDirection.UP: + yDirection = -1 + break + case GuardDirection.DOWN: + yDirection = 1 + break + case GuardDirection.LEFT: + xDirection = -1 + break + case GuardDirection.RIGHT: + xDirection = 1 + break + default: + break + } + + if ( + guard.yPos === grid.board.length - 1 && + guard.xPos === (grid.board[0] as string[])?.length - 1 + ) { + // Exit if the coordinates are at the edge of the Grid + guard.exit() + } + + const xAhead = guard.xPos + xDirection + const yAhead = guard.yPos + yDirection + const symbol = grid.board[yAhead]?.[xAhead] + + if (symbol !== grid.obstruction) { + // Walk and mark the current coordinates + grid.mark(guard.xPos, guard.yPos) + guard.walk(xDirection, yDirection) + } else { + // Turn (rotate clockwise) if the symbol ahead is an obstruction `"#" + guard.turn() + } + } + + if (printTrail) grid.print() + return grid +} diff --git a/src/2024/2024-12-06/lib/guardControllerLoop.ts b/src/2024/2024-12-06/lib/guardControllerLoop.ts new file mode 100644 index 0000000..33d7307 --- /dev/null +++ b/src/2024/2024-12-06/lib/guardControllerLoop.ts @@ -0,0 +1,122 @@ +import { GuardDirection, GuardStatus } from './guard.types.js' +import { Guard } from './guard.js' +import { Grid } from './grid.js' +import { guardController } from './guardController.js' + +/** + * Runs the `Guard` on a `Grid` with obstacles, checking the placement of paths and obstacles that will make the `Guard` walk in an infinite loop + * @param {string[][]} data 2D string array containing the original `Grid` inserted with a new obstruction symbol `"#" + * @param {boolean} printTrail Flag to display the distinct guard positions in the grid + * @returns {number} Number of unique guard positions + */ +export const gridHasInfiniteLoop = ( + data: string[][], + printTrail: boolean = false +): boolean => { + const grid = new Grid(data, '#') + const initialPosition = grid.findGuardStartPosition() + + const guard = new Guard({ + ...initialPosition, + yPos: initialPosition.yPos - 1 + }) + + const trail = [] + let isLoop = false + + while ( + guard.status !== GuardStatus.EXIT && + !grid.isOutOfBounds(guard.xPos, guard.yPos) && + !isLoop + ) { + let yDirection = 0 + let xDirection = 0 + + // Set the guard's direction in the grid + switch(guard.direction) { + case GuardDirection.UP: + yDirection = -1 + break + case GuardDirection.DOWN: + yDirection = 1 + break + case GuardDirection.LEFT: + xDirection = -1 + break + case GuardDirection.RIGHT: + xDirection = 1 + break + default: + break + } + + if ( + guard.yPos === grid.board.length - 1 && + guard.xPos === (grid.board[0] as string[])?.length - 1 + ) { + // Exit if the coordinates are at the edge of the Grid + guard.exit() + } + + const xAhead = guard.xPos + xDirection + const yAhead = guard.yPos + yDirection + const symbol = grid.board[yAhead]?.[xAhead] + + if (symbol !== grid.obstruction) { + const coord = `${guard.xPos},${guard.yPos},${guard.direction}` + + if (trail.indexOf(coord) >= 0) { + // If the coordinates and direction repeat, + // Guard has already entered an infinite loop + isLoop = true + } else { + // Note the (x,y) coordinates and direction + trail.push(coord) + } + + grid.mark(guard.xPos, guard.yPos) + guard.walk(xDirection, yDirection) + } else { + // Turn (rotate clockwise) if the symbol ahead is an obstruction `"#" + guard.turn() + } + } + + if (printTrail) grid.print() + return isLoop +} + +/** + * Counts the number of positions in the `Grid` in which inserting one (1) + * obstruction symbol # will cause the `Guard` to walk in an infinite loop. + * @param {number} data data 2D string array containing the `Grid` paths + */ +export const findObstructionPositions = (data: string[][], printGrid: boolean = false) => { + let loopCount = 0 + const paths: Grid = guardController(data) + const extras = Array.from({ length: data.length }, () => Array(data.length).fill('.')) + + for (let y = 0; y < paths.board.length; y += 1) { + const row = paths.board[y] as string[] + + for (let x = 0; x < row.length; x += 1) { + if (row[x] !== 'X') continue + if (x === paths.start.x && y === paths.start.y) { + continue + } + + const xData: string[][] = structuredClone(data) + xData[y]![x] = '#' + + const isLoop = gridHasInfiniteLoop(xData) + + if (isLoop) { + extras[y]![x] = '0' + loopCount += 1 + } + } + } + + if (printGrid) console.log(extras.map(x => x.join(' '))) + return loopCount +} diff --git a/src/2024/2024-12-06/main.ts b/src/2024/2024-12-06/main.ts new file mode 100644 index 0000000..285dded --- /dev/null +++ b/src/2024/2024-12-06/main.ts @@ -0,0 +1,37 @@ +import path from 'path' +import { readAOCInputFile, AOC_OUTPUT_TYPE } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { guardController } from './lib/guardController.js' +import { findObstructionPositions } from './lib/guardControllerLoop.js' + +const file = readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D +}) as string [][] + +/** + * Part 1/2 of the 2024-12-06 quiz + * Counts the number of distinct guard positions in a grid + */ +export const quiz20241206_01 = () => { + const grid = guardController(file, true) + + console.log('Distinct guard positions:', grid.positionCount) + return grid.positionCount +} + +/** + * Part 2/2 of the 2024-12-06 quiz + * Counts the number of positions in the Grid in which placing an + * obstacle will cause the Guard to walk in an infinite loop + */ +export const quiz20241206_02 = () => { + const infinitePositions = findObstructionPositions(file) + + console.log('Obstruction positions for infinite walk:', infinitePositions) + return infinitePositions +} + +quiz20241206_01() +quiz20241206_02() diff --git a/src/2024/2024-12-06/sample.test.ts b/src/2024/2024-12-06/sample.test.ts new file mode 100644 index 0000000..d5629f5 --- /dev/null +++ b/src/2024/2024-12-06/sample.test.ts @@ -0,0 +1,21 @@ +import path from 'path' +import { test, expect } from 'vitest' + +import { readAOCInputFile, AOC_OUTPUT_TYPE } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { guardController } from './lib/guardController.js' +import { findObstructionPositions } from './lib/guardControllerLoop.js' + +const file = readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D +}) as string [][] + +test('Count distinct guard positions', () => { + expect(guardController(file).positionCount).toBe(26) +}) + +test('Count obstacle positions', () => { + expect(findObstructionPositions(file)).toBe(2) +}) diff --git a/src/2024/2024-12-07/README.md b/src/2024/2024-12-07/README.md new file mode 100644 index 0000000..b32d271 --- /dev/null +++ b/src/2024/2024-12-07/README.md @@ -0,0 +1,29 @@ +## Day 7: Bridge Repair + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/7
+**Status:** Complete ⭐⭐ + +## Code + +### `totalCalibration.ts` + +- `operatorCombinations()` + - Finds the possible combinations in which to place operator symbols for an `N`-length array + +- `doEquation()` + - Returns the result of an AoC text equation and a set of operator symbols with the condition: left to write equation processing, disregarding operator precedence + +- `totalCalibrationResult()` + - Counts the total calibration sum of input lines whose elements (numbers) match the line's target sum after processing with one of N possible combinations of `+` and `*` operator placements + +### `totalCalibrationConcat.ts` + +- `doEquationWithConcat()` + - Returns the result of an AoC text equation and a set of operator symbols with the conditions: + - Concatenate two (2) numbers with the `||` symbol in between + - Left to write equation processing, disregarding operator precedence + +- `totalCalibrationConcat()` + - Counts the total calibration sum of input lines whose single or concatenated "joined" elements (numbers) match the line's target sum after processing with one of N possible combinations of `+`, `*` and `||` (concatenator) operator placements \ No newline at end of file diff --git a/src/2024/2024-12-07/input.txt b/src/2024/2024-12-07/input.txt new file mode 100644 index 0000000..a7efc4b --- /dev/null +++ b/src/2024/2024-12-07/input.txt @@ -0,0 +1,9 @@ +180: 10 18 +3124: 64 44 19 +76: 21 4 +194: 3 5 +1432: 1 1 1 3 +15: 5 5 2 3 +12345: 6 4 9 10 +100: 25 50 10 15 +125: 12 5 \ No newline at end of file diff --git a/src/2024/2024-12-07/lib/totalCalibration.ts b/src/2024/2024-12-07/lib/totalCalibration.ts new file mode 100644 index 0000000..4c97e46 --- /dev/null +++ b/src/2024/2024-12-07/lib/totalCalibration.ts @@ -0,0 +1,85 @@ +/** + * Finds the possible combinations in which to place operator symbols for an `N`-length array + * @param {string[]} operators String array containing operator symbols + * @param {number} N Length of a linear array + * @returns {string[][]} List (2D-string array) of possible operators placement combinations + */ +export const operatorCombinations = ( + operators: string[] = ['+', '*'], + N: number +): string[][] => { + const combinations: string[][] = [] + const totalCombinations = Math.pow(operators.length, N) + + for (let i = 0; i < totalCombinations; i += 1) { + const list: string[] = [] + let seed = i + + for (let j = 0; j < N; j += 1) { + list.push(operators[seed % operators.length] as string) + seed = Math.floor(seed / operators.length) + } + + combinations.push(list) + } + + return combinations +} + +/** + * Returns the result of an AoC text equation and a set of operator symbols with the condition: + * - Left to write equation processing, disregarding operator precedence + * @param {string} eqnString Math equation expressed as string + * @returns {number} Result of the `eqnString` + */ +export const doEquation = (numbers: number[], operators: string[]): number => { + let sum = numbers[0] as number + + operators.forEach((operator, index) => { + if (operator === '+') { + sum += numbers[index + 1] as number + } + + if (operator === '*') { + sum *= numbers[index + 1] as number + } + }) + + return sum +} + +/** + * Counts the total calibration sum of input lines whose elements (numbers) match the line's target sum + * after processing with one of N possible combinations of `+` and `*` operator placements + * @param input Input string array + * @returns {number} Total calibration sum + */ +export const totalCalibrationResult = (input: string[]): number => { + const operators = ['+', '*'] + let sum = 0 + + for (let i = 0; i < input.length; i += 1) { + const line = input[i] + + if (line === undefined) break + + const [targetSum, data] = line.split(': ') + const numbers = data?.split(' ').map(Number) as number[] + + // Find all operator placement combinations + const combinations = operatorCombinations(operators, numbers.length - 1) + + // Build the text equation + for (let j = 0; j < combinations.length; j += 1) { + // Process equation + const result = doEquation(numbers, combinations[j] as string[]) + + if (result === Number(targetSum)) { + sum += result + break + } + } + } + + return sum +} diff --git a/src/2024/2024-12-07/lib/totalCalibrationConcat.ts b/src/2024/2024-12-07/lib/totalCalibrationConcat.ts new file mode 100644 index 0000000..b8a6cb8 --- /dev/null +++ b/src/2024/2024-12-07/lib/totalCalibrationConcat.ts @@ -0,0 +1,70 @@ +import { operatorCombinations } from './totalCalibration.js' + +/** + * Returns the result of an AoC text equation and a set of operator symbols with the condition: + * - Concatenate two (2) numbers with the `||` symbol in between + * - Left to write equation processing, disregarding operator precedence + * @param {string} eqnString Math equation expressed as string + * @returns {number} Result of the `eqnString` + */ +const doEquationWithConcat = (numbers: number[], operators: string[]): number => { + let sum = numbers[0] as number + + operators.forEach((operator, index) => { + if (operator === '+') { + sum += numbers[index + 1] as number + } + + if (operator === '*') { + sum *= numbers[index + 1] as number + } + + if (operator === '||') { + const concatNum = `${sum}${numbers[index + 1]}` + sum = Number(concatNum) + } + }) + + return sum +} + +/** + * Counts the total calibration sum of input lines whose single or concatenated "joined" elements (numbers) + * match the line's target sum after processing with one of N possible combinations + * of `+`, `*` and `||` (concatenator) operator placements + * @param input Input string array + * @returns {number} Total calibration sum + */ +export const totalCalibrationConcat = (input: string[]): number => { + let sum = 0 + + for (let i = 0; i < input.length; i += 1) { + const line = input[i] + + if (line === undefined) break + + const [targetSum, data] = line.split(': ') + const numbers = data?.split(' ').map(Number) as number[] + + // Find all operator placement combinations + const combinations = operatorCombinations( + ['+', '*', '||'], + numbers.length - 1 + ) + + // Process the text equations + for (let j = 0; j < combinations.length; j += 1) { + const result = doEquationWithConcat( + numbers, + combinations[j] as string[] + ) + + if (result === Number(targetSum)) { + sum += result + break + } + } + } + + return sum +} diff --git a/src/2024/2024-12-07/main.ts b/src/2024/2024-12-07/main.ts new file mode 100644 index 0000000..2db9ac2 --- /dev/null +++ b/src/2024/2024-12-07/main.ts @@ -0,0 +1,35 @@ +import path from 'path' +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { totalCalibrationResult } from './lib/totalCalibration.js' +import { totalCalibrationConcat } from './lib/totalCalibrationConcat.js' + +// Read and process the input file +const input = (readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING +}) as string) + .split('\n') + +/** + * Part 1/2 of the 2024-12-07 quiz + * Counts the total calibration result of the input data + */ +const quiz20241207_01 = () => { + const total = totalCalibrationResult(input) + console.log('Calibration total:', total) +} + +/** + * Part 2/2 of the 2024-12-07 quiz + * Counts the total calibration result of the input data including + * "concatenated" numbers + */ +const quiz20241207_02 = () => { + const total = totalCalibrationConcat(input) + console.log('Calibration total (with concat):', total) +} + +quiz20241207_01() +quiz20241207_02() diff --git a/src/2024/2024-12-07/sample.test.ts b/src/2024/2024-12-07/sample.test.ts new file mode 100644 index 0000000..710bdba --- /dev/null +++ b/src/2024/2024-12-07/sample.test.ts @@ -0,0 +1,23 @@ +import path from 'path' +import { test, expect } from 'vitest' + +import { readAOCInputFile, AOC_OUTPUT_TYPE } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { totalCalibrationResult } from './lib/totalCalibration.js' +import { totalCalibrationConcat } from './lib/totalCalibrationConcat.js' + +// Read and process the input file +const input = (readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING +}) as string) + .split('\n') + +test('1/2: Total calibration result', () => { + expect(totalCalibrationResult(input)).toBe(295) +}) + +test('2/2: Total calibration result (with concat)', () => { + expect(totalCalibrationConcat(input)).toBe(420) +}) diff --git a/src/2024/2024-12-08/README.md b/src/2024/2024-12-08/README.md new file mode 100644 index 0000000..b917a5e --- /dev/null +++ b/src/2024/2024-12-08/README.md @@ -0,0 +1,22 @@ +## Day 8: Resonant Collinearity + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/8
+**Status:** Complete ⭐⭐ + +## Code + +### `GridAntinodes.ts` + +- `GridAntiNodes` class - Object that tracks and manages `Antennas` and `Antinodes` in a 2D grid array + +### `uniqueAntinodes.ts` + +- `uniqueAntinodes()` - Counts the unique locations in the grid that contains an antinode + +### `allAntinodes.ts` + +- `getAntinodesInPath()` - Finds all `Antinode` coordinates along a path within a 2D array (grid) given a `Point`, increment steps and a +/- direction + +- `countAllAntinodes()` - Counts the unique locations in the grid that contains all locations of antinodes along a path diff --git a/src/2024/2024-12-08/input.txt b/src/2024/2024-12-08/input.txt new file mode 100644 index 0000000..1ed83fc --- /dev/null +++ b/src/2024/2024-12-08/input.txt @@ -0,0 +1,12 @@ +............ +........x... +.....x...... +.......x.... +....x....... +......B..... +............ +............ +........B... +.........B.. +............ +............ \ No newline at end of file diff --git a/src/2024/2024-12-08/lib/GridAntinodes.ts b/src/2024/2024-12-08/lib/GridAntinodes.ts new file mode 100644 index 0000000..a335253 --- /dev/null +++ b/src/2024/2024-12-08/lib/GridAntinodes.ts @@ -0,0 +1,105 @@ +import type { Antenna } from './types.js' + +/** + * @class GridAntiNodes + * @description Object that tracks `Antennas` and `Antinodes` in a 2D grid array + */ +export class GridAntiNodes { + /** 2D string user input array */ + board: string[][] = [] + + /** Array containing Antenna (y,x) coordinates */ + antennas: Antenna[] = [] + + /** List of unique Antinode (y,x) coordinate strings */ + antinodes = new Set() + + /** Grid array index pointing to the current antenna in the `this.antennas[]` list */ + currentAntIndex: number = 0 + + /** Grid array index pointing to the next antenna after `this.currentAntIndex` */ + nextAntIndex: number = 1 + + /** + * Creates an instance of the `GridAntiNodes` class + * @constructor + * @param {string[][]} inputFile 2D string array containing grid paths and `Antennas` + */ + constructor(inputFile: string[][]) { + this.board = inputFile + this.findAntennas() + } + + /** + * Finds and stores the (x,y) coordinates of valid `Antennas` + * from the 2D string array into the `this.antennas[]` array + */ + findAntennas (): void { + for (let row = 0; row < this.board.length; row += 1) { + const indices: Antenna[] = this.board[row]!.reduce((list: Antenna[], item, index) => { + if (item.match(/^[A-Za-z0-9]+$/g)) { + this.board[row]![index] = item + + return [ + ...list, { frequency: item, x: index, y: row } + ] + } + + return list + }, []) + + this.antennas = [...this.antennas, ...indices] + } + } + + /** + * Increments the index counters used in traversing the `Antinodes` list + */ + incrementCursors (): void { + if (this.nextAntIndex === this.antennas.length - 1) { + this.currentAntIndex += 1 + this.nextAntIndex = this.currentAntIndex + 1 + } else { + this.nextAntIndex += 1 + } + } + + /** + * Resets the index counters used in traversing the `Antinodes` list + */ + resetCursors (): void { + this.currentAntIndex = 0 + this.nextAntIndex = 1 + } + + /** + * Stores the (x,y) coordinates of an `Antinode` + * @param {string} antinodeCoord String-concatenated (y,x) coordinate of an `Antinode` + */ + storeAntinode (antinodeCoord: string): void { + this.antinodes.add(antinodeCoord) + } + + /** + * Prints the 2D grid (board) to the screen + * @param {boolean} withAntinodes Flag to display the `Antinodes` in the grid + */ + printGrid (withAntinodes: boolean = false): void { + if (withAntinodes) { + const printBoard = structuredClone(this.board) + + for (const antinode of this.antinodes) { + const coord = (antinode as string).split(',').map(item => Number(item)) + const character = this.board[coord[0] as number]![coord[1] as number] + + if (character === '.') { + printBoard[coord[0] as number]![coord[1] as number] = '#' + } + } + + console.log(printBoard.map(row => row.join(' '))) + } else { + console.log(this.board.map(row => row.join(' '))) + } + } +} diff --git a/src/2024/2024-12-08/lib/allAntinodes.ts b/src/2024/2024-12-08/lib/allAntinodes.ts new file mode 100644 index 0000000..e63b7cb --- /dev/null +++ b/src/2024/2024-12-08/lib/allAntinodes.ts @@ -0,0 +1,93 @@ +import type { Antenna, Point } from './types.js' +import { GridAntiNodes } from './GridAntinodes.js' + +/** + * Finds all `Antinode` coordinates along a path within a 2D array (grid) given a `Point`, increment steps and a +/- direction + * @param {Point} point (y,x) coordinate of an `Antenna` qualified for creating an `Antinode` + * @param {Point} increments Amount of increments to increase/decrease the (y,x) offsets of a `Point` + * @param {number} direction `+/-` positive or negative direction for increasing/decreasing a `Point`'s coordinates + * @typedef {Object} board Dimensions of the 2D grid array + * @param {number} board.length Length of the 2D array + * @param {number} board.width Width of the 2D array + * @returns {Set} All `Antinode` (y,x) coordinates along the path + */ +const getAntinodesInPath = ( + point: Point, + increments: Point, + direction: number, + board: { length: number, width: number } +): Set => { + const antinodes = new Set() + const startPoint = { ...point } + + while ( + startPoint.x >= 0 && startPoint.x < board.length && + startPoint.y >= 0 && startPoint.y < board.width + ) { + antinodes.add(`${startPoint.y},${startPoint.x}`) + + startPoint.x += increments.x * direction + startPoint.y += increments.y * direction + } + + return antinodes +} + +/** + * Counts the unique locations in the grid that contains all locations of antinodes along a path + * @param {string[][]} inputFile 2D string array containing grid paths and `Antennas` + * @returns {number} Total number of unique antinodes in the grid + */ +export const countAllAntinodes = (inputFile: string[][]): number => { + const grid = new GridAntiNodes(inputFile) + + while(grid.currentAntIndex < grid.antennas.length - 1) { + // Antennas + const a1 = grid.antennas[grid.currentAntIndex] as Antenna + const a2 = grid.antennas[grid.nextAntIndex] as Antenna + + const gridDimensions = { + length: grid.board.length, + width: grid.board[0]!.length + } + + // Skip processing antennas with different frequencies + if (a1.frequency !== a2.frequency) { + grid.incrementCursors() + continue + } + + // Antenna coordinate difference + const diff = { + x: a2.x - a1.x, + y: a2.y - a1.y + } + + if ( + a1.y < inputFile.length && a1.y >= 0 && + a1.x < inputFile[0]!.length && a1.x >= 0 + ) { + // Find all aligned antinodes + getAntinodesInPath(a1, diff, -1, gridDimensions) + .forEach( + item => grid.storeAntinode(item) + ) + } + + if ( + a2.y < inputFile.length && a2.y >= 0 && + a2.x < inputFile[0]!.length && a2.x >= 0 + ) { + // Find all aligned antinodes + getAntinodesInPath(a2, diff, 1, gridDimensions) + .forEach( + item => grid.storeAntinode(item) + ) + } + + grid.incrementCursors() + } + + grid.printGrid(true) + return grid.antinodes.size +} diff --git a/src/2024/2024-12-08/lib/types.ts b/src/2024/2024-12-08/lib/types.ts new file mode 100644 index 0000000..868a9c2 --- /dev/null +++ b/src/2024/2024-12-08/lib/types.ts @@ -0,0 +1,8 @@ +export interface Point { + x: number; + y: number; +} + +export interface Antenna extends Point { + frequency: string; +} diff --git a/src/2024/2024-12-08/lib/uniqueAntinodes.ts b/src/2024/2024-12-08/lib/uniqueAntinodes.ts new file mode 100644 index 0000000..12c2882 --- /dev/null +++ b/src/2024/2024-12-08/lib/uniqueAntinodes.ts @@ -0,0 +1,60 @@ +import type { Antenna, Point } from './types.js' +import { GridAntiNodes } from './GridAntinodes.js' + +/** + * Counts the unique locations in the grid that contains an antinode + * @param {string[][]} inputFile 2D string array containing grid paths and `Antennas` + * @returns {number} Total number of unique antinodes in the grid + */ +export const countAntinodes = (inputFile: string[][]): number => { + const grid = new GridAntiNodes(inputFile) + + while(grid.currentAntIndex < grid.antennas.length - 1) { + // Antennas + const a1 = grid.antennas[grid.currentAntIndex] as Antenna + const a2 = grid.antennas[grid.nextAntIndex] as Antenna + + // Skip processing antennas with different frequencies + if (a1.frequency !== a2.frequency) { + grid.incrementCursors() + continue + } + + // Antenna coordinate difference + const diff = { + x: a2.x - a1.x, + y: a2.y - a1.y + } + + // Antinode 1 coordinates + const node1: Point = { + x: a1.x - diff.x, + y: a1.y - diff.y + } + + // Antinode 2 coordinates + const node2: Point = { + x: a2.x + diff.x, + y: a2.y + diff.y + } + + if ( + node1.y < inputFile.length && node1.y >= 0 && + node1.x < inputFile[0]!.length && node1.x >= 0 + ) { + grid.storeAntinode(`${node1.y},${node1.x}`) + } + + if ( + node2.y < inputFile.length && node2.y >= 0 && + node2.x < inputFile[0]!.length && node2.x >= 0 + ) { + grid.storeAntinode(`${node2.y},${node2.x}`) + } + + grid.incrementCursors() + } + + grid.printGrid(true) + return grid.antinodes.size +} diff --git a/src/2024/2024-12-08/main.ts b/src/2024/2024-12-08/main.ts new file mode 100644 index 0000000..68ace42 --- /dev/null +++ b/src/2024/2024-12-08/main.ts @@ -0,0 +1,32 @@ +import path from 'path' +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { countAntinodes } from './lib/uniqueAntinodes.js' +import { countAllAntinodes } from './lib/allAntinodes.js' + +const input = readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D +}) as string[][] + +/** + * Part 1/2 of the 2024-12-08 quiz + * Counts the unique locations in the grid that contains an antinode + */ +const quiz20241208_01 = () => { + const count = countAntinodes(input) + console.log('Antinodes in unique locations:', count) +} + +/** + * Part 2/2 of the 2024-12-08 quiz + * Counts the unique locations in the grid of all antinodes that contains an antinode + */ +const quiz20241208_02 = () => { + const count = countAllAntinodes(input) + console.log('All Antinodes count:', count) +} + +quiz20241208_01() +quiz20241208_02() diff --git a/src/2024/2024-12-08/sample.test.ts b/src/2024/2024-12-08/sample.test.ts new file mode 100644 index 0000000..43dd301 --- /dev/null +++ b/src/2024/2024-12-08/sample.test.ts @@ -0,0 +1,21 @@ +import path from 'path' +import { test, expect } from 'vitest' + +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { countAntinodes } from './lib/uniqueAntinodes.js' +import { countAllAntinodes } from './lib/allAntinodes.js' + +const input = readAOCInputFile({ + filePath: path.join(directory(import.meta.url), 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D +}) as string[][] + +test('Antinodes in unique locations', () => { + expect(countAntinodes(input)).toBe(14) +}) + +test('All antinodes in line', () => { + expect(countAllAntinodes(input)).toBe(34) +}) diff --git a/src/2024/2024-12-09/README.md b/src/2024/2024-12-09/README.md new file mode 100644 index 0000000..70cc9e1 --- /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..ecaa955 --- /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 + } + }) + } + + /** + * 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. + * @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..7d13b4e --- /dev/null +++ b/src/2024/2024-12-09/main.ts @@ -0,0 +1,37 @@ +import path from 'path' + +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { directory } from '@/utils/file.js' + +import { CompactDisk } from './lib/compact.js' +import { WholeDisk } from './lib/whole.js' + +const input = readAOCInputFile({ + filePath: path.join(directory(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) +} + +/** + * Part 2/2 of the 2024-12-09 quiz + * Counts the check sum of a defragment disk whose file blocks were moved to empty spaces as whole blocks + */ +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..35ffb07 --- /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 { directory } from '@/utils/file.js' + +import { CompactDisk } from './lib/compact.js' +import { WholeDisk } from './lib/whole.js' + +const input = readAOCInputFile({ + filePath: path.join(directory(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) +}) diff --git a/src/2024/2024-12-10/README.md b/src/2024/2024-12-10/README.md new file mode 100644 index 0000000..5faaddb --- /dev/null +++ b/src/2024/2024-12-10/README.md @@ -0,0 +1,18 @@ +## Day 10: Hoof It + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/10
+**Status:** Complete ⭐⭐ + +## Code + +### `utils.ts` + +- Utility and helper functions. + +### `scoresRatings.ts` + +- `countTrailScores()` + - Finds valid hiking trails (trailheads) from a point coordinate in a 2D array starting with `0` and ending in `9` symbol and calculates the **scores** for each trailhead. + - Calculates the trailhead **ratings** instead of the trailhead scores if provided with the optional `{ isRating: true }` parameter. Defaults to `false` diff --git a/src/2024/2024-12-10/assets/grid_01.png b/src/2024/2024-12-10/assets/grid_01.png new file mode 100644 index 0000000..d5e068b Binary files /dev/null and b/src/2024/2024-12-10/assets/grid_01.png differ diff --git a/src/2024/2024-12-10/assets/grid_02.png b/src/2024/2024-12-10/assets/grid_02.png new file mode 100644 index 0000000..b87edf4 Binary files /dev/null and b/src/2024/2024-12-10/assets/grid_02.png differ diff --git a/src/2024/2024-12-10/assets/grid_03.png b/src/2024/2024-12-10/assets/grid_03.png new file mode 100644 index 0000000..cafb93b Binary files /dev/null and b/src/2024/2024-12-10/assets/grid_03.png differ diff --git a/src/2024/2024-12-10/assets/grid_04.png b/src/2024/2024-12-10/assets/grid_04.png new file mode 100644 index 0000000..7954e75 Binary files /dev/null and b/src/2024/2024-12-10/assets/grid_04.png differ diff --git a/src/2024/2024-12-10/input.txt b/src/2024/2024-12-10/input.txt new file mode 100644 index 0000000..da9f063 --- /dev/null +++ b/src/2024/2024-12-10/input.txt @@ -0,0 +1,8 @@ +89010123 +78121874 +87430965 +96749874 +45278903 +32019012 +01329801 +10456732 \ No newline at end of file diff --git a/src/2024/2024-12-10/lib/scoresRatings.ts b/src/2024/2024-12-10/lib/scoresRatings.ts new file mode 100644 index 0000000..8d3176a --- /dev/null +++ b/src/2024/2024-12-10/lib/scoresRatings.ts @@ -0,0 +1,105 @@ +import type { Point } from '../../2024-12-08/lib/types.js' +import type { InputOptions, PointSteps, PointDirection, TrailScores } from './types.js' + +import { + findValidSteps, + findZeroCoordinatePositions, + getCoordinateSymbol +} from './utils.js' + +// List of trailhead scores +const scores: Record = {} +let activeZeroIndex = '' + +/** + * Finds valid hiking trails (trailheads) from a point coordinate in a 2D array starting `0` and ending in `9` symbols and + * calculates the scores for each trailhead. + * @param {PointDirection} pointVector - Point (y,x) coordinate in a 2D array with a list of valid coordinates from its location. + * @param {number[][]} data - 2D number array containing hiking trail data + * @param {boolean} isRating - If `true`, calculates the trailhead ratings instead of the trailhead scores. Defaults to `false` + * @returns {void} + */ +const findPaths = (pointVector: PointDirection, data: number[][], isRating: boolean = false) => { + const grid = { + length: data.length, width: data[0]!.length + } + + if (pointVector.validSteps.length > 0) { + while (pointVector.validSteps.length > 0) { + const step = pointVector.validSteps.pop() + + if (step === undefined) continue + const pt = getCoordinateSymbol(step, data) + + if (pt.symbol === 9) { + if (isRating) { + // Rating: count all trails ending in 9's + scores[activeZeroIndex]?.push(pt.coordinate) + } else { + // Scores: count unique ending 9's that match with the starting 0 + if (!scores[activeZeroIndex]!.includes(pt.coordinate)) { + scores[activeZeroIndex]?.push(pt.coordinate) + } + } + } + + const point: PointDirection = { + x: step!.x, + y: step!.y, + validSteps: findValidSteps(step as Point, grid, data) as PointSteps[] + } + + findPaths(point, data, isRating) + } + } +} + +/** + * Finds valid trailheads and counts each trailhead score. + * @param {number[][]} data - 2D number array containing hiking trail data + * @param {boolean} [printLog] - Flag to display the processing and total score logs + * @typedef {InputOptions} params - Input and logging parameter options + * @param {boolean} [params.printLog] - (Optional) Flag to display the miscellaneous data processing logs. + * @param {boolean} [params.isRating] - (Optional) Flag to calculate the trailhead rating instead of the score. + * @returns {TrailScores} + */ +export const countTrailScores = (data: number[][], params?: InputOptions): TrailScores => { + // Find starting positions + const starts = findZeroCoordinatePositions(data) + + const grid = { + length: data.length, width: data[0]!.length + } + + for (let i = 0; i < starts.length; i += 1) { + const initStep: PointDirection = { + x: starts[i]!.x, + y: starts[i]!.y, + validSteps: findValidSteps(starts[i] as Point, grid, data) as PointSteps[] + } + + const pt = getCoordinateSymbol(starts[i] as Point, data) + activeZeroIndex = pt.coordinate + scores[activeZeroIndex] = [] + + findPaths(initStep, data, params?.isRating) + } + + const total = Object + .values(scores) + .map(x => x.length) + .reduce((sum, item) => sum += item, 0) + + if (params?.printLog) { + for (const key in scores) { + console.log(`[${key}]: ${scores[key]?.length} score`) + } + + console.log('--TOTAL SCORE', total) + } + + return { + scores, + total + } +} diff --git a/src/2024/2024-12-10/lib/types.ts b/src/2024/2024-12-10/lib/types.ts new file mode 100644 index 0000000..5318951 --- /dev/null +++ b/src/2024/2024-12-10/lib/types.ts @@ -0,0 +1,60 @@ +/** + * Point (y,x) coordinate in a 2D array with direction + * @type {Object} PointSteps + * @property {number} x - x-coordinate of the point + * @property {number} y - y-coordinate of the point + * @property {Object} direction - Direction vector + * @property {number} direction.x - Left/right x-direction denoted by `+1` or `-1` + * @property {number} direction.y - Up/down y-direction denoted by `-1` or `+1` + */ +export type PointSteps = { + x: number; + y: number; + direction: { + x: number; + y: number; + } +} + +/** + * Point (y,x) coordinate in a 2D array with a list of valid coordinates from its location. + * @type {Object} PointDirection + * @property {number} x - x-coordinate of the point + * @property {number} y - y-coordinate of the point + * @property {PointSteps[]} validSteps - List of valid up/down/left/right coordinates from the (y,x) position. + */ +export type PointDirection = { + x: number; + y: number; + validSteps: PointSteps[] +} + +/** + * Represents a "(y,x)" coordinate in string and its value from a 2D array. + * @param {string} coordinate - String version of an "(y,x)" coordinate + * @param {string | number} symbol - Number or character in a 2D arary denoted by the (y,x) coordinate + */ +export type GridCoordinateSymbol = { + coordinate: string; + symbol: string | number; +} + +/** + * Data returned by the trailhead scores counting function. + * @param {Record} scores - Object list of trailhead scores per (y,x) coordinate that starts with a unique `0` and ends with a `9` + * @param {number} total - Total sum of the `scores` + */ +export type TrailScores = { + scores: Record, + total: number; +} + +/** + * Input settings options parameters for the Day 10 quiz + * @param {boolean} [printLog] - (Optional) Flag to display the miscellaneous data processing logs. + * @param {boolean} [isRating] - (Optional) Flag to calculate the trailhead rating instead of the score. + */ +export type InputOptions = { + printLog?: boolean; + isRating?: boolean; +} diff --git a/src/2024/2024-12-10/lib/utils.ts b/src/2024/2024-12-10/lib/utils.ts new file mode 100644 index 0000000..dcb90f6 --- /dev/null +++ b/src/2024/2024-12-10/lib/utils.ts @@ -0,0 +1,79 @@ +import type { Point } from '@/2024/2024-12-08/lib/types.js' +import type { GridDimensions } from '@/2024/2024-12-06/lib/grid.types.js' +import type { GridCoordinateSymbol, PointSteps } from './types.js' + +/** + * Converts a 2D `Point` point object to string and returns its value from the 2D array + * @param {Point} point - (y,x) coordinatate in the 2D array + * @param {number[][]} data - 2D number array containing hiking trail data + * @returns {GridCoordinateSymbol} Returns the `poiint` (x,y) coordinate expressed in string and its value + */ +export const getCoordinateSymbol = (point: Point, data: number[][]): GridCoordinateSymbol => { + return { + coordinate: `${point!.x},${point!.y}`, + symbol: data[point!.y]![point!.x] as number + } +} + +/** + * Finds the (y,x) coordinates of starting positions in a trailhead grid + * @param {number[][]} data - 2D number array containing hiking trail data + * @param {number} [symbol] - (Optional) Number indicating the symbol of a trailhead's start. Defaults to `0`. + * @returns {Point[]} Array of (y,x) coortinates of starting positions + */ +export const findZeroCoordinatePositions = (data: number[][], symbol?: number): Point[] => { + return data.reduce((list: Point[], row, y) => { + const rowItems = row.reduce((inner: Point[], item, x) => { + if (item === (symbol ?? 0)) return [...inner, { y, x }] + return inner + }, []) + + return [...list, ...rowItems] + }, []) +} + +/** + * Checks if a (y,x) coordinate is out of the grid area + * @param {Point} point - (y,x) coordinate + * @param {GridDimensions} gridMeta - Length and width definitions of a 2D array (grid) + * @returns {boolean} Flag if a coordinate is out of the grid area + */ +export const isOutOfBounds = (point: Point, gridMeta: GridDimensions): boolean => { + return ( + point.x < 0 || point.x >= gridMeta.width || + point.y < 0 || point.y >= gridMeta.length + ) +} + +/** + * Finds valid positions and coordinates of next steps from a coordinate. + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {GridDimensions} gridMeta - grid length and width + * @param {number[][]} grid - 2D number array input + * @returns {PointSteps[]} Array of valid grid positions from the given `point` + */ +export const findValidSteps = ( + point: Point, + gridMeta: GridDimensions, + grid: number[][] +): PointSteps[] | undefined => { + if (isOutOfBounds(point, gridMeta)) return + + // All 4 possible locations (directions) from a coordinate + const steps = [ + { ...point, x: point.x - 1, direction: { x: -1, y: 0 } }, // left + { ...point, x: point.x + 1, direction: { x: 1, y: 0 } }, // right + { ...point, y: point.y - 1, direction: { x: 0, y: -1 } }, // down + { ...point, y: point.y + 1, direction: { x: 0, y: 1 } } // up + ] + + // Filter valid items + return steps.filter(step => { + return ( + // Within the grid area + !isOutOfBounds(step, gridMeta) && + // Difference (elevation) with the current grid value is 1 + (grid[step.y]![step.x] ?? 0) - (grid[point.y]![point.x] ?? 0) === 1 + ) + }) +} diff --git a/src/2024/2024-12-10/main.ts b/src/2024/2024-12-10/main.ts new file mode 100644 index 0000000..13621d6 --- /dev/null +++ b/src/2024/2024-12-10/main.ts @@ -0,0 +1,38 @@ + +import { readAOCInputFile, AOC_OUTPUT_TYPE } from '@/utils/aocInputFile.js' +import { file } from '@/utils/file.js' + +import { countTrailScores } from './lib/scoresRatings.js' + +const input = readAOCInputFile({ + filePath: file(import.meta.url, 'input.txt'), + type: AOC_OUTPUT_TYPE.NUMBER_ARRAY_2D +}) as number[][] + +/** + * Part 1/2 of the 2024-12-10 quiz + * Counts the total trailhead scores + */ +const quiz20241210_01 = () => { + const totalScore = countTrailScores(input, { + printLog: true + }) + + console.log('Total trailhead score:', totalScore.total) +} + +/** + * Part 2/2 of the 2024-12-10 quiz + * Counts the total trailhead ratings + */ +const quiz20241210_02 = () => { + const totalScore = countTrailScores(input, { + printLog: true, + isRating: true + }) + + console.log('Total trailhead ratings:', totalScore.total) +} + +quiz20241210_01() +quiz20241210_02() diff --git a/src/2024/2024-12-10/sample.test.ts b/src/2024/2024-12-10/sample.test.ts new file mode 100644 index 0000000..8b456c8 --- /dev/null +++ b/src/2024/2024-12-10/sample.test.ts @@ -0,0 +1,23 @@ +import { test, expect } from 'vitest' + +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { file } from '@/utils/file.js' + +import { countTrailScores } from './lib/scoresRatings.js' + +const input = readAOCInputFile({ + filePath: file(import.meta.url, 'input.txt'), + type: AOC_OUTPUT_TYPE.NUMBER_ARRAY_2D +}) as number[][] + +test('Total trailhead score:', () => { + expect( + countTrailScores(input).total + ).toBe(17) +}) + +test('Trailhead rating:', () => { + expect( + countTrailScores(input, { isRating: true }).total + ).toBe(25) +}) diff --git a/src/utils/aocInputFile.ts b/src/utils/aocInputFile.ts new file mode 100644 index 0000000..56e99f2 --- /dev/null +++ b/src/utils/aocInputFile.ts @@ -0,0 +1,63 @@ +// This code contains file input readers for common AoC input types. +import { readFile } from './file.js' + +export enum AOC_OUTPUT_TYPE { + STRING = 'string', + STRING_ARRAY = 'string_array', + STRING_ARRAY_2D = '2d_string_array', + NUMBER_ARRAY = 'number_array', + NUMBER_ARRAY_2D = '2d_number_array' +} + +type FileInput = { + /** @param filePath {string} Full file path to input file */ + filePath: string; + type: AOC_OUTPUT_TYPE; + delimiter?: string; +} + +type Output = string | string[] | string[][] | + number[] | number[][] + +/** + * Reads common AoC input text files. + * @typedef param {FileInput} File input definitions + * @param param.filePath {string} Full file path to an input text file + * @param param.type {AOC_OUTPUT_TYPE} Type to convert the input text file + * @param param.delimiter {string} String delimiter + * @throws {Error} + */ +export const readAOCInputFile = (param: FileInput): Output => { + const file = readFile(param.filePath) + const delimiter = param?.delimiter ?? '' + + if (file === undefined) { + throw new Error('Undefined file') + } + + switch(param.type) { + case AOC_OUTPUT_TYPE.STRING: + return file as string + + case AOC_OUTPUT_TYPE.STRING_ARRAY: + return file.split(delimiter) as string[] || [] + + case AOC_OUTPUT_TYPE.STRING_ARRAY_2D: + return file + .split('\n') + .map(row => row.split(delimiter)) as string[][] || [] + + case AOC_OUTPUT_TYPE.NUMBER_ARRAY: + return file + .split('') + .map(Number) as number[] || [] + + case AOC_OUTPUT_TYPE.NUMBER_ARRAY_2D: + return file + .split('\n') + .map(row => row.split(delimiter).map(Number)) as number[][] || [] + + default: + throw new Error('Unsupported type') + } +} diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 5748cd4..f8ce6b2 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -20,14 +20,18 @@ export const arrangeArray = (order: ARRAY_ORDERING) => } /** - * Checks if array elements have the same type and has no null or undefined values + * Checks if array elements have the same type using `typeof` and has no null or undefined values * @param items {S[]} array of elements - * @param type {T} type name of the elements inside the `items` array + * @param type {T} primitive type name of the elements inside the array (e.g., number, string, boolean) * @returns {boolean} Flag indicating if all array elements have the same type */ -export const uniformArrayElements = (items: S[], type: T): boolean => { +export const uniformArrayElements = ( + items: S[], + type: T +): boolean => { return ( - items.filter(value => typeof value === type).length === items.length + items.filter(value => typeof value === type) + .length === items.length ) } diff --git a/src/utils/file.ts b/src/utils/file.ts index 247667b..1530bdc 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,22 +1,33 @@ import fs from 'fs' -import { dirname } from 'path' +import path, { dirname } from 'path' import { fileURLToPath } from 'url' /** - * Get the full file path of the current directory. - * @param moduleFile {string} File URL of the current module being executed: `"import.meta.url"` - * @returns Full file path to the directory of the calling file/module + * Get the full file path of the current directory of a module file equivalent to `"__dirname"`from + * scripts running as ESM modules whose package.json has `"type": "module"`. + * @param {string} moduleFile - File URL of the current module being executed: `"import.meta.url"` + * @returns {string} Full file path to the directory of the calling file/module also know as `__dirname` in CommonJS */ -export const currentDirectory = (moduleFile: string): string => { +export const directory = (moduleFile: string): string => { const filePath = fileURLToPath(moduleFile) return dirname(filePath) } /** * Reads file from disk - * @param pathToFile Full file path to a target file - * @returns {string} String version of the file + * @param {string} pathToFile - Full file path to a target file + * @returns {string} String version of the file contents */ export const readFile = (pathToFile: string): string => { return fs.readFileSync(pathToFile, 'utf-8') } + +/** + * Returns the full system file path to a file + * @param {string} moduleFile - File URL of the current module being executed: `"import.meta.url"` + * @param {string} fileName - File name relative to the calling directory (`moduleFile`), eg: `input.txt`, `../input.txt`, or `some/folder/input.txt` + * @returns {string} Full file path to a file + */ +export const file = (moduleFile: string, fileName: string) => { + return path.join(directory(moduleFile), fileName) +}