feat(random/unstable): basic randomization functions (#5626)

Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
lionel-rowe 2024-09-05 13:17:10 +08:00 committed by GitHub
parent 4ac8373230
commit 149839b60c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1058 additions and 0 deletions

View File

@ -64,6 +64,7 @@ type Mod =
| "msgpack" | "msgpack"
| "net" | "net"
| "path" | "path"
| "random"
| "regexp" | "regexp"
| "semver" | "semver"
| "streams" | "streams"
@ -107,6 +108,7 @@ const ENTRYPOINTS: Record<Mod, string[]> = {
msgpack: ["mod.ts"], msgpack: ["mod.ts"],
net: ["mod.ts"], net: ["mod.ts"],
path: ["mod.ts"], path: ["mod.ts"],
random: ["mod.ts"],
regexp: ["mod.ts"], regexp: ["mod.ts"],
semver: ["mod.ts"], semver: ["mod.ts"],
streams: ["mod.ts"], streams: ["mod.ts"],

View File

@ -62,6 +62,7 @@ const ENTRY_POINTS = [
"../path/mod.ts", "../path/mod.ts",
"../path/posix/mod.ts", "../path/posix/mod.ts",
"../path/windows/mod.ts", "../path/windows/mod.ts",
"../random/mod.ts",
"../regexp/mod.ts", "../regexp/mod.ts",
"../semver/mod.ts", "../semver/mod.ts",
"../streams/mod.ts", "../streams/mod.ts",

View File

@ -36,6 +36,7 @@
"./msgpack", "./msgpack",
"./net", "./net",
"./path", "./path",
"./random",
"./regexp", "./regexp",
"./semver", "./semver",
"./streams", "./streams",

View File

@ -77,6 +77,7 @@
"./msgpack", "./msgpack",
"./net", "./net",
"./path", "./path",
"./random",
"./regexp", "./regexp",
"./semver", "./semver",
"./streams", "./streams",

View File

@ -36,6 +36,7 @@
"@std/net": "jsr:@std/net@^1.0.2", "@std/net": "jsr:@std/net@^1.0.2",
"@std/path": "jsr:@std/path@^1.0.4", "@std/path": "jsr:@std/path@^1.0.4",
"@std/regexp": "jsr:@std/regexp@^1.0.0", "@std/regexp": "jsr:@std/regexp@^1.0.0",
"@std/random": "jsr:@std/random@^0.1.0",
"@std/semver": "jsr:@std/semver@^1.0.3", "@std/semver": "jsr:@std/semver@^1.0.3",
"@std/streams": "jsr:@std/streams@^1.0.4", "@std/streams": "jsr:@std/streams@^1.0.4",
"@std/tar": "jsr:@std/tar@^0.1.0", "@std/tar": "jsr:@std/tar@^0.1.0",

100
random/_pcg32.ts Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Based on Rust `rand` crate (https://github.com/rust-random/rand). Apache-2.0 + MIT license.
/** Multiplier for the PCG32 algorithm. */
const MUL: bigint = 6364136223846793005n;
/** Initial increment for the PCG32 algorithm. Only used during seeding. */
const INC: bigint = 11634580027462260723n;
// Constants are for 64-bit state, 32-bit output
const ROTATE = 59n; // 64 - 5
const XSHIFT = 18n; // (5 + 32) / 2
const SPARE = 27n; // 64 - 32 - 5
/**
* Internal state for the PCG32 algorithm.
* `state` prop is mutated by each step, whereas `inc` prop remains constant.
*/
type PcgMutableState = {
state: bigint;
inc: bigint;
};
/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L129-L135
*/
export function fromSeed(seed: Uint8Array) {
const d = new DataView(seed.buffer);
return fromStateIncr(d.getBigUint64(0, true), d.getBigUint64(8, true) | 1n);
}
/**
* Mutates `pcg` by advancing `pcg.state`.
*/
function step(pgc: PcgMutableState) {
pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n));
}
/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L99-L105
*/
function fromStateIncr(state: bigint, inc: bigint): PcgMutableState {
const pcg: PcgMutableState = { state, inc };
// Move away from initial value
pcg.state = BigInt.asUintN(64, state + inc);
step(pcg);
return pcg;
}
/**
* Internal PCG32 implementation, used by both the public seeded random
* function and the seed generation algorithm.
*
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L140-L153
*
* `pcg.state` is internally advanced by the function.
*
* @param pcg The state and increment values to use for the PCG32 algorithm.
* @returns The next pseudo-random 32-bit integer.
*/
export function nextU32(pcg: PcgMutableState): number {
const state = pcg.state;
step(pcg);
// Output function XSH RR: xorshift high (bits), followed by a random rotate
const rot = state >> ROTATE;
const xsh = BigInt.asUintN(32, (state >> XSHIFT ^ state) >> SPARE);
return Number(rotateRightU32(xsh, rot));
}
// `n`, `rot`, and return val are all u32
function rotateRightU32(n: bigint, rot: bigint): bigint {
const left = BigInt.asUintN(32, n << (-rot & 31n));
const right = n >> rot;
return left | right;
}
/**
* Convert a scalar bigint seed to a Uint8Array of the specified length.
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
*/
export function seedFromU64(state: bigint, numBytes: number): Uint8Array {
const seed = new Uint8Array(numBytes);
const pgc: PcgMutableState = { state: BigInt.asUintN(64, state), inc: INC };
// We advance the state first (to get away from the input value,
// in case it has low Hamming Weight).
step(pgc);
for (let i = 0; i < Math.floor(numBytes / 4); ++i) {
new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true);
}
const rem = numBytes % 4;
if (rem) {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, nextU32(pgc), true);
seed.set(bytes.subarray(0, rem), numBytes - rem);
}
return seed;
}

