170 lines
4.9 KiB
TypeScript
170 lines
4.9 KiB
TypeScript
import { sumBy } from './array.ts';
|
|
|
|
export const Sides = {
|
|
TOP: 0,
|
|
RIGHT: 1,
|
|
DOWN: 2,
|
|
LEFT: 3
|
|
} as const;
|
|
|
|
export type Side = (typeof Sides)[keyof typeof Sides];
|
|
|
|
export const CoordinateIndices = {
|
|
x: 0,
|
|
y: 1,
|
|
side: 2
|
|
} as const;
|
|
|
|
export type CoordinateIndex =
|
|
(typeof CoordinateIndices)[keyof typeof CoordinateIndices];
|
|
|
|
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).
|
|
* @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 | CoordinateSide>) =>
|
|
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 | CoordinateSide>,
|
|
b: Readonly<Coordinate | CoordinateSide>
|
|
): boolean {
|
|
return b[0] === a[0] && b[1] === a[1];
|
|
}
|
|
|
|
/**
|
|
* Check if a xy-coordinate has integer coefficients.
|
|
* @param coordinate Coordinate
|
|
*/
|
|
export function isInteger(coordinate: Readonly<Coordinate | CoordinateSide>) {
|
|
return coordinate[0] % 1 === 0 && coordinate[1] % 1 === 0;
|
|
}
|
|
|
|
export type DirectNeighbours = 'top' | 'left' | 'right' | 'down';
|
|
|
|
export type DiagonalNeighbours =
|
|
| 'topLeft'
|
|
| 'topRight'
|
|
| 'downLeft'
|
|
| 'downRight';
|
|
|
|
/**
|
|
* Get all eight adjacent cells from a starting cell.
|
|
* Returns undefined, when the cell is invalid (out of bounds).
|
|
* @param rows Input xy-Matrix, string array
|
|
* @param startX Center cell x Coordinate
|
|
* @param startY Center cell y Coordinate
|
|
*/
|
|
export function getAdjacent(
|
|
rows: string[],
|
|
startX: number,
|
|
startY: number
|
|
): Record<DirectNeighbours | DiagonalNeighbours, string | undefined> {
|
|
const top = rows[startY - 1]?.[startX];
|
|
const right = rows[startY]?.[startX + 1];
|
|
const down = rows[startY + 1]?.[startX];
|
|
const left = rows[startY]?.[startX - 1];
|
|
|
|
const topLeft = rows[startY - 1]?.[startX - 1];
|
|
const topRight = rows[startY - 1]?.[startX + 1];
|
|
const downLeft = rows[startY + 1]?.[startX - 1];
|
|
const downRight = rows[startY + 1]?.[startX + 1];
|
|
|
|
return { top, right, left, down, topLeft, topRight, downLeft, downRight };
|
|
}
|
|
|
|
/**
|
|
* Find all xy-Coordinates of a digit or custom regex match.
|
|
* @param rows Input xy-Matrix, string array
|
|
* @param digit Digit to search for, or custom regex pattern
|
|
*/
|
|
export function findCoordsOfDigit(
|
|
rows: string[],
|
|
digit: string | RegExp
|
|
): Coordinates {
|
|
return rows.reduce<Coordinates>(
|
|
(coords, row, y) =>
|
|
row
|
|
.matchAll(typeof digit === 'string' ? new RegExp(digit, 'g') : digit)
|
|
.reduce(
|
|
(coordinates, m) => [...coordinates, [m.index, y] as Coordinate],
|
|
coords
|
|
),
|
|
[]
|
|
);
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Calculate the variance (difference from the average) of one dimension from coordinates.
|
|
* @param coords Array of Coordinates
|
|
* @param index `CoordinateIndices.x` for x, `CoordinateIndices.y` for y, `CoordinateIndices.side` for side
|
|
*/
|
|
export function calculateVariance<C extends Coordinate | CoordinateSide>(
|
|
coords: C[],
|
|
index: C extends Coordinate ? 0 | 1 : CoordinateIndex
|
|
) {
|
|
const mean =
|
|
sumBy(coords as CoordinateSides, (c) => c[index]) / coords.length;
|
|
return (
|
|
sumBy(coords as CoordinateSides, (c) => Math.pow(c[index] - mean, 2)) /
|
|
coords.length
|
|
);
|
|
}
|