forked from denoland/std
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(random/unstable): basic randomization functions (denoland#5626)
Co-authored-by: Yoshiya Hinosawa <[email protected]> Co-authored-by: Asher Gomez <[email protected]>
- Loading branch information
1 parent
4ac8373
commit 149839b
Showing
20 changed files
with
1,058 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,7 @@ | |
"./msgpack", | ||
"./net", | ||
"./path", | ||
"./random", | ||
"./regexp", | ||
"./semver", | ||
"./streams", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,7 @@ | |
"./msgpack", | ||
"./net", | ||
"./path", | ||
"./random", | ||
"./regexp", | ||
"./semver", | ||
"./streams", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.