122
random/_pcg32_test.ts Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "../assert/equals.ts";
import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts";
Deno.test("seedFromU64() generates seeds from bigints", async (t) => {
await t.step("first 10 16-bit seeds are same as rand crate", async (t) => {
/**
* Expected results obtained by copying the Rust code from
* https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
* but directly returning `seed` instead of `Self::from_seed(seed)`
*/
// deno-fmt-ignore
const expectedResults = [
[236, 242, 115, 249, 129, 181, 205, 69, 135, 240, 70, 115, 6, 173, 108, 173],
[234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46],
[77, 209, 16, 204, 177, 124, 55, 30, 237, 239, 68, 142, 238, 125, 215, 7],
[108, 90, 247, 27, 160, 186, 6, 71, 76, 124, 221, 142, 87, 133, 92, 175],
[197, 166, 196, 87, 44, 68, 69, 62, 55, 32, 34, 218, 130, 107, 171, 170],
[60, 64, 172, 11, 74, 188, 224, 128, 161, 112, 220, 75, 85, 212, 145, 251],
[177, 93, 150, 16, 48, 3, 23, 51, 155, 104, 76, 121, 82, 134, 239, 107],
[200, 12, 64, 59, 208, 32, 108, 9, 55, 166, 59, 111, 242, 79, 37, 30],
[222, 11, 88, 159, 202, 89, 63, 215, 36, 57, 0, 156, 63, 131, 114, 90],
[21, 119, 90, 241, 241, 191, 180, 229, 150, 199, 126, 251, 25, 141, 7, 4],
];
for (const [i, expected] of expectedResults.entries()) {
await t.step(`With seed ${i}n`, () => {
const actual = Array.from(seedFromU64(BigInt(i), 16));
assertEquals(actual, expected);
});
}
});
await t.step(
"generates arbitrary-length seed data from a single bigint",
async (t) => {
// deno-fmt-ignore
const expectedBytes = [234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46, 186];
for (const i of expectedBytes.keys()) {
const slice = expectedBytes.slice(0, i + 1);
await t.step(`With length ${i + 1}`, () => {
const actual = Array.from(seedFromU64(1n, i + 1));
assertEquals(actual, slice);
});
}
},
);
const U64_CEIL = 2n ** 64n;
await t.step("wraps bigint input to u64", async (t) => {
await t.step("exact multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(BigInt(0n), 16));
const actual = Array.from(seedFromU64(U64_CEIL * 99n, 16));
assertEquals(actual, expected);
});
await t.step("multiple of U64_CEIL + 1", () => {
const expected = Array.from(seedFromU64(1n, 16));
const actual = Array.from(seedFromU64(1n + U64_CEIL * 3n, 16));
assertEquals(actual, expected);
});
await t.step("multiple of U64_CEIL - 1", () => {
const expected = Array.from(seedFromU64(-1n, 16));
const actual = Array.from(seedFromU64(U64_CEIL - 1n, 16));
assertEquals(actual, expected);
});
await t.step("negative multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(0n, 16));
const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16));
assertEquals(actual, expected);
});
await t.step("negative multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(0n, 16));
const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16));
assertEquals(actual, expected);
});
});
});
Deno.test("nextU32() generates random 32-bit integers", async (t) => {
/**
* Expected results obtained from the Rust `rand` crate as follows:
* ```rs
* use rand_pcg::rand_core::{RngCore, SeedableRng};
* use rand_pcg::Lcg64Xsh32;
*
* let mut rng = Lcg64Xsh32::seed_from_u64(0);
* for _ in 0..10 {
* println!("{}", rng.next_u32());
* }
* ```
*/
const expectedResults = [
298703107,
4236525527,
336081875,
1056616254,
1060453275,
1616833669,
501767310,
2864049166,
56572352,
2362354238,
];
const pgc = fromSeed(seedFromU64(0n, 16));
const next = () => nextU32(pgc);
for (const [i, expected] of expectedResults.entries()) {
await t.step(`#${i + 1} generated uint32`, () => {
const actual = next();
assertEquals(actual, expected);
});
}
});

