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 };
+}