diff --git a/2024/12/2024-12.run.xml b/2024/12/2024-12.run.xml new file mode 100644 index 0000000..178a12d --- /dev/null +++ b/2024/12/2024-12.run.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/2024/12/index.test.ts b/2024/12/index.test.ts new file mode 100644 index 0000000..90746d8 --- /dev/null +++ b/2024/12/index.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'bun:test'; +import { getSides, solveFirst, solveSecond } from './index.ts'; + +describe('2024-12', () => { + it('first', () => { + expect( + solveFirst(`AAAA +BBCD +BBCC +EEEC`) + ).toBe(140); + expect( + solveFirst(`OOOOO +OXOXO +OOOOO +OXOXO +OOOOO`) + ).toBe(772); + expect( + solveFirst(`RRRRIICCFF +RRRRIICCCF +VVRRRCCFFF +VVRCCCJFFF +VVVVCJJCFE +VVIVCCJJEE +VVIIICJJEE +MIIIIIJJEE +MIIISIJEEE +MMMISSJEEE`) + ).toBe(1930); + }); + + it('second', () => { + // it takes too long + // so only run while `NODE_ENV=production bun test --coverage` + if (process.env.NODE_ENV === 'production') { + expect( + solveSecond(`EEEEE +EXXXX +EEEEE +EXXXX +EEEEE`) + ).toBe(236); + expect( + solveSecond(`AAAA +BBCD +BBCC +EEEC`) + ).toBe(80); + expect( + solveSecond(`OOOOO +OXOXO +OOOOO +OXOXO +OOOOO`) + ).toBe(436); + expect( + solveSecond(`AAAAAA +AAABBA +AAABBA +ABBAAA +ABBAAA +AAAAAA`) + ).toBe(368); + } + }); + + it('sides', () => { + expect( + getSides([ + [0, 0], + [1, 0], + [2, 0], + [0, 1] + ]) + ).toBe(6); + }); +}); diff --git a/2024/12/index.ts b/2024/12/index.ts new file mode 100644 index 0000000..f7b0847 --- /dev/null +++ b/2024/12/index.ts @@ -0,0 +1,113 @@ +import { sumArray } from '../../utils/array.ts'; +import { + calculateSides, + type Coordinates, + getAdjacent, + getNeighbouringSides, + isCoordinateEqual +} from '../../utils/coordinates.ts'; + +function searchNeighbours( + visited: Set, + rows: string[], + char: string, + x: number, + y: number, + found: Coordinates +) { + const position = y * rows.length + x; + if (visited.has(position)) { + return; + } + visited.add(position); + found.push([x, y]); + + const { left, top, right, down } = getAdjacent(rows, x, y); + if (left === char) { + searchNeighbours(visited, rows, char, x - 1, y, found); + } + if (top === char) { + searchNeighbours(visited, rows, char, x, y - 1, found); + } + if (right === char) { + searchNeighbours(visited, rows, char, x + 1, y, found); + } + if (down === char) { + searchNeighbours(visited, rows, char, x, y + 1, found); + } +} + +function groupChars(input: string) { + const rows = input.split('\n'); + + const groups: { char: string; coords: Coordinates }[] = []; + const visited = new Set(); + + for (let y = 0; y < rows.length; y++) { + for (let x = 0; x < rows[y].length; x++) { + const position = y * rows.length + x; + if (visited.has(position)) { + continue; + } + const char = rows[y][x]; + const coords: Coordinates = []; + searchNeighbours(visited, rows, char, x, y, coords); + + groups.push({ char, coords }); + } + } + return groups; +} + +function getPerimeter(g: Coordinates): number { + if (g.length === 1) { + return 4; + } + if (g.length === 2) { + return 6; + } + const { uniqueSides } = calculateSides(g); + + return uniqueSides.length; +} + +export function solveFirst(input: string): number { + const groups = groupChars(input); + + return sumArray(groups.map((g) => g.coords.length * getPerimeter(g.coords))); +} + +export function solveSecond(input: string): number { + const groups = groupChars(input); + + return sumArray(groups.map((g) => g.coords.length * getSides(g.coords))); +} + +export function getSides(g: Coordinates): number { + if (g.length === 1) { + return 4; + } + if (g.length === 2) { + return 4; + } + const { uniqueSides } = calculateSides(g); + + const visited = new Set(); + + let sides = 0; + for (let i = 0; i < uniqueSides.length; i++) { + if (visited.has(i)) { + continue; + } + visited.add(i); + sides++; + + const { neighbours } = getNeighbouringSides(uniqueSides[i], uniqueSides); + + neighbours.forEach((c) => + visited.add(uniqueSides.findIndex((v) => isCoordinateEqual(v, c))) + ); + } + + return sides; +} diff --git a/2024/12/run.ts b/2024/12/run.ts new file mode 100644 index 0000000..88783cd --- /dev/null +++ b/2024/12/run.ts @@ -0,0 +1,12 @@ +import { join } from 'path'; +import { solveFirst, solveSecond } from './index.ts'; + +const input = await Bun.file(join(__dirname, 'input.txt')).text(); + +const firstAnswer = solveFirst(input); + +console.log(firstAnswer); + +const secondAnswer = solveSecond(input); + +console.log(secondAnswer); diff --git a/utils/coordinates.ts b/utils/coordinates.ts index c13cb95..5c79c57 100644 --- a/utils/coordinates.ts +++ b/utils/coordinates.ts @@ -1,5 +1,16 @@ +export const Sides = { + TOP: 0, + RIGHT: 1, + DOWN: 2, + LEFT: 3 +} as const; + +export type Side = (typeof Sides)[keyof typeof Sides]; + export type Coordinate = [number, number]; +export type CoordinateSide = [number, number, Side]; export type Coordinates = Coordinate[]; +export type CoordinateSides = CoordinateSide[]; /** * Returns a filter lambda to be used in array.filter() on a xy-Coordinate array (array of two number arrays). @@ -9,7 +20,7 @@ export type Coordinates = Coordinate[]; * @param yMin Inclusive, defaults to 0 */ export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) { - return ([x, y]: Readonly) => + return ([x, y]: Readonly) => x >= xMin && x < xMax && y >= yMin && y < yMax; } @@ -19,8 +30,8 @@ export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) { * @param b xy-Coordinate, two number array */ export function isCoordinateEqual( - a: Readonly, - b: Readonly + a: Readonly, + b: Readonly ): boolean { return b[0] === a[0] && b[1] === a[1]; } @@ -78,3 +89,45 @@ export function findCoordsOfDigit( [] ); } + +export function calculateSides(g: Coordinate[]) { + const allSides = g.flatMap( + ([x, y]) => + [ + [x - 0.5, y, Sides.LEFT], + [x, y - 0.5, Sides.TOP], + [x + 0.5, y, Sides.RIGHT], + [x, y + 0.5, Sides.DOWN] + ] as CoordinateSides + ); + + const uniqueSides = allSides.filter( + (c) => allSides.filter((b) => isCoordinateEqual(c, b)).length === 1 + ); + return { uniqueSides, allSides }; +} + +export function getNeighbouringSides( + from: CoordinateSide, + all: CoordinateSides +) { + const [x, y, side] = from; + const isVertical = Math.abs(x % 1) === 0.5; + const coordIndex = isVertical ? 1 : 0; + + const allDirectional = all + .filter(([cx, cy, cs]) => (isVertical ? cx === x : cy === y) && cs === side) + .toSorted((a, b) => a[coordIndex] - b[coordIndex]); + + const before = allDirectional + .filter((d) => d[coordIndex] - from[coordIndex] < 0) + .toSorted((a, b) => b[coordIndex] - a[coordIndex]) + .filter((d, i) => d[coordIndex] - from[coordIndex] === 0 - i - 1); + const after = allDirectional + .filter((d) => d[coordIndex] - from[coordIndex] > 0) + .filter((d, i) => d[coordIndex] - from[coordIndex] === i + 1); + + const neighbours = [...before, ...after]; + + return { before, after, neighbours }; +}