27
random/_types.ts Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
/**
* A pseudo-random number generator implementing the same contract as
* `Math.random`, i.e. taking zero arguments and returning a random number in
* the range `[0, 1)`. The behavior of a function that accepts a `Prng` an
* option may be customized by passing a `Prng` with different behavior from
* `Math.random`, for example it may be seeded.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type Prng = typeof Math.random;
/**
* Options for random number generation.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type RandomOptions = {
/**
* A pseudo-random number generator returning a random number in the range
* `[0, 1)`, used for randomization.
* @default {Math.random}
*/
prng?: Prng;
};

51
random/between.ts Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import type { Prng, RandomOptions } from "./_types.ts";
export type { Prng, RandomOptions };
/**
* Generates a random number between the provided minimum and maximum values.
*
* The number is in the range `[min, max)`, i.e. `min` is included but `max` is excluded.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param min The minimum value (inclusive)
* @param max The maximum value (exclusive)
* @param options The options for the random number generator
* @returns A random number between the provided minimum and maximum values
*
* @example Usage
* ```ts no-assert
* import { randomBetween } from "@std/random";
*
* randomBetween(1, 10); // 6.688009464410508
* randomBetween(1, 10); // 3.6267118101712006
* randomBetween(1, 10); // 7.853320239013774
* ```
*/
export function randomBetween(
min: number,
max: number,
options?: RandomOptions,
): number {
if (!Number.isFinite(min)) {
throw new RangeError(
`Cannot generate a random number: min cannot be ${min}`,
);
}
if (!Number.isFinite(max)) {
throw new RangeError(
`Cannot generate a random number: max cannot be ${max}`,
);
}
if (max < min) {
throw new RangeError(
`Cannot generate a random number as max must be greater than or equal to min: max=${max}, min=${min}`,
);
}
const x = (options?.prng ?? Math.random)();
const y = min * (1 - x) + max * x;
return y >= min && y < max ? y : min;
}

115
random/between_test.ts Normal file
View File

