mirror of
https://github.com/denoland/std.git
synced 2024-11-21 12:40:03 +00:00
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:
parent
4ac8373230
commit
149839b60c
@ -64,6 +64,7 @@ type Mod =
|
||||
| "msgpack"
|
||||
| "net"
|
||||
| "path"
|
||||
| "random"
|
||||
| "regexp"
|
||||
| "semver"
|
||||
| "streams"
|
||||
@ -107,6 +108,7 @@ const ENTRYPOINTS: Record<Mod, string[]> = {
|
||||
msgpack: ["mod.ts"],
|
||||
net: ["mod.ts"],
|
||||
path: ["mod.ts"],
|
||||
random: ["mod.ts"],
|
||||
regexp: ["mod.ts"],
|
||||
semver: ["mod.ts"],
|
||||
streams: ["mod.ts"],
|
||||
|
@ -62,6 +62,7 @@ const ENTRY_POINTS = [
|
||||
"../path/mod.ts",
|
||||
"../path/posix/mod.ts",
|
||||
"../path/windows/mod.ts",
|
||||
"../random/mod.ts",
|
||||
"../regexp/mod.ts",
|
||||
"../semver/mod.ts",
|
||||
"../streams/mod.ts",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"./msgpack",
|
||||
"./net",
|
||||
"./path",
|
||||
"./random",
|
||||
"./regexp",
|
||||
"./semver",
|
||||
"./streams",
|
||||
|
@ -77,6 +77,7 @@
|
||||
"./msgpack",
|
||||
"./net",
|
||||
"./path",
|
||||
"./random",
|
||||
"./regexp",
|
||||
"./semver",
|
||||
"./streams",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"@std/net": "jsr:@std/net@^1.0.2",
|
||||
"@std/path": "jsr:@std/path@^1.0.4",
|
||||
"@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/streams": "jsr:@std/streams@^1.0.4",
|
||||
"@std/tar": "jsr:@std/tar@^0.1.0",
|
||||
|
100
random/_pcg32.ts
Normal file
100
random/_pcg32.ts
Normal 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
122
random/_pcg32_test.ts
Normal 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
27
random/_types.ts
Normal 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
51
random/between.ts
Normal 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
115
random/between_test.ts
Normal 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
12
random/deno.json
Normal 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
36
random/integer_between.ts
Normal 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),
|
||||
);
|
||||
}
|
94
random/integer_between_test.ts
Normal file
94
random/integer_between_test.ts
Normal 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
26
random/mod.ts
Normal 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
97
random/sample.ts
Normal 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
152
random/sample_test.ts
Normal 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
41
random/seeded.ts
Normal 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
63
random/seeded_test.ts
Normal 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
47
random/shuffle.ts
Normal 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/Fisher–Yates_shuffle#The_modern_algorithm
|
||||
// -- To shuffle an array a of n elements (indices 0..n-1):
|
||||
// for i from n−1 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
69
random/shuffle_test.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user