Add more utils
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+4
-8
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+82
-1
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,3 +90,50 @@ export function groupBy<E, K>(
|
||||
export function getMiddleElement<E>(arr: ReadonlyArray<E>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
Reference in New Issue
Block a user