@ -0,0 +1,115 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { randomBetween } from "./between.ts";
import { randomSeeded } from "./seeded.ts";
import {
assert,
assertAlmostEquals,
assertEquals,
assertGreaterOrEqual,
assertLessOrEqual,
assertNotEquals,
assertThrows,
} from "@std/assert";
Deno.test("randomBetween() generates a random number between the provided minimum and maximum values", () => {
const prng = randomSeeded(0n);
const results = Array.from(
{ length: 1e4 },
() => randomBetween(1, 10, { prng }),
);
const min = Math.min(...results);
const max = Math.max(...results);
assertGreaterOrEqual(min, 1);
assertLessOrEqual(max, 10);
assertAlmostEquals(min, 1, 0.01);
assertAlmostEquals(max, 10, 0.01);
const avg = results.reduce((sum, n) => sum + n, 0) / results.length;
assertAlmostEquals(avg, 5.5, 0.1);
});
Deno.test("randomBetween() throws if min or max are NaN", () => {
assertThrows(
() => randomBetween(NaN, 1),
RangeError,
"Cannot generate a random number: min cannot be NaN",
);
assertThrows(
() => randomBetween(1, NaN),
RangeError,
"Cannot generate a random number: max cannot be NaN",
);
});
Deno.test("randomBetween() throws if min or max are +/-Infinity", () => {
assertThrows(
() => randomBetween(-Infinity, 1),
RangeError,
"Cannot generate a random number: min cannot be -Infinity",
);
assertThrows(
() => randomBetween(1, Infinity),
RangeError,
"Cannot generate a random number: max cannot be Infinity",
);
});
Deno.test("randomBetween() throws if max is less than min", () => {
assertThrows(
() => randomBetween(10, 1),
RangeError,
"Cannot generate a random number as max must be greater than or equal to min: max=1, min=10",
);
});
Deno.test("randomBetween() allows negative min and max", () => {
const prng = randomSeeded(0n);
const results = Array.from(
{ length: 3 },
() => randomBetween(-10, -1, { prng }),
);
assertEquals(results, [
-9.374074870022014,
-1.1224633122328669,
-9.295748566510156,
]);
});
Deno.test("randomBetween() allows non-integer min and max", () => {
const prng = randomSeeded(0n);
const results = Array.from(
{ length: 3 },
() => randomBetween(1.5, 2.5, { prng }),
);
assertEquals(results, [
1.5695472366642207,
2.4863929653074592,
1.5782501592766494,
]);
});
Deno.test("randomBetween() allows min and max to be the same, in which case it returns constant values", () => {
const results = Array.from({ length: 3 }, () => randomBetween(9.99, 9.99));
assertEquals(results, [9.99, 9.99, 9.99]);
});
Deno.test("randomBetween() never returns max, even if the prng returns its max value", () => {
const prng = () => 0.9999999999999999;
const result = randomBetween(1, 2, { prng });
assertNotEquals(result, 2);
});
Deno.test("randomBetween() doesn't overflow even for min = -Number.MAX_VALUE; max = Number.MAX_VALUE", () => {
for (const val of [0, 0.5, 0.9999999999999999]) {
const result = randomBetween(
-Number.MAX_VALUE,
Number.MAX_VALUE,
{ prng: () => val! },
);
assert(Number.isFinite(result));
}
});

12
random/deno.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "@std/random",
"version": "0.1.0",
"exports": {
".": "./mod.ts",
"./between": "./between.ts",
"./integer-between": "./integer_between.ts",
"./sample": "./sample.ts",
"./seeded": "./seeded.ts",
"./shuffle": "./shuffle.ts"
}
}

36
random/integer_between.ts Normal file
View File

@ -0,0 +1,36 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import type { Prng, RandomOptions } from "./_types.ts";
import { randomBetween } from "./between.ts";
export type { Prng, RandomOptions };
/**
* Generates a random integer between the provided minimum and maximum values.
*
* The number is in the range `[min, max]`, i.e. both `min` and `max` are included.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param min The minimum value (inclusive)
* @param max The maximum value (inclusive)
* @param options The options for the random number generator
* @returns A random integer between the provided minimum and maximum values
*
* @example Usage
* ```ts no-assert
* import { randomIntegerBetween } from "@std/random";
*
* randomIntegerBetween(1, 10); // 7
* randomIntegerBetween(1, 10); // 9
* randomIntegerBetween(1, 10); // 2
* ```
*/
export function randomIntegerBetween(
min: number,
max: number,
options?: RandomOptions,
): number {
return Math.floor(
randomBetween(Math.ceil(min), Math.floor(max) + 1, options),
);
}

