├── .npmrc ├── .gitattributes ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── index.test-d.ts ├── license ├── package.json ├── index.js ├── index.d.ts ├── readme.md └── test.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | - 14 15 | - 12 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: actions/setup-node@v5 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import Randoma, {Options, Color} from './index.js'; 3 | 4 | const options: Options = {seed: 10}; 5 | 6 | const random = new Randoma(options); 7 | new Randoma({seed: '🦄'}); // eslint-disable-line no-new 8 | new Randoma({seed: Randoma.seed()}); // eslint-disable-line no-new 9 | 10 | expectType(random.integer()); 11 | expectType(random.integerInRange(0, 1)); 12 | expectType(random.float()); 13 | expectType(random.floatInRange(0, 1)); 14 | expectType(random.boolean()); 15 | expectType(random.arrayItem(['🦄'])); 16 | expectType(random.date()); 17 | expectType(random.dateInRange(new Date(), new Date())); 18 | expectType(random.color()); 19 | random 20 | .color(0.5) 21 | .hex() 22 | .toString(); 23 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "randoma", 3 | "version": "2.0.1", 4 | "description": "User-friendly pseudorandom number generator (PRNG)", 5 | "license": "MIT", 6 | "repository": "sindresorhus/randoma", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "types": "./index.d.ts", 16 | "sideEffects": false, 17 | "engines": { 18 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 19 | }, 20 | "scripts": { 21 | "//test": "xo && ava && tsd", 22 | "test": "ava && tsd" 23 | }, 24 | "files": [ 25 | "index.js", 26 | "index.d.ts" 27 | ], 28 | "keywords": [ 29 | "pseudorandom", 30 | "number", 31 | "generator", 32 | "algorithm", 33 | "random", 34 | "integer", 35 | "int", 36 | "float", 37 | "boolean", 38 | "date", 39 | "seed", 40 | "seeded", 41 | "prng", 42 | "rng" 43 | ], 44 | "dependencies": { 45 | "@sindresorhus/string-hash": "^2.0.0", 46 | "@types/color": "^3.0.2", 47 | "color": "^4.0.1", 48 | "park-miller": "^2.0.1" 49 | }, 50 | "devDependencies": { 51 | "@sindresorhus/is": "^4.2.0", 52 | "ava": "^3.15.0", 53 | "tsd": "^0.18.0", 54 | "xo": "^0.46.4" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import ParkMiller from 'park-miller'; 2 | import stringHash from '@sindresorhus/string-hash'; 3 | import color from 'color'; 4 | 5 | const MAX_INT32 = 2_147_483_647; 6 | const GOLDEN_RATIO_CONJUGATE = 0.618_033_988_749_895; 7 | 8 | export default class Randoma { 9 | static seed() { 10 | return Math.floor(Math.random() * MAX_INT32); 11 | } 12 | 13 | #random; 14 | 15 | constructor({seed}) { 16 | if (typeof seed === 'string') { 17 | seed = stringHash(seed); 18 | } 19 | 20 | if (!Number.isInteger(seed)) { 21 | throw new TypeError('Expected `seed` to be a `integer`'); 22 | } 23 | 24 | this.#random = new ParkMiller(seed); 25 | } 26 | 27 | integer() { 28 | return this.#random.integer(); 29 | } 30 | 31 | integerInRange(minimum, maximum) { 32 | return this.#random.integerInRange(minimum, maximum); 33 | } 34 | 35 | float() { 36 | return this.#random.float(); 37 | } 38 | 39 | floatInRange(minimum, maximum) { 40 | return this.#random.floatInRange(minimum, maximum); 41 | } 42 | 43 | boolean() { 44 | return this.#random.boolean(); 45 | } 46 | 47 | arrayItem(array) { 48 | return array[Math.floor(this.float() * array.length)]; 49 | } 50 | 51 | date() { 52 | return new Date(Date.now() * this.float()); 53 | } 54 | 55 | dateInRange(startDate, endDate) { 56 | return new Date(this.integerInRange(startDate.getTime(), endDate.getTime())); 57 | } 58 | 59 | color(saturation = 0.5) { 60 | // Advance the generator once to avoid poor distribution of first values from fresh seeds 61 | this.#random.float(); 62 | 63 | let hue = this.float(); 64 | hue += GOLDEN_RATIO_CONJUGATE; 65 | hue %= 1; 66 | 67 | return color({ 68 | h: hue * 360, 69 | s: saturation * 100, 70 | v: 95, 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import Color from 'color'; 2 | 3 | export interface Options { 4 | /** 5 | [Initialization seed.](https://en.wikipedia.org/wiki/Random_seed) 6 | 7 | Multiple instances of `Randoma` with the same seed will generate the same random numbers. 8 | */ 9 | readonly seed: string | number; 10 | } 11 | 12 | export default class Randoma { 13 | /** 14 | @returns A random seed you could use in the `seed` option if you for some reason don't want deterministic randomness. 15 | */ 16 | static seed(): number; 17 | 18 | /** 19 | User-friendly [pseudorandom number generator (PRNG)](https://en.wikipedia.org/wiki/Pseudorandom_number_generator). 20 | 21 | This is not cryptographically secure. 22 | 23 | @example 24 | ``` 25 | import Randoma from 'randoma'; 26 | 27 | const random = new Randoma({seed: 10}); 28 | 29 | random.integer(); 30 | //=> 2027521326 31 | 32 | random.integer(); 33 | //=> 677268843 34 | 35 | (new Randoma({seed: '🦄'}).integer()); 36 | //=> 1659974344 37 | 38 | (new Randoma({seed: '🦄'}).integer()); 39 | //=> 1659974344 40 | ``` 41 | */ 42 | constructor(options: Options); 43 | 44 | integer(): number; 45 | integerInRange(minimum: number, maximum: number): number; 46 | float(): number; 47 | floatInRange(minimum: number, maximum: number): number; 48 | boolean(): boolean; 49 | arrayItem(array: readonly T[]): T; 50 | date(): Date; 51 | dateInRange(startDate: Date, endDate: Date): Date; 52 | 53 | /** 54 | @param saturation - A percentage in the range `0...1`. Default: `0.5`. 55 | @returns A random [aesthetically pleasing color](https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/) as a [`color`](https://github.com/Qix-/color) object. 56 | 57 | @example 58 | ``` 59 | random.color(0.5).hex().toString() 60 | //=> '#AAF2B0' 61 | ``` 62 | */ 63 | color(saturation?: number): Color; 64 | } 65 | 66 | export {Color}; 67 | 68 | // TODO: When `color` package is ESM. 69 | // export {default as Color} from 'color'; 70 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # randoma 2 | 3 | > User-friendly [pseudorandom number generator (PRNG)](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) 4 | 5 | This is not cryptographically secure. 6 | 7 | *“Pull request welcome” for additional commonly used random methods.* 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install randoma 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```js 18 | import Randoma from 'randoma'; 19 | 20 | const random = new Randoma({seed: 10}); 21 | 22 | random.integer(); 23 | //=> 2027521326 24 | 25 | random.integer(); 26 | //=> 677268843 27 | 28 | 29 | (new Randoma({seed: '🦄'}).integer()); 30 | //=> 1659974344 31 | 32 | (new Randoma({seed: '🦄'}).integer()); 33 | //=> 1659974344 34 | ``` 35 | 36 | ## API 37 | 38 | ### `const random = new Randoma(options)` 39 | 40 | #### options 41 | 42 | Type: `object` 43 | 44 | ##### seed 45 | 46 | *Required*\ 47 | Type: `string | number` 48 | 49 | [Initialization seed.](https://en.m.wikipedia.org/wiki/Random_seed) 50 | 51 | Multiple instances of `Randoma` with the same seed will generate the same random numbers. 52 | 53 | #### random.integer() 54 | #### random.integerInRange(minimum, maximum) 55 | #### random.float() 56 | #### random.floatInRange(minimum, maximum) 57 | #### random.boolean() 58 | #### random.arrayItem(array) 59 | #### random.date() 60 | #### random.dateInRange(startDate, endDate) 61 | 62 | #### random.color(saturation?) 63 | 64 | Returns a random [aesthetically pleasing color](https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/) as a [`color`](https://github.com/Qix-/color) object. 65 | 66 | ```js 67 | random.color(0.5).hex().toString() 68 | //=> '#AAF2B0' 69 | ``` 70 | 71 | ##### saturation 72 | 73 | Type: `number`\ 74 | Default: `0.5` 75 | 76 | A percentage in the range `0...1`. 77 | 78 | ### Randoma.seed() 79 | 80 | Returns a random seed you could use in the `seed` option if you for some reason don't want deterministic randomness. 81 | 82 | ## Related 83 | 84 | - [park-miller](https://github.com/sindresorhus/park-miller) - Park-Miller pseudorandom number generator (PRNG) 85 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import is from '@sindresorhus/is'; 3 | import Randoma from './index.js'; 4 | 5 | const MAX_INT32 = 2_147_483_647; 6 | const ITERATIONS = 1000; 7 | 8 | function assertInteger(t, seed) { 9 | const random = new Randoma({seed}); 10 | 11 | for (let index = 0; index < ITERATIONS; index++) { 12 | const result = random.integer(); 13 | t.true(is.integer(result)); 14 | t.true(result <= MAX_INT32); 15 | } 16 | } 17 | 18 | function assertIntegerInRange(t, seed) { 19 | const random = new Randoma({seed}); 20 | const min = 33; 21 | const max = 2242; 22 | 23 | for (let index = 0; index < ITERATIONS; index++) { 24 | const result = random.integerInRange(min, max); 25 | t.true(is.integer(result)); 26 | t.true(result >= min); 27 | t.true(result <= max); 28 | } 29 | } 30 | 31 | function assertFloat(t, seed) { 32 | const random = new Randoma({seed}); 33 | 34 | for (let index = 0; index < ITERATIONS; index++) { 35 | const result = random.float(); 36 | t.false(is.integer(result)); 37 | t.true(result <= MAX_INT32); 38 | } 39 | } 40 | 41 | function assertFloatInRange(t, seed) { 42 | const random = new Randoma({seed}); 43 | const min = 0.33; 44 | const max = 0.522_42; 45 | 46 | for (let index = 0; index < ITERATIONS; index++) { 47 | const result = random.floatInRange(min, max); 48 | t.false(is.integer(result)); 49 | t.true(result >= min); 50 | t.true(result <= max); 51 | } 52 | } 53 | 54 | function assertBoolean(t, seed) { 55 | const random = new Randoma({seed}); 56 | let average = 0; 57 | 58 | for (let index = 0; index < ITERATIONS; index++) { 59 | const result = random.boolean(); 60 | t.true(is.boolean(result)); 61 | average += result ? 1 : -1; 62 | } 63 | 64 | t.true(average < 10_000); 65 | } 66 | 67 | function assertArrayItem(t, seed) { 68 | const random = new Randoma({seed}); 69 | const fixture = [1, 2, 3, 4, 5]; 70 | const set = new Set(); 71 | 72 | for (let index = 0; index < ITERATIONS; index++) { 73 | const result = random.arrayItem(fixture); 74 | t.true(is.number(result)); 75 | set.add(result); 76 | } 77 | 78 | t.deepEqual([...set].sort(), fixture); 79 | } 80 | 81 | function assertDate(t, seed) { 82 | const random = new Randoma({seed}); 83 | 84 | for (let index = 0; index < ITERATIONS; index++) { 85 | const result = random.date(); 86 | t.true(is.date(result)); 87 | t.true(is.function(result.getTime)); 88 | } 89 | } 90 | 91 | function assertDateInRange(t, seed) { 92 | const random = new Randoma({seed}); 93 | const startDate = new Date('2009'); 94 | const endDate = new Date('2010'); 95 | 96 | for (let index = 0; index < ITERATIONS; index++) { 97 | const result = random.dateInRange(startDate, endDate); 98 | t.true(is.date(result)); 99 | t.true(result >= startDate); 100 | t.true(result <= endDate); 101 | } 102 | } 103 | 104 | function runFn(fn) { 105 | const random = new Randoma({seed: 33}); 106 | const values = []; 107 | 108 | for (let index = 0; index < ITERATIONS; index++) { 109 | values.push(random[fn]()); 110 | } 111 | 112 | return values; 113 | } 114 | 115 | function runAsserts(t, fn, assertFn) { 116 | const seeds = [ 117 | 0, 118 | 1, 119 | 10, 120 | -10, 121 | Number.MIN_SAFE_INTEGER, 122 | Number.MAX_SAFE_INTEGER, 123 | ]; 124 | 125 | for (const seed of seeds) { 126 | assertFn(t, seed); 127 | } 128 | 129 | if (['integer', 'float', 'boolean'].includes(fn)) { 130 | // Ensure it generates numbers deterministically 131 | t.deepEqual(runFn(fn), runFn(fn)); 132 | t.deepEqual(runFn(fn), runFn(fn)); 133 | } 134 | } 135 | 136 | test('.integer()', t => { 137 | runAsserts(t, 'integer', assertInteger); 138 | }); 139 | 140 | test('.integerInRange()', t => { 141 | runAsserts(t, 'integerInRange', assertIntegerInRange); 142 | }); 143 | 144 | test('.float()', t => { 145 | runAsserts(t, 'float', assertFloat); 146 | }); 147 | 148 | test('.floatInRange()', t => { 149 | runAsserts(t, 'floatInRange', assertFloatInRange); 150 | }); 151 | 152 | test('.boolean()', t => { 153 | runAsserts(t, 'boolean', assertBoolean); 154 | }); 155 | 156 | test('.arrayItem()', t => { 157 | runAsserts(t, 'arrayItem', assertArrayItem); 158 | }); 159 | 160 | test('.date()', t => { 161 | runAsserts(t, 'date', assertDate); 162 | }); 163 | 164 | test('.dateInRange()', t => { 165 | runAsserts(t, 'dateInRange', assertDateInRange); 166 | }); 167 | 168 | test('.color()', t => { 169 | const random = new Randoma({seed: 1}); 170 | t.is(random.color(0.5).hex().toString(), '#B579F2'); 171 | }); 172 | 173 | test('.color() distribution', t => { 174 | // Test that first colors from different seeds have good distribution 175 | const colors = []; 176 | const seedCount = 1000; 177 | 178 | for (let seed = 0; seed < seedCount; seed++) { 179 | const random = new Randoma({seed}); 180 | colors.push(random.color().hex()); 181 | } 182 | 183 | const uniqueColors = new Set(colors); 184 | // Should have significantly more unique colors than the old buggy behavior (~57) 185 | // With the fix, we get ~685/1000 (68.5%), which is much better 186 | t.true(uniqueColors.size > 600); // Should have at least 600 unique colors out of 1000 187 | }); 188 | 189 | test('string seed', t => { 190 | const seed = '🦄'; 191 | 192 | const random = new Randoma({seed}); 193 | t.not(random.integer(), random.integer()); 194 | 195 | t.is( 196 | (new Randoma({seed})).integer(), 197 | (new Randoma({seed})).integer(), 198 | ); 199 | }); 200 | 201 | test('Randoma.seed()', t => { 202 | const seed = Randoma.seed(); 203 | t.true(Number.isInteger(seed)); 204 | 205 | const random = new Randoma({seed}); 206 | t.not(random.integer(), random.integer()); 207 | }); 208 | --------------------------------------------------------------------------------