Add more utils

This commit is contained in:
2024-12-08 19:44:46 +01:00
parent 3bcb52f5a0
commit 0d36b97a28
7 changed files with 199 additions and 13 deletions
+6 -3
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { solveFirst } from './index.ts'; import { solveFirst, solveSecond } from './index.ts';
describe('2024-06', () => { describe('2024-06', () => {
const testInput = `....#..... const testInput = `....#.....
@@ -16,7 +16,10 @@ describe('2024-06', () => {
expect(solveFirst(testInput)).toBe(41); expect(solveFirst(testInput)).toBe(41);
}); });
it('second', () => { it('second', () => {
// that takes too long // it takes too long
// expect(solveSecond(testInput)).toBe(6); // so only run while `NODE_ENV=production bun test --coverage`
if (process.env.NODE_ENV === 'production') {
expect(solveSecond(testInput)).toBe(6);
}
}); });
}); });
+4 -8
View File
@@ -1,8 +1,4 @@
import { sumBy } from '../../utils/array.ts'; import { replaceChar, 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);
}
const directions = [ const directions = [
{ d: '^', x: 0, y: -1 }, { d: '^', x: 0, y: -1 },
@@ -28,7 +24,7 @@ export function solveFirst(input: string): number {
direction = (direction + 1) % 4; direction = (direction + 1) % 4;
} }
replace(rows, x, y, 'X'); replaceChar(rows, x, y, 'X');
x += directions[direction].x; x += directions[direction].x;
y += directions[direction].y; y += directions[direction].y;
} }
@@ -98,12 +94,12 @@ export function solveRow(
continue; continue;
} }
replace(rows, column, row, '#'); replaceChar(rows, column, row, '#');
if (!finishes(rows, startX, startY, startDirection)) { if (!finishes(rows, startX, startY, startDirection)) {
found++; found++;
} }
replace(rows, column, row, '.'); replaceChar(rows, column, row, '.');
} }
return found; return found;
} }
+2 -1
View File
@@ -7,7 +7,8 @@
"format": "prettier --write .", "format": "prettier --write .",
"check": "prettier --check .", "check": "prettier --check .",
"test": "bun test", "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": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
+82 -1
View File
@@ -1,10 +1,14 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import { import {
combinations,
getMiddleElement, getMiddleElement,
groupBy,
multArray, multArray,
multBy, multBy,
replaceChar,
sumArray, sumArray,
sumBy sumBy,
unique
} from './array.ts'; } from './array.ts';
describe('array utils', () => { describe('array utils', () => {
@@ -133,10 +137,87 @@ describe('array utils', () => {
expect(multBy([], (e) => e)).toBeFinite(); expect(multBy([], (e) => e)).toBeFinite();
expect(multBy([], (e) => e)).toBe(0); 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', () => { it('getMiddleElement', () => {
expect(getMiddleElement([1, 2, 3, 4])).toBe(3); expect(getMiddleElement([1, 2, 3, 4])).toBe(3);
expect(getMiddleElement([1, 2, 3])).toBe(2); expect(getMiddleElement([1, 2, 3])).toBe(2);
expect(getMiddleElement([1])).toBe(1); expect(getMiddleElement([1])).toBe(1);
expect(getMiddleElement([])).toBeUndefined(); 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);
});
}); });
+47
View File
@@ -90,3 +90,50 @@ export function groupBy<E, K>(
export function getMiddleElement<E>(arr: ReadonlyArray<E>) { export function getMiddleElement<E>(arr: ReadonlyArray<E>) {
return arr[Math.floor(arr.length / 2)]; 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<E>(arr: ReadonlyArray<E>): { 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<E>(
isEqual: (a: E, b: E) => boolean = (a, b) => a === b
) {
return (self: E, index: number, arr: ReadonlyArray<E>) =>
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);
}
+33
View File
@@ -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();
});
});
+25
View File
@@ -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<Coordinate>) =>
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<Coordinate>,
b: Readonly<Coordinate>
): boolean {
return b[0] === a[0] && b[1] === a[1];
}