@freeword/meta
Version:
Meta package for Freeword: exports all core types, constants, and utilities from the src/ directory.
447 lines (411 loc) • 22.1 kB
text/typescript
import _ from 'lodash'
import { prng_alea as SeededRNGFactory, type AleaPRNGenerator as PRNGenerator } from 'esm-seedrandom'
import { throwable } from './Outcome.ts'
import * as Consts from '../UtilityConsts.ts'
import { MAX_UINT32 } from '../UtilityConsts.ts'
export type RNGSeed = string
export type RNGSpecQ = RNGSeed | RNGFunction
export type RNGSpec = RNGSeed | RNGFunction | undefined
export type RNGFunction = () => number
export interface HiLo { lo?: number | undefined, hi?: number | undefined }
export interface CharHiLo { lo?: string | undefined, hi?: string | undefined }
export interface HiLoCt extends HiLo { count: number }
export interface CharHiLoCt extends CharHiLo { count: number }
export interface HiLoSeed extends HiLo { seed: RNGSeed }
export interface HiLoCtSeed extends HiLoCt { seed: RNGSeed }
// /** RNGOpts; you are trusted to specify either `seed` or `rng`, not both */
// export interface RNGOpts { using?: RNGSpec }
// /** specify a range `lo <= x < hi` and the RNG to generate them */
// export interface HiLoRNG extends HiLo, RNGOpts { }
// /** specify a range `lo <= x < hi`, the RNG to generate them, and the number of values to generate */
// export interface HiLoCtRNG extends HiLoCt, RNGOpts { }
/** The possible reasons for a throwable error */
export type RNGGist = 'badSeed' | 'badRNG'
export type CharCode = number
export const CHARCODE_0 = '0'.charCodeAt(0) // = 48
export const CHARCODE_9 = '9'.charCodeAt(0) // = 57
export const CHARCODE_A = 'A'.charCodeAt(0) // = 65
export const CHARCODE_Z = 'Z'.charCodeAt(0) // = 90
export const CHARCODE_a = 'a'.charCodeAt(0) // = 97
export const CHARCODE_z = 'z'.charCodeAt(0) // = 122
const CharCodeA52m1 = 52 + CHARCODE_A - 1 // 26 upper and 26 lower
const CharCodeA62m1 = 62 + CHARCODE_A - 1 // 26 upper, 26 lower, and 10 digits
// we baseline on 'A': 65-90 -> A-Z, 91-117 -> 97-122 (a-z), 117-126 -> 48-57 (0-9)
const CharOffsetA52toa = 1 + CHARCODE_Z - CHARCODE_a // if we are at 'Z+1', we want to be at 'a': Z+1-(Z+1-a) -> a
const CharOffsetA62to0 = 27 + CHARCODE_Z - CHARCODE_0 // if we are at 'Z+27', we want to be at '0': Z+27-(Z+27-0) -> 0
const SINT32_TO_UINT32 = 2 ** 31
export class BaseRandomFactory {
declare readonly rng: RNGFunction
// == [Construction]
constructor(rng: RNGFunction) {
Object.defineProperty(this, 'rng', { value: rng, configurable: true, enumerable: false })
}
static make<CT extends { new (rng: RNGFunction): RandomFactory, rngFor(rng?: RNGSpec): RNGFunction }>(this: CT, rngspec?: RNGSpec | undefined) {
return new this(this.rngFor(rngspec))
}
static rngFor(rngspec: RNGSpec = Math.random): RNGFunction {
if (typeof rngspec === 'function') { return rngspec }
if (rngspec && (typeof rngspec === 'string')) {
return this._seededRNG(rngspec)
}
throw throwable('RNG must be a non-blank string, a function, or undefined', 'badRNG', { given: rngspec })
}
protected static _seededRNG(seed: string): RNGFunction {
return SeededRNGFactory(seed).double
}
// --
// == [One-shot numeric methods]
/** Random float, `0 <= x < 1` */
rand01() { return this.rng() }
/** Random float, `lo <= x < hi` */
rand({ lo = 0, hi:hiIn = lo + 1 }: HiLo = {}) {
const range = _.max([lo, hiIn])! - lo
return lo + (this.rng() * range)
}
/** Random integer, `lo <= x <= hi` */
int(opts: HiLo = {}) {
const { lo = 0, hi = lo + 9 } = opts
return Math.floor(this.rand({ lo, hi: hi + 1 }))
}
/** Random integer, `0 <= x <= 2^32 - 1`. On some factories this is more efficient than `int({ hi: MAX_UINT32 })`. */
uint32() {
return Math.floor(this.rand01() * MAX_UINT32)
}
/** Random integer, `0 <= x <= 2^32 - 1`. On some factories this is more efficient than `int({ hi: MAX_UINT32 })`. */
sint32() {
return this.uint32() - SINT32_TO_UINT32
}
/** Random boolean */
bool() { return (this.rand01() < 0.5) }
// --
// == [One-shot character methods]
/** Random character `loCharCode <= x < hiCharCode` */
char({ lo = CHARCODE_a, hi = CHARCODE_z }: HiLo = {}) {
return String.fromCharCode(this.int({ lo, hi }))
}
/** Random character `loChar <= x < hiChar` */
charBetween({ lo:loChar = 'a', hi:hiChar = 'z' }: CharHiLo = {}) {
const lo = loChar.charCodeAt(0)
const hi = hiChar.charCodeAt(0)
return this.char({ lo, hi })
}
/** Random lower-case letter `'a'…'z'` */
lower() { return this.charBetween({ lo: 'a', hi: 'z' }) }
/** Random upper-case letter `'A'…'Z'` */
upper() { return this.charBetween({ lo: 'A', hi: 'Z' }) }
/** Random letter `'A'…'Z'` */
azAZ() {
const charCode = this.int({ lo: CHARCODE_A, hi: CharCodeA52m1 })
if (charCode <= CHARCODE_Z) { return String.fromCharCode(charCode) }
return String.fromCharCode(charCode - CharOffsetA52toa)
}
/** Random character `'0'..'9'…'A'..'Z'…'a'…'z'` */
azAZ09() {
const charCode = this.int({ lo: CHARCODE_A, hi: CharCodeA62m1 })
if (charCode <= CHARCODE_Z) { return String.fromCharCode(charCode) }
if (charCode <= CharCodeA52m1) { return String.fromCharCode(charCode - CharOffsetA52toa) }
return String.fromCharCode(charCode - CharOffsetA62to0)
}
/** Random numeral `'0'…'9'` */
numeral() { return this.charBetween({ lo: '0', hi: '9' }) }
// --
// == [Streming character methods]
/** Stream of `count` random characters, `lo <= x <= hi` */
* charsStar(opts: HiLoCt) {
for (const charCode of this.intsStar(opts)) {
yield String.fromCharCode(charCode)
}
}
/** Stream of `count` random characters, `lo <= char <= hi` */
* charsBetweenStar({ lo = 'a', hi = 'z', ...opts }: CharHiLoCt) {
const loCharCode = lo.charCodeAt(0)
const hiCharCode = hi.charCodeAt(0)
for (const charCode of this.intsStar({ ...opts, lo: loCharCode, hi: hiCharCode })) {
yield String.fromCharCode(charCode)
}
}
* lowersStar(count: number) { yield * this.charsStar({ lo: CHARCODE_a, hi: CHARCODE_z, count }) }
* uppersStar(count: number) { yield * this.charsStar({ lo: CHARCODE_A, hi: CHARCODE_Z, count }) }
* numeralsStar(count: number) { yield * this.charsStar({ lo: CHARCODE_0, hi: CHARCODE_9, count }) }
* azAZStar(count: number) {
for (const charCode of this.intsStar({ lo: CHARCODE_A, hi: CharCodeA52m1, count })) {
if (charCode <= CHARCODE_Z) { yield String.fromCharCode(charCode); continue }
yield String.fromCharCode(charCode - CharOffsetA52toa)
}
}
* azAZ09Star(count: number) {
for (const charCode of this.intsStar({ lo: CHARCODE_A, hi: CharCodeA62m1, count })) {
if (charCode <= CHARCODE_Z) { yield String.fromCharCode(charCode); continue }
if (charCode <= CharCodeA52m1) { yield String.fromCharCode(charCode - CharOffsetA52toa); continue }
yield String.fromCharCode(charCode - CharOffsetA62to0)
}
}
// --
/** Random float, `0 <= x < 1`. @note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static rand01(rngspec: RNGSpec = Math.random) {
return this.rngFor(rngspec)()
}
/** Random float, `lo <= x < hi`. @note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static rand(opts: HiLo = {}, rngspec: RNGSpec = Math.random) {
const { lo = 0, hi:hiIn = lo + 1 } = opts
const hi = _.max([lo, hiIn])!
return lo + ((hi - lo) * this.rand01(rngspec))
}
/** Random integer, `lo <= x <= hi`. @note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static int(opts: HiLo = {}, rngspec: RNGSpec = Math.random) {
const { lo = 0, hi = lo + 9 } = opts
return Math.floor(this.rand({ ...opts, lo, hi: hi + 1 }, rngspec))
}
/** Random integer, `0 <= x <= 2^32 - 1`. Some subclasses can do this more efficiently. @note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static uint32(rngspec: RNGSpec = Math.random) { return Math.floor(this.rand01(rngspec) * MAX_UINT32) }
/** Random integer, `-2^31 <= x <= 2^31 - 1`. Some subclasses can do this more efficiently. @note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static sint32(rngspec: RNGSpec = Math.random) { return Math.floor(this.rand01(rngspec) * SINT32_TO_UINT32) }
/** Random float, `lo <= x < hi`, with only 32 bits of randomness guaranteed. Some subclasses can do this more efficiently.@note: this makes a new factory for each call. But if you're making enough numbers to matter, you should choose one of the subclasses anduse the stream methods. */
static loose01(rngspec: RNGSpec = Math.random) { return this.rand01(rngspec) }
// --
// == [Fundamental Stream methods] -- these do the work, other stuff is sugar
/** Stream of `count` random numbers, `0 <= x < 1` */
* rand01sStar(count: number): Generator<number, undefined, number | undefined> {
for (let seq = 0; seq < count; seq++) {
yield this.rng()
}
}
/** Stream of `count` random numbers, `lo <= x < hi` (0 and lo + 1 by default) */
* randsStar(opts: HiLoCt): Generator<number, undefined, number | undefined> {
const { lo = 0, hi:hiIn = lo + 1, count } = opts
const hi = _.max([lo, hiIn])!
if (hi === 1) { // don't have to multiply if hi is 1
if (lo === 0) { yield * this.rand01sStar(count); return } // don't have to multiply or add
for (const num01 of this.rand01sStar(count)) { yield lo + num01 }; return
}
if (lo === 0) { // don't have to add if lo is 0
for (const num01 of this.rand01sStar(count)) { yield num01 * hi }; return
}
const range = hi - lo
for (const num01 of this.rand01sStar(count)) {
yield lo + (num01 * range)
}
}
/** Stream of `count` random integers, `lo <= x <= hi` -- (by default, `lo = 0` and `hi = lo + 9`) */
* intsStar(opts: HiLoCt): Generator<number, undefined, number | undefined> {
const { lo = 0, hi = lo + 9, count } = opts
for (const float of this.randsStar({ lo, hi: hi + 1, count })) {
yield Math.floor(float)
}
}
/** Stream of `count` random integers, `0 <= x <= 2^32 - 1`. (Some subclasses can be more efficient at this than `IntsStar`.) */
* uint32sStar(count: number): Generator<number, undefined, number | undefined> {
for (const float of this.rand01sStar(count)) {
yield Math.floor(float * MAX_UINT32)
}
}
/** Stream of `count` random integers, `0 <= x <= 2^32 - 1`. (Some subclasses can be more efficient at this than `IntsStar`.) */
* sint32sStar(count: number): Generator<number, undefined, number | undefined> {
for (const uint of this.uint32sStar(count)) {
yield uint - SINT32_TO_UINT32
}
}
/** Stream of `count` random numbers with **32 bits of randomness**, `0 <= x < 1`. (Some subclasses can be more efficient if only 32 bits of randomness are needed.)
*/
* loose01sStar(count: number): Generator<number, undefined, number | undefined> {
yield * this.rand01sStar(count)
}
/** Stream of `count` random numbers with **32 bits of randomness**, `lo <= x < hi`. (Some subclasses can be more efficient if only 32 bits of randomness are needed.)
* (Some subclasses can be more efficient if only 32 bits of randomness are needed.
*/
* loosesStar(opts: HiLoCt): Generator<number, undefined, number | undefined> {
const { lo = 0, hi = lo + 1, count } = opts
if (lo === 0 && hi === 1) { yield * this.loose01sStar(count); return }
const range = hi - lo
for (const num01 of this.loose01sStar(count)) { yield lo + (num01 * range) }
}
// --
// == [Array methods]
/** Array of `count` random numbers, `0 <= x < 1` */
rand01s(count: number) { return [...this.rand01sStar(count)] }
/** Array of `count` random numbers, `lo <= x < hi` */
rands(opts: HiLoCt) { return [...this.randsStar(opts)] }
/** Array of `count` random integers, `lo <= x <= hi` */
randInts(opts: HiLoCt) { return [...this.intsStar(opts)] }
/** Array of `count` random integers, `0 <= x <= 2^32 - 1` */
randUint32s(count: number) { return [...this.uint32sStar(count)] }
// --
// == [Typed Array methods]
/** Uint32Array of `count` random integers, `0 <= x < 2^32 - 1` */
randUint32Array(count: number): Uint32Array {
const array = new Uint32Array(count)
let index = 0
for (const value of this.uint32sStar(count)) { array[index++] = value }
return array
}
/** Float32Array of `count` random numbers, `0 <= x < 1` */
randFloat32Array01(count: number): Float32Array {
const array = new Float32Array(count)
let index = 0
for (const value of this.loose01sStar(count)) { array[index++] = value }
return array
}
/** Float32Array of `count` random numbers, `0 <= x < 1` */
randFloat32Array(opts: HiLoCt): Float32Array {
const array = new Float32Array(opts.count)
let index = 0
for (const value of this.loosesStar(opts)) { array[index++] = value }
return array
}
/** Float64Array of `count` random numbers with 56 bits of randomness, `0 <= x < 1`. **NOTE: that's 56 bits of randomness, not 64 bits**. */
randFloat64Array01(count: number): Float64Array {
const array = new Float64Array(count)
let index = 0
for (const value of this.rand01sStar(count)) { array[index++] = value }
return array
}
/** Float64Array of `count` random numbers with 56 bits of randomness, `0 <= x < 1`. **NOTE: that's 56 bits of randomness, not 64 bits**. */
randFloat64Array(opts: HiLoCt): Float64Array {
const array = new Float64Array(opts.count)
let index = 0
for (const value of this.randsStar(opts)) { array[index++] = value }
return array
}
// --
// == [static methods for rand01s] -- stream of `count` numbers, `0 <= x < 1`]
/** Stream of `count` random numbers, `0 <= x < 1` */
static * rand01sStar(count: number, rngspec?: RNGSpec): Generator<number, undefined, number | undefined> {
if (! _.isNumber(count)) { throw throwable('count must be a number', 'absentVal', count) }
if (count < 0) { return }
const factory = this.make(rngspec)
yield * factory.rand01sStar(count)
}
/** Array of `count` random numbers, `0 <= x < 1` */
static rand01s(count: number, rngspec?: RNGSpec) { return [...this.rand01sStar(count, rngspec)] }
/** Float32Array of `count` random numbers, `0 <= x < 1` */
static randFloat32Array01(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randFloat32Array01(opts.count) }
/** Float64Array of `count` random numbers with 56 bits of randomness, `0 <= x < 1`. **NOTE: that's 56 bits of randomness, not 64 bits**. */
static randFloat64Array01(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randFloat64Array01(opts.count) }
// --
// == [static methods for rands] -- stream of `count` numbers, `lo <= x < hi`]
/** Stream of `count` random numbers, `lo <= x < hi` */
static * randsStar(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); yield * factory.randsStar(opts) }
/** Array of `count` random numbers, `lo <= x < hi` */
static rands(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.rands(opts) }
/** Float32Array of `count` random numbers, `lo <= x < hi` */
static randFloat32Array(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randFloat32Array(opts) }
/** Float64Array of `count` random numbers with 56 bits of randomness, `lo <= x < hi`. **NOTE: that's 56 bits of randomness, not 64 bits**. */
static randFloat64Array(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randFloat64Array(opts) }
// --
// == [static methods for randInts] -- stream of `count` integers, `lo <= x <= hi`]
/** Stream of `count` random integers, `lo <= x <= hi` */
static * intsStar(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); yield * factory.intsStar(opts) }
/** Array of `count` random integers, `lo <= x <= hi` */
static randInts(opts: HiLoCt, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randInts(opts) }
// --
// == [static methods for randUint32s] -- stream of `count` integers, `0 <= x < 2^32 - 1`]
/** Stream of `count` random integers, `0 <= x <= 2^32 - 1` */
static * uint32sStar(count: number, rngspec?: RNGSpec) { const factory = this.make(rngspec); yield * factory.uint32sStar(count) }
/** Array of `count` random integers, `0 <= x <= 2^32 - 1` */
static randUint32s(count: number, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randUint32s(count) }
/** Uint32Array of `count` random integers, `0 <= x < 2^32 - 1` */
static randUint32Array(count: number, rngspec?: RNGSpec) { const factory = this.make(rngspec); return factory.randUint32Array(count) }
// --
// == [Random Selection methods] --
* RandIndicesStar(items: readonly any[] | number, opts: { count: number }): Generator<number, undefined, number | undefined> {
const { count } = opts
const hi = (typeof items === 'number') ? items : items.length
for (const float of this.rand01sStar(count)) {
yield Math.floor(hi * float)
}
}
* RandChoicesStar<VT>(items: readonly VT[], opts: { count: number }): Generator<VT, undefined, VT | undefined> {
for (const index of this.RandIndicesStar(items, opts)) {
yield items[index]!
}
}
/** Naive sampling function storing the already picked values in a Set.
* Performance of this function will decrease dramatically when `k` is a
* high proportion of `n`.
*/
naiveSample<VT>(items: readonly VT[], opts: { count: number }): VT[] {
const { count } = opts
if (count >= items.length) { return Array.from(items) }
const seen = new Set<number>()
const result = [] as VT[]
for (const index of this.RandIndicesStar(items, { count: Infinity })) {
if (seen.has(index)) { continue }
seen.add(index)
result.push(items[index]!)
if (result.length >= count) { break }
}
return result
}
* TuplesStar<VT>(items: readonly VT[], opts: { count: number, itemLen: number}) {
const { count, itemLen } = opts
const generator = this.intsStar({ lo: 0, hi: items.length, count: count * itemLen })
for (let seq = 0; seq < count; seq++) {
const tuple = [] as VT[]
for (let pos = 0; pos < itemLen; pos++) {
tuple.push(items[generator.next().value!]!)
}
yield tuple
}
}
/** Stream of `count` random sequences, each having `lo <= len <= hi` elements from `items`; by default, `lo = 1` and `hi = lo + 9`. Note: lo and hi are **inclusive**. */
* SequencesStar<VT>(items: readonly VT[], opts: { count: number, lo?: number | undefined, hi: number }): Generator<VT[], undefined, VT[] | undefined> {
const tupleLens = this.intsStar({ ...opts, count: Infinity })
const elements = this.RandChoicesStar(items, { count: Infinity })
const { count } = opts
for (let seq = 0; seq < count; seq++) {
const tuple = [] as VT[]
const len = tupleLens.next().value!
for (let pos = 0; pos < len; pos++) {
tuple.push(elements.next().value!)
}
yield tuple
}
}
// --
// == [Accessories for randStrings] -- string built of `count` independently chosen entries from `dictionary`]
* RandStrsStar(substrings: readonly string[] = Consts.CharsAZ09Bar, opts: { count: number, itemLen: number}) {
for (const segs of this.TuplesStar(substrings, opts)) {
yield segs.join('')
}
}
* RandLenStrsStar(substrings: readonly string[] = Consts.CharsAZ09Bar, opts: { count: number, lo?: number | undefined, hi: number }) {
for (const segs of this.SequencesStar(substrings, opts)) {
yield segs.join('')
}
}
// --
}
export class RandomFactory extends BaseRandomFactory {
}
export class SeededRandomFactory extends RandomFactory {
declare readonly rng: PRNGenerator
constructor(seed: RNGSeed) {
const rng = SeededRNGFactory(seed)
super(rng.double)
Object.defineProperty(this, 'rng', { value: rng, configurable: true, enumerable: false })
}
static override make(seed: RNGSeed) { return new this(seed) }
static override rngFor(seed: RNGSeed) { return SeededRNGFactory(seed).double }
/** Random float, `0 <= x < 1`. @note: this makes a new factory for each call */
static override loose01(seed: RNGSeed) { return SeededRNGFactory(seed).quick() }
/** Random integer, `0 <= x <= 2^32 - 1`. @note: this makes a new factory for each call */
static override uint32(seed: RNGSeed) { return SeededRNGFactory(seed).int32() + SINT32_TO_UINT32 }
/** Random integer, `0 <= x <= 2^32 - 1`. @note: this makes a new factory for each call */
static override sint32(seed: RNGSeed) { return SeededRNGFactory(seed).int32() }
override * loose01sStar(count: number): Generator<number, undefined, number | undefined> {
for (let index = 0; index < count; index++) {
yield this.rng.quick()
}
}
override * loosesStar(opts: HiLoCt): Generator<number, undefined, number | undefined> {
const { lo = 0, hi = lo + 1, count } = opts
if (lo === 0 && hi === 1) { yield * this.loose01sStar(count); return }
const range = hi - lo
for (const num01 of this.loose01sStar(count)) { yield lo + (num01 * range) }
}
override * uint32sStar(count: number): Generator<number, undefined, number | undefined> {
for (let index = 0; index < count; index++) {
yield this.rng.int32()
}
}
}