View File

@ -0,0 +1,94 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { randomIntegerBetween } from "./integer_between.ts";
import { randomSeeded } from "./seeded.ts";
import {
assert,
assertAlmostEquals,
assertEquals,
assertThrows,
} from "@std/assert";
Deno.test("randomIntegerBetween() generates a random integer between the provided minimum and maximum values", () => {
const prng = randomSeeded(0n);
const results = Array.from(
{ length: 1e4 },
() => randomIntegerBetween(1, 10, { prng }),
);
assertEquals(Math.min(...results), 1);
assertEquals(Math.max(...results), 10);
for (let i = 1; i <= 10; ++i) {
assert(results.includes(i));
}
const avg = results.reduce((sum, n) => sum + n, 0) / results.length;
assertAlmostEquals(avg, 5.5, 0.1);
});
Deno.test("randomIntegerBetween() throws if min or max are NaN", () => {
assertThrows(
() => randomIntegerBetween(NaN, 1),
RangeError,
"Cannot generate a random number: min cannot be NaN",
);
assertThrows(
() => randomIntegerBetween(1, NaN),
RangeError,
"Cannot generate a random number: max cannot be NaN",
);
});
Deno.test("randomIntegerBetween() throws if min or max are +/-Infinity", () => {
assertThrows(
() => randomIntegerBetween(-Infinity, 1),
RangeError,
"Cannot generate a random number: min cannot be -Infinity",
);
assertThrows(
() => randomIntegerBetween(1, Infinity),
RangeError,
"Cannot generate a random number: max cannot be Infinity",
);
});
Deno.test("randomIntegerBetween() throws if max is less than min", () => {
assertThrows(
() => randomIntegerBetween(10, 1),
RangeError,
"Cannot generate a random number as max must be greater than or equal to min: max=2, min=10",
);
});
Deno.test("randomIntegerBetween() allows negative min and max", () => {
const prng = randomSeeded(1n);
const results = Array.from(
{ length: 3 },
() => randomIntegerBetween(-10, -1, { prng }),
);
assertEquals(results, [-8, -6, -3]);
});
Deno.test(
"randomIntegerBetween() returns evenly-distributed values in [ceil(min), floor(max)] for non-integer min and max",
() => {
const prng = randomSeeded(1n);
const getRand = () => randomIntegerBetween(-0.001, 1.999, { prng });
const length = 1000;
const results = Array.from({ length }, () => getRand());
const { 0: zeroes = [], 1: ones = [] } = Object.values(
Object.groupBy(results, (result) => Math.floor(result)),
);
assertAlmostEquals(zeroes.length, length / 2, length / 10);
assertAlmostEquals(ones.length, length / 2, length / 10);
},
);
Deno.test("randomIntegerBetween() allows min and max to be the same, in which case it returns constant values", () => {
const results = Array.from({ length: 3 }, () => randomIntegerBetween(99, 99));
assertEquals(results, [99, 99, 99]);
});

26
random/mod.ts Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
/**
* Utilities for generating random numbers.
*
* ```ts
* import { randomIntegerBetween } from "@std/random";
* import { randomSeeded } from "@std/random";
* import { assertEquals } from "@std/assert";
*
* const prng = randomSeeded(1n);
*
* assertEquals(randomIntegerBetween(1, 10, { prng }), 3);
* ```
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @module
*/
export * from "./between.ts";
export * from "./integer_between.ts";
export * from "./sample.ts";
export * from "./seeded.ts";
export * from "./shuffle.ts";

