UNPKG

random-js

Version:

A mathematically correct random number generator library for JavaScript.

951 lines (914 loc) 28.9 kB
const SMALLEST_UNSAFE_INTEGER = 0x20000000000000; const LARGEST_SAFE_INTEGER = SMALLEST_UNSAFE_INTEGER - 1; const UINT32_MAX = -1 >>> 0; const UINT32_SIZE = UINT32_MAX + 1; const INT32_SIZE = UINT32_SIZE / 2; const INT32_MAX = INT32_SIZE - 1; const UINT21_SIZE = 1 << 21; const UINT21_MAX = UINT21_SIZE - 1; /** * Returns a value within [-0x80000000, 0x7fffffff] */ function int32(engine) { return engine.next() | 0; } function add(distribution, addend) { if (addend === 0) { return distribution; } else { return engine => distribution(engine) + addend; } } /** * Returns a value within [-0x20000000000000, 0x1fffffffffffff] */ function int53(engine) { const high = engine.next() | 0; const low = engine.next() >>> 0; return ((high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0)); } /** * Returns a value within [-0x20000000000000, 0x20000000000000] */ function int53Full(engine) { while (true) { const high = engine.next() | 0; if (high & 0x400000) { if ((high & 0x7fffff) === 0x400000 && (engine.next() | 0) === 0) { return SMALLEST_UNSAFE_INTEGER; } } else { const low = engine.next() >>> 0; return ((high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0)); } } } /** * Returns a value within [0, 0xffffffff] */ function uint32(engine) { return engine.next() >>> 0; } /** * Returns a value within [0, 0x1fffffffffffff] */ function uint53(engine) { const high = engine.next() & UINT21_MAX; const low = engine.next() >>> 0; return high * UINT32_SIZE + low; } /** * Returns a value within [0, 0x20000000000000] */ function uint53Full(engine) { while (true) { const high = engine.next() | 0; if (high & UINT21_SIZE) { if ((high & UINT21_MAX) === 0 && (engine.next() | 0) === 0) { return SMALLEST_UNSAFE_INTEGER; } } else { const low = engine.next() >>> 0; return (high & UINT21_MAX) * UINT32_SIZE + low; } } } function isPowerOfTwoMinusOne(value) { return ((value + 1) & value) === 0; } function bitmask(masking) { return (engine) => engine.next() & masking; } function downscaleToLoopCheckedRange(range) { const extendedRange = range + 1; const maximum = extendedRange * Math.floor(UINT32_SIZE / extendedRange); return engine => { let value = 0; do { value = engine.next() >>> 0; } while (value >= maximum); return value % extendedRange; }; } function downscaleToRange(range) { if (isPowerOfTwoMinusOne(range)) { return bitmask(range); } else { return downscaleToLoopCheckedRange(range); } } function isEvenlyDivisibleByMaxInt32(value) { return (value | 0) === 0; } function upscaleWithHighMasking(masking) { return engine => { const high = engine.next() & masking; const low = engine.next() >>> 0; return high * UINT32_SIZE + low; }; } function upscaleToLoopCheckedRange(extendedRange) { const maximum = extendedRange * Math.floor(SMALLEST_UNSAFE_INTEGER / extendedRange); return engine => { let ret = 0; do { const high = engine.next() & UINT21_MAX; const low = engine.next() >>> 0; ret = high * UINT32_SIZE + low; } while (ret >= maximum); return ret % extendedRange; }; } function upscaleWithinU53(range) { const extendedRange = range + 1; if (isEvenlyDivisibleByMaxInt32(extendedRange)) { const highRange = ((extendedRange / UINT32_SIZE) | 0) - 1; if (isPowerOfTwoMinusOne(highRange)) { return upscaleWithHighMasking(highRange); } } return upscaleToLoopCheckedRange(extendedRange); } function upscaleWithinI53AndLoopCheck(min, max) { return engine => { let ret = 0; do { const high = engine.next() | 0; const low = engine.next() >>> 0; ret = (high & UINT21_MAX) * UINT32_SIZE + low + (high & UINT21_SIZE ? -SMALLEST_UNSAFE_INTEGER : 0); } while (ret < min || ret > max); return ret; }; } /** * Returns a Distribution to return a value within [min, max] * @param min The minimum integer value, inclusive. No less than -0x20000000000000. * @param max The maximum integer value, inclusive. No greater than 0x20000000000000. */ function integer(min, max) { min = Math.floor(min); max = Math.floor(max); if (min < -SMALLEST_UNSAFE_INTEGER || !isFinite(min)) { throw new RangeError(`Expected min to be at least ${-SMALLEST_UNSAFE_INTEGER}`); } else if (max > SMALLEST_UNSAFE_INTEGER || !isFinite(max)) { throw new RangeError(`Expected max to be at most ${SMALLEST_UNSAFE_INTEGER}`); } const range = max - min; if (range <= 0 || !isFinite(range)) { return () => min; } else if (range === UINT32_MAX) { if (min === 0) { return uint32; } else { return add(int32, min + INT32_SIZE); } } else if (range < UINT32_MAX) { return add(downscaleToRange(range), min); } else if (range === LARGEST_SAFE_INTEGER) { return add(uint53, min); } else if (range < LARGEST_SAFE_INTEGER) { return add(upscaleWithinU53(range), min); } else if (max - 1 - min === LARGEST_SAFE_INTEGER) { return add(uint53Full, min); } else if (min === -SMALLEST_UNSAFE_INTEGER && max === SMALLEST_UNSAFE_INTEGER) { return int53Full; } else if (min === -SMALLEST_UNSAFE_INTEGER && max === LARGEST_SAFE_INTEGER) { return int53; } else if (min === -LARGEST_SAFE_INTEGER && max === SMALLEST_UNSAFE_INTEGER) { return add(int53, 1); } else if (max === SMALLEST_UNSAFE_INTEGER) { return add(upscaleWithinI53AndLoopCheck(min - 1, max - 1), 1); } else { return upscaleWithinI53AndLoopCheck(min, max); } } function isLeastBitTrue(engine) { return (engine.next() & 1) === 1; } function lessThan(distribution, value) { return engine => distribution(engine) < value; } function probability(percentage) { if (percentage <= 0) { return () => false; } else if (percentage >= 1) { return () => true; } else { const scaled = percentage * UINT32_SIZE; if (scaled % 1 === 0) { return lessThan(int32, (scaled - INT32_SIZE) | 0); } else { return lessThan(uint53, Math.round(percentage * SMALLEST_UNSAFE_INTEGER)); } } } function bool(numerator, denominator) { if (denominator == null) { if (numerator == null) { return isLeastBitTrue; } return probability(numerator); } else { if (numerator <= 0) { return () => false; } else if (numerator >= denominator) { return () => true; } return lessThan(integer(0, denominator - 1), numerator); } } /** * Returns a Distribution that returns a random `Date` within the inclusive * range of [`start`, `end`]. * @param start The minimum `Date` * @param end The maximum `Date` */ function date(start, end) { const distribution = integer(+start, +end); return engine => new Date(distribution(engine)); } /** * Returns a Distribution to return a value within [1, sideCount] * @param sideCount The number of sides of the die */ function die(sideCount) { return integer(1, sideCount); } /** * Returns a distribution that returns an array of length `dieCount` of values * within [1, `sideCount`] * @param sideCount The number of sides of each die * @param dieCount The number of dice */ function dice(sideCount, dieCount) { const distribution = die(sideCount); return engine => { const result = []; for (let i = 0; i < dieCount; ++i) { result.push(distribution(engine)); } return result; }; } // tslint:disable:unified-signatures // has 2**x chars, for faster uniform distribution const DEFAULT_STRING_POOL = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"; function string(pool = DEFAULT_STRING_POOL) { const poolLength = pool.length; if (!poolLength) { throw new Error("Expected pool not to be an empty string"); } const distribution = integer(0, poolLength - 1); return (engine, length) => { let result = ""; for (let i = 0; i < length; ++i) { const j = distribution(engine); result += pool.charAt(j); } return result; }; } const LOWER_HEX_POOL = "0123456789abcdef"; const lowerHex = string(LOWER_HEX_POOL); const upperHex = string(LOWER_HEX_POOL.toUpperCase()); /** * Returns a Distribution that returns a random string comprised of numbers * or the characters `abcdef` (or `ABCDEF`) of length `length`. * @param length Length of the result string * @param uppercase Whether the string should use `ABCDEF` instead of `abcdef` */ function hex(uppercase) { if (uppercase) { return upperHex; } else { return lowerHex; } } function convertSliceArgument(value, length) { if (value < 0) { return Math.max(value + length, 0); } else { return Math.min(value, length); } } function toInteger(value) { const num = +value; if (num < 0) { return Math.ceil(num); } else { return Math.floor(num); } } /** * Returns a random value within the provided `source` within the sliced * bounds of `begin` and `end`. * @param source an array of items to pick from * @param begin the beginning slice index (defaults to `0`) * @param end the ending slice index (defaults to `source.length`) */ function pick(engine, source, begin, end) { const length = source.length; if (length === 0) { throw new RangeError("Cannot pick from an empty array"); } const start = begin == null ? 0 : convertSliceArgument(toInteger(begin), length); const finish = end === void 0 ? length : convertSliceArgument(toInteger(end), length); if (start >= finish) { throw new RangeError(`Cannot pick between bounds ${start} and ${finish}`); } const distribution = integer(start, finish - 1); return source[distribution(engine)]; } function multiply(distribution, multiplier) { if (multiplier === 1) { return distribution; } else if (multiplier === 0) { return () => 0; } else { return engine => distribution(engine) * multiplier; } } /** * Returns a floating-point value within [0.0, 1.0) */ function realZeroToOneExclusive(engine) { return uint53(engine) / SMALLEST_UNSAFE_INTEGER; } /** * Returns a floating-point value within [0.0, 1.0] */ function realZeroToOneInclusive(engine) { return uint53Full(engine) / SMALLEST_UNSAFE_INTEGER; } /** * Returns a floating-point value within [min, max) or [min, max] * @param min The minimum floating-point value, inclusive. * @param max The maximum floating-point value. * @param inclusive If true, `max` will be inclusive. */ function real(min, max, inclusive = false) { if (!isFinite(min)) { throw new RangeError("Expected min to be a finite number"); } else if (!isFinite(max)) { throw new RangeError("Expected max to be a finite number"); } return add(multiply(inclusive ? realZeroToOneInclusive : realZeroToOneExclusive, max - min), min); } const sliceArray = Array.prototype.slice; /** * Shuffles an array in-place * @param engine The Engine to use when choosing random values * @param array The array to shuffle * @param downTo minimum index to shuffle. Only used internally. */ function shuffle(engine, array, downTo = 0) { const length = array.length; if (length) { for (let i = (length - 1) >>> 0; i > downTo; --i) { const distribution = integer(0, i); const j = distribution(engine); if (i !== j) { const tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } } return array; } /** * From the population array, produce an array with sampleSize elements that * are randomly chosen without repeats. * @param engine The Engine to use when choosing random values * @param population An array that has items to choose a sample from * @param sampleSize The size of the result array */ function sample(engine, population, sampleSize) { if (sampleSize < 0 || sampleSize > population.length || !isFinite(sampleSize)) { throw new RangeError("Expected sampleSize to be within 0 and the length of the population"); } if (sampleSize === 0) { return []; } const clone = sliceArray.call(population); const length = clone.length; if (length === sampleSize) { return shuffle(engine, clone, 0); } const tailLength = length - sampleSize; return shuffle(engine, clone, tailLength - 1).slice(tailLength); } const stringRepeat = (() => { try { if ("x".repeat(3) === "xxx") { return (pattern, count) => pattern.repeat(count); } } catch (_) { // nothing to do here } return (pattern, count) => { let result = ""; while (count > 0) { if (count & 1) { result += pattern; } count >>= 1; pattern += pattern; } return result; }; })(); function zeroPad(text, zeroCount) { return stringRepeat("0", zeroCount - text.length) + text; } /** * Returns a Universally Unique Identifier Version 4. * * See http://en.wikipedia.org/wiki/Universally_unique_identifier */ function uuid4(engine) { const a = engine.next() >>> 0; const b = engine.next() | 0; const c = engine.next() | 0; const d = engine.next() >>> 0; return (zeroPad(a.toString(16), 8) + "-" + zeroPad((b & 0xffff).toString(16), 4) + "-" + zeroPad((((b >> 4) & 0x0fff) | 0x4000).toString(16), 4) + "-" + zeroPad(((c & 0x3fff) | 0x8000).toString(16), 4) + "-" + zeroPad(((c >> 4) & 0xffff).toString(16), 4) + zeroPad(d.toString(16), 8)); } /** * An int32-producing Engine that uses `Math.random()` */ const nativeMath = { next() { return (Math.random() * UINT32_SIZE) | 0; } }; // tslint:disable:unified-signatures /** * A wrapper around an Engine that provides easy-to-use methods for * producing values based on known distributions */ class Random { /** * Creates a new Random wrapper * @param engine The engine to use (defaults to a `Math.random`-based implementation) */ constructor(engine = nativeMath) { this.engine = engine; } /** * Returns a value within [-0x80000000, 0x7fffffff] */ int32() { return int32(this.engine); } /** * Returns a value within [0, 0xffffffff] */ uint32() { return uint32(this.engine); } /** * Returns a value within [0, 0x1fffffffffffff] */ uint53() { return uint53(this.engine); } /** * Returns a value within [0, 0x20000000000000] */ uint53Full() { return uint53Full(this.engine); } /** * Returns a value within [-0x20000000000000, 0x1fffffffffffff] */ int53() { return int53(this.engine); } /** * Returns a value within [-0x20000000000000, 0x20000000000000] */ int53Full() { return int53Full(this.engine); } /** * Returns a value within [min, max] * @param min The minimum integer value, inclusive. No less than -0x20000000000000. * @param max The maximum integer value, inclusive. No greater than 0x20000000000000. */ integer(min, max) { return integer(min, max)(this.engine); } /** * Returns a floating-point value within [0.0, 1.0] */ realZeroToOneInclusive() { return realZeroToOneInclusive(this.engine); } /** * Returns a floating-point value within [0.0, 1.0) */ realZeroToOneExclusive() { return realZeroToOneExclusive(this.engine); } /** * Returns a floating-point value within [min, max) or [min, max] * @param min The minimum floating-point value, inclusive. * @param max The maximum floating-point value. * @param inclusive If true, `max` will be inclusive. */ real(min, max, inclusive = false) { return real(min, max, inclusive)(this.engine); } bool(numerator, denominator) { return bool(numerator, denominator)(this.engine); } /** * Return a random value within the provided `source` within the sliced * bounds of `begin` and `end`. * @param source an array of items to pick from * @param begin the beginning slice index (defaults to `0`) * @param end the ending slice index (defaults to `source.length`) */ pick(source, begin, end) { return pick(this.engine, source, begin, end); } /** * Shuffles an array in-place * @param array The array to shuffle */ shuffle(array) { return shuffle(this.engine, array); } /** * From the population array, returns an array with sampleSize elements that * are randomly chosen without repeats. * @param population An array that has items to choose a sample from * @param sampleSize The size of the result array */ sample(population, sampleSize) { return sample(this.engine, population, sampleSize); } /** * Returns a value within [1, sideCount] * @param sideCount The number of sides of the die */ die(sideCount) { return die(sideCount)(this.engine); } /** * Returns an array of length `dieCount` of values within [1, sideCount] * @param sideCount The number of sides of each die * @param dieCount The number of dice */ dice(sideCount, dieCount) { return dice(sideCount, dieCount)(this.engine); } /** * Returns a Universally Unique Identifier Version 4. * * See http://en.wikipedia.org/wiki/Universally_unique_identifier */ uuid4() { return uuid4(this.engine); } string(length, pool) { return string(pool)(this.engine, length); } /** * Returns a random string comprised of numbers or the characters `abcdef` * (or `ABCDEF`) of length `length`. * @param length Length of the result string * @param uppercase Whether the string should use `ABCDEF` instead of `abcdef` */ hex(length, uppercase) { return hex(uppercase)(this.engine, length); } /** * Returns a random `Date` within the inclusive range of [`start`, `end`]. * @param start The minimum `Date` * @param end The maximum `Date` */ date(start, end) { return date(start, end)(this.engine); } } /** * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int32Array */ const I32Array = (() => { try { const buffer = new ArrayBuffer(4); const view = new Int32Array(buffer); view[0] = INT32_SIZE; if (view[0] === -INT32_SIZE) { return Int32Array; } } catch (_) { // nothing to do here } return Array; })(); let data = null; const COUNT = 128; let index = COUNT; /** * An Engine that relies on the globally-available `crypto.getRandomValues`, * which is typically available in modern browsers. * * See https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues * * If unavailable or otherwise non-functioning, then `browserCrypto` will * likely `throw` on the first call to `next()`. */ const browserCrypto = { next() { if (index >= COUNT) { if (data === null) { data = new I32Array(COUNT); } crypto.getRandomValues(data); index = 0; } return data[index++] | 0; } }; /** * Returns an array of random int32 values, based on current time * and a random number engine * * @param engine an Engine to pull random values from, default `nativeMath` * @param length the length of the Array, minimum 1, default 16 */ function createEntropy(engine = nativeMath, length = 16) { const array = []; array.push(new Date().getTime() | 0); for (let i = 1; i < length; ++i) { array[i] = engine.next() | 0; } return array; } /** * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul */ const imul = (() => { try { if (Math.imul(UINT32_MAX, 5) === -5) { return Math.imul; } } catch (_) { // nothing to do here } const UINT16_MAX = 0xffff; return (a, b) => { const ah = (a >>> 16) & UINT16_MAX; const al = a & UINT16_MAX; const bh = (b >>> 16) & UINT16_MAX; const bl = b & UINT16_MAX; // the shift by 0 fixes the sign on the high part // the final |0 converts the unsigned value into a signed value return (al * bl + (((ah * bl + al * bh) << 16) >>> 0)) | 0; }; })(); const ARRAY_SIZE = 624; const ARRAY_MAX = ARRAY_SIZE - 1; const M = 397; const ARRAY_SIZE_MINUS_M = ARRAY_SIZE - M; const A = 0x9908b0df; /** * An Engine that is a pseudorandom number generator using the Mersenne * Twister algorithm based on the prime 2**19937 − 1 * * See http://en.wikipedia.org/wiki/Mersenne_twister */ class MersenneTwister19937 { /** * MersenneTwister19937 should not be instantiated directly. * Instead, use the static methods `seed`, `seedWithArray`, or `autoSeed`. */ constructor() { this.data = new I32Array(ARRAY_SIZE); this.index = 0; // integer within [0, 624] this.uses = 0; } /** * Returns a MersenneTwister19937 seeded with an initial int32 value * @param initial the initial seed value */ static seed(initial) { return new MersenneTwister19937().seed(initial); } /** * Returns a MersenneTwister19937 seeded with zero or more int32 values * @param source A series of int32 values */ static seedWithArray(source) { return new MersenneTwister19937().seedWithArray(source); } /** * Returns a MersenneTwister19937 seeded with the current time and * a series of natively-generated random values */ static autoSeed() { return MersenneTwister19937.seedWithArray(createEntropy()); } /** * Returns the next int32 value of the sequence */ next() { if ((this.index | 0) >= ARRAY_SIZE) { refreshData(this.data); this.index = 0; } const value = this.data[this.index]; this.index = (this.index + 1) | 0; this.uses += 1; return temper(value) | 0; } /** * Returns the number of times that the Engine has been used. * * This can be provided to an unused MersenneTwister19937 with the same * seed, bringing it to the exact point that was left off. */ getUseCount() { return this.uses; } /** * Discards one or more items from the engine * @param count The count of items to discard */ discard(count) { if (count <= 0) { return this; } this.uses += count; if ((this.index | 0) >= ARRAY_SIZE) { refreshData(this.data); this.index = 0; } while (count + this.index > ARRAY_SIZE) { count -= ARRAY_SIZE - this.index; refreshData(this.data); this.index = 0; } this.index = (this.index + count) | 0; return this; } seed(initial) { let previous = 0; this.data[0] = previous = initial | 0; for (let i = 1; i < ARRAY_SIZE; i = (i + 1) | 0) { this.data[i] = previous = (imul(previous ^ (previous >>> 30), 0x6c078965) + i) | 0; } this.index = ARRAY_SIZE; this.uses = 0; return this; } seedWithArray(source) { this.seed(0x012bd6aa); seedWithArray(this.data, source); return this; } } function refreshData(data) { let k = 0; let tmp = 0; for (; (k | 0) < ARRAY_SIZE_MINUS_M; k = (k + 1) | 0) { tmp = (data[k] & INT32_SIZE) | (data[(k + 1) | 0] & INT32_MAX); data[k] = data[(k + M) | 0] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } for (; (k | 0) < ARRAY_MAX; k = (k + 1) | 0) { tmp = (data[k] & INT32_SIZE) | (data[(k + 1) | 0] & INT32_MAX); data[k] = data[(k - ARRAY_SIZE_MINUS_M) | 0] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } tmp = (data[ARRAY_MAX] & INT32_SIZE) | (data[0] & INT32_MAX); data[ARRAY_MAX] = data[M - 1] ^ (tmp >>> 1) ^ (tmp & 0x1 ? A : 0); } function temper(value) { value ^= value >>> 11; value ^= (value << 7) & 0x9d2c5680; value ^= (value << 15) & 0xefc60000; return value ^ (value >>> 18); } function seedWithArray(data, source) { let i = 1; let j = 0; const sourceLength = source.length; let k = Math.max(sourceLength, ARRAY_SIZE) | 0; let previous = data[0] | 0; for (; (k | 0) > 0; --k) { data[i] = previous = ((data[i] ^ imul(previous ^ (previous >>> 30), 0x0019660d)) + (source[j] | 0) + (j | 0)) | 0; i = (i + 1) | 0; ++j; if ((i | 0) > ARRAY_MAX) { data[0] = data[ARRAY_MAX]; i = 1; } if (j >= sourceLength) { j = 0; } } for (k = ARRAY_MAX; (k | 0) > 0; --k) { data[i] = previous = ((data[i] ^ imul(previous ^ (previous >>> 30), 0x5d588b65)) - i) | 0; i = (i + 1) | 0; if ((i | 0) > ARRAY_MAX) { data[0] = data[ARRAY_MAX]; i = 1; } } data[0] = INT32_SIZE; } let data$1 = null; const COUNT$1 = 128; let index$1 = COUNT$1; /** * An Engine that relies on the node-available * `require('crypto').randomBytes`, which has been available since 0.58. * * See https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback * * If unavailable or otherwise non-functioning, then `nodeCrypto` will * likely `throw` on the first call to `next()`. */ const nodeCrypto = { next() { if (index$1 >= COUNT$1) { data$1 = new Int32Array(new Int8Array(require("crypto").randomBytes(4 * COUNT$1)).buffer); index$1 = 0; } return data$1[index$1++] | 0; } }; /** * Returns a Distribution to random value within the provided `source` * within the sliced bounds of `begin` and `end`. * @param source an array of items to pick from * @param begin the beginning slice index (defaults to `0`) * @param end the ending slice index (defaults to `source.length`) */ function picker(source, begin, end) { const clone = sliceArray.call(source, begin, end); if (clone.length === 0) { throw new RangeError(`Cannot pick from a source with no items`); } const distribution = integer(0, clone.length - 1); return engine => clone[distribution(engine)]; } export { Random, browserCrypto, nativeMath, MersenneTwister19937, nodeCrypto, bool, date, dice, die, hex, int32, int53, int53Full, integer, pick, picker, real, realZeroToOneExclusive, realZeroToOneInclusive, sample, shuffle, string, uint32, uint53, uint53Full, uuid4, createEntropy }; //# sourceMappingURL=random-js.esm.js.map