├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── assert-in-range.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.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 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /assert-in-range.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import inRange from 'in-range'; 3 | 4 | export default function assertInRange(t, value, {start = 0, end}) { 5 | t.true( 6 | inRange(value, {start, end}), 7 | `${start} ${start <= value ? '≤' : chalk.red('≰')} ${chalk.yellow(value)} ${value <= end ? '≤' : chalk.red('≰')} ${end}`, 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Generate random numbers that are consecutively unique, meaning that each number in the sequence is distinct from the one immediately before it. 3 | 4 | @returns A function, that when called, will return a random number that is never the same as the previous. 5 | 6 | @example 7 | ``` 8 | import {consecutiveUniqueRandom} from 'unique-random'; 9 | 10 | const random = consecutiveUniqueRandom(1, 10); 11 | 12 | console.log(random(), random(), random()); 13 | //=> 5 2 6 14 | ``` 15 | 16 | The returned function is also an iterable which consumes from the same source as the function: 17 | 18 | @example 19 | ``` 20 | import {consecutiveUniqueRandom} from 'unique-random'; 21 | 22 | const random = consecutiveUniqueRandom(1, 10); 23 | 24 | for (const number of random) { 25 | console.log(number); 26 | 27 | // The unique numbers will be iterated over infinitely 28 | if (stopCondition) { 29 | break; 30 | } 31 | } 32 | ``` 33 | 34 | Note: If `minimum` is equal to `maximum`, the same value will always be returned. 35 | */ 36 | export function consecutiveUniqueRandom(minimum: number, maximum: number): (() => number) & {[Symbol.iterator](): Iterator}; 37 | 38 | /** 39 | Generate random numbers that do not repeat until the entire range has appeared. 40 | 41 | @returns A function, that when called, will return a random number that is never the same as any previously returned until the entire range of possible numbers has been returned. 42 | 43 | @example 44 | ``` 45 | import {exhaustiveUniqueRandom} from 'unique-random'; 46 | 47 | const random = exhaustiveUniqueRandom(1, 10); 48 | 49 | console.log(random(), random(), random()); 50 | //=> 5 2 6 51 | ``` 52 | 53 | The returned function is also an iterable which consumes from the same source as the function: 54 | 55 | @example 56 | ``` 57 | import {exhaustiveUniqueRandom} from 'unique-random'; 58 | 59 | const random = exhaustiveUniqueRandom(1, 10); 60 | 61 | for (const number of random) { 62 | console.log(number); 63 | 64 | // The unique numbers will be iterated over infinitely 65 | if (stopCondition) { 66 | break; 67 | } 68 | } 69 | ``` 70 | 71 | Note: If `minimum` is equal to `maximum`, the same value will always be returned. 72 | */ 73 | export function exhaustiveUniqueRandom(minimum: number, maximum: number): (() => number) & {[Symbol.iterator](): Iterator}; 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function * range(minimum, maximum) { 2 | for (let number = minimum; number <= maximum; number++) { 3 | yield number; 4 | } 5 | } 6 | 7 | function randomInteger(minimum, maximum) { 8 | return Math.floor(Math.random() * (maximum - minimum + 1)) + minimum; 9 | } 10 | 11 | function randomIntegerWithout(minimum, maximum, excludedValue) { 12 | const number = randomInteger(minimum, maximum - 1); 13 | 14 | return number >= excludedValue ? number + 1 : number; 15 | } 16 | 17 | function makeCallable(generator) { 18 | const iterator = generator(); 19 | 20 | function random() { 21 | return iterator.next().value; 22 | } 23 | 24 | random[Symbol.iterator] = function * () { 25 | while (true) { 26 | yield random(); 27 | } 28 | }; 29 | 30 | return random; 31 | } 32 | 33 | export function consecutiveUniqueRandom(minimum, maximum) { 34 | return makeCallable(function * () { 35 | if (minimum === maximum) { 36 | while (true) { 37 | yield minimum; 38 | } 39 | } 40 | 41 | let previousValue = randomInteger(minimum, maximum); 42 | yield previousValue; 43 | 44 | while (true) { 45 | previousValue = randomIntegerWithout(minimum, maximum, previousValue); 46 | yield previousValue; 47 | } 48 | }); 49 | } 50 | 51 | export function exhaustiveUniqueRandom(minimum, maximum) { 52 | return makeCallable(function * () { 53 | if (minimum === maximum) { 54 | while (true) { 55 | yield minimum; 56 | } 57 | } 58 | 59 | let unconsumedValues = [...range(minimum, maximum)]; 60 | 61 | while (true) { 62 | while (unconsumedValues.length > 1) { 63 | yield unconsumedValues.splice( 64 | randomInteger(0, unconsumedValues.length - 1), 65 | 1, 66 | )[0]; 67 | } 68 | 69 | const [previousValue] = unconsumedValues; 70 | 71 | yield previousValue; 72 | 73 | unconsumedValues = [...range(minimum, maximum)]; 74 | 75 | yield unconsumedValues.splice( 76 | randomIntegerWithout(0, unconsumedValues.length - 1, previousValue - minimum), 77 | 1, 78 | )[0]; 79 | } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectAssignable, expectType} from 'tsd'; 2 | import {consecutiveUniqueRandom, exhaustiveUniqueRandom} from './index.js'; 3 | 4 | const random1 = consecutiveUniqueRandom(1, 10); 5 | 6 | expectAssignable<() => number>(random1); 7 | expectType(random1()); 8 | 9 | for (const number of random1) { 10 | expectType(number); 11 | } 12 | 13 | const random2 = exhaustiveUniqueRandom(1, 10); 14 | 15 | expectAssignable<() => number>(random2); 16 | expectType(random2()); 17 | 18 | for (const number of random2) { 19 | expectType(number); 20 | } 21 | -------------------------------------------------------------------------------- /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": "unique-random", 3 | "version": "4.0.0", 4 | "description": "Generate random numbers that are consecutively unique", 5 | "license": "MIT", 6 | "repository": "sindresorhus/unique-random", 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": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "unique", 31 | "random", 32 | "number", 33 | "single", 34 | "generate", 35 | "non-repeating", 36 | "consecutively", 37 | "iterable", 38 | "iterator", 39 | "generator" 40 | ], 41 | "devDependencies": { 42 | "ava": "^6.1.2", 43 | "chalk": "^5.3.0", 44 | "in-range": "^3.0.0", 45 | "tsd": "^0.31.0", 46 | "xo": "^0.58.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # unique-random 2 | 3 | > Generate random numbers that are consecutively unique 4 | 5 | Useful for things like slideshows where you don't want to have the same slide twice in a row. 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install unique-random 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import {consecutiveUniqueRandom} from 'unique-random'; 17 | 18 | const random = consecutiveUniqueRandom(1, 10); 19 | 20 | console.log(random(), random(), random()); 21 | //=> 5 2 6 22 | ``` 23 | 24 | ## API 25 | 26 | ### consecutiveUniqueRandom(minimum, maximum) 27 | 28 | Generate random numbers that are consecutively unique, meaning that each number in the sequence is distinct from the one immediately before it. 29 | 30 | ### exhaustiveUniqueRandom(minimum, maximum) 31 | 32 | Generate random numbers that do not repeat until the entire range has appeared. 33 | 34 | ### `consecutiveUniqueRandom` and `exhaustiveUniqueRandom` 35 | 36 | Returns a function, that when called, will return the generated number. 37 | 38 | The returned function is also an iterable which consumes from the same source as the function: 39 | 40 | ```js 41 | import {exhaustiveUniqueRandom} from 'unique-random'; 42 | 43 | const random = exhaustiveUniqueRandom(1, 10); 44 | 45 | for (const number of random) { 46 | console.log(number); 47 | 48 | // The unique numbers will be iterated over infinitely 49 | if (stopCondition) { 50 | break; 51 | } 52 | } 53 | ``` 54 | 55 | > [!NOTE] 56 | > If `minimum` is equal to `maximum`, the same value will always be returned. 57 | 58 | ## Related 59 | 60 | - [unique-random-array](https://github.com/sindresorhus/unique-random-array) - Get consecutively unique elements from an array 61 | - [random-int](https://github.com/sindresorhus/random-int) - Generate a random integer 62 | - [random-float](https://github.com/sindresorhus/random-float) - Generate a random float 63 | - [random-item](https://github.com/sindresorhus/random-item) - Get a random item from an array 64 | - [random-obj-key](https://github.com/sindresorhus/random-obj-key) - Get a random key from an object 65 | - [random-obj-prop](https://github.com/sindresorhus/random-obj-prop) - Get a random property from an object 66 | - [unique-random-at-depth](https://github.com/Aweary/unique-random-at-depth) - This module with an optional depth argument 67 | - [crypto-random-string](https://github.com/sindresorhus/crypto-random-string) - Generate a cryptographically strong random string 68 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import assertInRange from './assert-in-range.js'; 3 | import {consecutiveUniqueRandom, exhaustiveUniqueRandom} from './index.js'; 4 | 5 | function testConsecutiveUniqueness(t, uniqueRandom) { 6 | const random = uniqueRandom(1, 10); 7 | let previousValue; 8 | 9 | for (let count = 0; count < 1000; count++) { 10 | const currentValue = random(); 11 | 12 | assertInRange(t, currentValue, {start: 1, end: 10}); 13 | if (previousValue !== undefined) { 14 | t.not(currentValue, previousValue); 15 | } 16 | 17 | previousValue = currentValue; 18 | } 19 | } 20 | 21 | test('consecutiveUniqueRandom - main', t => { 22 | testConsecutiveUniqueness(t, consecutiveUniqueRandom); 23 | }); 24 | 25 | test('consecutiveUniqueRandom - iterator', t => { 26 | t.plan(3); // In case the for-of loop doesn't run 27 | 28 | const random = consecutiveUniqueRandom(1, 10); 29 | 30 | for (const number of random) { // eslint-disable-line no-unreachable-loop 31 | assertInRange(t, number, {start: 1, end: 10}); 32 | break; 33 | } 34 | 35 | const {value, done} = random[Symbol.iterator]().next(); 36 | 37 | assertInRange(t, value, {start: 1, end: 10}); 38 | t.false(done); 39 | }); 40 | 41 | test('exhaustiveUniqueRandom - main', t => { 42 | const random = exhaustiveUniqueRandom(1, 5); 43 | const seenValuesCount = new Map(Array.from({length: 5}, (_, index) => [index + 1, 0])); 44 | 45 | for (let count = 1; count <= 10; count++) { 46 | const value = random(); 47 | assertInRange(t, value, {start: 1, end: 5}); 48 | t.true(seenValuesCount.get(value) < 2, 'Value should only appear twice'); 49 | seenValuesCount.set(value, seenValuesCount.get(value) + 1); 50 | } 51 | }); 52 | 53 | test('exhaustiveUniqueRandom - consecutive uniqueness', t => { 54 | testConsecutiveUniqueness(t, exhaustiveUniqueRandom); 55 | }); 56 | 57 | test('exhaustiveUniqueRandom - iterator', t => { 58 | t.plan(3); // In case the for-of loop doesn't run 59 | 60 | const random = exhaustiveUniqueRandom(1, 10); 61 | 62 | for (const number of random) { // eslint-disable-line no-unreachable-loop 63 | assertInRange(t, number, {start: 1, end: 10}); 64 | break; 65 | } 66 | 67 | const {value, done} = random[Symbol.iterator]().next(); 68 | 69 | assertInRange(t, value, {start: 1, end: 10}); 70 | t.false(done); 71 | }); 72 | 73 | test('consecutiveUniqueRandom - ensures maximum value can appear', t => { 74 | const minimum = 1; 75 | const maximum = 2; // Reduced range to increase the chance of seeing both values quickly. 76 | const random = consecutiveUniqueRandom(minimum, maximum); 77 | const seenValues = new Set(); 78 | 79 | for (let count = 0; count < 1000; count++) { 80 | const value = random(); 81 | seenValues.add(value); 82 | if (seenValues.size === (maximum - minimum + 1)) { 83 | break; 84 | } // Exit early if all possible values have been seen. 85 | } 86 | 87 | t.is(seenValues.size, maximum - minimum + 1, 'Should be able to produce all values in the range'); 88 | }); 89 | 90 | test('consecutiveUniqueRandom - edge case minimum equals maximum', t => { 91 | const random = consecutiveUniqueRandom(5, 5); 92 | for (let count = 0; count < 100; count++) { 93 | t.is(random(), 5); 94 | } 95 | }); 96 | 97 | test('exhaustiveUniqueRandom - edge case minimum equals maximum', t => { 98 | const random = exhaustiveUniqueRandom(5, 5); 99 | for (let count = 0; count < 100; count++) { 100 | t.is(random(), 5); 101 | } 102 | }); 103 | 104 | test('exhaustiveUniqueRandom - resets after full cycle', t => { 105 | const rangeSize = 5; 106 | const random = exhaustiveUniqueRandom(1, rangeSize); 107 | const seenValues = new Set(); 108 | 109 | for (let count = 0; count < rangeSize * 2; count++) { 110 | const value = random(); 111 | assertInRange(t, value, {start: 1, end: rangeSize}); 112 | 113 | if (seenValues.has(value)) { 114 | // We expect a reset to happen here, clearing the seen values 115 | seenValues.clear(); 116 | } 117 | 118 | seenValues.add(value); 119 | } 120 | 121 | t.is(seenValues.size, rangeSize, 'Generator did not properly reset after exhausting unique values.'); 122 | }); 123 | --------------------------------------------------------------------------------