97
random/sample.ts Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import type { Prng, RandomOptions } from "./_types.ts";
import { randomIntegerBetween } from "./integer_between.ts";
export type { Prng, RandomOptions };
/**
* Options for {@linkcode sample}.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type SampleOptions = RandomOptions & {
/**
* An array of weights corresponding to each item in the input array.
* If supplied, this is used to determine the probability of each item being
* selected.
*/
weights?: ArrayLike<number>;
};
/**
* Returns a random element from the given array.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam T The type of the elements in the array.
* @typeParam O The type of the accumulator.
*
* @param array The array to sample from.
* @param options Options modifying the sampling behavior.
*
* @returns A random element from the given array, or `undefined` if the array
* is empty.
*
* @example Basic usage
* ```ts
* import { sample } from "@std/random/sample";
* import { assertArrayIncludes } from "@std/assert";
*
* const numbers = [1, 2, 3, 4];
* const sampled = sample(numbers);
*
* assertArrayIncludes(numbers, [sampled]);
* ```
*
* @example Using `weights` option
* ```ts no-assert
* import { sample } from "@std/random/sample";
*
* const values = ["a", "b", "c"];
* const weights = [5, 3, 2];
* const result = sample(values, { weights });
* // gives "a" 50% of the time, "b" 30% of the time, and "c" 20% of the time
* ```
*/
export function sample<T>(
array: ArrayLike<T>,
options?: SampleOptions,
): T | undefined {
const { weights } = { ...options };
if (weights) {
if (weights.length !== array.length) {
throw new RangeError(
"Cannot sample an item: The length of the weights array must match the length of the input array",
);
}
if (!array.length) return undefined;
const total = Object.values(weights).reduce((sum, n) => sum + n, 0);
if (total <= 0) {
throw new RangeError(
"Cannot sample an item: Total weight must be greater than 0",
);
}
const rand = (options?.prng ?? Math.random)() * total;
let current = 0;
for (let i = 0; i < array.length; ++i) {
current += weights[i]!;
if (rand < current) {
return array[i]!;
}
}
// this line should never be hit, but in case of rounding errors etc.
return array[0]!;
}
const length = array.length;
return length
? array[randomIntegerBetween(0, length - 1, options)]
: undefined;
}

152
random/sample_test.ts Normal file
View File

@ -0,0 +1,152 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { sample } from "./sample.ts";
import {
assertAlmostEquals,
assertArrayIncludes,
assertEquals,
assertThrows,
} from "@std/assert";
import { randomSeeded } from "./seeded.ts";
Deno.test({
name: "sample() handles no mutation",
fn() {
const array = ["a", "abc", "ba"];
sample(array);
assertEquals(array, ["a", "abc", "ba"]);
},
});
Deno.test({
name: "sample() returns undefined if the array is empty",
fn() {
const actual = sample([]);
assertEquals(actual, undefined);
},
});
Deno.test({
name: "sample() handles array of numbers",
fn() {
const input = [1, 2, 3];
const actual = sample(input);
assertArrayIncludes(input, [actual]);
},
});
Deno.test({
name: "sample() handles array of objects",
fn() {
const input = [
{
name: "Anna",
age: 18,
},
{
name: "Kim",
age: 24,
},
];
const actual = sample(input);
assertArrayIncludes(input, [actual]);
},
});
Deno.test("sample() picks a random item from the provided items", () => {
const items = ["a", "b", "c"];
const prng = randomSeeded(0n);
const picks = Array.from(
{ length: 10 },
() => sample(items, { prng }),
);
assertEquals(picks, ["a", "c", "a", "a", "a", "b", "a", "c", "a", "b"]);
});
Deno.test("sample() with weights returns undefined if the array is empty", () => {
const items: unknown[] = [];
const weights: number[] = [];
assertEquals(sample(items, { weights }), undefined);
});
Deno.test("sample() with weights throws if the total weight is 0", () => {
const items = ["a", "b", "c"];
const weights = [0, 0, 0];
assertThrows(
() => sample(items, { weights }),
RangeError,
"Total weight must be greater than 0",
);
});
Deno.test("sample() with weights throws if the wrong number of weights is provided", () => {
const items = ["a", "b", "c"] as const;
const weights = [1, 2, 3, 4] as const;
assertThrows(
() => sample(items, { weights }),
RangeError,
"The length of the weights array must match the length of the input array",
);
});
Deno.test("sample() works with typed arrays", () => {
const items = new Uint8Array([0, 1, 2]);
const weights = new Uint8Array([10, 5, 250]);
const prng = randomSeeded(1n);
assertEquals(sample(items, { weights, prng }), 2);
});
Deno.test("sample() with weights never picks an item with weight of 0", () => {
const items = ["a", "b"];
const weights = [1, 0];
assertEquals(sample(items, { weights }), "a");
});
Deno.test("sample() with weights picks a random item from the provided items with a weighted probability", () => {
const weightedItems = [["a", 1], ["b", 2], ["c", 3]] as const;
const weights = weightedItems.map(([, weight]) => weight);
const values = weightedItems.map(([value]) => value);
const totalWeight = weights.reduce((sum, n) => sum + n, 0);
const prng = randomSeeded(0n);
const picks = Array.from(
{ length: 1000 },
() => sample(values, { prng, weights }),
);
const groups = Object.values(
Object.groupBy(picks, (item) => item!),
) as string[][];
assertEquals(groups.length, 3);
for (const group of groups) {
const [, weight] = weightedItems.find(([item]) => item === group[0]!)!;
assertAlmostEquals(
group.length,
picks.length / totalWeight * weight,
picks.length / 10,
);
}
});
Deno.test("sample() with weights works with a Map", () => {
const weightedItems = new Map([["a", 1], ["b", 2], ["c", 999]]);
const prng = randomSeeded(0n);
const result = sample([...weightedItems.keys()], {
weights: [...weightedItems.values()],
prng,
});
assertEquals(result, "c");
});

