diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e859aab..53509dc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,8 +26,7 @@ jobs: run: npm run lint - name: Transpile - run: npm run transpile + run: npm run transpile:noemit - name: Test run: npm test - diff --git a/README.md b/README.md index dfc92f4..05f923e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ ## ✨ adventofcode -This repository contains solutions and a local development environment for the [Advent of Code](https://adventofcode.com/) event puzzles using TypeScript/JavaScript. +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. +- The code repository structure follows a way that discusses and walks through the solution steps for the AoC quizzes rather than focusing on AoC's competitive programming. +- The quizzes were solved for fun (unlocking the 2024 AoC Chrismas symbol 🎄) and brain exercise purposes. Therefore, no GPT or AI completion was used for solving, as advised on the AoC website. +- These codes may get occasional optimization updates or solutions using other languages from time to time. ### 🎄 Advent of Code Quiz Information @@ -20,6 +22,7 @@ The codes are structured in a way that discusses and walks through the solution - Day 9: Disk Fragmenter [[link]](/src/2024/2024-12-09/README.md) - Day 10: Hoof It [[link]](/src/2024/2024-12-10/README.md) - Day 11: Plutonian Pebbles [[link]](/src/2024/2024-12-11/README.md) +- Day 12: Garden Groups [[link]](/src/2024/2024-12-12/README.md) @@ -67,6 +70,7 @@ It follows the directory structure: Each Advent of Code (AOC) event quiz has its folder under **`"/src//"`** containing: - **/lib**: Folder containing main quiz solution logic - **input.txt**: Random quiz input + > _**INFO:** The sample quiz inputs were slightly altered from the original AoC input text and quiz samples as advised on their website_ - **main.ts**: Main program entry point containing quiz answer(s) using random input - **sample.test.ts**: Minimal sample input with expected correct answers - **README.md**: Reference and other notes about the AOC quiz question diff --git a/package.json b/package.json index 270121f..c60f7a1 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev": "vitest", "dev:debug": "vite-node src/sample/sample.ts", "transpile": "tsc -p tsconfig.json && tsc-alias", + "transpile:noemit": "tsc -p tsconfig.json --noEmit", "watch": "tsc -p tsconfig.json --watch", "watch:docker:win": "tsc -p tsconfig.json --watch --watchFile dynamicPriorityPolling --watchDirectory dynamicPriorityPolling", "lint": "eslint \"src/**/*.ts\" *.mjs *.ts", diff --git a/src/2024/2024-12-10/lib/utils.ts b/src/2024/2024-12-10/lib/utils.ts index dcb90f6..ac34313 100644 --- a/src/2024/2024-12-10/lib/utils.ts +++ b/src/2024/2024-12-10/lib/utils.ts @@ -8,7 +8,7 @@ import type { GridCoordinateSymbol, PointSteps } from './types.js' * @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 => { +export const getCoordinateSymbol = (point: Point, data: number[][] | string[][]): GridCoordinateSymbol => { return { coordinate: `${point!.x},${point!.y}`, symbol: data[point!.y]![point!.x] as number diff --git a/src/2024/2024-12-11/README.md b/src/2024/2024-12-11/README.md index 1b3ca56..2be84da 100644 --- a/src/2024/2024-12-11/README.md +++ b/src/2024/2024-12-11/README.md @@ -7,8 +7,9 @@ Visit the Advent of Code website for more information on this puzzle at: ## Code -### `utils.ts` - contains helper and utility scripts for the quiz. +### `utils.ts` +- contains helper and utility scripts for the quiz. - `isEvenDigits()` - Checks if a number has an even number of digits - `halfDigit()` - Divides the number into two (2) separate groups (digits) if they have an even number of digits, each group having half the original number's digits. - `arrayToObject()` - Converts an array of numbers to an Object, using the array number elements as keys with a default numeric value of `1` diff --git a/src/2024/2024-12-12/README.md b/src/2024/2024-12-12/README.md new file mode 100644 index 0000000..38bafe6 --- /dev/null +++ b/src/2024/2024-12-12/README.md @@ -0,0 +1,56 @@ +## Day 12: Garden Groups + +Visit the Advent of Code website for more information on this puzzle at: + +**Source:** https://adventofcode.com/2024/day/12
+**Status:** Complete ⭐⭐ + +## Code + +### `garden.ts` + +- **Garden** class + - A set of methods and properties for calculating Garden, Region, and Plots data + - `Garden.calculatePlot()` - Calculates the per-plot perimeter and total area of all garden regions in a 2D grid, each defined by an initial symbol at a starting (y,x) coordinate. + - `Garden.calculateFencePrice()` - Calculates the total fencing price of all regions in a garden per connected plot using the formula: area * perimeter (per plot). + +### `wholesale.ts` + +- **WholesaleGarden** class + - A set of methods and properties for calculating the wholesale fencing price of garden regions + - `WholesaleGarden.calculateRegionCorners()` - Calculates the number of corners (sides) of a whole region. + - `WholesaleGarden.calculateFencePrice()` - Calculates the total fencing price of all regions in a garden using the formula: area * perimeter (of whole region). + +### `utils.ts` + +- A script containing helper and utility scripts for the quiz +- `findNeighbors()` - Finds all coordinates of the neighboring plots from a specified coordinate (up/down/left/right) +- `isIllegalCoordinate()` - Checks if a point at coordinate (y,x) in a grid is illegal: if it's out of bounds of the grid area or if its symbol differs from the symbol parameter. +- `getDiagonalNeighbors()` - Retrieves the four (4) diagonally-aligned (y,x) coordinates and the symbol character from a `Point` in the grid. Substitutes a `"*"` symbol character in the `NeighborPoint.symbol`if the `point` is out of the grid bounds. +- `getCrossNeighbors()` - Retrieves the four (4) horizontal/vertical aligned (y,x) coordinates and the symbol character from a `Point` in the grid. Substitutes a `"*"` symbol character in the `NeighborPoint.symbol`if the `point` is out of the grid bounds. +- `isDiagonal()` - Checks if two (2) `Points` are diagonally-aligned +- `innerCorners()` - Counts the "inner" corners from groups of valid L-shaped `Points` that originate from a `Point` coordinate. + + +## Notes + +1. Garden Plot + - only 1 plant + - single letter + - 4 sides + +2. Region + - garden plots that are touching (vertically/horizontally) + +3. Region: Area + - number of garden plots in the region + +4. Region: Perimeter + - no. of sides of garden plots in the region that do not touch another garden plot + +5. Thoughts/tips across the subreddit/google: + - no. of corners = no. of sides 😉 + +## References + +[[1]](https://en.wikipedia.org/wiki/Flood_fill) Flood Fill Algorithm diff --git a/src/2024/2024-12-12/assets/garden_03.png b/src/2024/2024-12-12/assets/garden_03.png new file mode 100644 index 0000000..4f9a13d Binary files /dev/null and b/src/2024/2024-12-12/assets/garden_03.png differ diff --git a/src/2024/2024-12-12/assets/garden_04.png b/src/2024/2024-12-12/assets/garden_04.png new file mode 100644 index 0000000..e18cd68 Binary files /dev/null and b/src/2024/2024-12-12/assets/garden_04.png differ diff --git a/src/2024/2024-12-12/assets/gardern_01.png b/src/2024/2024-12-12/assets/gardern_01.png new file mode 100644 index 0000000..0b09ab1 Binary files /dev/null and b/src/2024/2024-12-12/assets/gardern_01.png differ diff --git a/src/2024/2024-12-12/assets/gardern_02.png b/src/2024/2024-12-12/assets/gardern_02.png new file mode 100644 index 0000000..9dda474 Binary files /dev/null and b/src/2024/2024-12-12/assets/gardern_02.png differ diff --git a/src/2024/2024-12-12/input.txt b/src/2024/2024-12-12/input.txt new file mode 100644 index 0000000..5e25500 --- /dev/null +++ b/src/2024/2024-12-12/input.txt @@ -0,0 +1,4 @@ +ZZZZ +XXCD +XXCC +YYYC \ No newline at end of file diff --git a/src/2024/2024-12-12/input2.txt b/src/2024/2024-12-12/input2.txt new file mode 100644 index 0000000..80ae449 --- /dev/null +++ b/src/2024/2024-12-12/input2.txt @@ -0,0 +1,5 @@ +AAAAA +AXAXA +AAAAA +AXAXA +AAAAA \ No newline at end of file diff --git a/src/2024/2024-12-12/input3.txt b/src/2024/2024-12-12/input3.txt new file mode 100644 index 0000000..5213deb --- /dev/null +++ b/src/2024/2024-12-12/input3.txt @@ -0,0 +1,10 @@ +AAAAIICCGG +AAAAIICCCG +XXAAACCGGG +XXACCCJGGG +XXXXCJJCGP +XXIXCCJJPP +XXIIICJJPP +MIIIIIJJPP +MIIISIJPPP +MMMISSJPPP \ No newline at end of file diff --git a/src/2024/2024-12-12/input4.txt b/src/2024/2024-12-12/input4.txt new file mode 100644 index 0000000..603d5bf --- /dev/null +++ b/src/2024/2024-12-12/input4.txt @@ -0,0 +1,5 @@ +PPPPP +PEEEE +PPPPP +PEEEE +PPPPP \ No newline at end of file diff --git a/src/2024/2024-12-12/input5.txt b/src/2024/2024-12-12/input5.txt new file mode 100644 index 0000000..c7541ac --- /dev/null +++ b/src/2024/2024-12-12/input5.txt @@ -0,0 +1,6 @@ +AAAAAA +AAABBA +AAABBA +ABBAAA +ABBAAA +AAAAAA \ No newline at end of file diff --git a/src/2024/2024-12-12/lib/garden.ts b/src/2024/2024-12-12/lib/garden.ts new file mode 100644 index 0000000..7e29e38 --- /dev/null +++ b/src/2024/2024-12-12/lib/garden.ts @@ -0,0 +1,106 @@ +import type { GardenRegionDetails } from './types.js' +import type { GridDimensions } from '@/2024/2024-12-06/lib/grid.types.js' +import type { Point } from '@/2024/2024-12-08/lib/types.js' + +import { findNeighbors, isIllegalCoordinate } from './utils.js' + +/** + * @class Garden + * @description A set of methods and properties for calculating Garden, Region and per-plot perimeter data + */ +export class Garden { + /** Temporary storage containing processed coordinates */ + processed: string[] = [] + + /** Total fencing price for all regions in the garden */ + totalPrice: number = 0 + + /** + * Calculates the per-plot perimeter and total area of all garden regions in a 2D grid, each defined + * by an initial symbol at a starting (y,x) coordinate. + * Reference: https://en.wikipedia.org/wiki/Flood_fill + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {string} symbol - Character to check in the `point` coordinate. + * @param {string[][]} grid - 2D string array input + * @returns {GardenRegionDetails} Area and perimeter of a garden region. + */ + calculatePlot (point: Point, symbol: string, grid: GridDimensions, data: string[][]): GardenRegionDetails { + let list: Point[] = [point] + let perimeter = 0 + let area = 0 + + while (list.length > 0) { + const point = list.pop() + const coordStr = `${point!.x},${point!.y}` + + const isIllegal = isIllegalCoordinate({ + point: point as Point, + symbol, + gridMeta: grid, + grid: data + }) + + // Illegal coordinates count as edges (perimeters) + if (isIllegal) { + perimeter += 1 + continue + } + + if (this.processed.includes(coordStr)) continue + this.processed.push(coordStr) + + // Flood-fill included points + area += 1 + + const steps = findNeighbors(point as Point) as Point[] + list = [...list, ...steps] + } + + return { + area, perimeter + } + } + + /** + * Calculates the total fencing price of all regions in a garden per connected plot using the formula: area * perimeter (per plot). + * @param {string[][]} data - 2D string array input + * @param {boolean} log - Flag to display the log messages. Defaults to `false` + * @returns {number} Total fencing price + */ + calculateFencePrice (data: string[][], log: boolean = false): number { + const gridMeta = { + width: data[0]!.length, + length: data.length + } + + this.processed = [] + this.totalPrice = 0 + + for (let y = 0; y < data.length; y += 1) { + for (let x = 0; x < data[0]!.length; x += 1) { + const coordKey = `${x},${y}` + + if (!this.processed.includes(coordKey)) { + const point = { x, y } + const symbol = data[y]![x] + + const { area, perimeter } = this.calculatePlot( + point, + symbol as string, + gridMeta, + data + ) + + const price = area * perimeter + this.totalPrice += price + + if (log) { + console.log(`A region of ${symbol} plants with price ${area} * ${perimeter} = ${price}`) + } + } + } + } + + return this.totalPrice + } +} diff --git a/src/2024/2024-12-12/lib/types.ts b/src/2024/2024-12-12/lib/types.ts new file mode 100644 index 0000000..4b05820 --- /dev/null +++ b/src/2024/2024-12-12/lib/types.ts @@ -0,0 +1,40 @@ +import type { GridDimensions } from '@/2024/2024-12-06/lib/grid.types.js' +import type { Point } from '@/2024/2024-12-08/lib/types.js' + +/** + * @type {Object} GardenRegionDetails + * @property {number} area - Area of a garden region - sum of connected plots with similar symbols. + * @property {number} perimeter - Perimeter of a garden region - sum of outer edges per plot. + */ +export type GardenRegionDetails = { + area: number; + perimeter: number; +} + +/** + * @type {Object} GardenRegionDetails + * @property {number} area - Area of a garden region - sum of connected plots with similar symbols. + * @property {number} sides - Number of sides (corners) of a region. + */ +export type GardenRegionSides = { + area: number; + sides: number; +} + +/** + * @type {Object} IllegalCoordinateParams + * @property {Point} point - (y,x) coordinate object in a 2D array grid + * @property {string} symbol - Character to check in the `point` coordinate. + * @property {GridDimensions} gridMeta - Grid length and width + * @property {string[][]} grid - 2D string array input + */ +export type IllegalCoordinateParams = { + point: Point; + symbol: string; + gridMeta: GridDimensions; + grid: string[][]; +} + +export interface NeighborPoint extends Point { + symbol: string; +} diff --git a/src/2024/2024-12-12/lib/utils.ts b/src/2024/2024-12-12/lib/utils.ts new file mode 100644 index 0000000..e44785d --- /dev/null +++ b/src/2024/2024-12-12/lib/utils.ts @@ -0,0 +1,158 @@ +import type { Point } from '@/2024/2024-12-08/lib/types.js' +import type { IllegalCoordinateParams, NeighborPoint } from './types.js' + +import { getGridDimensions } from '@/utils/grid/utils.js' +import { isOutOfBounds } from '@/2024/2024-12-10/lib/utils.js' + +/** + * Finds all coordinates of the neighboring plots from a specified coordinate (up/down/left/right) + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {GridDimensions} gridMeta - grid length and width + * @param {string[][]} grid - 2D string array input + * @returns {PointSteps[]} Array of neighboring similar plants from the given `point` + */ +export const findNeighbors = (point: Point): Point[] | undefined => { + // All 4 possible locations (directions) from a coordinate + const steps = [ + { ...point, x: point.x - 1 }, // left + { ...point, x: point.x + 1 }, // right + { ...point, y: point.y - 1 }, // down + { ...point, y: point.y + 1 } // up + ] + + return steps +} + +/** + * Checks if a point at coordinate (y,x) in a grid is illegal: if it's out of bounds of the grid area + * or if its symbol differs from the symbol parameter. + * @param params {IllegalCoordinateParams} - Object input parameters. + * See the `IllegalCoordinateParams` for more information. + * @returns {boolean} Flag if a coordinate is illegae + */ +export const isIllegalCoordinate = (params: IllegalCoordinateParams): boolean => { + const { point, symbol, gridMeta } = params + + return ( + point.x < 0 || + point.x >= gridMeta.width || + point.y < 0 || + point.y >= gridMeta.length || + params.grid[point.y]![point.x] !== symbol + ) +} + +/** + * Retrieves the four (4) diagonally-aligned (y,x) coordinates and the symbol character from a `Point` in the grid. + * Substitutes a `"*"` symbol character in the `NeighborPoint.symbol`if the `point` is out of the grid bounds. + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {string[][]} data - 2D string array input + * @returns {NeighborPoint[]} Array of `NeighborPoint` with symbol characters. + */ +export const getDiagonalNeighbors = (point: Point, data: string[][]): NeighborPoint[] => { + const grid = getGridDimensions(data) + + // diagonal coordinates + const diagonals = [ + { x: point.x + 1, y: point.y - 1 }, + { x: point.x - 1, y: point.y - 1 }, + { x: point.x + 1, y: point.y + 1 }, + { x: point.x - 1, y: point.y + 1 } + ] + + return diagonals.reduce((list: NeighborPoint[], pt) => { + const item = { x: pt.x, y: pt.y, symbol: '*' } + + if (!isOutOfBounds(pt, grid)) { + item.symbol = data[pt.y]![pt.x] as string + } + + return [...list, item] + }, []) +} + +/** + * Retrieves the four (4) horizontal/vertical aligned (y,x) coordinates and the symbol character from a `Point` in the grid. + * Substitutes a `"*"` symbol character in the `NeighborPoint.symbol`if the `point` is out of the grid bounds. + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {string[][]} data - 2D string array input + * @returns {NeighborPoint[]} Array of `NeighborPoint` with symbol characters. + */ +export const getCrossNeighbors = (point: Point, data: string[][]): NeighborPoint[] => { + const neighbors = findNeighbors(point) as Point[] + const grid = getGridDimensions(data) + + return neighbors.reduce((list: NeighborPoint[], pt) => { + const item = { x: pt.x, y: pt.y, symbol: '*' } + + if (!isOutOfBounds(pt, grid)) { + item.symbol = data[pt.y]![pt.x] as string + } + + return [...list, item] + }, []) +} + +/** + * Checks if two (2) `Points` are diagonally-aligned + * @param {Point} p1 - (y,x) Start `Point` + * @param {Point} p2 - (y,x) End `Point` + * @returns {boolean} Flag indicating if 2 `Points` are diagonally aligned + */ +export const isDiagonal = (p1: Point, p2: Point): boolean => { + const diff = { + x: Math.abs(p1.x - p2.x), + y: Math.abs(p1.y - p2.y) + } + + return diff.x == 1 && diff.y === 1 +} + +/** + * Counts the "inner" corners from groups of valid L-shaped `Points` that originate from a `Point` coordinate. + * @param {Point} point - Origin (y,x) coordinate object in a 2D array grid + * @param {string} symbol - Character to check in the `point` coordinate. + * @param {string[][]} data - 2D string array input + * @returns {number} Number of inner corners associated with a `Point` + */ +export const innerCorners = ( + point: Point, + symbol: string, + data: string[][] +): number => { + const { x, y } = point + const grid = getGridDimensions(data) + + const diagonals = getDiagonalNeighbors(point, data) + .filter(pt => pt.symbol !== symbol) + + let innerCount = 0 + + for (let i = 0; i < diagonals.length; i += 1) { + const diff = { x: diagonals[i]!.x - x, y: diagonals[i]!.y - y } + + const direction = { + x: diff.x > 0 ? 1 : -1, + y: diff.y > 0 ? 1 : -1 + } + + const vertical = { + x, y: point.y + 1 * direction.y + } + + const horizontal = { + x: point.x + 1 * direction.x, y + } + + if ( + !isOutOfBounds(vertical, grid) && + !isOutOfBounds(horizontal, grid) && + data[vertical.y]![vertical.x] === symbol && + data[horizontal.y]![horizontal.x] === symbol + ) { + innerCount += 1 + } + } + + return innerCount +} diff --git a/src/2024/2024-12-12/lib/wholesale.ts b/src/2024/2024-12-12/lib/wholesale.ts new file mode 100644 index 0000000..7fed7e2 --- /dev/null +++ b/src/2024/2024-12-12/lib/wholesale.ts @@ -0,0 +1,102 @@ +import type { Point } from '@/2024/2024-12-08/lib/types.js' + +import { isOutOfBounds } from '@/2024/2024-12-10/lib/utils.js' +import { getGridDimensions } from '@/utils/grid/utils.js' +import { getCrossNeighbors, innerCorners, isDiagonal } from './utils.js' +import type { GardenRegionSides } from './types.js' + +/** + * @class WholesaleGarden + * @description A set of methods and properties for calculating the wholesale fencing price of garden regions + */ +export class WholesaleGarden { + visited: string[] = [] + + /** + * Calculates the number of corners (sides) of a whole region. + * @param {Point} point - (y,x) coordinate object in a 2D array grid + * @param {string} symbol - Character to check in the `point` coordinate. + * @param {string[][]} data - 2D string array input + * @returns {GardenRegionDetails} Object containing the area and whole region perimeter + */ + calculateRegionCorners (point: Point, symbol: string, data: string[][]): GardenRegionSides { + const stack: Point[] = [point] + const grid = getGridDimensions(data) + + let corners = 0 + let area = 0 + + while (stack.length > 0) { + const pt = stack.pop() as Point + const coordKey = `${pt.x},${pt.y}` + + if (this.visited.includes(coordKey)) continue + + // Vertical/horizontal coordinates data + const neighbors = getCrossNeighbors(pt, data) + const invalidPoints = neighbors.filter(item => item.symbol !== symbol) + + const next = neighbors.filter(item => + !isOutOfBounds(item, grid) && + item.symbol === symbol + ) + + // Coordinates containing other symbols count as corners 2x + if (invalidPoints.length >= 3) { + corners += 2 * Math.floor(invalidPoints.length / 2) + } + + // Diagonally-aligned other symbols (if only 2) count as a corner + if (invalidPoints.length === 2) { + if (isDiagonal(invalidPoints[0] as Point, invalidPoints[1] as Point)) { + corners += 1 + } + } + + // Count the inner corner(s) in L-shaped Points + corners += innerCorners(pt, symbol, data) + area += 1 + + this.visited.push(coordKey) + next.forEach(item => stack.push(item)) + } + + return { + sides: corners, + area + } + } + + /** + * Calculates the total fencing price of all regions in a garden using the formula: area * perimeter (of whole region). + * @param {string[][]} data - 2D string array input + * @param {boolean} log - Flag to display the log messages. Defaults to `false` + * @returns {number} Total fencing price + */ + calculateFencePrice (data: string[][], log: boolean = false): number { + let total = 0 + const grid = getGridDimensions(data) + + this.visited = [] + + for (let y = 0; y < grid.length; y += 1) { + for (let x = 0; x < grid.width; x += 1) { + const point = { x, y } + const pointStr = `${x},${y}` + const symbol = data[y]![x] as string + + if (this.visited.includes(pointStr)) continue + + const { sides, area } = this.calculateRegionCorners(point, symbol, data) + const price = sides * area + total += price + + if (log) { + console.log(`Region ${symbol}: sides ${sides} * ${area} = ${price}`) + } + } + } + + return total + } +} diff --git a/src/2024/2024-12-12/main.ts b/src/2024/2024-12-12/main.ts new file mode 100644 index 0000000..41993a2 --- /dev/null +++ b/src/2024/2024-12-12/main.ts @@ -0,0 +1,35 @@ +import { AOC_OUTPUT_TYPE, readAOCInputFile } from '@/utils/aocInputFile.js' +import { file } from '@/utils/file.js' + +import { Garden } from './lib/garden.js' +import { WholesaleGarden } from './lib/wholesale.js' + +const input = readAOCInputFile({ + filePath: file(import.meta.url, 'input.txt'), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D +}) as string[][] + +/** + * Part 1/2 of the 2024-12-12 quiz + * Counts the total fencing price: area * perimeter per plot + */ +const quiz20241231_01 = () => { + const garden = new Garden() + const totalPrice = garden.calculateFencePrice(input, true) + + console.log('---Total fencing price', totalPrice) +} + +/** + * Part 2/2 of the 2024-12-12 quiz + * Counts the wholesale fencing price: area * perimeter of whole region (edges) + */ +const quiz20241231_02 = () => { + const garden = new WholesaleGarden() + const totalPrice = garden.calculateFencePrice(input, true) + + console.log('---Wholesale fencing price', totalPrice) +} + +quiz20241231_01() +quiz20241231_02() diff --git a/src/2024/2024-12-12/sample.test.ts b/src/2024/2024-12-12/sample.test.ts new file mode 100644 index 0000000..1becc11 --- /dev/null +++ b/src/2024/2024-12-12/sample.test.ts @@ -0,0 +1,62 @@ +import { test, expect } from 'vitest' +import { AOC_OUTPUT_TYPE } from '@/utils/aocInputFile.js' +import { file } from '@/utils/file.js' + +import { readAOCInputFileAsync } from '@/utils/aocInputFile.js' + +import { Garden } from './lib/garden.js' +import { WholesaleGarden } from './lib/wholesale.js' + +const inputFiles = [ + 'input.txt', + 'input2.txt', + 'input3.txt', + 'input4.txt', + 'input5.txt' +] + +// Read input files +const files: Promise[] = [] + +for (let i = 0; i < inputFiles.length; i += 1) { + files.push(readAOCInputFileAsync({ + filePath: file(import.meta.url, inputFiles[i] as string), + type: AOC_OUTPUT_TYPE.STRING_ARRAY_2D + })) +} + +const [ + input, + input2, + input3, + input4, + input5 +] = await Promise.all(files) + +// Test area * perimeter per plot +test('Total fencing price', async () => { + const garden = new Garden() + + const totalPrice = garden.calculateFencePrice(input as string[][]) + const totalPrice2 = garden.calculateFencePrice(input2 as string[][]) + const totalPrice3 = garden.calculateFencePrice(input3 as string[][]) + + expect(totalPrice).toBe(140) + expect(totalPrice2).toBe(772) + expect(totalPrice3).toBe(1930) +}) + +// Test area * perimeter per region (wholesale) +test('Wholesale fencing price', async () => { + const garden = new WholesaleGarden() + + const totalPrice = garden.calculateFencePrice(input as string[][]) + const totalPrice2 = garden.calculateFencePrice(input2 as string[][]) + const totalPrice4 = garden.calculateFencePrice(input4 as string[][]) + const totalPrice5 = garden.calculateFencePrice(input5 as string[][]) + + expect(totalPrice).toBe(80) + expect(totalPrice2).toBe(436) + expect(totalPrice4).toBe(236) + expect(totalPrice5).toBe(368) +}) diff --git a/src/utils/aocInputFile.ts b/src/utils/aocInputFile.ts index 56e99f2..690acae 100644 --- a/src/utils/aocInputFile.ts +++ b/src/utils/aocInputFile.ts @@ -1,6 +1,14 @@ // This code contains file input readers for common AoC input types. import { readFile } from './file.js' +/** + * Definitions of the privimite types (`AOCFileOutput`) in which to convert the AoC quiz input text file. + * - `STRING` - String of text: `string` + * - `STRING_ARRAY`- Array of strings: `string[]` + * - `STRING_ARRAY_2D` - 2D array of strings: `string[][]` + * - `NUMBER_ARRAY` - Array of numbers: `number[]` + * - `NUMBER_ARRAY_2D` - 2D array of numbers: `number[][]` + */ export enum AOC_OUTPUT_TYPE { STRING = 'string', STRING_ARRAY = 'string_array', @@ -9,25 +17,32 @@ export enum AOC_OUTPUT_TYPE { NUMBER_ARRAY_2D = '2d_number_array' } -type FileInput = { - /** @param filePath {string} Full file path to input file */ +/** + * Input parameters indicating details about the AoC quiz input file. + * @param {string} filePath - Full file path to an AoC input text file. + * @param {AOC_OUTPUT_TYPE} type - Type to convert the input text file. One of `AOC_OUTPUT_TYPE`. + * @param {delimiter} delimiter - String delimiter to `split()` between characters in the original text file. Defaults to none. + */ +type AOCFileInput = { filePath: string; type: AOC_OUTPUT_TYPE; delimiter?: string; } -type Output = string | string[] | string[][] | +/** + * Represents common primitive types in which to convert the AoC quiz input + */ +export type AOCFileOutput = 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 + * Reads common AoC input text files and converts them into one of `AOCFileOutput` for data processing. + * @param {AOCFileInput} param File input definitions. + * - See `AOCFileInput` type for detailed definitions of the properties. + * @returns {AOCFileOutput} Input text file converted into one of `AOCFileOutput` * @throws {Error} */ -export const readAOCInputFile = (param: FileInput): Output => { +export const readAOCInputFile = (param: AOCFileInput): AOCFileOutput => { const file = readFile(param.filePath) const delimiter = param?.delimiter ?? '' @@ -49,7 +64,7 @@ export const readAOCInputFile = (param: FileInput): Output => { case AOC_OUTPUT_TYPE.NUMBER_ARRAY: return file - .split('') + .split(delimiter) .map(Number) as number[] || [] case AOC_OUTPUT_TYPE.NUMBER_ARRAY_2D: @@ -61,3 +76,19 @@ export const readAOCInputFile = (param: FileInput): Output => { throw new Error('Unsupported type') } } + +/** + * Reads common AoC input text files asynchronously and converts them into one of `AOCFileOutput` for data processing. + * @param {AOCFileInput} param File input definitions. + * - See `AOCFileInput` type for detailed definitions of the properties. + * @returns {Promise} Input text file converted into one of `AOCFileOutput` + */ +export const readAOCInputFileAsync = (param: AOCFileInput): Promise => { + return new Promise((resolve, reject) => { + try { + resolve(readAOCInputFile(param) as T) + } catch (error) { + reject(error) + } + }) +} diff --git a/src/utils/grid/utils.ts b/src/utils/grid/utils.ts new file mode 100644 index 0000000..084dad2 --- /dev/null +++ b/src/utils/grid/utils.ts @@ -0,0 +1,13 @@ +import type { GridDimensions } from '@/2024/2024-12-06/lib/grid.types.js' + +/** + * Retrieves the length and width of a 2D string or number array + * @param {string[][] | number[][]} data - 2D string or number array + * @returns {GridDimensions} Object containig the length and width of the 2D array + */ +export const getGridDimensions = (data: T[][]): GridDimensions => { + return { + length: data.length, + width: data[0]!.length + } +}