UNPKG

jsproptest

Version:

Javascript Property-based Testing

177 lines (160 loc) 7.29 kB
// import Rand from 'rand-seed' import { MersenneTwister19937, int32, int53, real, bool } from 'random-js' /** * Provides methods for generating pseudo-random numbers using the Mersenne Twister 19937 algorithm. * Allows seeding for reproducible sequences and cloning the generator state. */ export class Random { /** * Predefined boundary values useful for testing edge cases involving safe integers. */ static readonly LONG_BOUNDS = [ 0, -128, 127, -32768, 32767, -2147483648, 2147483647, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, ] /** * Predefined boundary values useful for testing edge cases involving 32-bit integers. */ static readonly INT_BOUNDS = [0, -128, 127, -32768, 32767, -2147483648, 2147483647] private mt: MersenneTwister19937 /** * Creates a new Random instance. * @param initialSeed Optional seed value. If empty, an auto-generated seed is used. * @param useCount Optional number of times the generator has been used, to restore state. */ constructor(readonly initialSeed: string = '', useCount: number = 0) { if (initialSeed === '') { const autoSeed = MersenneTwister19937.autoSeed().next() this.mt = MersenneTwister19937.seed(autoSeed).discard(useCount) this.initialSeed = autoSeed.toString() } else this.mt = MersenneTwister19937.seed(Number.parseInt(this.initialSeed)).discard(useCount) } // FIXME: not an exact implementation [0,1) * N. How about some large number with same precisions? /** * Generates a random floating-point number within the specified range (inclusive). * Defaults to the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`. * @param min The minimum value (inclusive). * @param max The maximum value (inclusive). * @returns A random number. */ nextNumber(min: number = Number.MIN_SAFE_INTEGER, max: number = Number.MAX_SAFE_INTEGER): number { return real(min, max, true)(this.mt) } /** * Generates a random floating-point number between 0 (inclusive) and 1 (exclusive). * @returns A random probability value. */ nextProb(): number { return real(0, 1, false)(this.mt) } //FIXME: find good reason for reliable min and max /** * Generates a random "long" (safe integer) number. * With a certain probability (`boundProb`), it returns a boundary value from `LONG_BOUNDS`. * Otherwise, it returns a random 53-bit integer. * @param boundProb The probability (0 to 1) of returning a boundary value. Defaults to 0.2. * @returns A random safe integer, potentially a boundary value. */ nextLong(boundProb = 0.2): number { // add boundary number generation with some probability if (this.nextBoolean(boundProb)) { return Random.LONG_BOUNDS[this.interval(0, Random.LONG_BOUNDS.length - 1)] } return int53(this.mt) } /** * Generates a random 32-bit integer. * With a certain probability (`boundProb`), it returns a boundary value from `INT_BOUNDS`. * Otherwise, it returns a random 32-bit integer. * @param boundProb The probability (0 to 1) of returning a boundary value. Defaults to 0.2. * @returns A random 32-bit integer, potentially a boundary value. */ nextInt(boundProb = 0.2): number { // add integer boundary number generation with some probability if (this.nextBoolean(boundProb)) { return Random.INT_BOUNDS[this.interval(0, Random.INT_BOUNDS.length - 1)] } else return int32(this.mt) } /** * Generates a random boolean value. * @param trueProb The probability (0 to 1) of returning `true`. Defaults to 0.5. * @returns A random boolean. */ nextBoolean(trueProb: number = 0.5): boolean { return bool(trueProb)(this.mt) } /** * Generates a random safe integer within the specified inclusive interval [min, max]. * Uses the underlying 53-bit integer generator. * @param min The minimum value (inclusive). * @param max The maximum value (inclusive). * @returns A random safe integer within the interval. */ interval(min: number, max: number): number { // return integer(min, max)(this.mt) return this._interval(int53(this.mt), -0x20000000000000, 0x1fffffffffffff, min, max) } /** * Generates a random safe integer within the specified range [fromInclusive, toExclusive). * @param fromInclusive The minimum value (inclusive). * @param toExclusive The maximum value (exclusive). * @returns A random safe integer within the range. */ inRange(fromInclusive: number, toExclusive: number): number { return this.interval(fromInclusive, toExclusive - 1) } /** * Generates a random 32-bit integer within the specified inclusive interval [min, max]. * Uses the underlying 32-bit integer generator. * @param min The minimum value (inclusive). * @param max The maximum value (inclusive). * @returns A random 32-bit integer within the interval. */ intInterval(min: number, max: number): number { return this._interval(int32(this.mt), -0x80000000, 0x7fffffff, min, max) } /** * Generates a random 32-bit integer within the specified range [fromInclusive, toExclusive). * @param fromInclusive The minimum value (inclusive). * @param toExclusive The maximum value (exclusive). * @returns A random 32-bit integer within the range. */ intInRange(fromInclusive: number, toExclusive: number): number { return this.intInterval(fromInclusive, toExclusive - 1) } /** * Creates a clone of the current Random instance, preserving the generator state. * @returns A new Random instance with the same seed and usage count. */ clone(): Random { return new Random(this.initialSeed, this.mt.getUseCount()) } // Internal helper to map a generated number within its native range [genMin, genMax] // to a target range [min, max]. _interval(genNum: number, genMin: number, genMax: number, min: number, max: number): number { if (genNum < genMin) throw new RangeError('genMin(' + genMin + ') greater than num(' + genNum + ')') if (genNum > genMax) throw new RangeError('num(' + genNum + ') greater than genMax(' + genMax + ')') if (genMin >= genMax) throw new RangeError('genMin(' + genMin + ') greater or equal to genMax(' + genMax + ')') if (min > max) throw new RangeError('min(' + min + ') greater or equal to max(' + max + ')') // Avoid division by zero if the generator range has only one value (shouldn't happen with MT) // or if the target range has only one value. if (genMin === genMax || min === max) { return min; } // min: 2 max: 3 num: 2 or 3 // (2-2) / (3-2) = 0 / 1 = 0 // (3-2) / (3-2) = 1 / 1 = 1 // min: 2 max: 2 num: 2 // (2-2) / (2-2) = 0/0 const frac = (genNum - genMin) / (genMax - genMin) return Math.round(frac * (max - min) + min) } }