41
random/seeded.ts Normal file
View File

@ -0,0 +1,41 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts";
import type { Prng } from "./_types.ts";
/**
* Creates a pseudo-random number generator that generates random numbers in
* the range `[0, 1)`, based on the given seed. The algorithm used for
* generation is {@link https://www.pcg-random.org/download.html | PCG32}.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param seed The seed used to initialize the random number generator's state.
* @returns A pseudo-random number generator function, which will generate
* different random numbers on each call.
*
* @example Usage
* ```ts
* import { randomSeeded } from "@std/random";
* import { assertEquals } from "@std/assert";
*
* const prng = randomSeeded(1n);
*
* assertEquals(prng(), 0.20176767697557807);
* assertEquals(prng(), 0.4911644416861236);
* assertEquals(prng(), 0.7924694607499987);
* ```
*/
export function randomSeeded(seed: bigint): Prng {
const pcg = fromSeed(seedFromU64(seed, 16));
return () => uint32ToFloat64(nextU32(pcg));
}
/**
* Convert a 32-bit unsigned integer to a float64 in the range `[0, 1)`.
* This operation is lossless, i.e. it's always possible to get the original
* value back by multiplying by 2 ** 32.
*/
function uint32ToFloat64(u32: number): number {
return u32 / 2 ** 32;
}

63
random/seeded_test.ts Normal file
View File

@ -0,0 +1,63 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { randomSeeded } from "./seeded.ts";
import { assertAlmostEquals, assertEquals } from "@std/assert";
Deno.test("randomSeeded() generates random numbers", () => {
const prng = randomSeeded(1n);
assertEquals(prng(), 0.20176767697557807);
assertEquals(prng(), 0.4911644416861236);
assertEquals(prng(), 0.7924694607499987);
});
Deno.test("randomSeeded() gives relatively uniform distribution of random numbers", async (t) => {
const prng = randomSeeded(1n);
const results = Array.from({ length: 1e4 }, prng);
await t.step("all results are in [0, 1)", () => {
assertEquals(results.every((result) => result >= 0 && result < 1), true);
});
await t.step("all (or almost all) results are unique", () => {
assertAlmostEquals(new Set(results).size, results.length, 10);
});
await t.step("the mean average of the results is close to 0.5", () => {
const avg = results.reduce((sum, n) => sum + n, 0) / results.length;
assertAlmostEquals(avg, 0.5, 0.05);
});
await t.step(
"approximately one tenth of the results lie in each decile",
() => {
const deciles = Object.values(
Object.groupBy(results, (result) => Math.floor(result * 10)),
) as number[][];
assertEquals(deciles.length, 10);
for (const decile of deciles) {
assertAlmostEquals(
decile.length,
results.length / 10,
results.length / 50,
);
}
},
);
await t.step(
"the mean average of each thousand results is close to 0.5",
() => {
const slices = Object.values(
Object.groupBy(results, (_, idx) => Math.floor(idx / 1000)),
) as number[][];
for (const results of slices) {
const average = results.reduce((sum, result) => sum + result, 0) /
results.length;
assertAlmostEquals(average, 0.5, 0.05);
}
},
);
});

