import { BitArray } from "../bitarray/index.js"; import { asArray } from "../utils.js"; import { AsyncRandomOptions, ElementPredicate, ElementWeight, RandomGenerator, RandomOptions } from "./types.js"; export const alwaysTrue: ElementPredicate = () => true; export const weightOfOne: ElementWeight = () => 1.0; export const mathRandom: RandomGenerator = () => Math.random(); const defaultOptions = Object.freeze>({ predicate: alwaysTrue, weight: weightOfOne, random: mathRandom }); function mergeOptions(first: RandomOptions | undefined, second: RandomOptions | undefined): RandomOptions | undefined { if (!first) { return second; } if (!second) { return first; } const firstPredicate = first.predicate; const secondPredicate = second.predicate; return { predicate: firstPredicate ? secondPredicate ? (i, o) => firstPredicate(i, o) && secondPredicate(i, o) : firstPredicate : secondPredicate, weight: first.weight ?? second.weight, random: first.random ?? second.random }; } function withDefaultOptions(options: RandomOptions | undefined): Required>; function withDefaultOptions(options: AsyncRandomOptions | undefined): Required>; function withDefaultOptions(options: RandomOptions | AsyncRandomOptions | undefined): Required | AsyncRandomOptions> { if (!options || options === defaultOptions) { return defaultOptions; } return { predicate: options.predicate ?? defaultOptions.predicate, weight: options.weight ?? defaultOptions.weight, random: options.random ?? defaultOptions.random }; } function _getRandomElement(sequence: Iterable, options: Required>) { const { predicate, weight, random } = options; let result: T | undefined = undefined; let resultIndex = -1; let index = 0; let weightAcc = 0.0; for (const element of sequence) { const currentIndex = index++; if (predicate(currentIndex, element)) { const w = weight(currentIndex, element); if (w <= 0.0) { continue; } weightAcc += w; if (random() * weightAcc < w) { result = element; resultIndex = currentIndex; } } } return { element: result, index: resultIndex }; } export function getRandomElement(sequence: Iterable, options?: RandomOptions) { return _getRandomElement(sequence, withDefaultOptions(options)); } async function _getRandomElementAsync(sequence: AsyncIterable, options: Required>) { const { predicate, weight, random } = options; let result: T | undefined = undefined; let resultIndex = -1; let index = 0; let weightAcc = 0.0; for await (const element of sequence) { const currentIndex = index++; if (await predicate(currentIndex, element)) { const w = await weight(currentIndex, element); if (w <= 0.0) { continue; } weightAcc += w; if (random() * weightAcc < w) { result = element; resultIndex = currentIndex; } } } return { element: result, index: resultIndex }; } export async function getRandomElementAsync(sequence: AsyncIterable, options?: AsyncRandomOptions) { return await _getRandomElementAsync(sequence, withDefaultOptions(options)); } export class RandomPicker { readonly #elements: Iterable; readonly #flags: BitArray; readonly #options: Required>; public constructor(elements: Iterable, length: number, options?: RandomOptions) { this.#elements = elements; this.#flags = BitArray.create(length); this.#options = withDefaultOptions(mergeOptions({ predicate: i => this.#flags.get(i) }, options)); this.reset(); } public static from(iterable: Iterable, options?: RandomOptions) { const arr = asArray(iterable); return new this(arr, arr.length, options); } public static of(options?: RandomOptions, ...values: T[]) { return new this(values, values.length, options); } public get state() { return this.#flags; } public reset() { this.#flags.fill(true); } public next() { const result = _getRandomElement(this.#elements, this.#options); if (result.index < 0) { this.reset(); } else { this.#flags.set(result.index, false); if (this.#flags.isEmpty()) { this.reset(); } } return result.element; } }