From 0d36b97a28afd1f68939564b896f61e01c542641 Mon Sep 17 00:00:00 2001 From: Pascal Syma Date: Sun, 8 Dec 2024 19:44:46 +0100 Subject: [PATCH] Add more utils --- 2024/06/index.test.ts | 9 +++-- 2024/06/index.ts | 12 ++---- package.json | 3 +- utils/array.test.ts | 83 ++++++++++++++++++++++++++++++++++++++- utils/array.ts | 47 ++++++++++++++++++++++ utils/coordinates.test.ts | 33 ++++++++++++++++ utils/coordinates.ts | 25 ++++++++++++ 7 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 utils/coordinates.test.ts create mode 100644 utils/coordinates.ts diff --git a/2024/06/index.test.ts b/2024/06/index.test.ts index c074600..58a1f38 100644 --- a/2024/06/index.test.ts +++ b/2024/06/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { solveFirst } from './index.ts'; +import { solveFirst, solveSecond } from './index.ts'; describe('2024-06', () => { const testInput = `....#..... @@ -16,7 +16,10 @@ describe('2024-06', () => { expect(solveFirst(testInput)).toBe(41); }); it('second', () => { - // that takes too long - // expect(solveSecond(testInput)).toBe(6); + // it takes too long + // so only run while `NODE_ENV=production bun test --coverage` + if (process.env.NODE_ENV === 'production') { + expect(solveSecond(testInput)).toBe(6); + } }); }); diff --git a/2024/06/index.ts b/2024/06/index.ts index 28dad22..6931e53 100644 --- a/2024/06/index.ts +++ b/2024/06/index.ts @@ -1,8 +1,4 @@ -import { sumBy } from '../../utils/array.ts'; - -export function replace(arr: string[], x: number, y: number, char: string) { - arr[y] = arr[y].substring(0, x) + char + arr[y].substring(x + 1); -} +import { replaceChar, sumBy } from '../../utils/array.ts'; const directions = [ { d: '^', x: 0, y: -1 }, @@ -28,7 +24,7 @@ export function solveFirst(input: string): number { direction = (direction + 1) % 4; } - replace(rows, x, y, 'X'); + replaceChar(rows, x, y, 'X'); x += directions[direction].x; y += directions[direction].y; } @@ -98,12 +94,12 @@ export function solveRow( continue; } - replace(rows, column, row, '#'); + replaceChar(rows, column, row, '#'); if (!finishes(rows, startX, startY, startDirection)) { found++; } - replace(rows, column, row, '.'); + replaceChar(rows, column, row, '.'); } return found; } diff --git a/package.json b/package.json index 9e1a428..d7bb010 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "format": "prettier --write .", "check": "prettier --check .", "test": "bun test", - "test:dev": "bun test --watch" + "test:dev": "bun test --watch 2024", + "test:utils": "NODE_ENV=production bun test --watch --coverage utils/*" }, "devDependencies": { "@types/bun": "latest", diff --git a/utils/array.test.ts b/utils/array.test.ts index b25e365..005d444 100644 --- a/utils/array.test.ts +++ b/utils/array.test.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from 'bun:test'; import { + combinations, getMiddleElement, + groupBy, multArray, multBy, + replaceChar, sumArray, - sumBy + sumBy, + unique } from './array.ts'; describe('array utils', () => { @@ -133,10 +137,87 @@ describe('array utils', () => { expect(multBy([], (e) => e)).toBeFinite(); expect(multBy([], (e) => e)).toBe(0); }); + it('groupBy', () => { + const arr = [0, 1, 2, 2, 1, 3]; + const actual = groupBy(arr, (e) => e); + const expected = [ + { key: 0, elements: [0] }, + { key: 1, elements: [1, 1] }, + { key: 2, elements: [2, 2] }, + { key: 3, elements: [3] } + ]; + + expect(actual).toHaveLength(actual.length); + expect(Object.keys(actual.map((e) => e.key))).toEqual( + expect.arrayContaining(Object.keys(expected.map((e) => e.key))) + ); + expected.forEach(({ key, elements }) => { + const actualGroup = actual.find((e) => e.key === key); + expect(actualGroup).toBeTruthy(); + expect(actualGroup).toBeObject(); + expect(actualGroup?.elements || []).toHaveLength(elements.length); + expect(actualGroup?.elements || []).toEqual( + expect.arrayContaining(elements) + ); + }); + }); it('getMiddleElement', () => { expect(getMiddleElement([1, 2, 3, 4])).toBe(3); expect(getMiddleElement([1, 2, 3])).toBe(2); expect(getMiddleElement([1])).toBe(1); expect(getMiddleElement([])).toBeUndefined(); }); + it('combinations', () => { + const numbers = [0, 1, 2]; + const n = numbers.length; + const actual = combinations(numbers); + const expected = [ + { a: 0, b: 1 }, + { a: 0, b: 2 }, + { a: 1, b: 0 }, + { a: 1, b: 2 }, + { a: 2, b: 0 }, + { a: 2, b: 1 } + ]; + expect(actual).toHaveLength(n * (n - 1)); + expect(actual).toEqual(expect.arrayContaining(expected)); + expect(expected).toEqual(expect.arrayContaining(actual)); + }); + it('unique numbers', () => { + const arr = [0, 1, 2, 2, 1, 3]; + const actual = arr.filter(unique()); + const expected = [0, 1, 2, 3]; + + expect(actual).toHaveLength(actual.length); + expect(actual).toEqual(expect.arrayContaining(expected)); + expect(expected).toEqual(expect.arrayContaining(actual)); + }); + it('unique objects', () => { + const arr = [{ a: 0 }, { a: 1 }, { a: 2 }, { a: 2 }, { a: 1 }, { a: 3 }]; + const actual = arr.filter(unique((a, b) => a.a === b.a)); + const expected = [{ a: 0 }, { a: 1 }, { a: 2 }, { a: 3 }]; + + expect(actual).toHaveLength(actual.length); + expect(actual).toEqual(expect.arrayContaining(expected)); + expect(expected).toEqual(expect.arrayContaining(actual)); + + // should just pass everything through since objects equality check is by reference + expect(arr.filter(unique())).toHaveLength(arr.length); + }); + it('replaceChar', () => { + const input = `000000 +111111 +222222 +333333`; + const rows = input.split('\n'); + replaceChar(rows, 1, 2, 'b'); + const actual = rows.join('\n'); + const expected = `000000 +111111 +2b2222 +333333`; + + expect(actual).toHaveLength(expected.length); + expect(actual).toEqual(expected); + }); }); diff --git a/utils/array.ts b/utils/array.ts index f56c7ed..cdcc2b8 100644 --- a/utils/array.ts +++ b/utils/array.ts @@ -90,3 +90,50 @@ export function groupBy( export function getMiddleElement(arr: ReadonlyArray) { return arr[Math.floor(arr.length / 2)]; } + +/** + * Returns an array with every combination of every element in an array. + * The combination array will have a length of `n * (n-1)` for an array of length `n`. + * @example + * ```typescript + * const arr = [0, 1, 2] + * + * combinations(arr) === [ + * {a: 0, b: 1}, + * {a: 0, b: 2}, + * {a: 1, b: 0}, + * {a: 1, b: 2}, + * {a: 2, b: 0}, + * {a: 2, b: 1}, + * ] + * ``` + * @param arr + */ +export function combinations(arr: ReadonlyArray): { a: E; b: E }[] { + return arr + .flatMap((a, _, arr) => arr.flatMap((b) => (a !== b ? [{ a, b }] : null))) + .filter((c) => !!c); +} + +/** + * Returns a filter lambda to be used in array.filter() to only allow the + * first occurrence of duplicate values. + * @param isEqual Custom equality check, defaults to `a === b` + */ +export function unique( + isEqual: (a: E, b: E) => boolean = (a, b) => a === b +) { + return (self: E, index: number, arr: ReadonlyArray) => + arr.findIndex((v) => isEqual(v, self)) === index; +} + +/** + * Replace a char in a two-dimensional array of strings. + * @param arr two-dimensional string array + * @param x column + * @param y row + * @param char Char + */ +export function replaceChar(arr: string[], x: number, y: number, char: string) { + arr[y] = arr[y].substring(0, x) + char + arr[y].substring(x + 1); +} diff --git a/utils/coordinates.test.ts b/utils/coordinates.test.ts new file mode 100644 index 0000000..02aac22 --- /dev/null +++ b/utils/coordinates.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'bun:test'; +import { type Coordinate, inBounds, isCoordinateEqual } from './coordinates.ts'; + +describe('coordinates utils', () => { + it('inBounds', () => { + expect(inBounds(10, 10)([0, 0])).toBeTrue(); + expect(inBounds(10, 10)([9, 3])).toBeTrue(); + expect(inBounds(10, 10)([9, 9])).toBeTrue(); + expect(inBounds(10, 10)([10, 9])).toBeFalse(); + expect(inBounds(10, 10)([10, 0])).toBeFalse(); + expect(inBounds(10, 10)([-1, 0])).toBeFalse(); + expect(inBounds(10, 10)([-0.1, 0])).toBeFalse(); + expect(inBounds(10, 10)([0, 10000])).toBeFalse(); + expect(inBounds(10, 10)([10000, 10000])).toBeFalse(); + expect(inBounds(10, 10, 5, 5)([0, 0])).toBeFalse(); + expect(inBounds(10, 10, 5, 0)([5, 0])).toBeTrue(); + expect(inBounds(10, 10, 5, 0)([4, 0])).toBeFalse(); + }); + + it('inBounds', () => { + expect(isCoordinateEqual([0, 0], [0, 0])).toBeTrue(); + expect(isCoordinateEqual([0, 1], [0, 1])).toBeTrue(); + expect(isCoordinateEqual([0, 1], [1, 0])).toBeFalse(); + expect(isCoordinateEqual([1 + 2, 3 + 4], [3, 7])).toBeTrue(); + + const a = [0, 0] as const; + expect(isCoordinateEqual(a, a)).toBeTrue(); + + const b: Coordinate = [0, 0]; + expect(isCoordinateEqual(b, b)).toBeTrue(); + expect(isCoordinateEqual(a, b)).toBeTrue(); + }); +}); diff --git a/utils/coordinates.ts b/utils/coordinates.ts new file mode 100644 index 0000000..442560f --- /dev/null +++ b/utils/coordinates.ts @@ -0,0 +1,25 @@ +export type Coordinate = [number, number]; + +/** + * Returns a filter lambda to be used in array.filter() on a xy-Coordinate array (array of two number arrays). + * @param xMax Exclusive + * @param yMax Exclusive + * @param xMin Inclusive, defaults to 0 + * @param yMin Inclusive, defaults to 0 + */ +export function inBounds(xMax: number, yMax: number, xMin = 0, yMin = 0) { + return ([x, y]: Readonly) => + x >= xMin && x < xMax && y >= yMin && y < yMax; +} + +/** + * Check the equality of two xy-coordinates. Checks both x and y independently for number equality. + * @param a xy-Coordinate, two number array + * @param b xy-Coordinate, two number array + */ +export function isCoordinateEqual( + a: Readonly, + b: Readonly +): boolean { + return b[0] === a[0] && b[1] === a[1]; +}