47
random/shuffle.ts Normal file
View File

@ -0,0 +1,47 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
import type { Prng, RandomOptions } from "./_types.ts";
import { randomIntegerBetween } from "./integer_between.ts";
export type { Prng, RandomOptions };
/**
* Shuffles the provided array, returning a copy and without modifying the original array.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam T The type of the items in the array
* @param items The items to shuffle
* @param options The options for the random number generator
* @returns A shuffled copy of the provided items
*
* @example Usage
* ```ts no-assert
* import { shuffle } from "@std/random";
*
* const items = [1, 2, 3, 4, 5];
*
* shuffle(items); // [2, 5, 1, 4, 3]
* shuffle(items); // [3, 4, 5, 1, 2]
* shuffle(items); // [5, 2, 4, 3, 1]
*
* items; // [1, 2, 3, 4, 5] (original array is unchanged)
* ```
*/
export function shuffle<T>(
items: readonly T[],
options?: RandomOptions,
): T[] {
const result = [...items];
// https://en.wikipedia.org/wiki/FisherYates_shuffle#The_modern_algorithm
// -- To shuffle an array a of n elements (indices 0..n-1):
// for i from n1 down to 1 do
for (let i = result.length - 1; i >= 1; --i) {
// j ← random integer such that 0 ≤ j ≤ i
const j = randomIntegerBetween(0, i, options);
// exchange a[j] and a[i]
[result[i], result[j]] = [result[j]!, result[i]!];
}
return result;
}

69
random/shuffle_test.ts Normal file
View File

@ -0,0 +1,69 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { randomSeeded } from "./seeded.ts";
import { shuffle } from "./shuffle.ts";
import {
assertAlmostEquals,
assertEquals,
assertNotStrictEquals,
} from "@std/assert";
Deno.test("shuffle() handles empty arrays", () => {
const array: number[] = [];
const shuffled = shuffle(array);
assertEquals(shuffled, array);
assertNotStrictEquals(shuffled, array);
});
Deno.test("shuffle() handles arrays with only one item", () => {
const array = [1];
const shuffled = shuffle(array);
assertEquals(shuffled, array);
assertNotStrictEquals(shuffled, array);
});
Deno.test("shuffle() shuffles the provided array", () => {
const prng = randomSeeded(0n);
const items = [1, 2, 3, 4, 5];
assertEquals(shuffle(items, { prng }), [2, 3, 5, 4, 1]);
assertEquals(shuffle(items, { prng }), [3, 4, 1, 5, 2]);
assertEquals(shuffle(items, { prng }), [2, 4, 5, 3, 1]);
});
Deno.test("shuffle() returns a copy and without modifying the original array", () => {
const items = [1, 2, 3, 4, 5];
const originalItems = [...items];
for (let i = 0; i < 10; ++i) {
shuffle(items);
assertEquals(items, originalItems);
}
});
Deno.test("shuffle() gives relatively uniform distribution of results", () => {
const prng = randomSeeded(0n);
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const results = Array.from(
{ length: 1e3 },
() => shuffle(items, { prng }),
);
for (const idx of items.keys()) {
const groupsByidx = Object.values(
Object.groupBy(results.map((result) => result[idx]), (item) => item!),
) as number[][];
for (const group of groupsByidx) {
assertAlmostEquals(
group.length,
results.length / items.length,
results.length / 10,
);
}
}
});