UNPKG

@byloth/core

Version:

An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧

915 lines (845 loc) • 27 kB
import { ValueException } from "../models/index.js"; /** * A wrapper class around the native {@link Math.random} function that * provides a set of methods to generate random values more easily. * It can be used to generate random numbers, booleans and other different values. * * The class exposes two coexisting surfaces: * - A **static API** (`Random.Integer`, `Random.Boolean`, …) that uses * {@link Math.random} and is therefore non-deterministic. * - An **instance API** with the same method names that uses a seeded PRNG * (Mulberry32) for reproducible sequences. Instances are created via the * {@link Random.FromSeed} factory; the constructor is private. */ export default class Random { static #Mulberry32(seed: number): () => number { let state = seed | 0; return () => { state = (state + 0x6D2B79F5) | 0; let t = state; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } private static _Boolean(random: () => number, ratio: number): boolean { return (random() < ratio); } private static _Integer(random: () => number, min: number, max?: number): number { if (max === undefined) { return Math.floor(random() * min); } return Math.floor(random() * (max - min) + min); } private static _Decimal(random: () => number, min?: number, max?: number): number { if (min === undefined) { return random(); } if (max === undefined) { return (random() * min); } return (random() * (max - min) + min); } private static _Index<T>(random: () => number, elements: readonly T[]): number { if (elements.length === 0) { throw new ValueException("You must provide at least one element."); } return Random._Integer(random, elements.length); } private static _Choice<T>(random: () => number, elements: readonly T[]): T { return elements[Random._Index(random, elements)]; } private static _Sample<T>( random: () => number, elements: readonly T[], count: number, weights?: readonly number[] ): T[] { const length = elements.length; if (length === 0) { throw new ValueException("You must provide at least one element."); } if (count < 0) { throw new ValueException("Count must be non-negative."); } if (count > length) { throw new ValueException("Count cannot exceed the number of elements."); } if (count === 0) { return []; } if (weights === undefined) { const pool = Array.from(elements); const result: T[] = new Array(count); for (let index = 0; index < count; index += 1) { const randomIndex = Random._Integer(random, index, length); result[index] = pool[randomIndex]; pool[randomIndex] = pool[index]; } return result; } if (weights.length !== length) { throw new ValueException("Weights array must have the same length as elements array."); } const keys: ({ index: number, key: number })[] = new Array(length); for (let index = 0; index < length; index += 1) { if (weights[index] <= 0) { throw new ValueException(`Weight for element #${index} must be greater than zero.`); } keys[index] = { index: index, key: Math.pow(random(), 1 / weights[index]) }; } keys.sort((a, b) => b.key - a.key); const result: T[] = new Array(count); for (let index = 0; index < count; index += 1) { result[index] = elements[keys[index].index]; } return result; } static #Split(random: () => number, total: number, parts: number): number[] { const cuts: number[] = new Array(parts - 1); for (let index = 0; index < cuts.length; index += 1) { cuts[index] = random() * total; } cuts.sort((a, b) => (a - b)); const boundaries = [0, ...cuts, total]; const values: number[] = new Array(parts); for (let index = 0; index < parts; index += 1) { values[index] = Math.floor(boundaries[index + 1] - boundaries[index]); } let remainder = total - values.reduce((sum, val) => (sum + val), 0); while (remainder > 0) { values[Random._Integer(random, parts)] += 1; remainder -= 1; } return values; } private static _Split<T>( random: () => number, totalOrElements: number | Iterable<T>, parts: number ): number[] | T[][] { if (parts < 1) { throw new ValueException("The number of splits must be greater than zero."); } if (typeof totalOrElements === "number") { if (totalOrElements < 0) { throw new ValueException("The total must be a non-negative number."); } return Random.#Split(random, totalOrElements, parts); } const elements = Array.from(totalOrElements); const length = elements.length; if (length === 0) { throw new ValueException("You must provide at least one element."); } if (parts > length) { throw new ValueException("The number of splits cannot exceed the number of elements."); } const sizes = Random.#Split(random, length, parts); const groups: T[][] = new Array(parts); let offset = 0; for (let index = 0; index < parts; index += 1) { groups[index] = elements.slice(offset, offset + sizes[index]); offset += sizes[index]; } return groups; } /** * Generates a random boolean value. * See also {@link Random.boolean} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * if (Random.Boolean()) * { * // Do something... * } * ``` * * --- * * @param ratio * The probability of generating `true`. * * It must be included between `0` and `1`. Default is `0.5`. * * @returns A random boolean value. */ public static Boolean(ratio = 0.5): boolean { return Random._Boolean(Math.random, ratio); } /** * Generates a random integer value between `0` (included) and `max` (excluded). * See also {@link Random.integer} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * Random.Integer(5); // [0, 5) * ``` * * --- * * @param max The maximum value (excluded). * * @returns A random integer value. */ public static Integer(max: number): number; /** * Generates a random integer value between `min` (included) and `max` (excluded). * See also {@link Random.integer} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * Random.Integer(2, 7); // [2, 7) * ``` * * --- * * @param min The minimum value (included). * @param max The maximum value (excluded). * * @returns A random integer value. */ public static Integer(min: number, max: number): number; public static Integer(min: number, max?: number): number { return Random._Integer(Math.random, min, max); } /** * Generates a random decimal value between `0` (included) and `1` (excluded). * See also {@link Random.decimal} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * Random.Decimal(); // e.g. 0.123456789 * ``` * * --- * * @returns A random decimal value. */ public static Decimal(): number; /** * Generates a random decimal value between `0` (included) and `max` (excluded). * See also {@link Random.decimal} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * Random.Decimal(5); // e.g. 2.3456789 * ``` * * --- * * @param max The maximum value (excluded). * * @returns A random decimal value. */ public static Decimal(max: number): number; /** * Generates a random decimal value between `min` (included) and `max` (excluded). * See also {@link Random.decimal} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * Random.Decimal(2, 7); // e.g. 4.56789 * ``` * * --- * * @param min The minimum value (included). * @param max The maximum value (excluded). * * @returns A random decimal value. */ public static Decimal(min: number, max: number): number; public static Decimal(min?: number, max?: number): number { return Random._Decimal(Math.random, min, max); } /** * Picks a random valid index from a given array of elements. * See also {@link Random.index} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * const elements = ["a", "b", "c"]; * * Random.Index(elements); // 0, 1, or 2 * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to pick from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @returns A valid random index from the given array. */ public static Index<T>(elements: readonly T[]): number { return Random._Index(Math.random, elements); } /** * Picks a random element from a given array of elements. * See also {@link Random.choice} for the seeded & deterministic counterpart. * * --- * * @example * ```ts * const elements = ["a", "b", "c"]; * * Random.Choice(elements); // "a", "b", or "c" * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to pick from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @returns A random element from the given array. */ public static Choice<T>(elements: readonly T[]): T { return Random._Choice(Math.random, elements); } /** * Picks a random sample of elements from a given array without replacement. * See also {@link Random.sample} for the seeded & deterministic counterpart. * * Uses the Fisher-Yates shuffle algorithm for uniform sampling, * which is O(count) instead of O(n log n) for a full shuffle. * * --- * * @example * ```ts * Random.Sample([1, 2, 3, 4, 5], 3); // e.g. [4, 1, 5] * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to sample from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param count * The number of elements to sample. * * It must be between `0` and `elements.length`. Otherwise, a {@link ValueException} will be thrown. * * @returns An array containing the randomly sampled elements. */ public static Sample<T>(elements: readonly T[], count: number): T[]; /** * Picks a weighted random sample of elements from a given array without replacement. * See also {@link Random.sample} for the seeded & deterministic counterpart. * * Uses the Efraimidis-Spirakis algorithm for weighted sampling. * Elements with higher weights have a higher probability of being selected. * * --- * * @example * ```ts * // Element "a" is 3x more likely to be picked than "b" or "c" * Random.Sample(["a", "b", "c"], 2, [3, 1, 1]); // e.g. ["a", "c"] * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to sample from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param count * The number of elements to sample. * * It must be between `0` and `elements.length`. Otherwise, a {@link ValueException} will be thrown. * * @param weights * The weights associated with each element. * * It must have the same length as the elements array. * All weights must be greater than zero. Otherwise, a {@link ValueException} will be thrown. * * @returns An array containing the randomly sampled elements. */ public static Sample<T>(elements: readonly T[], count: number, weights: readonly number[]): T[]; public static Sample<T>(elements: readonly T[], count: number, weights?: readonly number[]): T[] { return Random._Sample(Math.random, elements, count, weights); } /** * Splits a total amount into a given number of randomly balanced integer parts that sum to the total. * See also {@link Random.split} for the seeded & deterministic counterpart. * * Uses random cut-points to generate a uniform distribution of parts. * * --- * * @example * ```ts * Random.Split(100, 3); // e.g. [28, 41, 31] * Random.Split(10, 4); // e.g. [3, 1, 4, 2] * ``` * * --- * * @param total * The total amount to split. * * It must be non-negative. Otherwise, a {@link ValueException} will be thrown. * * @param parts * The number of parts to split the total into. * * It must be at least `1`. Otherwise, a {@link ValueException} will be thrown. * * @returns An array of integers that sum to the given total. */ public static Split(total: number, parts: number): number[]; /** * Splits an iterable of elements into a given number of randomly balanced groups. * See also {@link Random.split} for the seeded & deterministic counterpart. * * The elements are distributed into groups whose sizes are * determined by a random split of the total number of elements. * * --- * * @example * ```ts * Random.Split([1, 2, 3, 4, 5], 2); // e.g. [[1, 2], [3, 4, 5]] * Random.Split([1, 2, 3, 4, 5], 2); // e.g. [[1, 2, 3, 4], [5]] * Random.Split("abcdef", 3); // e.g. [["a"], ["b", "c", "d"], ["e", "f"]] * ``` * * --- * * @template T The type of the elements in the iterable. * * @param elements * The iterable of elements to split into groups. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param groups * The number of groups to split the elements into. * * It must be between `1` and the number of elements. * Otherwise, a {@link ValueException} will be thrown. * * @returns An array of arrays, each containing a subset of the original elements. */ public static Split<T>(elements: Iterable<T>, groups: number): T[][]; public static Split<T>(totalOrElements: number | Iterable<T>, parts: number): number[] | T[][] { return Random._Split(Math.random, totalOrElements, parts); } /** * Creates a new seedable {@link Random} generator instance. * * The returned instance exposes the same API as the static {@link Random} class, * but produces deterministic sequences driven by the given seed. * Two instances built with the same seed will emit the same values in the same order. * * Internally, values are produced by a Mulberry32 PRNG. * * --- * * @example * ```ts * const rng = Random.FromSeed(42); // deterministic — same seed, same sequence * * rng.integer(100); // 60 * rng.decimal(); // 0.44829055899754167 * ``` * * --- * * @param seed The 32-bit integer seed used to initialize the generator. * * @returns A new {@link Random} instance bound to the given seed. */ public static FromSeed(seed: number): Random { return new Random(seed); } private readonly _next: () => number; private constructor(seed: number) { this._next = Random.#Mulberry32(seed); } /** * Generates a random boolean value. * See also {@link Random.Boolean} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * if (rng.boolean()) * { * // Do something... * } * ``` * * --- * * @param ratio * The probability of generating `true`. * * It must be included between `0` and `1`. Default is `0.5`. * * @returns A random boolean value. */ public boolean(ratio = 0.5): boolean { return Random._Boolean(this._next, ratio); } /** * Generates a random integer value between `0` (included) and `max` (excluded). * See also {@link Random.Integer} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.integer(5); // [0, 5) * ``` * * --- * * @param max The maximum value (excluded). * * @returns A random integer value. */ public integer(max: number): number; /** * Generates a random integer value between `min` (included) and `max` (excluded). * See also {@link Random.Integer} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.integer(2, 7); // [2, 7) * ``` * * --- * * @param min The minimum value (included). * @param max The maximum value (excluded). * * @returns A random integer value. */ public integer(min: number, max: number): number; public integer(min: number, max?: number): number { return Random._Integer(this._next, min, max); } /** * Generates a random decimal value between `0` (included) and `1` (excluded). * See also {@link Random.Decimal} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.decimal(); // e.g. 0.123456789 * ``` * * --- * * @returns A random decimal value. */ public decimal(): number; /** * Generates a random decimal value between `0` (included) and `max` (excluded). * See also {@link Random.Decimal} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.decimal(5); // e.g. 2.3456789 * ``` * * --- * * @param max The maximum value (excluded). * * @returns A random decimal value. */ public decimal(max: number): number; /** * Generates a random decimal value between `min` (included) and `max` (excluded). * See also {@link Random.Decimal} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.decimal(2, 7); // e.g. 4.56789 * ``` * * --- * * @param min The minimum value (included). * @param max The maximum value (excluded). * * @returns A random decimal value. */ public decimal(min: number, max: number): number; public decimal(min?: number, max?: number): number { return Random._Decimal(this._next, min, max); } /** * Picks a random valid index from a given array of elements. * See also {@link Random.Index} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.index(["a", "b", "c"]); // 0, 1, or 2 * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to pick from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @returns A valid random index from the given array. */ public index<T>(elements: readonly T[]): number { return Random._Index(this._next, elements); } /** * Picks a random element from a given array of elements. * See also {@link Random.Choice} for the static & non-deterministic counterpart. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.choice(["a", "b", "c"]); // "a", "b", or "c" * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to pick from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @returns A random element from the given array. */ public choice<T>(elements: readonly T[]): T { return Random._Choice(this._next, elements); } /** * Picks a random sample of elements from a given array without replacement. * See also {@link Random.Sample} for the static & non-deterministic counterpart. * * Uses the Fisher-Yates shuffle algorithm for uniform sampling, * which is O(count) instead of O(n log n) for a full shuffle. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.sample([1, 2, 3, 4, 5], 3); // e.g. [4, 1, 5] * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to sample from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param count * The number of elements to sample. * * It must be between `0` and `elements.length`. Otherwise, a {@link ValueException} will be thrown. * * @returns An array containing the randomly sampled elements. */ public sample<T>(elements: readonly T[], count: number): T[]; /** * Picks a weighted random sample of elements from a given array without replacement. * See also {@link Random.Sample} for the static & non-deterministic counterpart. * * Uses the Efraimidis-Spirakis algorithm for weighted sampling. * Elements with higher weights have a higher probability of being selected. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * // Element "a" is 3x more likely to be picked than "b" or "c" * rng.sample(["a", "b", "c"], 2, [3, 1, 1]); // e.g. ["a", "c"] * ``` * * --- * * @template T The type of the elements in the array. * * @param elements * The array of elements to sample from. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param count * The number of elements to sample. * * It must be between `0` and `elements.length`. Otherwise, a {@link ValueException} will be thrown. * * @param weights * The weights associated with each element. * * It must have the same length as the elements array. * All weights must be greater than zero. Otherwise, a {@link ValueException} will be thrown. * * @returns An array containing the randomly sampled elements. */ public sample<T>(elements: readonly T[], count: number, weights: readonly number[]): T[]; public sample<T>(elements: readonly T[], count: number, weights?: readonly number[]): T[] { return Random._Sample(this._next, elements, count, weights); } /** * Splits a total amount into a given number of randomly balanced integer parts that sum to the total. * See also {@link Random.Split} for the static & non-deterministic counterpart. * * Uses random cut-points to generate a uniform distribution of parts. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.split(100, 3); // e.g. [28, 41, 31] * rng.split(10, 4); // e.g. [3, 1, 4, 2] * ``` * * --- * * @param total * The total amount to split. * * It must be non-negative. Otherwise, a {@link ValueException} will be thrown. * * @param parts * The number of parts to split the total into. * * It must be at least `1`. Otherwise, a {@link ValueException} will be thrown. * * @returns An array of integers that sum to the given total. */ public split(total: number, parts: number): number[]; /** * Splits an iterable of elements into a given number of randomly balanced groups. * See also {@link Random.Split} for the static & non-deterministic counterpart. * * The elements are distributed into groups whose sizes are * determined by a random split of the total number of elements. * * --- * * @example * ```ts * const rng = Random.FromSeed(...); * * rng.split([1, 2, 3, 4, 5], 2); // e.g. [[1, 2], [3, 4, 5]] * rng.split([1, 2, 3, 4, 5], 2); // e.g. [[1, 2, 3, 4], [5]] * rng.split("abcdef", 3); // e.g. [["a"], ["b", "c", "d"], ["e", "f"]] * ``` * * --- * * @template T The type of the elements in the iterable. * * @param elements * The iterable of elements to split into groups. * * It must contain at least one element. Otherwise, a {@link ValueException} will be thrown. * * @param groups * The number of groups to split the elements into. * * It must be between `1` and the number of elements. * Otherwise, a {@link ValueException} will be thrown. * * @returns An array of arrays, each containing a subset of the original elements. */ public split<T>(elements: Iterable<T>, groups: number): T[][]; public split<T>(totalOrElements: number | Iterable<T>, parts: number): number[] | T[][] { return Random._Split(this._next, totalOrElements, parts); } public readonly [Symbol.toStringTag]: string = "Random"; }