├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── choice.d.ts ├── choice.js ├── dangerously-mutating-sample.d.ts ├── dangerously-mutating-sample.js ├── experiments ├── fisher-yates-robustness.js ├── random-pair-sampling.js └── random-pairs.js ├── fisher-yates-permutation.d.ts ├── fisher-yates-permutation.js ├── fisher-yates-sample.d.ts ├── fisher-yates-sample.js ├── geometric-reservoir-sample.d.ts ├── geometric-reservoir-sample.js ├── index.d.ts ├── index.js ├── naive-sample.d.ts ├── naive-sample.js ├── package-lock.json ├── package.json ├── random-boolean.d.ts ├── random-boolean.js ├── random-float.d.ts ├── random-float.js ├── random-index.d.ts ├── random-index.js ├── random-ordered-pair.d.ts ├── random-ordered-pair.js ├── random-pair.d.ts ├── random-pair.js ├── random-string.d.ts ├── random-string.js ├── random-typed-int.d.ts ├── random-typed-int.js ├── random.d.ts ├── random.js ├── reservoir-sample.d.ts ├── reservoir-sample.js ├── sample-ordered-pairs.d.ts ├── sample-ordered-pairs.js ├── sample-pairs.d.ts ├── sample-pairs.js ├── sample-with-replacements.d.ts ├── sample-with-replacements.js ├── shuffle-in-place.d.ts ├── shuffle-in-place.js ├── shuffle.d.ts ├── shuffle.js ├── test-types.ts ├── test.js ├── types.d.ts ├── utils.d.ts ├── utils.js ├── weighted-choice.d.ts ├── weighted-choice.js ├── weighted-random-index.d.ts ├── weighted-random-index.js ├── weighted-reservoir-sample.d.ts └── weighted-reservoir-sample.js /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | matrix: 9 | node-version: 10 | - 12.x 11 | - 13.x 12 | - 14.x 13 | - 15.x 14 | - 16.x 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Set up Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install, Lint & Test 27 | run: | 28 | npm i -g npm 29 | npm i 30 | npm run prepublishOnly 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .vscode 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | experiments 3 | node_modules 4 | test.js 5 | test-types.ts 6 | .vscode 7 | .github 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.4.1 4 | 5 | - Renaming undocumented `FisherYatesPermutation.shrinkAndReset` to `FisherYatesPermutation.shrink`. 6 | 7 | ## 2.4.0 8 | 9 | - Adding `randomUint32`. 10 | - Adding undocumented `FisherYatesPermutation`. 11 | 12 | ## 2.3.0 13 | 14 | - Adding `weightedReservoirSample`. 15 | - Adding `samplePairs`. 16 | - Adding `sampleOrderedPairs`. 17 | 18 | ## 2.2.0 19 | 20 | - Adding `randomPair`. 21 | - Adding `randomOrderedPair`. 22 | - Improving `randomIndex` performance. 23 | - Improving `sampleWithReplacements` performance. 24 | - Improving `geometricReservoirSample` performance. 25 | 26 | ## 2.1.0 27 | 28 | - Adding `randomBoolean`. 29 | - Adding `ReservoirSampler`. 30 | - `geometricReservoirSample` can now take a length. 31 | - Fixing `naiveSample`. 32 | - Fixing type declarations. 33 | - Refactoring root-level exports. 34 | - Harmonizing `k` as sample size argument across the library. 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2020 Guillaume Plique (Yomguithereal) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/Yomguithereal/pandemonium/workflows/Tests/badge.svg)](https://github.com/Yomguithereal/pandemonium/actions) 2 | 3 | # Pandemonium 4 | 5 | Pandemonium is a dead simple JavaScript/TypeScript library providing typical random-related functions such as `choice`, `sample` etc. 6 | 7 | The library also provides a way to create any of the available functions using a custom random source ([seedrandom](https://www.npmjs.com/package/seedrandom), for instance). 8 | 9 | # Installation 10 | 11 | ``` 12 | npm install --save pandemonium 13 | ``` 14 | 15 | # Usage 16 | 17 | ## Summary 18 | 19 | _Typical helpers_ 20 | 21 | - [choice](#choice) 22 | - [random](#random) 23 | - [randomBoolean](#randomboolean) 24 | - [randomFloat](#randomfloat) 25 | - [randomIndex](#randomindex) 26 | - [randomString](#randomstring) 27 | - [randomUint32](#randomuint32) 28 | - [randomPair](#randompair) 29 | - [randomOrderedPair](#randomorderedpair) 30 | - [shuffle](#shuffle) 31 | - [shuffleInPlace](#shuffleinplace) 32 | - [weightedChoice](#weightedchoice) 33 | - [weightedRandomIndex](#weightedrandomindex) 34 | 35 | _Sampling_ 36 | 37 | `n` being the number of items in the sampled sequence and `k` being the number of items to be sampled. 38 | 39 | | Method | Time | Memory | Note | 40 | | ------------------------------------------------------- | --------------- | ------ | --------------------------------------------------------------------------- | 41 | | [dangerouslyMutatingSample](#dangerouslymutatingsample) | `O(k)` | `O(k)` | Must be able to mutate given array to work. | 42 | | [fisherYatesSample](#fisheryatessample) | `O(n)` | `O(n)` | Probably not a good idea. | 43 | | [geometricReservoirSample](#geometricreservoirsample) | `O(k*log(n/k))` | `O(k)` | Probably the best way of sampling from a random access data structure. | 44 | | [naiveSample](#naivesample) | `Ω(k)`, `O(∞)` | `O(k)` | Only useful if `k << n`. | 45 | | [reservoirSample](#reservoirsample) | `O(n)` | `O(k)` | Useful if pulling a sample from a stream. | 46 | | [sampleWithReplacements](#samplewithreplacements) | `O(k)` | `O(k)` | Performant but allows replacements. | 47 | | [samplePairs](#samplepairs) | `Ω(k)`, `O(∞)` | `O(k)` | Variant of [naiveSample](#naivesample) for unordered pairs. | 48 | | [sampleOrderedPairs](#sampleorderedpairs) | `Ω(k)`, `O(∞)` | `O(k)` | Variant of [naiveSample](#naivesample) for ordered pairs. | 49 | | [weightedReservoirSample](#weightedreservoirsample) | `O(n)` | `O(k)` | Variant of [reservoirSample](#reservoirsample) working with weighted items. | 50 | 51 | ## choice 52 | 53 | Function returning a random item from the given array. 54 | 55 | ```js 56 | import choice from 'pandemonium/choice'; 57 | // Or 58 | import {choice} from 'pandemonium'; 59 | 60 | choice(['apple', 'orange', 'pear']); 61 | >>> 'orange' 62 | 63 | // To create your own function using custom RNG 64 | import {createChoice} from 'pandemonium/choice'; 65 | 66 | const customChoice = createChoice(rng); 67 | ``` 68 | 69 | ## random 70 | 71 | Function returning a random integer between given `a` & `b`. 72 | 73 | ```js 74 | import random from 'pandemonium/random'; 75 | // Or 76 | import {random} from 'pandemonium'; 77 | 78 | random(3, 7); 79 | >>> 4 80 | 81 | // To create your own function using custom RNG 82 | import {createRandom} from 'pandemonium/random'; 83 | 84 | const customRandom = createRandom(rng); 85 | ``` 86 | 87 | ## randomBoolean 88 | 89 | Function returning a random boolean. 90 | 91 | ```js 92 | import randomBoolean from 'pandemonium/random-boolean'; 93 | // Or 94 | import {randomBoolean} from 'pandemonium'; 95 | 96 | randomBoolean(); 97 | >>> true 98 | 99 | // To create your own function using custom RNG 100 | import {createrandomBoolean} from 'pandemonium/random-boolean'; 101 | 102 | const customRandomBoolean = createRandomBoolean(rng); 103 | ``` 104 | 105 | ## randomFloat 106 | 107 | Function returning a random float between given `a` & `b`. 108 | 109 | ```js 110 | import randomFloat from 'pandemonium/random-float'; 111 | // Or 112 | import {randomFloat} from 'pandemonium'; 113 | 114 | randomFloat(-5, 55); 115 | >>> 6.756482 116 | 117 | // To create your own function using custom RNG 118 | import {createRandomFloat} from 'pandemonium/random-float'; 119 | 120 | const customRandomFloat = createRandomFloat(rng); 121 | ``` 122 | 123 | ## randomIndex 124 | 125 | Function returning a random index of the given array. 126 | 127 | ```js 128 | import randomIndex from 'pandemonium/random-index'; 129 | // Or 130 | import {randomIndex} from 'pandemonium'; 131 | 132 | randomIndex(['apple', 'orange', 'pear']); 133 | >>> 1 134 | 135 | // Alternatively, you can give the array's length instead 136 | randomIndex(3); 137 | >>> 2 138 | 139 | // To create your own function using custom RNG 140 | import {createRandomIndex} from 'pandemonium/random-index'; 141 | 142 | const customRandomIndex = createRandomIndex(rng); 143 | ``` 144 | 145 | ## randomString 146 | 147 | Function returning a random string. 148 | 149 | ```js 150 | import randomString from 'pandemonium/random-string'; 151 | // Or 152 | import {randomString} from 'pandemonium'; 153 | 154 | // To generate a string of fixed length 155 | randomString(5); 156 | >>> 'gHepM' 157 | 158 | // To generate a string of variable length 159 | randomString(3, 7); 160 | >>> 'hySf3' 161 | 162 | // To create your own function using custom RNG 163 | import {createRandomString} from 'pandemonium/random-string'; 164 | 165 | const customRandomString = createRandomString(rng); 166 | 167 | // If you need a custom alphabet 168 | const customRandomString = createRandomString(rng, 'ATGC'); 169 | ``` 170 | 171 | ## randomUint32 172 | 173 | Function returning a random unsigned 32bits number. 174 | 175 | ```js 176 | import {randomUint32} from 'pandemonium/random-typed-int'; 177 | // Or 178 | import {randomUint32} from 'pandemonium'; 179 | 180 | randomUint32(); 181 | >>> 397536 182 | 183 | // To create your own function using custom RNG 184 | import {createRandomUint32} from 'pandemonium/random-typed-int'; 185 | 186 | const customRandomUint32 = createRandomUint32(rng); 187 | ``` 188 | 189 | ## randomPair 190 | 191 | Function returning a random pair from the given array. 192 | 193 | Note that this function will return unordered pairs (i.e. `[0, 1]` and `[1, 0]` are to be considered the same) and will not return pairs containing twice the same item (i.e. `[0, 0]`). 194 | 195 | ```js 196 | import randomPair from 'pandemonium/random-pair'; 197 | // Or 198 | import {randomPair} from 'pandemonium'; 199 | 200 | randomPair(['apple', 'orange', 'pear', 'cherry']); 201 | >>> ['orange', 'cherry'] 202 | 203 | // Alternatively, you can give the array's length instead and get a pair of indices 204 | randomPair(4); 205 | >>> [1, 3] 206 | 207 | // To create your own function using custom RNG 208 | import {createRandomPair} from 'pandemonium/random-pair'; 209 | 210 | const customRandomPair = createRandomPair(rng); 211 | ``` 212 | 213 | ## randomOrderedPair 214 | 215 | Function returning a random ordered pair (i.e. `[0, 1]` won't be considered to be the same as `[1, 0]`) from the given array. 216 | 217 | ```js 218 | import randomOrderedPair from 'pandemonium/random-ordered-pair'; 219 | // Or 220 | import {randomOrderedPair} from 'pandemonium'; 221 | 222 | randomOrderedPair(['apple', 'orange', 'pear', 'cherry']); 223 | >>> ['cherry', 'apple'] 224 | 225 | // Alternatively, you can give the array's length instead and get a pair of indices 226 | randomOrderedPair(4); 227 | >>> [3, 0] 228 | 229 | // To create your own function using custom RNG 230 | import {createRandomOrderedPair} from 'pandemonium/random-ordered-pair'; 231 | 232 | const customRandomPair = createRandomOrderedPair(rng); 233 | ``` 234 | 235 | ## shuffle 236 | 237 | Function returning a shuffled version of the given array using the Fisher-Yates algorithm. 238 | 239 | If what you need is to shuffle the original array in place, check out [`shuffleInPlace`](#shuffleinplace). 240 | 241 | ```js 242 | import shuffle from 'pandemonium/shuffle'; 243 | // Or 244 | import {shuffle} from 'pandemonium'; 245 | 246 | shuffle(['apple', 'orange', 'pear', 'pineapple']); 247 | >>> ['pear', 'orange', 'apple', 'pineapple'] 248 | 249 | // To create your own function using custom RNG 250 | import {createShuffle} from 'pandemonium/shuffle'; 251 | 252 | const customShuffle = createShuffle(rng); 253 | ``` 254 | 255 | ## shuffleInPlace 256 | 257 | Function shuffling the given array in place using the Fisher-Yates algorithm. 258 | 259 | ```js 260 | import shuffleInPlace from 'pandemonium/shuffle-in-place'; 261 | // Or 262 | import {shuffleInPlace} from 'pandemonium'; 263 | 264 | const array = ['apple', 'orange', 'pear', 'pineapple']; 265 | shuffleInPlace(array); 266 | 267 | // Array was mutated: 268 | array >>> ['pear', 'orange', 'apple', 'pineapple']; 269 | 270 | // To create your own function using custom RNG 271 | import {createShuffleInPlace} from 'pandemonium/shuffle-in-place'; 272 | 273 | const customShuffleInPlace = createShuffleInPlace(rng); 274 | ``` 275 | 276 | ## weightedChoice 277 | 278 | Function returning a random item from the given array of weights. 279 | 280 | Note that weights don't need to be relative. 281 | 282 | ```js 283 | import weightedChoice from 'pandemonium/weighted-choice'; 284 | // Or 285 | import {weightedChoice} from 'pandemonium'; 286 | 287 | const array = [.1, .1, .4, .3, .1]; 288 | weightedChoice(array); 289 | >>> .4 290 | 291 | // To create your own function using custom RNG 292 | import {createWeightedChoice} from 'pandemonium/weighted-choice'; 293 | 294 | const customWeightedChoice = createWeightedChoice(rng); 295 | 296 | // If you have an array of objects 297 | const customWeightedChoice = createWeightedChoice({ 298 | rng: rng, 299 | getWeight: (item, index) => { 300 | return item.weight; 301 | } 302 | }); 303 | 304 | const array = [{fruit: 'pear', weight: 4}, {fruit: 'apple', weight: 30}]; 305 | customWeightedChoice(array); 306 | >>> 'apple' 307 | 308 | 309 | // If you intent to call the function multiple times on the same array, 310 | // you should use the cached version instead: 311 | import {createCachedWeightedChoice} from 'pandemonium/weighted-choice'; 312 | 313 | const array = [.1, .1, .4, .3, .1]; 314 | const customWeightedChoice = createCachedWeightedChoice(rng, array); 315 | 316 | customWeightedChoice(); 317 | >>> .3 318 | ``` 319 | 320 | ## weightedRandomIndex 321 | 322 | Function returning a random index from the given array of weights. 323 | 324 | Note that weights don't need to be relative. 325 | 326 | ```js 327 | import weightedRandomIndex from 'pandemonium/weighted-random-index'; 328 | // Or 329 | import {weightedRandomIndex} from 'pandemonium'; 330 | 331 | const array = [.1, .1, .4, .3, .1]; 332 | weightedRandomIndex(array); 333 | >>> 2 334 | 335 | // To create your own function using custom RNG 336 | import {createWeightedRandomIndex} from 'pandemonium/weighted-random-index'; 337 | 338 | const customWeightedRandomIndex = createWeightedRandomIndex(rng); 339 | 340 | // If you have an array of objects 341 | const customWeightedRandomIndex = createWeightedRandomIndex({ 342 | rng: rng, 343 | getWeight: (item, index) => { 344 | return item.weight; 345 | } 346 | }); 347 | 348 | const array = [{fruit: 'pear', weight: 4}, {fruit: 'apple', weight: 30}]; 349 | customWeightedRandomIndex(array); 350 | >>> 1 351 | 352 | 353 | // If you intent to call the function multiple times on the same array, 354 | // you should use the cached version instead: 355 | import {createCachedWeightedRandomIndex} from 'pandemonium/weighted-random-index'; 356 | 357 | const array = [.1, .1, .4, .3, .1]; 358 | const customWeightedRandomIndex = createCachedWeightedRandomIndex(rng, array); 359 | 360 | customWeightedRandomIndex(); 361 | >>> 3 362 | ``` 363 | 364 | ## dangerouslyMutatingSample 365 | 366 | Function returning a random sample of size `k` from the given array. 367 | 368 | This function runs in `O(k)` time & memory but is somewhat dangerous because it will mutate the given array while performing its Fisher-Yates shuffle before reverting the mutations at the end. 369 | 370 | ```js 371 | import dangerouslyMutatingSample from 'pandemonium/dangerously-mutating-sample'; 372 | // Or 373 | import {dangerouslyMutatingSample} from 'pandemonium'; 374 | 375 | dangerouslyMutatingSample(2, ['apple', 'orange', 'pear', 'pineapple']); 376 | >>> ['apple', 'pear'] 377 | 378 | // To create your own function using custom RNG 379 | import {createDangerouslyMutatingSample} from 'pandemonium/dangerously-mutating-sample'; 380 | 381 | const customSample = createDangerouslyMutatingSample(rng); 382 | ``` 383 | 384 | ## fisherYatesSample 385 | 386 | Function returning a random sample of size `k` from the given array. 387 | 388 | This function uses a partial Fisher-Yates shuffle and therefore runs in `O(k)` time but must clone the given array to work, which adds `O(n)` time & memory. 389 | 390 | ```js 391 | import fisherYatesSample from 'pandemonium/fisher-yates-sample'; 392 | // Or 393 | import {fisherYatesSample} from 'pandemonium'; 394 | 395 | fisherYatesSample(2, ['apple', 'orange', 'pear', 'pineapple']); 396 | >>> ['apple', 'pear'] 397 | 398 | // To create your own function using custom RNG 399 | import {createFisherYatesSample} from 'pandemonium/fisherYatesSample'; 400 | 401 | const customFisherYatesSample = createFisherYatesSample(rng); 402 | ``` 403 | 404 | ## geometricReservoirSample 405 | 406 | Function returning a random sample of size `k` from the given array. 407 | 408 | This function runs in `O(k * (1 + log(n / k)))` time & `O(k)` memory using "Algorithm L" taken from the following paper: 409 | 410 | > Li, Kim-Hung. "Reservoir-sampling algorithms of time complexity O(n (1+ log (N/n)))." ACM Transactions on Mathematical Software (TOMS) 20.4 (1994): 481-493. 411 | 412 | Note that this function is able to sample indices without requiring you to represent the range of indices in memory. 413 | 414 | ```js 415 | import geometricReservoirSample from 'pandemonium/geometric-reservoir-sample'; 416 | // Or 417 | import {geometricReservoirSample} from 'pandemonium'; 418 | 419 | geometricReservoirSample(2, ['apple', 'orange', 'pear', 'pineapple']); 420 | >>> ['apple', 'pear'] 421 | 422 | // Alternatively, you can pass a length and get a sample of indices back 423 | geometricReservoirSample(2, 4); 424 | >>> [0, 2] 425 | 426 | // To create your own function using custom RNG 427 | import {createGeometricReservoirSample} from 'pandemonium/geometric-reservoir-sample'; 428 | 429 | const customSample = createGeometricReservoirSample(rng); 430 | ``` 431 | 432 | ## naiveSample 433 | 434 | Function returning a random sample of size `k` from the given array. 435 | 436 | This function works by keeping a `Set` of the already picked items and choosing a random item in the array until we have the desired `k` items. 437 | 438 | While it is a good pick for cases when `k` is little compared to the size of your array, this function will see its performance drop really fast when `k` becomes proportionally bigger. 439 | 440 | Note that this function is able to sample indices without requiring you to represent the range of indices in memory. 441 | 442 | ```js 443 | import naiveSample from 'pandemonium/naive-sample'; 444 | // Or 445 | import {naiveSample} from 'pandemonium'; 446 | 447 | naiveSample(2, ['apple', 'orange', 'pear', 'pineapple']); 448 | >>> ['apple', 'pear'] 449 | 450 | // Alternatively, you can pass a length and get a sample of indices back 451 | naiveSample(2, 4); 452 | >>> [0, 2] 453 | 454 | // To create your own function using custom RNG 455 | import {createNaiveSample} from 'pandemonium/naive-sample'; 456 | 457 | const customSample = createNaiveSample(rng); 458 | ``` 459 | 460 | ## reservoirSample 461 | 462 | Function returning a random sample of size `k` from the given array. 463 | 464 | This function runs in `O(n)` time and `O(k)` memory. 465 | 466 | A helper class able to work on an arbitrary stream of data that does not need to fit into memory is also available if you need it. 467 | 468 | ```js 469 | import reservoirSample from 'pandemonium/reservoir-sample'; 470 | // Or 471 | import {reservoirSample} from 'pandemonium'; 472 | 473 | reservoirSample(2, ['apple', 'orange', 'pear', 'pineapple']); 474 | >>> ['apple', 'pear'] 475 | 476 | // To create your own function using custom RNG 477 | import {createReservoirSample} from 'pandemonium/reservoir-sample'; 478 | 479 | const customReservoirSample = createReservoirSample(rng); 480 | 481 | // To use the helper class 482 | import {ReservoirSampler} from 'pandemonium/reservoir-sample'; 483 | 484 | // If RNG is not provided, will default to Math.random 485 | const sampler = new ReservoirSampler(10, rng); 486 | 487 | for (const value of lazyIterable) { 488 | sampler.process(value); 489 | } 490 | 491 | // To retrieve the sample once every value has been consumed 492 | const sample = sampler.end(); 493 | ``` 494 | 495 | ## sampleWithReplacements 496 | 497 | Function returning a random sample of size `k` with replacements from the given array. This prosaically means that an items from the array might occur several times in the resulting sample. 498 | 499 | The function runs in both `O(k)` time & space complexity. 500 | 501 | ```js 502 | import sampleWithReplacements from 'pandemonium/sample-with-replacements'; 503 | // Or 504 | import {sampleWithReplacements} from 'pandemonium'; 505 | 506 | sampleWithReplacements(3, ['apple', 'orange', 'pear', 'pineapple']); 507 | >>> ['apple', 'pear', 'apple'] 508 | 509 | // To create your own function using custom RNG 510 | import {createSampleWithReplacements} from 'pandemonium/sample-with-replacements'; 511 | 512 | const customSample = createSampleWithReplacements(rng); 513 | ``` 514 | 515 | ## samplePairs 516 | 517 | Function returning a random sample of `k` unique unordered pairs from the given array. 518 | 519 | It works by storing a unique key created from the picked pairs, making it a specialized variant of [naiveSample](#naivesample). 520 | 521 | It is usually quite efficient because when sampling pairs, the total size of the population, being combinatorial, is often magnitudes larger than the size of the sample we need to retrieve. 522 | 523 | Note finally that this function is able to sample pairs of indices without requiring you to represent the range of indices in memory. 524 | 525 | ```js 526 | import samplePairs from 'pandemonium/sample-pairs'; 527 | // Or 528 | import {samplePairs} from 'pandemonium'; 529 | 530 | samplePairs(2, ['apple', 'orange', 'pear', 'pineapple']); 531 | >>> [['apple', 'pear'], ['orange', 'pear']] 532 | 533 | // Alternatively, you can pass a length and get a sample of pairs of indices 534 | samplePairs(2, 4); 535 | >>> [[0, 2], [1, 2]] 536 | 537 | // To create your own function using custom RNG 538 | import {createSamplePairs} from 'pandemonium/sample-pairs'; 539 | 540 | const customSamplePairs = createSamplePairs(rng); 541 | ``` 542 | 543 | ## sampleOrderedPairs 544 | 545 | Function returning a random sample of `k` unique ordered pairs from the given array. 546 | 547 | It works by storing a unique key created from the picked pairs, making it a specialized variant of [naiveSample](#naivesample). 548 | 549 | It is usually quite efficient because when sampling pairs, the total size of the population, being combinatorial, is often magnitudes larger than the size of the sample we need to retrieve. 550 | 551 | Note finally that this function is able to sample pairs of indices without requiring you to represent the range of indices in memory. 552 | 553 | ```js 554 | import sampleOrderedPairs from 'pandemonium/sample-pairs'; 555 | // Or 556 | import {sampleOrderedPairs} from 'pandemonium'; 557 | 558 | sampleOrderedPairs(2, ['apple', 'orange', 'pear', 'pineapple']); 559 | >>> [['apple', 'pear'], ['pear', 'orange']] 560 | 561 | // Alternatively, you can pass a length and get a sample of pairs of indices 562 | sampleOrderedPairs(2, 4); 563 | >>> [[0, 2], [2, 1]] 564 | 565 | // To create your own function using custom RNG 566 | import {createSampleOrderedPairs} from 'pandemonium/sample-ordered-pairs'; 567 | 568 | const customSampleOrderedPairs = createSampleOrderedPairs(rng); 569 | ``` 570 | 571 | ## weightedReservoirSample 572 | 573 | Function returning a random sample of size `k` from a given array of weighted items. 574 | 575 | The result is a sample without replacement, which, in the case of weighted items, has been chosen to mean that subsequent items are picked based on the proportional total weight of the remaining items. 576 | 577 | We use algorithm "A-ES" from the following papers: 578 | 579 | > Pavlos S. Efraimidis, Paul G. Spirakis. "Weighted random sampling with a reservoir." https://arxiv.org/pdf/1012.0256.pdf 580 | 581 | > Pavlos S. Efraimidis. "Weighted Random Sampling over Data Streams." 582 | 583 | This function runs in `O(n)` time and `O(k)` memory. 584 | 585 | A helper class working able to work on an arbitrary stream of data that does not need to fit into memory is also available if you need it. 586 | 587 | ```js 588 | import weightedReservoirSample from 'pandemonium/weighted-reservoir-sample'; 589 | // Or 590 | import {weightedReservoirSample} from 'pandemonium'; 591 | 592 | weightedReservoirSample(2, [.1, .1, .4, .3, .05]); 593 | >>> [.3, .4] 594 | 595 | // To create your own function using custom RNG 596 | import {createWeightedReservoirSample} from 'pandemonium/weighted-reservoir-sample'; 597 | 598 | const customWeightedReservoirSample = createWeightedReservoirSample(rng); 599 | 600 | // To sample arbitrary items 601 | const data = [{label: 'orange', importance: 34}, ...]; 602 | 603 | const customWeightedReservoirSample = createWeightedReservoirSample({ 604 | getWeight: item => item.importance 605 | }); 606 | 607 | // To use the helper class 608 | import {WeightedReservoirSampler} from 'pandemonium/weighted-reservoir-sample'; 609 | 610 | // If RNG is not provided, will default to Math.random 611 | const sampler = new WeightedReservoirSampler(10, {rng, getWeight}); 612 | 613 | for (const value of lazyIterable) { 614 | sampler.process(value); 615 | } 616 | 617 | // To retrieve the sample once every value has been consumed 618 | const sample = sampler.end(); 619 | ``` 620 | 621 | # Contribution 622 | 623 | Contributions are obviously welcome. Please be sure to lint the code & add the relevant unit tests before submitting any PR. 624 | 625 | ``` 626 | git clone git@github.com:Yomguithereal/pandemonium.git 627 | cd pandemonium 628 | npm install 629 | 630 | # To lint the code 631 | npm run lint 632 | 633 | # To run the unit tests 634 | npm test 635 | ``` 636 | 637 | # License 638 | 639 | [MIT](LICENSE.txt) 640 | -------------------------------------------------------------------------------- /choice.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type ChoiceFunction = (array: Array) => T; 4 | 5 | declare const choice: { 6 | (array: Array): T; 7 | createChoice(rng: RNGFunction): ChoiceFunction; 8 | }; 9 | 10 | export function createChoice(rng: RNGFunction): ChoiceFunction; 11 | 12 | export default choice; 13 | -------------------------------------------------------------------------------- /choice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Choice 3 | * =================== 4 | * 5 | * Choice function. 6 | */ 7 | var createRandomIndex = require('./random-index.js').createRandomIndex; 8 | 9 | /** 10 | * Creating a function returning a random item from the given array. 11 | * 12 | * @param {function} rng - RNG function returning uniform random. 13 | * @return {function} - The created function. 14 | */ 15 | function createChoice(rng) { 16 | var customRandomIndex = createRandomIndex(rng); 17 | 18 | /** 19 | * Random function. 20 | * 21 | * @param {array} array - Target array. 22 | * @return {any} 23 | */ 24 | return function (array) { 25 | return array[customRandomIndex(array)]; 26 | }; 27 | } 28 | 29 | /** 30 | * Default choice using `Math.random`. 31 | */ 32 | var choice = createChoice(Math.random); 33 | 34 | /** 35 | * Exporting. 36 | */ 37 | choice.createChoice = createChoice; 38 | module.exports = choice; 39 | -------------------------------------------------------------------------------- /dangerously-mutating-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type DangerouslyMutatingSampleFunction = ( 4 | k: number, 5 | array: Array 6 | ) => Array; 7 | 8 | declare const dangerouslyMutatingSample: { 9 | (k: number, array: Array): Array; 10 | createDangerouslyMutatingSample( 11 | rng: RNGFunction 12 | ): DangerouslyMutatingSampleFunction; 13 | }; 14 | 15 | export function createDangerouslyMutatingSample( 16 | rng: RNGFunction 17 | ): DangerouslyMutatingSampleFunction; 18 | 19 | export default dangerouslyMutatingSample; 20 | -------------------------------------------------------------------------------- /dangerously-mutating-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Dangerously Mutating Sample 3 | * ======================================== 4 | * 5 | * Performant sampling function running in O(k) memory & time but mutating the 6 | * given array before reversing the mutations at the end of the process. 7 | */ 8 | var createRandom = require('./random.js').createRandom; 9 | 10 | /** 11 | * Creating a function returning a sample of size k using the provided RNG. 12 | * 13 | * @param {function} rng - The RNG to use. 14 | * @return {function} - The created function. 15 | */ 16 | function createDangerouslyMutatingSample(rng) { 17 | var customRandom = createRandom(rng); 18 | 19 | /** 20 | * Function returning sample of size k from array. 21 | * 22 | * @param {number} k - Size of the sample. 23 | * @param {array} sequence - Target sequence. 24 | * @return {array} - The random sample. 25 | */ 26 | return function (k, sequence) { 27 | if (k >= sequence.length) return sequence.slice(); 28 | 29 | var result = new Array(k), 30 | swaps = new Array(k), 31 | lastIndex = sequence.length - 1; 32 | 33 | var index = -1, 34 | value, 35 | swap, 36 | r; 37 | 38 | while (++index < k) { 39 | r = customRandom(index, lastIndex); 40 | value = sequence[r]; 41 | 42 | sequence[r] = sequence[index]; 43 | sequence[index] = value; 44 | result[index] = value; 45 | 46 | // Storing the swap so we can reverse it 47 | swaps[index] = r; 48 | } 49 | 50 | // Reversing the mutations 51 | while (--index >= 0) { 52 | swap = swaps[index]; 53 | value = sequence[index]; 54 | 55 | sequence[index] = sequence[swap]; 56 | sequence[swap] = value; 57 | } 58 | 59 | return result; 60 | }; 61 | } 62 | 63 | /** 64 | * Default dangerous sample using `Math.random`. 65 | */ 66 | var dangerouslyMutatingSample = createDangerouslyMutatingSample(Math.random); 67 | 68 | /** 69 | * Exporting. 70 | */ 71 | dangerouslyMutatingSample.createDangerouslyMutatingSample = 72 | createDangerouslyMutatingSample; 73 | module.exports = dangerouslyMutatingSample; 74 | -------------------------------------------------------------------------------- /experiments/fisher-yates-robustness.js: -------------------------------------------------------------------------------- 1 | var FisherYatesPermutation = 2 | require('../fisher-yates-permutation').FisherYatesPermutation; 3 | var MultiSet = require('mnemonist/multi-set'); 4 | 5 | function factorial(n) { 6 | if (n === 1) return n; 7 | return n * factorial(n - 1); 8 | } 9 | 10 | var N = 8; 11 | 12 | console.log('N =', N); 13 | console.log('N! =', factorial(N)); 14 | 15 | var p = new FisherYatesPermutation(N); 16 | 17 | var RUNS = factorial(N) * 10; 18 | 19 | var stats = new MultiSet(); 20 | 21 | for (var i = 0; i < RUNS; i++) { 22 | p.reset(); 23 | 24 | var r = new Array(N); 25 | 26 | for (var j = 0; j < N; j++) { 27 | r[j] = p.permute(); 28 | } 29 | 30 | stats.add(r.join(',')); 31 | } 32 | 33 | console.log('P =', stats.dimension); 34 | console.log('S =', stats.top(10)); 35 | 36 | N -= 2; 37 | 38 | console.log(); 39 | console.log('shrinking to ', N); 40 | console.log('N =', N); 41 | console.log('N! =', factorial(N)); 42 | 43 | p.shrinkAndReset(N); 44 | 45 | RUNS = factorial(N) * 10; 46 | 47 | stats = new MultiSet(); 48 | 49 | for (var i = 0; i < RUNS; i++) { 50 | p.reset(); 51 | 52 | var r = new Array(N); 53 | 54 | for (var j = 0; j < N; j++) { 55 | r[j] = p.permute(); 56 | } 57 | 58 | stats.add(r.join(',')); 59 | } 60 | 61 | console.log('P =', stats.dimension); 62 | console.log('S =', stats.top(10)); 63 | -------------------------------------------------------------------------------- /experiments/random-pair-sampling.js: -------------------------------------------------------------------------------- 1 | var randomOrderedPair = require('../random-ordered-pair.js'); 2 | var randomPair = require('../random-pair.js'); 3 | var utils = require('../utils.js'); 4 | var lib = require('../'); 5 | 6 | var {exp, log, floor} = Math; 7 | 8 | /** 9 | * Helpers. 10 | */ 11 | function hashPair(pair) { 12 | return pair[0] + ',' + pair[1]; 13 | } 14 | 15 | /** 16 | * Functions. 17 | */ 18 | function sampleUnorderedPairsNaive(n, k) { 19 | var alreadySeen = new Set(); 20 | var pairs = new Array(k); 21 | 22 | var i = 0; 23 | var candidate, key; 24 | 25 | while (i !== k) { 26 | var candidate = randomPair(n); 27 | var key = candidate[0] * n + candidate[1]; 28 | 29 | if (alreadySeen.has(key)) continue; 30 | 31 | alreadySeen.add(key); 32 | pairs[i++] = candidate; 33 | } 34 | 35 | return pairs; 36 | } 37 | 38 | function sampleOrderedPairsNaive(n, k) { 39 | var alreadySeen = new Set(); 40 | var pairs = new Array(k); 41 | 42 | var i = 0; 43 | var candidate, key; 44 | 45 | while (i !== k) { 46 | var candidate = randomOrderedPair(n); 47 | var key = candidate[0] * n + candidate[1]; 48 | 49 | if (alreadySeen.has(key)) continue; 50 | 51 | alreadySeen.add(key); 52 | pairs[i++] = candidate; 53 | } 54 | 55 | return pairs; 56 | } 57 | 58 | function sampleUnorderedPairsGeometric(l, k) { 59 | var n = utils.triuLinearLength(l); 60 | var sample = new Array(k); 61 | var i; 62 | 63 | for (i = 0; i < k; i++) sample[i] = utils.linearIndexToTriuCoordsFast(i); 64 | 65 | // NOTE: from this point, formulae consider i to be 1-based 66 | var w = exp(log(Math.random()) / k); 67 | 68 | if (i > n) return sample; 69 | 70 | while (true) { 71 | i += floor(log(Math.random()) / log(1 - w)) + 1; 72 | 73 | if (i <= n) { 74 | sample[floor(k * Math.random())] = utils.linearIndexToTriuCoords(i - 1); 75 | w *= exp(log(Math.random()) / k); 76 | } else { 77 | break; 78 | } 79 | } 80 | 81 | return sample; 82 | } 83 | 84 | /** 85 | * Benchmark. 86 | */ 87 | function bench(fn, name) { 88 | // var MultiSet = require('mnemonist/multi-set'); 89 | // var set = new MultiSet(); 90 | 91 | if (!name) name = fn.name; 92 | 93 | var pairs; 94 | 95 | var T = 100000; 96 | 97 | var N = 20000; 98 | var P = 500; 99 | 100 | // var N = 5; 101 | // var P = 5; 102 | 103 | console.time(name); 104 | for (var i = 0; i < T; i++) { 105 | pairs = fn(N, P); 106 | // pairs.forEach(pair => set.add(hashPair(pair))); 107 | } 108 | console.timeEnd(name); 109 | 110 | // console.log(set, set.dimension); 111 | } 112 | 113 | console.log('\nOrdered'); 114 | bench(sampleOrderedPairsNaive); 115 | bench((a1, a2) => lib.sampleOrderedPairs(a2, a1), 'lib'); 116 | 117 | console.log('\nUnordered'); 118 | bench(sampleUnorderedPairsNaive); 119 | bench(sampleUnorderedPairsGeometric); 120 | bench((a1, a2) => lib.samplePairs(a2, a1), 'lib'); 121 | -------------------------------------------------------------------------------- /experiments/random-pairs.js: -------------------------------------------------------------------------------- 1 | var utils = require('../utils.js'); 2 | 3 | /** 4 | * Helpers. 5 | */ 6 | function derive(n, i) { 7 | var k = 1 + Math.floor(Math.random() * (n - 1)); 8 | var j = (i + k) % n; 9 | 10 | return j; 11 | } 12 | 13 | function swapPair(pair) { 14 | var tmp = pair[0]; 15 | pair[0] = pair[1]; 16 | pair[1] = tmp; 17 | } 18 | 19 | function hashPair(pair) { 20 | return pair[0] + ',' + pair[1]; 21 | } 22 | 23 | /** 24 | * Functions. 25 | */ 26 | function randomOrderedPairByDerivation(n) { 27 | var i = Math.floor(Math.random() * n); 28 | var k = 1 + Math.floor(Math.random() * (n - 1)); 29 | var j = (i + k) % n; 30 | 31 | return [i, j]; 32 | } 33 | 34 | function randomUnorderedPairByDerivation(n) { 35 | var i = Math.floor(Math.random() * n); 36 | var k = 1 + Math.floor(Math.random() * (n - 1)); 37 | var j = (i + k) % n; 38 | 39 | if (i > j) return [j, i]; 40 | 41 | return [i, j]; 42 | } 43 | 44 | function randomUnorderedPairTriu(n) { 45 | const L = utils.triuLinearLength(n); 46 | 47 | return utils.linearIndexToTriuCoordsFast(Math.floor(Math.random() * L)); 48 | } 49 | 50 | function randomOrderedPairTriu(n) { 51 | var c = randomUnorderedPairTriu(n); 52 | 53 | if (Math.random() < 0.5) swapPair(c); 54 | 55 | return c; 56 | } 57 | 58 | function randomOrderedPairNaive(n) { 59 | var i = Math.floor(Math.random() * n); 60 | 61 | var j; 62 | 63 | do { 64 | j = Math.floor(Math.random() * n); 65 | } while (i === j); 66 | 67 | return [i, j]; 68 | } 69 | 70 | function randomUnorderedPairNaive(n) { 71 | var i = Math.floor(Math.random() * n); 72 | 73 | var j; 74 | 75 | do { 76 | j = Math.floor(Math.random() * n); 77 | } while (i === j); 78 | 79 | if (i > j) return [j, i]; 80 | 81 | return [i, j]; 82 | } 83 | 84 | /** 85 | * Benchmark. 86 | */ 87 | function bench(fn) { 88 | // var MultiSet = require('mnemonist/multi-set'); 89 | // var set = new MultiSet(); 90 | 91 | var name = fn.name; 92 | var pair; 93 | 94 | var T = 100000000; 95 | var N = 20000; 96 | 97 | console.time(name); 98 | for (var i = 0; i < T; i++) { 99 | pair = fn(N); 100 | // set.add(hashPair(pair)); 101 | } 102 | console.timeEnd(name); 103 | 104 | // console.log(set, set.dimension); 105 | } 106 | 107 | console.log('\nOrdered'); 108 | bench(randomOrderedPairByDerivation); 109 | bench(randomOrderedPairTriu); 110 | bench(randomOrderedPairNaive); 111 | 112 | console.log('\nUnordered'); 113 | bench(randomUnorderedPairByDerivation); 114 | bench(randomUnorderedPairTriu); 115 | bench(randomUnorderedPairNaive); 116 | -------------------------------------------------------------------------------- /fisher-yates-permutation.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | export class FisherYatesPermutation { 4 | length: number; 5 | 6 | constructor(length: number, rng?: RNGFunction); 7 | permute(): number; 8 | reset(): void; 9 | shrink(newLength: number): void; 10 | } 11 | -------------------------------------------------------------------------------- /fisher-yates-permutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Fisher-Yates Permutation 3 | * ===================================== 4 | * 5 | * Helper class that can be used to cycle through Fisher-Yates permutations 6 | * using a convenient interface. 7 | */ 8 | var typed = require('mnemonist/utils/typed-arrays'); 9 | 10 | function FisherYatesPermutation(length, rng) { 11 | if (!rng) rng = Math.random; 12 | 13 | var PointerArray = typed.getPointerArray(length); 14 | this.PointerArray = PointerArray; 15 | 16 | this.length = length; 17 | this.indices = new PointerArray(length); 18 | this.rng = rng; 19 | 20 | this.reset(); 21 | this.reorder(); 22 | } 23 | 24 | FisherYatesPermutation.prototype.reorder = function () { 25 | for (var i = 0; i < this.length; i++) { 26 | this.indices[i] = i; 27 | } 28 | }; 29 | 30 | FisherYatesPermutation.prototype.reset = function () { 31 | this.state = this.length; 32 | }; 33 | 34 | FisherYatesPermutation.prototype.shrink = function (newLength) { 35 | if (newLength >= this.length) 36 | throw new Error( 37 | 'pandemonium/fisher-yates-permutation: new length should be less than current length.' 38 | ); 39 | 40 | this.length = newLength; 41 | this.reset(); 42 | this.reorder(); 43 | }; 44 | 45 | FisherYatesPermutation.prototype.permute = function () { 46 | if (this.state === 0) { 47 | throw new Error( 48 | 'pandemonium/fisher-yates-permutation: permutation is exhausted.' 49 | ); 50 | } else if (this.state === 1) { 51 | this.state--; 52 | return this.indices[0]; 53 | } 54 | 55 | var r = Math.floor(this.rng() * this.state); 56 | 57 | this.state--; 58 | 59 | var v = this.indices[r]; 60 | this.indices[r] = this.indices[this.state]; 61 | this.indices[this.state] = v; 62 | 63 | return v; 64 | }; 65 | 66 | exports.FisherYatesPermutation = FisherYatesPermutation; 67 | -------------------------------------------------------------------------------- /fisher-yates-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type FisherYatesSampleFunction = (k: number, array: Array) => Array; 4 | 5 | declare const fisherYatesSample: { 6 | (k: number, array: Array): Array; 7 | createFisherYatesSample(rng: RNGFunction): FisherYatesSampleFunction; 8 | }; 9 | 10 | export function createFisherYatesSample( 11 | rng: RNGFunction 12 | ): FisherYatesSampleFunction; 13 | 14 | export default fisherYatesSample; 15 | -------------------------------------------------------------------------------- /fisher-yates-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Fisher-Yates Sample 3 | * ================================ 4 | * 5 | * Sample function using `k` iterations of the Fisher-Yates over a copy of the 6 | * provided array. 7 | */ 8 | var createRandom = require('./random.js').createRandom; 9 | 10 | /** 11 | * Creating a function returning a sample of size k using the provided RNG. 12 | * 13 | * @param {function} rng - The RNG to use. 14 | * @return {function} - The created function. 15 | */ 16 | function createFisherYatesSample(rng) { 17 | var customRandom = createRandom(rng); 18 | 19 | /** 20 | * Function returning sample of size k from array. 21 | * 22 | * @param {number} k - Size of the sample. 23 | * @param {array} sequence - Target sequence. 24 | * @return {array} - The random sample. 25 | */ 26 | return function (k, sequence) { 27 | var result = sequence.slice(), 28 | lastIndex = result.length - 1; 29 | 30 | if (k >= sequence.length) return result; 31 | 32 | var index = -1; 33 | 34 | while (++index < k) { 35 | var r = customRandom(index, lastIndex), 36 | value = result[r]; 37 | 38 | result[r] = result[index]; 39 | result[index] = value; 40 | } 41 | 42 | // Clamping the array 43 | result.length = k; 44 | 45 | return result; 46 | }; 47 | } 48 | 49 | /** 50 | * Default sample using `Math.random`. 51 | */ 52 | var fisherYatesSample = createFisherYatesSample(Math.random); 53 | 54 | /** 55 | * Exporting. 56 | */ 57 | fisherYatesSample.createFisherYatesSample = createFisherYatesSample; 58 | module.exports = fisherYatesSample; 59 | -------------------------------------------------------------------------------- /geometric-reservoir-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type GeometricReservoirSampleFunction = 4 | | ((k: number, array: Array) => Array) 5 | | ((k: number, length: number) => Array); 6 | 7 | declare const geometricReservoirSample: { 8 | (k: number, length: number): Array; 9 | (k: number, array: Array): Array; 10 | createGeometricReservoirSample( 11 | rng: RNGFunction 12 | ): GeometricReservoirSampleFunction; 13 | }; 14 | 15 | export function createGeometricReservoirSample( 16 | rng: RNGFunction 17 | ): GeometricReservoirSampleFunction; 18 | 19 | export default geometricReservoirSample; 20 | -------------------------------------------------------------------------------- /geometric-reservoir-sample.js: -------------------------------------------------------------------------------- 1 | /* eslint no-constant-condition: 0 */ 2 | /** 3 | * Pandemonium Geometric Reservoir Sample 4 | * ======================================= 5 | * 6 | * Reservoir sampling function using "Algorithm L". 7 | * 8 | * [Reference]: 9 | * Li, Kim-Hung. "Reservoir-sampling algorithms of time complexity 10 | * O(n (1+ log (N/n)))." ACM Transactions on Mathematical Software (TOMS) 20.4 11 | * (1994): 481-493. 12 | * 13 | * https://en.wikipedia.org/wiki/Reservoir_sampling#An_optimal_algorithm 14 | */ 15 | var createRandomIndex = require('./random-index.js').createRandomIndex; 16 | var utils = require('./utils.js'); 17 | 18 | var exp = Math.exp; 19 | var log = Math.log; 20 | var floor = Math.floor; 21 | 22 | /** 23 | * Creating a function returning a sample of size n using the provided RNG. 24 | * 25 | * @param {function} rng - The RNG to use. 26 | * @return {function} - The created function. 27 | */ 28 | function createGeometricReservoirSample(rng) { 29 | var customRandomIndex = createRandomIndex(rng); 30 | 31 | /** 32 | * Function returning sample of size n from array. 33 | * 34 | * @param {number} k - Size of the sample. 35 | * @param {array|number} sequence - Target sequence or its length. 36 | * @return {array} - The random sample. 37 | */ 38 | return function (k, sequence) { 39 | var needItems = typeof sequence !== 'number'; 40 | 41 | var n = needItems ? sequence.length : sequence; 42 | 43 | // Sample size gte sequence's length 44 | if (needItems) { 45 | if (k >= n) return sequence.slice(); 46 | } else if (k >= n) { 47 | return utils.indices(sequence); 48 | } 49 | 50 | var sample = new Array(k); 51 | var i; 52 | 53 | for (i = 0; i < k; i++) sample[i] = needItems ? sequence[i] : i; 54 | 55 | // NOTE: from this point, formulae consider i to be 1-based 56 | var w = exp(log(rng()) / k); 57 | 58 | if (i > n) return sample; 59 | 60 | while (true) { 61 | i += floor(log(rng()) / log(1 - w)) + 1; 62 | 63 | if (i <= n) { 64 | sample[customRandomIndex(k)] = needItems ? sequence[i - 1] : i - 1; 65 | w *= exp(log(rng()) / k); 66 | } else { 67 | break; 68 | } 69 | } 70 | 71 | return sample; 72 | }; 73 | } 74 | 75 | /** 76 | * Default reservoir sample using `Math.random`. 77 | */ 78 | var geometricReservoirSample = createGeometricReservoirSample(Math.random); 79 | 80 | /** 81 | * Exporting. 82 | */ 83 | geometricReservoirSample.createGeometricReservoirSample = 84 | createGeometricReservoirSample; 85 | module.exports = geometricReservoirSample; 86 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export {default as choice, createChoice} from './choice'; 2 | export { 3 | default as dangerouslyMutatingSample, 4 | createDangerouslyMutatingSample 5 | } from './dangerously-mutating-sample'; 6 | export {FisherYatesPermutation} from './fisher-yates-permutation'; 7 | export { 8 | default as fisherYatesSample, 9 | createFisherYatesSample 10 | } from './fisher-yates-sample'; 11 | export { 12 | default as geometricReservoirSample, 13 | createGeometricReservoirSample 14 | } from './geometric-reservoir-sample'; 15 | export {default as naiveSample, createNaiveSample} from './naive-sample'; 16 | export {default as random, createRandom} from './random'; 17 | export {default as randomBoolean, createRandomBoolean} from './random-boolean'; 18 | export {default as randomFloat, createRandomFloat} from './random-float'; 19 | export {default as randomIndex, createRandomIndex} from './random-index'; 20 | export {default as randomPair, createRandomPair} from './random-pair'; 21 | export { 22 | default as randomOrderedPair, 23 | createRandomOrderedPair 24 | } from './random-ordered-pair'; 25 | export {default as randomString, createRandomString} from './random-string'; 26 | export {randomUint32, createRandomUint32} from './random-typed-int'; 27 | export { 28 | default as reservoirSample, 29 | createReservoirSample, 30 | ReservoirSampler 31 | } from './reservoir-sample'; 32 | export { 33 | default as weightedReservoirSample, 34 | createWeightedReservoirSample, 35 | WeightedReservoirSampler 36 | } from './weighted-reservoir-sample'; 37 | export {default as samplePairs, createSamplePairs} from './sample-pairs'; 38 | export { 39 | default as sampleOrderedPairs, 40 | createSampleOrderedPairs 41 | } from './sample-ordered-pairs'; 42 | export { 43 | default as sampleWithReplacements, 44 | createSampleWithReplacements 45 | } from './sample-with-replacements'; 46 | export {default as shuffle, createShuffle} from './shuffle'; 47 | export { 48 | default as shuffleInPlace, 49 | createShuffleInPlace 50 | } from './shuffle-in-place'; 51 | export { 52 | default as weightedChoice, 53 | createWeightedChoice, 54 | createCachedWeightedChoice 55 | } from './weighted-choice'; 56 | export { 57 | default as weightedRandomIndex, 58 | createWeightedRandomIndex, 59 | createCachedWeightedRandomIndex 60 | } from './weighted-random-index'; 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Library Endpoint 3 | * ============================= 4 | * 5 | * Exporting the library's functions. 6 | */ 7 | var choice = require('./choice.js'); 8 | var dangerouslyMutatingSample = require('./dangerously-mutating-sample.js'); 9 | var FisherYatesPermutation = require('./fisher-yates-permutation.js'); 10 | var fisherYatesSample = require('./fisher-yates-sample.js'); 11 | var geometricReservoirSample = require('./geometric-reservoir-sample.js'); 12 | var naiveSample = require('./naive-sample.js'); 13 | var random = require('./random.js'); 14 | var randomBoolean = require('./random-boolean.js'); 15 | var randomFloat = require('./random-float.js'); 16 | var randomIndex = require('./random-index.js'); 17 | var randomPair = require('./random-pair.js'); 18 | var randomOrderedPair = require('./random-ordered-pair.js'); 19 | var randomString = require('./random-string.js'); 20 | var randomTypedInt = require('./random-typed-int.js'); 21 | var reservoirSample = require('./reservoir-sample.js'); 22 | var weightedReservoirSample = require('./weighted-reservoir-sample.js'); 23 | var samplePairs = require('./sample-pairs.js'); 24 | var sampleOrderedPairs = require('./sample-ordered-pairs.js'); 25 | var sampleWithReplacements = require('./sample-with-replacements.js'); 26 | var shuffle = require('./shuffle.js'); 27 | var shuffleInPlace = require('./shuffle-in-place.js'); 28 | var weightedChoice = require('./weighted-choice.js'); 29 | var weightedRandomIndex = require('./weighted-random-index.js'); 30 | 31 | exports.choice = choice; 32 | exports.createChoice = choice.createChoice; 33 | 34 | exports.dangerouslyMutatingSample = dangerouslyMutatingSample; 35 | exports.createDangerouslyMutatingSample = 36 | dangerouslyMutatingSample.createDangerouslyMutatingSample; 37 | 38 | exports.FisherYatesPermutation = FisherYatesPermutation.FisherYatesPermutation; 39 | 40 | exports.fisherYatesSample = fisherYatesSample; 41 | exports.createFisherYatesSample = fisherYatesSample.createFisherYatesSample; 42 | 43 | exports.geometricReservoirSample = geometricReservoirSample; 44 | exports.createGeometricReservoirSample = 45 | geometricReservoirSample.createGeometricReservoirSample; 46 | 47 | exports.naiveSample = naiveSample; 48 | exports.createNaiveSample = naiveSample.createNaiveSample; 49 | 50 | exports.random = random; 51 | exports.createRandom = random.createRandom; 52 | 53 | exports.randomBoolean = randomBoolean; 54 | exports.createRandomBoolean = randomBoolean.createRandomBoolean; 55 | 56 | exports.randomFloat = randomFloat; 57 | exports.createRandomFloat = randomFloat.createRandomFloat; 58 | 59 | exports.randomIndex = randomIndex; 60 | exports.createRandomIndex = randomIndex.createRandomIndex; 61 | 62 | exports.randomPair = randomPair; 63 | exports.createRandomPair = randomPair.createRandomPair; 64 | 65 | exports.randomOrderedPair = randomOrderedPair; 66 | exports.createRandomOrderedPair = randomOrderedPair.createRandomOrderedPair; 67 | 68 | exports.randomString = randomString; 69 | exports.createRandomString = randomString.createRandomString; 70 | 71 | exports.randomUint32 = randomTypedInt.randomUint32; 72 | exports.createRandomUint32 = randomTypedInt.createRandomUint32; 73 | 74 | exports.reservoirSample = reservoirSample; 75 | exports.createReservoirSample = reservoirSample.createReservoirSample; 76 | exports.ReservoirSampler = reservoirSample.ReservoirSampler; 77 | 78 | exports.weightedReservoirSample = weightedReservoirSample; 79 | exports.createWeightedReservoirSample = 80 | weightedReservoirSample.createWeightedReservoirSample; 81 | exports.WeightedReservoirSampler = 82 | weightedReservoirSample.WeightedReservoirSampler; 83 | 84 | exports.samplePairs = samplePairs; 85 | exports.createSamplePairs = samplePairs.createSamplePairs; 86 | 87 | exports.sampleOrderedPairs = sampleOrderedPairs; 88 | exports.createSampleOrderedPairs = sampleOrderedPairs.createSampleOrderedPairs; 89 | 90 | exports.sampleWithReplacements = sampleWithReplacements; 91 | exports.createSampleWithReplacements = 92 | sampleWithReplacements.createSampleWithReplacements; 93 | 94 | exports.shuffle = shuffle; 95 | exports.createShuffle = shuffle.createShuffle; 96 | 97 | exports.shuffleInPlace = shuffleInPlace; 98 | exports.createShuffleInPlace = shuffleInPlace.createShuffleInPlace; 99 | 100 | exports.weightedChoice = weightedChoice; 101 | exports.createWeightedChoice = weightedChoice.createWeightedChoice; 102 | exports.createCachedWeightedChoice = weightedChoice.createCachedWeightedChoice; 103 | 104 | exports.weightedRandomIndex = weightedRandomIndex; 105 | exports.createWeightedRandomIndex = 106 | weightedRandomIndex.createWeightedRandomIndex; 107 | exports.createCachedWeightedRandomIndex = 108 | weightedRandomIndex.createCachedWeightedRandomIndex; 109 | -------------------------------------------------------------------------------- /naive-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type NaiveSampleFunction = 4 | | ((k: number, array: Array) => Array) 5 | | ((k: number, length: number) => Array); 6 | 7 | declare const naiveSample: { 8 | (k: number, length: number): Array; 9 | (k: number, array: Array): Array; 10 | createNaiveSample(rng: RNGFunction): NaiveSampleFunction; 11 | }; 12 | 13 | export function createNaiveSample(rng: RNGFunction): NaiveSampleFunction; 14 | 15 | export default naiveSample; 16 | -------------------------------------------------------------------------------- /naive-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Naive Sample 3 | * ========================= 4 | * 5 | * Naive sampling function storing the already picked values in a Set. 6 | * Performance of this function will decrease dramatically when `k` is a 7 | * high proportion of `n`. 8 | */ 9 | var createRandomIndex = require('./random-index.js').createRandomIndex; 10 | var utils = require('./utils.js'); 11 | 12 | /** 13 | * Creating a function returning a sample of size k using the provided RNG. 14 | * 15 | * @param {function} rng - The RNG to use. 16 | * @return {function} - The created function. 17 | */ 18 | function createNaiveSample(rng) { 19 | var customRandomIndex = createRandomIndex(rng); 20 | 21 | /** 22 | * Function returning sample of size k from array. 23 | * 24 | * @param {number} k - Size of the sample. 25 | * @param {array|number} sequence - Target sequence or its length. 26 | * @return {array} - The random sample. 27 | */ 28 | return function (k, sequence) { 29 | var needItems = typeof sequence !== 'number'; 30 | 31 | if (needItems) { 32 | if (k >= sequence.length) return sequence.slice(); 33 | } else if (k >= sequence) { 34 | return utils.indices(sequence); 35 | } 36 | 37 | var items = new Set(); 38 | var result = new Array(k); 39 | var size = 0; 40 | var index; 41 | 42 | while (items.size < k) { 43 | index = customRandomIndex(sequence); 44 | 45 | items.add(index); 46 | 47 | if (items.size > size) { 48 | result[size++] = needItems ? sequence[index] : index; 49 | } 50 | } 51 | 52 | return result; 53 | }; 54 | } 55 | 56 | /** 57 | * Default naive sample using `Math.random`. 58 | */ 59 | var naiveSample = createNaiveSample(Math.random); 60 | 61 | /** 62 | * Exporting. 63 | */ 64 | naiveSample.createNaiveSample = createNaiveSample; 65 | module.exports = naiveSample; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pandemonium", 3 | "version": "2.4.1", 4 | "description": "Typical random-related functions for JavaScript.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "lint": "eslint *.js", 9 | "prepublishOnly": "npm run lint && npm test", 10 | "prettier": "prettier --write './**/*.js' './**/*.ts' './**/*.md'", 11 | "test": "mocha && npm run test:types", 12 | "test:types": "tsc --lib es2015,dom --noEmit --noImplicitAny --noImplicitReturns ./test-types.ts" 13 | }, 14 | "files": [ 15 | "*.d.ts", 16 | "*.js" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/yomguithereal/pandemonium.git" 21 | }, 22 | "keywords": [ 23 | "random", 24 | "choice", 25 | "sample", 26 | "shuffle" 27 | ], 28 | "author": { 29 | "name": "Guillaume Plique", 30 | "url": "http://github.com/Yomguithereal" 31 | }, 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/yomguithereal/pandemonium/issues" 35 | }, 36 | "homepage": "https://github.com/yomguithereal/pandemonium#readme", 37 | "dependencies": { 38 | "mnemonist": "^0.39.2" 39 | }, 40 | "devDependencies": { 41 | "@yomguithereal/eslint-config": "^4.4.0", 42 | "@yomguithereal/prettier-config": "^1.2.0", 43 | "eslint": "^8.23.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "mocha": "^9.2.2", 46 | "prettier": "^2.7.1", 47 | "seedrandom": "^3.0.5", 48 | "typescript": "^4.8.2" 49 | }, 50 | "prettier": "@yomguithereal/prettier-config", 51 | "eslintConfig": { 52 | "extends": [ 53 | "@yomguithereal/eslint-config", 54 | "eslint-config-prettier" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /random-boolean.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomBooleanFunction = () => boolean; 4 | 5 | declare const randomBoolean: { 6 | (): boolean; 7 | createRandomBoolean(rng: RNGFunction): RandomBooleanFunction; 8 | }; 9 | 10 | export function createRandomBoolean(rng: RNGFunction): RandomBooleanFunction; 11 | 12 | export default randomBoolean; 13 | -------------------------------------------------------------------------------- /random-boolean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Boolean 3 | * =========================== 4 | * 5 | * Function used to return a random boolean. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random boolean. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandomBoolean(rng) { 15 | /** 16 | * Random boolean function. 17 | * 18 | * @return {boolean} 19 | */ 20 | return function () { 21 | return rng() < 0.5; 22 | }; 23 | } 24 | 25 | /** 26 | * Default random boolean using `Math.random`. 27 | */ 28 | var randomBoolean = createRandomBoolean(Math.random); 29 | 30 | /** 31 | * Exporting. 32 | */ 33 | randomBoolean.createRandomBoolean = createRandomBoolean; 34 | module.exports = randomBoolean; 35 | -------------------------------------------------------------------------------- /random-float.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomFloatFunction = (min: number, max: number) => number; 4 | 5 | declare const randomFloat: { 6 | (min: number, max: number): number; 7 | createRandomFloat(rng: RNGFunction): RandomFloatFunction; 8 | }; 9 | 10 | export function createRandomFloat(rng: RNGFunction): RandomFloatFunction; 11 | 12 | export default randomFloat; 13 | -------------------------------------------------------------------------------- /random-float.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Float 3 | * ========================= 4 | * 5 | * Random float function. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random float such as a <= N <= b. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandomFloat(rng) { 15 | /** 16 | * Random float function. 17 | * 18 | * @param {number} a - From. 19 | * @param {number} b - To. 20 | * @return {number} 21 | */ 22 | return function (a, b) { 23 | return a + rng() * (b - a); 24 | }; 25 | } 26 | 27 | /** 28 | * Default random using `Math.random`. 29 | */ 30 | var randomFloat = createRandomFloat(Math.random); 31 | 32 | /** 33 | * Exporting. 34 | */ 35 | randomFloat.createRandomFloat = createRandomFloat; 36 | module.exports = randomFloat; 37 | -------------------------------------------------------------------------------- /random-index.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomIndexFunction = (array: Array | number) => number; 4 | 5 | declare const randomIndex: { 6 | (array: Array | number): number; 7 | createRandomIndex(rng: RNGFunction): RandomIndexFunction; 8 | }; 9 | 10 | export function createRandomIndex(rng: RNGFunction): RandomIndexFunction; 11 | 12 | export default randomIndex; 13 | -------------------------------------------------------------------------------- /random-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Index 3 | * ========================= 4 | * 5 | * Random index function. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random index from the given array. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandomIndex(rng) { 15 | /** 16 | * Random function. 17 | * 18 | * @param {array|number} array - Target array or length of the array. 19 | * @return {number} 20 | */ 21 | return function (length) { 22 | if (typeof length !== 'number') length = length.length; 23 | 24 | return Math.floor(rng() * length); 25 | }; 26 | } 27 | 28 | /** 29 | * Default random index using `Math.random`. 30 | */ 31 | var randomIndex = createRandomIndex(Math.random); 32 | 33 | /** 34 | * Exporting. 35 | */ 36 | randomIndex.createRandomIndex = createRandomIndex; 37 | module.exports = randomIndex; 38 | -------------------------------------------------------------------------------- /random-ordered-pair.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomOrderedPairFunction = (array: Array | number) => number; 4 | 5 | declare const randomOrderedPair: { 6 | (array: Array | number): number; 7 | createRandomOrderedPair(rng: RNGFunction): RandomOrderedPairFunction; 8 | }; 9 | 10 | export function createRandomOrderedPair( 11 | rng: RNGFunction 12 | ): RandomOrderedPairFunction; 13 | 14 | export default randomOrderedPair; 15 | -------------------------------------------------------------------------------- /random-ordered-pair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Ordered Pair 3 | * ================================ 4 | * 5 | * Random ordered pair function. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random ordered pair of the given array. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandomOrderedPair(rng) { 15 | /** 16 | * Random ordered pair function. 17 | * 18 | * @param {array|number} array - Target array or length of the array. 19 | * @return {number} 20 | */ 21 | return function (array) { 22 | var needItems = typeof array !== 'number'; 23 | 24 | var n = needItems ? array.length : array; 25 | 26 | if (n < 2) 27 | throw new Error( 28 | 'pandemonium/random-ordered-pair: cannot draw a random ordered pair for length < 2.' 29 | ); 30 | 31 | var i = Math.floor(rng() * n); 32 | var o = 1 + Math.floor(rng() * (n - 1)); 33 | var j = (i + o) % n; 34 | 35 | if (needItems) return [array[i], array[j]]; 36 | 37 | return [i, j]; 38 | }; 39 | } 40 | 41 | /** 42 | * Default random ordered pair using `Math.random`. 43 | */ 44 | var randomOrderedPair = createRandomOrderedPair(Math.random); 45 | 46 | /** 47 | * Exporting. 48 | */ 49 | randomOrderedPair.createRandomOrderedPair = createRandomOrderedPair; 50 | module.exports = randomOrderedPair; 51 | -------------------------------------------------------------------------------- /random-pair.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomPairFunction = (array: Array | number) => number; 4 | 5 | declare const randomPair: { 6 | (array: Array | number): number; 7 | createRandomPair(rng: RNGFunction): RandomPairFunction; 8 | }; 9 | 10 | export function createRandomPair(rng: RNGFunction): RandomPairFunction; 11 | 12 | export default randomPair; 13 | -------------------------------------------------------------------------------- /random-pair.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Pair 3 | * ======================== 4 | * 5 | * Random pair function. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random pair of the given array. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandomPair(rng) { 15 | /** 16 | * Random pair function. 17 | * 18 | * @param {array|number} array - Target array or length of the array. 19 | * @return {number} 20 | */ 21 | return function (array) { 22 | var needItems = typeof array !== 'number'; 23 | 24 | var n = needItems ? array.length : array; 25 | 26 | if (n < 2) 27 | throw new Error( 28 | 'pandemonium/random-pair: cannot draw a random pair for length < 2.' 29 | ); 30 | 31 | var i = Math.floor(rng() * n); 32 | var o = 1 + Math.floor(rng() * (n - 1)); 33 | var j = (i + o) % n; 34 | 35 | var tmp; 36 | 37 | // Swapping coordinates to produce an unordered pair 38 | if (i > j) { 39 | tmp = i; 40 | i = j; 41 | j = tmp; 42 | } 43 | 44 | if (needItems) return [array[i], array[j]]; 45 | 46 | return [i, j]; 47 | }; 48 | } 49 | 50 | /** 51 | * Default random pair using `Math.random`. 52 | */ 53 | var randomPair = createRandomPair(Math.random); 54 | 55 | /** 56 | * Exporting. 57 | */ 58 | randomPair.createRandomPair = createRandomPair; 59 | module.exports = randomPair; 60 | -------------------------------------------------------------------------------- /random-string.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type Alphabet = Array | string; 4 | type RandomStringFunction = (minLength: number, maxLength?: number) => string; 5 | 6 | declare const randomString: { 7 | (minLength: number, maxLength?: number): string; 8 | createRandomString( 9 | rng: RNGFunction, 10 | alphabet?: Alphabet 11 | ): RandomStringFunction; 12 | }; 13 | 14 | export function createRandomString( 15 | rng: RNGFunction, 16 | alphabet?: Alphabet 17 | ): RandomStringFunction; 18 | 19 | export default randomString; 20 | -------------------------------------------------------------------------------- /random-string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random String 3 | * ========================== 4 | * 5 | * Function generating random strings. 6 | */ 7 | var createRandom = require('./random.js').createRandom; 8 | 9 | /** 10 | * Constants. 11 | */ 12 | var DEFAULT_ALPHABET = 13 | 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789'; 14 | 15 | /** 16 | * Creating a function returning a random string composed from characters of 17 | * the given alphabet. 18 | * 19 | * @param {function} rng - RNG function returning uniform random. 20 | * @param {string|array} alphabet - Target alphabet. 21 | * @return {function} - The created function. 22 | */ 23 | function createRandomString(rng, alphabet) { 24 | if (!alphabet) alphabet = DEFAULT_ALPHABET; 25 | 26 | var customRandom = createRandom(rng), 27 | randomCharacterIndex = customRandom.bind(null, 0, alphabet.length - 1); 28 | 29 | /** 30 | * Random string function. 31 | * 32 | * @param {number} length - Desired string length. 33 | * @return {number} 34 | */ 35 | return function (length) { 36 | if (arguments.length > 1) { 37 | // We want to generate a string of variable length 38 | length = customRandom(arguments[0], arguments[1]); 39 | } 40 | 41 | var characters = new Array(length); 42 | 43 | for (var i = 0; i < length; i++) 44 | characters[i] = alphabet[randomCharacterIndex()]; 45 | 46 | return characters.join(''); 47 | }; 48 | } 49 | 50 | /** 51 | * Default random string using `Math.random`. 52 | */ 53 | var randomString = createRandomString(Math.random); 54 | 55 | /** 56 | * Exporting. 57 | */ 58 | randomString.createRandomString = createRandomString; 59 | module.exports = randomString; 60 | -------------------------------------------------------------------------------- /random-typed-int.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | export function createRandomUint32(rng: RNGFunction): () => number; 4 | export function randomUint32(): number; 5 | -------------------------------------------------------------------------------- /random-typed-int.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random Typed Int 3 | * ============================= 4 | * 5 | * Functions to generated random typed integers such as uint32. 6 | */ 7 | 8 | /** 9 | * Constants. 10 | */ 11 | var MAX_UINT32 = Math.pow(2, 32) - 1; 12 | 13 | function createRandomUint32(rng) { 14 | return function () { 15 | return Math.floor(rng() * MAX_UINT32); 16 | }; 17 | } 18 | 19 | /** 20 | * Default random using `Math.random`. 21 | */ 22 | var randomUint32 = createRandomUint32(Math.random); 23 | 24 | /** 25 | * Exporting. 26 | */ 27 | exports.createRandomUint32 = createRandomUint32; 28 | exports.randomUint32 = randomUint32; 29 | -------------------------------------------------------------------------------- /random.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type RandomFunction = (min: number, max: number) => number; 4 | 5 | declare const random: { 6 | (min: number, max: number): number; 7 | createRandom(rng: RNGFunction): RandomFunction; 8 | }; 9 | 10 | export function createRandom(rng: RNGFunction): RandomFunction; 11 | 12 | export default random; 13 | -------------------------------------------------------------------------------- /random.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Random 3 | * =================== 4 | * 5 | * Random function. 6 | */ 7 | 8 | /** 9 | * Creating a function returning a random integer such as a <= N <= b. 10 | * 11 | * @param {function} rng - RNG function returning uniform random. 12 | * @return {function} - The created function. 13 | */ 14 | function createRandom(rng) { 15 | /** 16 | * Random function. 17 | * 18 | * @param {number} a - From. 19 | * @param {number} b - To. 20 | * @return {number} 21 | */ 22 | return function (a, b) { 23 | return a + Math.floor(rng() * (b - a + 1)); 24 | }; 25 | } 26 | 27 | /** 28 | * Default random using `Math.random`. 29 | */ 30 | var random = createRandom(Math.random); 31 | 32 | /** 33 | * Exporting. 34 | */ 35 | random.createRandom = createRandom; 36 | module.exports = random; 37 | -------------------------------------------------------------------------------- /reservoir-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type ReservoirSampleFunction = (k: number, array: Array) => Array; 4 | 5 | export class ReservoirSampler { 6 | constructor(k: number, rng?: RNGFunction); 7 | process(item: T): void; 8 | end(): Array; 9 | } 10 | 11 | declare const reservoirSample: { 12 | (k: number, array: Array): Array; 13 | createReservoirSample(rng: RNGFunction): ReservoirSampleFunction; 14 | ReservoirSampler: typeof ReservoirSampler; 15 | }; 16 | 17 | export function createReservoirSample( 18 | rng: RNGFunction 19 | ): ReservoirSampleFunction; 20 | 21 | export default reservoirSample; 22 | -------------------------------------------------------------------------------- /reservoir-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Reservoir Sample 3 | * ============================= 4 | * 5 | * Reservoir sampling function. 6 | * 7 | * [Reference]: 8 | * https://en.wikipedia.org/wiki/Reservoir_sampling#Simple_algorithm 9 | */ 10 | var createRandomIndex = require('./random-index.js').createRandomIndex; 11 | 12 | /** 13 | * Helper class. 14 | */ 15 | function ReservoirSampler(k, rng) { 16 | if (!rng) rng = Math.random; 17 | 18 | this.randomIndex = createRandomIndex(rng); 19 | this.k = k; 20 | this.size = 0; 21 | this.sample = new Array(k); 22 | this.done = false; 23 | } 24 | 25 | ReservoirSampler.prototype.process = function (item) { 26 | if (this.done) { 27 | throw new Error( 28 | 'pandemonium/ReservoirSampler: this sampler has returned its result and cannot be used anymore.' 29 | ); 30 | } 31 | 32 | if (this.size < this.k) { 33 | this.sample[this.size++] = item; 34 | 35 | return; 36 | } 37 | 38 | var j = this.randomIndex(this.size); 39 | 40 | if (j < this.k) this.sample[j] = item; 41 | 42 | this.size++; 43 | }; 44 | 45 | ReservoirSampler.prototype.end = function () { 46 | this.done = true; 47 | 48 | if (this.size < this.k) this.sample.length = this.size; 49 | 50 | return this.sample; 51 | }; 52 | 53 | /** 54 | * Creating a function returning a sample of size n using the provided RNG. 55 | * 56 | * @param {function} rng - The RNG to use. 57 | * @return {function} - The created function. 58 | */ 59 | function createReservoirSample(rng) { 60 | var customRandomIndex = createRandomIndex(rng); 61 | 62 | /** 63 | * Function returning sample of size n from array. 64 | * 65 | * @param {number} k - Size of the sample. 66 | * @param {array|number} sequence - Target sequence or its length. 67 | * @return {array} - The random sample. 68 | */ 69 | return function (k, sequence) { 70 | var n = sequence.length; 71 | 72 | // Sample size gte sequence's length 73 | if (k >= n) return sequence.slice(); 74 | 75 | var sample = new Array(k); 76 | var i, j; 77 | 78 | for (i = 0; i < k; i++) sample[i] = sequence[i]; 79 | 80 | for (; i < n; i++) { 81 | j = customRandomIndex(i); 82 | 83 | if (j < k) sample[j] = sequence[i]; 84 | } 85 | 86 | return sample; 87 | }; 88 | } 89 | 90 | /** 91 | * Default reservoir sample using `Math.random`. 92 | */ 93 | var reservoirSample = createReservoirSample(Math.random); 94 | 95 | /** 96 | * Exporting. 97 | */ 98 | reservoirSample.createReservoirSample = createReservoirSample; 99 | reservoirSample.ReservoirSampler = ReservoirSampler; 100 | module.exports = reservoirSample; 101 | -------------------------------------------------------------------------------- /sample-ordered-pairs.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type SampleOrderedPairsFunction = 4 | | ((k: number, array: Array) => Array) 5 | | ((k: number, length: number) => Array); 6 | 7 | declare const sampleOrderedPairs: { 8 | (k: number, length: number): Array; 9 | (k: number, array: Array): Array; 10 | createSampleOrderedPairs(rng: RNGFunction): SampleOrderedPairsFunction; 11 | }; 12 | 13 | export function createSampleOrderedPairs( 14 | rng: RNGFunction 15 | ): SampleOrderedPairsFunction; 16 | 17 | export default sampleOrderedPairs; 18 | -------------------------------------------------------------------------------- /sample-ordered-pairs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Sample Ordered Pairs 3 | * ================================= 4 | * 5 | * Algorithm to sample ordered pairs without replacement from the given 6 | * population. 7 | */ 8 | var utils = require('./utils.js'); 9 | 10 | /** 11 | * Creating a function returning a sample of k pairs using the provided RNG. 12 | * 13 | * @param {function} rng - The RNG to use. 14 | * @return {function} - The created function. 15 | */ 16 | function createSampleOrderedPairs(rng) { 17 | /** 18 | * Function returning sample of k pairs from given array or range. 19 | * 20 | * @param {number} k - Size of the sample. 21 | * @param {array|number} sequence - Target sequence or its length. 22 | * @return {array} - The random sample. 23 | */ 24 | return function (k, sequence) { 25 | var needItems = typeof sequence !== 'number'; 26 | var n = needItems ? sequence.length : sequence; 27 | 28 | var key = utils.createPairKeyFunction(n); 29 | 30 | var items = new Set(); 31 | var result = new Array(k); 32 | var size = 0; 33 | var i, j, o; 34 | 35 | while (items.size < k) { 36 | i = Math.floor(rng() * n); 37 | o = 1 + Math.floor(rng() * (n - 1)); 38 | j = (i + o) % n; 39 | 40 | items.add(key(i, j)); 41 | 42 | if (items.size > size) { 43 | result[size++] = needItems ? [sequence[i], sequence[j]] : [i, j]; 44 | } 45 | } 46 | 47 | return result; 48 | }; 49 | } 50 | 51 | /** 52 | * Default sample pairs using `Math.random`. 53 | */ 54 | var sampleOrderedPairs = createSampleOrderedPairs(Math.random); 55 | 56 | /** 57 | * Exporting. 58 | */ 59 | sampleOrderedPairs.createSampleOrderedPairs = createSampleOrderedPairs; 60 | module.exports = sampleOrderedPairs; 61 | -------------------------------------------------------------------------------- /sample-pairs.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type SamplePairsFunction = 4 | | ((k: number, array: Array) => Array) 5 | | ((k: number, length: number) => Array); 6 | 7 | declare const samplePairs: { 8 | (k: number, length: number): Array; 9 | (k: number, array: Array): Array; 10 | createSamplePairs(rng: RNGFunction): SamplePairsFunction; 11 | }; 12 | 13 | export function createSamplePairs(rng: RNGFunction): SamplePairsFunction; 14 | 15 | export default samplePairs; 16 | -------------------------------------------------------------------------------- /sample-pairs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Sample Pairs 3 | * ========================= 4 | * 5 | * Algorithm to sample unordered pairs without replacement from the given 6 | * population. 7 | */ 8 | var utils = require('./utils.js'); 9 | 10 | /** 11 | * Creating a function returning a sample of k pairs using the provided RNG. 12 | * 13 | * @param {function} rng - The RNG to use. 14 | * @return {function} - The created function. 15 | */ 16 | function createSamplePairs(rng) { 17 | /** 18 | * Function returning sample of k pairs from given array or range. 19 | * 20 | * @param {number} k - Size of the sample. 21 | * @param {array|number} sequence - Target sequence or its length. 22 | * @return {array} - The random sample. 23 | */ 24 | return function (k, sequence) { 25 | var needItems = typeof sequence !== 'number'; 26 | var n = needItems ? sequence.length : sequence; 27 | 28 | var key = utils.createPairKeyFunction(n); 29 | 30 | var items = new Set(); 31 | var result = new Array(k); 32 | var size = 0; 33 | var i, j, o, tmp; 34 | 35 | while (items.size < k) { 36 | i = Math.floor(rng() * n); 37 | o = 1 + Math.floor(rng() * (n - 1)); 38 | j = (i + o) % n; 39 | 40 | // Swapping coordinates to produce an unordered pair 41 | if (i > j) { 42 | tmp = i; 43 | i = j; 44 | j = tmp; 45 | } 46 | 47 | items.add(key(i, j)); 48 | 49 | if (items.size > size) { 50 | result[size++] = needItems ? [sequence[i], sequence[j]] : [i, j]; 51 | } 52 | } 53 | 54 | return result; 55 | }; 56 | } 57 | 58 | /** 59 | * Default sample pairs using `Math.random`. 60 | */ 61 | var samplePairs = createSamplePairs(Math.random); 62 | 63 | /** 64 | * Exporting. 65 | */ 66 | samplePairs.createSamplePairs = createSamplePairs; 67 | module.exports = samplePairs; 68 | -------------------------------------------------------------------------------- /sample-with-replacements.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type SampleWithReplacementsFunction = ( 4 | k: number, 5 | array: Array 6 | ) => Array; 7 | 8 | declare const naiveSampleWithReplacements: { 9 | (k: number, array: Array): Array; 10 | createSampleWithReplacements( 11 | rng: RNGFunction 12 | ): SampleWithReplacementsFunction; 13 | }; 14 | 15 | export function createSampleWithReplacements( 16 | rng: RNGFunction 17 | ): SampleWithReplacementsFunction; 18 | 19 | export default naiveSampleWithReplacements; 20 | -------------------------------------------------------------------------------- /sample-with-replacements.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Sample With Replacements 3 | * ===================================== 4 | * 5 | * Straightforward sampling function that allows an item to exist multiple 6 | * times in the resulting sample. 7 | */ 8 | var createRandomIndex = require('./random-index.js').createRandomIndex; 9 | 10 | /** 11 | * Creating a function returning a sample of size k with replacements 12 | * using the provided RNG. 13 | * 14 | * @param {function} rng - The RNG to use. 15 | * @return {function} - The created function. 16 | */ 17 | function createSampleWithReplacements(rng) { 18 | var customRandomIndex = createRandomIndex(rng); 19 | 20 | /** 21 | * Function returning sample of size k from array with replacements. 22 | * 23 | * @param {number} k - Size of the sample. 24 | * @param {array} sequence - Target sequence. 25 | * @return {array} - The random sample. 26 | */ 27 | return function (k, sequence) { 28 | var sample = new Array(k), 29 | m = sequence.length, 30 | i, 31 | r; 32 | 33 | for (i = 0; i < k; i++) { 34 | r = customRandomIndex(m); 35 | sample[i] = sequence[r]; 36 | } 37 | 38 | return sample; 39 | }; 40 | } 41 | 42 | /** 43 | * Default sample with replacements using `Math.random`. 44 | */ 45 | var sampleWithReplacements = createSampleWithReplacements(Math.random); 46 | 47 | /** 48 | * Exporting. 49 | */ 50 | sampleWithReplacements.createSampleWithReplacements = 51 | createSampleWithReplacements; 52 | module.exports = sampleWithReplacements; 53 | -------------------------------------------------------------------------------- /shuffle-in-place.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type ShuffleInPlaceFunction = (array: Array) => void; 4 | 5 | declare const shuffleInPlace: { 6 | (array: Array): void; 7 | createShuffleInPlace(rng: RNGFunction): ShuffleInPlaceFunction; 8 | }; 9 | 10 | export function createShuffleInPlace( 11 | rng: RNGFunction 12 | ): ShuffleInPlaceFunction; 13 | 14 | export default shuffleInPlace; 15 | -------------------------------------------------------------------------------- /shuffle-in-place.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Shuffle In Place 3 | * ============================= 4 | * 5 | * Shuffle function applying the Fisher-Yates algorithm to the provided array. 6 | */ 7 | var createRandom = require('./random.js').createRandom; 8 | 9 | /** 10 | * Creating a function returning the given array shuffled. 11 | * 12 | * @param {function} rng - The RNG to use. 13 | * @return {function} - The created function. 14 | */ 15 | function createShuffleInPlace(rng) { 16 | var customRandom = createRandom(rng); 17 | 18 | /** 19 | * Function returning the shuffled array. 20 | * 21 | * @param {array} sequence - Target sequence. 22 | * @return {array} - The shuffled sequence. 23 | */ 24 | return function (sequence) { 25 | var length = sequence.length, 26 | lastIndex = length - 1; 27 | 28 | var index = -1; 29 | 30 | while (++index < length) { 31 | var r = customRandom(index, lastIndex), 32 | value = sequence[r]; 33 | 34 | sequence[r] = sequence[index]; 35 | sequence[index] = value; 36 | } 37 | }; 38 | } 39 | 40 | /** 41 | * Default shuffle in place using `Math.random`. 42 | */ 43 | var shuffleInPlace = createShuffleInPlace(Math.random); 44 | 45 | /** 46 | * Exporting. 47 | */ 48 | shuffleInPlace.createShuffleInPlace = createShuffleInPlace; 49 | module.exports = shuffleInPlace; 50 | -------------------------------------------------------------------------------- /shuffle.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type ShuffleFunction = (array: Array) => Array; 4 | 5 | declare const shuffle: { 6 | (array: Array): Array; 7 | createShuffle(rng: RNGFunction): ShuffleFunction; 8 | }; 9 | 10 | export function createShuffle(rng: RNGFunction): ShuffleFunction; 11 | 12 | export default shuffle; 13 | -------------------------------------------------------------------------------- /shuffle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Shuffle 3 | * ==================== 4 | * 5 | * Shuffle function which is basically just applying the Fisher-Yates sampling 6 | * function over the whole array. 7 | */ 8 | var createShuffleInPlace = 9 | require('./shuffle-in-place.js').createShuffleInPlace; 10 | 11 | /** 12 | * Creating a function returning the given array shuffled. 13 | * 14 | * @param {function} rng - The RNG to use. 15 | * @return {function} - The created function. 16 | */ 17 | function createShuffle(rng) { 18 | var customShuffleInPlace = createShuffleInPlace(rng); 19 | 20 | /** 21 | * Function returning the shuffled array. 22 | * 23 | * @param {array} sequence - Target sequence. 24 | * @return {array} - The shuffled sequence. 25 | */ 26 | return function (sequence) { 27 | var copy = sequence.slice(); 28 | customShuffleInPlace(copy); 29 | 30 | return copy; 31 | }; 32 | } 33 | 34 | /** 35 | * Default shuffle using `Math.random`. 36 | */ 37 | var shuffle = createShuffle(Math.random); 38 | 39 | /** 40 | * Exporting. 41 | */ 42 | shuffle.createShuffle = createShuffle; 43 | module.exports = shuffle; 44 | -------------------------------------------------------------------------------- /test-types.ts: -------------------------------------------------------------------------------- 1 | import * as lib from './'; 2 | import {createChoice} from './choice'; 3 | import shuffleInPlace from './shuffle-in-place'; 4 | 5 | const dummyRandom = () => 0.5; 6 | 7 | const customChoice = createChoice(dummyRandom); 8 | const chosen: number = customChoice([1, 2, 3]); 9 | 10 | console.log(chosen, lib); 11 | 12 | const arrayToShuffle = [1, 2, 3, 4, 5]; 13 | shuffleInPlace(arrayToShuffle); 14 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Library Unit Tests 3 | * =============================== 4 | */ 5 | var assert = require('assert'); 6 | var seedrandom = require('seedrandom'); 7 | 8 | var lib = require('./'); 9 | var utils = require('./utils.js'); 10 | 11 | var rng = function () { 12 | return seedrandom('shawarma'); 13 | }; 14 | 15 | var vec = function (size, fill) { 16 | var array = new Array(size); 17 | 18 | for (var i = 0; i < size; i++) array[i] = fill; 19 | 20 | return array; 21 | }; 22 | 23 | describe('utils', function () { 24 | describe('triu conversions', function () { 25 | // n = 5 26 | 27 | // Matrix representation 28 | // 0. 1. 2. 3. 4. 29 | // 0. 0 a0 a1 a2 a3 30 | // 1. 0 0 a4 a5 a6 31 | // 2 0 0 0 a7 a8 32 | // 3. 0 0 0 0 a9 33 | // 4. 0 0 0 0 0 34 | 35 | // Linear representation 36 | // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] 37 | // [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9] 38 | 39 | // Row representation 40 | // [0, 1, 2, 3, 4] 41 | 42 | var coords = [ 43 | [0, 1], 44 | [0, 2], 45 | [0, 3], 46 | [0, 4], 47 | [1, 2], 48 | [1, 3], 49 | [1, 4], 50 | [2, 3], 51 | [2, 4], 52 | [3, 4] 53 | ]; 54 | 55 | var linear = utils.indices(10); 56 | 57 | it('should be possible to assess the linear length of a triu matrix.', function () { 58 | var l = utils.triuLinearLength(5); 59 | 60 | assert.strictEqual(l, linear.length); 61 | }); 62 | 63 | it('should be possible to convert to triu coordinates.', function () { 64 | var results = linear.map(function (i) { 65 | return utils.linearIndexToTriuCoords(5, i); 66 | }); 67 | 68 | assert.deepStrictEqual(results, coords); 69 | }); 70 | 71 | it('should be possible to convert from triu coordinates.', function () { 72 | var results = coords.map(function (pair) { 73 | return utils.triuCoordsToLinearIndex(5, pair[0], pair[1]); 74 | }); 75 | 76 | assert.deepStrictEqual(results, linear); 77 | }); 78 | 79 | it('should be possible to convert efficiently to triu coordinates.', function () { 80 | var results = linear.map(function (i) { 81 | return utils.linearIndexToTriuCoordsFast(i); 82 | }); 83 | 84 | assert.deepStrictEqual(results, [ 85 | [1, 0], 86 | [2, 0], 87 | [2, 1], 88 | [3, 0], 89 | [3, 1], 90 | [3, 2], 91 | [4, 0], 92 | [4, 1], 93 | [4, 2], 94 | [4, 3] 95 | ]); 96 | 97 | results = utils.indices(utils.triuLinearLength(6)).map(function (i) { 98 | return utils.linearIndexToTriuCoordsFast(i); 99 | }); 100 | 101 | assert.deepStrictEqual(results, [ 102 | [1, 0], 103 | [2, 0], 104 | [2, 1], 105 | [3, 0], 106 | [3, 1], 107 | [3, 2], 108 | [4, 0], 109 | [4, 1], 110 | [4, 2], 111 | [4, 3], 112 | [5, 0], 113 | [5, 1], 114 | [5, 2], 115 | [5, 3], 116 | [5, 4] 117 | ]); 118 | }); 119 | }); 120 | 121 | describe('pair keys', function () { 122 | it('should return correct numerical keys.', function () { 123 | var matrix = [ 124 | [0, 1, 2], 125 | [3, 4, 5], 126 | [6, 7, 8] 127 | ]; 128 | 129 | var key = utils.createPairKeyFunction(matrix[0].length); 130 | 131 | matrix.forEach(function (row, j) { 132 | row.forEach(function (linearIndex, i) { 133 | assert.strictEqual(key(i, j), linearIndex); 134 | }); 135 | }); 136 | }); 137 | 138 | it('should fallback to string keys when numerical precision is overflown.', function () { 139 | var n = 94906265 * 2; 140 | 141 | var key = utils.createPairKeyFunction(n); 142 | 143 | assert.strictEqual(key(4, 7), '4,7'); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('#.createRandom', function () { 149 | var createRandom = lib.createRandom; 150 | 151 | it('should be possible to create a random function using supplied rng.', function () { 152 | var random = createRandom(rng()); 153 | 154 | var numbers = vec(10, 0).map(function () { 155 | return random(1, 3); 156 | }); 157 | 158 | assert.deepStrictEqual(numbers, [2, 1, 1, 2, 2, 1, 2, 3, 2, 3]); 159 | }); 160 | }); 161 | 162 | describe('#.createRandomBoolean', function () { 163 | var createRandomBoolean = lib.createRandomBoolean; 164 | 165 | it('should be possible to create a random function using supplied rng.', function () { 166 | var randomBoolean = createRandomBoolean(rng()); 167 | 168 | var bools = vec(10, 0).map(function () { 169 | return randomBoolean(); 170 | }); 171 | 172 | assert.deepStrictEqual(bools, [ 173 | true, 174 | true, 175 | true, 176 | true, 177 | false, 178 | true, 179 | true, 180 | false, 181 | true, 182 | false 183 | ]); 184 | }); 185 | }); 186 | 187 | describe('#.createRandomFloat', function () { 188 | var createRandomFloat = lib.createRandomFloat; 189 | 190 | it('should be possible to create a random function using supplied rng.', function () { 191 | var randomFloat = createRandomFloat(rng()); 192 | 193 | var numbers = vec(10, 0).map(function () { 194 | return randomFloat(-5, 5); 195 | }); 196 | 197 | assert.deepStrictEqual( 198 | numbers, 199 | [ 200 | -1.3882625149887873, -2.6951281957879387, -2.0503955872813586, 201 | -0.4793171781490271, 0.12873486129402423, -2.3493674464758856, 202 | -0.17451351278587435, 2.0272152885286534, -1.4110661264160034, 203 | 4.861209836885362 204 | ] 205 | ); 206 | }); 207 | }); 208 | 209 | describe('#.createRandomIndex', function () { 210 | var createRandomIndex = lib.createRandomIndex; 211 | 212 | it('should be possible to create a random index function using the supplied rng.', function () { 213 | var randomIndex = createRandomIndex(rng()); 214 | 215 | var fruits = ['apple', 'pear', 'orange']; 216 | 217 | var tests = vec(10, 0).map(function () { 218 | return randomIndex(fruits); 219 | }); 220 | 221 | assert.deepStrictEqual(tests, [1, 0, 0, 1, 1, 0, 1, 2, 1, 2]); 222 | }); 223 | 224 | it('should be possible to pass the length of the array instead of the array itself.', function () { 225 | var randomIndex = createRandomIndex(rng()); 226 | 227 | var fruits = ['apple', 'pear', 'orange']; 228 | 229 | var tests = vec(10, 0).map(function () { 230 | return randomIndex(fruits.length); 231 | }); 232 | 233 | assert.deepStrictEqual(tests, [1, 0, 0, 1, 1, 0, 1, 2, 1, 2]); 234 | }); 235 | }); 236 | 237 | describe('#.createChoice', function () { 238 | var createChoice = lib.createChoice; 239 | 240 | it('should be possible to create a choice function using the supplied rng.', function () { 241 | var choice = createChoice(rng()); 242 | 243 | var fruits = ['apple', 'pear', 'orange']; 244 | 245 | var tests = vec(10, 0).map(function () { 246 | return choice(fruits); 247 | }); 248 | 249 | assert.deepStrictEqual( 250 | tests, 251 | [1, 0, 0, 1, 1, 0, 1, 2, 1, 2].map(function (index) { 252 | return fruits[index]; 253 | }) 254 | ); 255 | }); 256 | }); 257 | 258 | describe('#.createFisherYatesSample', function () { 259 | var createFisherYatesSample = lib.createFisherYatesSample; 260 | 261 | var data = [13, 14, 15, 8, 20]; 262 | 263 | it('should be possible to create a sample function using the supplied rng.', function () { 264 | var fisherYatesSample = createFisherYatesSample(rng()); 265 | 266 | var tests = vec(5, 0).map(function () { 267 | return fisherYatesSample(2, data); 268 | }); 269 | 270 | assert.deepStrictEqual(tests, [ 271 | [14, 13], 272 | [14, 15], 273 | [15, 13], 274 | [15, 8], 275 | [14, 20] 276 | ]); 277 | }); 278 | 279 | it('should work when k >= n.', function () { 280 | var numbers = [1, 2, 3]; 281 | 282 | assert.deepStrictEqual( 283 | new Set(numbers), 284 | new Set(lib.fisherYatesSample(3, numbers)) 285 | ); 286 | assert.deepStrictEqual( 287 | new Set(numbers), 288 | new Set(lib.fisherYatesSample(14, numbers)) 289 | ); 290 | }); 291 | }); 292 | 293 | describe('#.createGeometricReservoirSample', function () { 294 | var createGeometricReservoirSample = lib.createGeometricReservoirSample; 295 | 296 | var data = [13, 14, 15, 8, 20]; 297 | 298 | it('should be possible to create a sample function using the supplied rng.', function () { 299 | var geometricReservoirSample = createGeometricReservoirSample(rng()); 300 | 301 | var tests = vec(10, 0).map(function () { 302 | return geometricReservoirSample(2, data); 303 | }); 304 | 305 | assert.deepStrictEqual(tests, [ 306 | [8, 14], 307 | [13, 20], 308 | [8, 20], 309 | [13, 14], 310 | [13, 20], 311 | [15, 8], 312 | [13, 20], 313 | [13, 20], 314 | [20, 8], 315 | [15, 14] 316 | ]); 317 | }); 318 | 319 | it('should always return coherent results.', function () { 320 | vec(50, 0).forEach(function () { 321 | var sample = lib.geometricReservoirSample(500, utils.indices(5000)); 322 | 323 | assert.strictEqual(sample.length, 500); 324 | assert.strictEqual(new Set(sample).size, 500); 325 | }); 326 | }); 327 | 328 | it("should be possible to give the sequence's length and get indices back.", function () { 329 | var sample = createGeometricReservoirSample(rng()); 330 | 331 | var numbers = [13, 14, 15, 8, 20, 20]; 332 | 333 | var tests = vec(7, 0).map(function () { 334 | return sample(2, numbers.length); 335 | }); 336 | 337 | assert.deepStrictEqual(tests, [ 338 | [5, 1], 339 | [0, 5], 340 | [5, 2], 341 | [3, 5], 342 | [2, 3], 343 | [5, 4], 344 | [0, 3] 345 | ]); 346 | }); 347 | 348 | it('should work when k >= n.', function () { 349 | var numbers = [1, 2, 3]; 350 | 351 | assert.deepStrictEqual( 352 | new Set(numbers), 353 | new Set(lib.geometricReservoirSample(3, numbers)) 354 | ); 355 | assert.deepStrictEqual( 356 | new Set(numbers), 357 | new Set(lib.geometricReservoirSample(14, numbers)) 358 | ); 359 | }); 360 | 361 | it('should work when k >= n with indices.', function () { 362 | var sample = createGeometricReservoirSample(rng()); 363 | 364 | var indices = sample(5, 3); 365 | 366 | assert.deepStrictEqual(indices, [0, 1, 2]); 367 | }); 368 | }); 369 | 370 | describe('#.createReservoirSample', function () { 371 | var createReservoirSample = lib.createReservoirSample; 372 | 373 | var data = [13, 14, 15, 8, 20]; 374 | 375 | it('should be possible to create a sample function using the supplied rng.', function () { 376 | var reservoirSample = createReservoirSample(rng()); 377 | 378 | var tests = vec(10, 0).map(function () { 379 | return reservoirSample(2, data); 380 | }); 381 | 382 | assert.deepStrictEqual(tests, [ 383 | [8, 20], 384 | [15, 20], 385 | [15, 20], 386 | [13, 15], 387 | [13, 15], 388 | [15, 14], 389 | [15, 14], 390 | [13, 8], 391 | [8, 14], 392 | [15, 14] 393 | ]); 394 | }); 395 | 396 | it('should always return coherent results.', function () { 397 | vec(50, 0).forEach(function () { 398 | var sample = lib.reservoirSample(500, utils.indices(5000)); 399 | 400 | assert.strictEqual(sample.length, 500); 401 | assert.strictEqual(new Set(sample).size, 500); 402 | }); 403 | }); 404 | 405 | it('should work when k >= n.', function () { 406 | var numbers = [1, 2, 3]; 407 | 408 | assert.deepStrictEqual( 409 | new Set(numbers), 410 | new Set(lib.reservoirSample(3, numbers)) 411 | ); 412 | assert.deepStrictEqual( 413 | new Set(numbers), 414 | new Set(lib.reservoirSample(14, numbers)) 415 | ); 416 | }); 417 | }); 418 | 419 | describe('ReservoirSampler', function () { 420 | it('should work as expected.', function () { 421 | var sampler = new lib.ReservoirSampler(10, rng()); 422 | 423 | utils.indices(100).forEach(function (i) { 424 | sampler.process(i); 425 | }); 426 | 427 | assert.deepStrictEqual( 428 | sampler.end(), 429 | [34, 70, 82, 15, 80, 79, 56, 16, 63, 91] 430 | ); 431 | 432 | // Cannot use after end 433 | assert.throws(function () { 434 | sampler.process(45); 435 | }); 436 | 437 | sampler = new lib.ReservoirSampler(15, rng()); 438 | 439 | utils.indices(15).forEach(function (i) { 440 | sampler.process(i); 441 | }); 442 | 443 | assert.deepStrictEqual(sampler.end(), utils.indices(15)); 444 | 445 | sampler = new lib.ReservoirSampler(15, rng()); 446 | 447 | utils.indices(5).forEach(function (i) { 448 | sampler.process(i); 449 | }); 450 | 451 | assert.deepStrictEqual(sampler.end(), utils.indices(5)); 452 | 453 | sampler = new lib.ReservoirSampler(15, rng()); 454 | 455 | assert.deepStrictEqual(sampler.end(), []); 456 | }); 457 | }); 458 | 459 | describe('#.createSampleWithReplacements', function () { 460 | var createSampleWithReplacements = lib.createSampleWithReplacements; 461 | 462 | var data = [13, 14, 15, 8, 20]; 463 | 464 | it('should be possible to create a sample function using the supplied rng.', function () { 465 | var sample = createSampleWithReplacements(rng()); 466 | 467 | var tests = vec(3, 0).map(function () { 468 | return sample(4, data); 469 | }); 470 | 471 | assert.deepStrictEqual(tests, [ 472 | [14, 14, 14, 15], 473 | [15, 14, 15, 8], 474 | [14, 20, 8, 8] 475 | ]); 476 | }); 477 | }); 478 | 479 | describe('#.createShuffle', function () { 480 | var createShuffle = lib.createShuffle; 481 | 482 | it('should be possible to create a shuffle function using the supplied rng.', function () { 483 | var shuffle = createShuffle(rng()); 484 | 485 | var shuffled = shuffle([1, 2, 3, 4, 5]); 486 | assert.deepStrictEqual(shuffled, [2, 1, 3, 4, 5]); 487 | }); 488 | }); 489 | 490 | describe('#.createDangerouslyMutatingSample', function () { 491 | var createDangerouslyMutatingSample = lib.createDangerouslyMutatingSample; 492 | 493 | var data = [13, 14, 15, 8, 20], 494 | copy = data.slice(); 495 | 496 | it('should be possible to create a sample function using the supplied rng.', function () { 497 | var sample = createDangerouslyMutatingSample(rng()); 498 | 499 | var tests = vec(7, 0).map(function () { 500 | return sample(2, data); 501 | }); 502 | 503 | assert.deepStrictEqual(tests, [ 504 | [14, 13], 505 | [14, 15], 506 | [15, 13], 507 | [15, 8], 508 | [14, 20], 509 | [8, 13], 510 | [20, 13] 511 | ]); 512 | 513 | // Ensuring the state of the array did not change 514 | assert.deepStrictEqual(copy, data); 515 | }); 516 | 517 | it('should work when k >= n.', function () { 518 | var numbers = [1, 2, 3]; 519 | 520 | assert.deepStrictEqual( 521 | new Set(numbers), 522 | new Set(lib.dangerouslyMutatingSample(3, numbers)) 523 | ); 524 | assert.deepStrictEqual( 525 | new Set(numbers), 526 | new Set(lib.dangerouslyMutatingSample(14, numbers)) 527 | ); 528 | }); 529 | }); 530 | 531 | describe('#.naiveSample', function () { 532 | var createNaiveSample = lib.createNaiveSample; 533 | 534 | it('should be possible to create a sample function using the supplied rng.', function () { 535 | var sample = createNaiveSample(rng()); 536 | 537 | var data = [13, 14, 15, 8, 20, 20]; 538 | 539 | var tests = vec(7, 0).map(function () { 540 | return sample(2, data); 541 | }); 542 | 543 | assert.deepStrictEqual(tests, [ 544 | [15, 14], 545 | [14, 15], 546 | [8, 14], 547 | [15, 20], 548 | [15, 20], 549 | [20, 8], 550 | [20, 15] 551 | ]); 552 | }); 553 | 554 | it("should be possible to give the sequence's length and get indices back.", function () { 555 | var sample = createNaiveSample(rng()); 556 | 557 | var data = [13, 14, 15, 8, 20, 20]; 558 | 559 | var tests = vec(7, 0).map(function () { 560 | return sample(2, data.length); 561 | }); 562 | 563 | assert.deepStrictEqual(tests, [ 564 | [2, 1], 565 | [1, 2], 566 | [3, 1], 567 | [2, 4], 568 | [2, 5], 569 | [4, 3], 570 | [4, 2] 571 | ]); 572 | }); 573 | 574 | it('should work when k >= n.', function () { 575 | var numbers = [1, 2, 3]; 576 | 577 | assert.deepStrictEqual( 578 | new Set(numbers), 579 | new Set(lib.naiveSample(3, numbers)) 580 | ); 581 | assert.deepStrictEqual( 582 | new Set(numbers), 583 | new Set(lib.naiveSample(14, numbers)) 584 | ); 585 | }); 586 | 587 | it('should work when k >= n with indices.', function () { 588 | var sample = createNaiveSample(rng()); 589 | 590 | var indices = sample(5, 3); 591 | 592 | assert.deepStrictEqual(indices, [0, 1, 2]); 593 | }); 594 | }); 595 | 596 | describe('#.createShuffleInPlace', function () { 597 | var createShuffleInPlace = lib.createShuffleInPlace; 598 | 599 | it('should be possible to create a shuffle in place function using the supplied rng.', function () { 600 | var shuffle = createShuffleInPlace(rng()), 601 | array = [1, 2, 3, 4, 5]; 602 | 603 | shuffle(array); 604 | assert.deepStrictEqual(array, [2, 1, 3, 4, 5]); 605 | }); 606 | }); 607 | 608 | describe('#.createWeightedRandomIndex', function () { 609 | var createWeightedRandomIndex = lib.createWeightedRandomIndex; 610 | 611 | function freq(fn, target) { 612 | var sample = 10 * 1000, 613 | af = {}, 614 | rf = {}, 615 | r; 616 | 617 | for (var i = 0; i < sample; i++) { 618 | r = fn(target); 619 | af[r] = af[r] || 0; 620 | af[r]++; 621 | } 622 | 623 | for (var k in af) { 624 | rf[k] = af[k] / sample; 625 | } 626 | 627 | return {absolute: af, relative: rf}; 628 | } 629 | 630 | it('should correctly return a random index from weighted list.', function () { 631 | var weightedRandomIndex = createWeightedRandomIndex(rng()), 632 | weights = [0.1, 0.1, 0.8]; 633 | 634 | var f = freq(weightedRandomIndex, weights); 635 | 636 | assert.deepStrictEqual(f.relative, { 637 | 0: 0.0977, 638 | 1: 0.0947, 639 | 2: 0.8076 640 | }); 641 | }); 642 | 643 | it('should also work with absolute weights.', function () { 644 | var weightedRandomIndex = createWeightedRandomIndex(rng()), 645 | weights = [4, 4, 32]; 646 | 647 | var f = freq(weightedRandomIndex, weights); 648 | 649 | assert.deepStrictEqual(f.relative, { 650 | 0: 0.0977, 651 | 1: 0.0947, 652 | 2: 0.8076 653 | }); 654 | }); 655 | 656 | it('should also work with a weight getter.', function () { 657 | var getter = function (item) { 658 | return item.weight; 659 | }; 660 | 661 | var mapper = function (value, index) { 662 | return { 663 | weight: value, 664 | index: index 665 | }; 666 | }; 667 | 668 | var relativeWeightedRandomIndex = createWeightedRandomIndex({ 669 | rng: rng(), 670 | getWeight: getter 671 | }), 672 | absoluteWeightedRandomIndex = createWeightedRandomIndex({ 673 | rng: rng(), 674 | getWeight: getter 675 | }); 676 | 677 | var relativeWeights = [0.1, 0.1, 0.8].map(mapper), 678 | absoluteWeights = [4, 4, 32].map(mapper); 679 | 680 | var relativeF = freq(relativeWeightedRandomIndex, relativeWeights), 681 | absoluteF = freq(absoluteWeightedRandomIndex, absoluteWeights); 682 | 683 | assert.deepStrictEqual(relativeF.relative, { 684 | 0: 0.0977, 685 | 1: 0.0947, 686 | 2: 0.8076 687 | }); 688 | 689 | assert.deepStrictEqual(absoluteF.relative, { 690 | 0: 0.0977, 691 | 1: 0.0947, 692 | 2: 0.8076 693 | }); 694 | }); 695 | }); 696 | 697 | describe('#.createCachedWeightedRandomIndex', function () { 698 | var createCachedWeightedRandomIndex = lib.createCachedWeightedRandomIndex; 699 | 700 | var RELATIVE_WEIGHTS = [0.1, 0.1, 0.8], 701 | ABSOLUTE_WEIGHTS = [4, 4, 32]; 702 | 703 | function freq(fn) { 704 | var sample = 10 * 1000, 705 | af = {}, 706 | rf = {}, 707 | r; 708 | 709 | for (var i = 0; i < sample; i++) { 710 | r = fn(); 711 | af[r] = af[r] || 0; 712 | af[r]++; 713 | } 714 | 715 | for (var k in af) { 716 | rf[k] = af[k] / sample; 717 | } 718 | 719 | return {absolute: af, relative: rf}; 720 | } 721 | 722 | it('should correctly return a random index from weighted list.', function () { 723 | var weightedRandomIndex = createCachedWeightedRandomIndex( 724 | rng(), 725 | RELATIVE_WEIGHTS 726 | ); 727 | 728 | var f = freq(weightedRandomIndex); 729 | 730 | assert.deepStrictEqual(f.relative, { 731 | 0: 0.1012, 732 | 1: 0.0999, 733 | 2: 0.7989 734 | }); 735 | }); 736 | 737 | it('should also work with absolute weights.', function () { 738 | var weightedRandomIndex = createCachedWeightedRandomIndex( 739 | rng(), 740 | ABSOLUTE_WEIGHTS 741 | ); 742 | 743 | var f = freq(weightedRandomIndex); 744 | 745 | assert.deepStrictEqual(f.relative, { 746 | 0: 0.1012, 747 | 1: 0.0999, 748 | 2: 0.7989 749 | }); 750 | }); 751 | 752 | it('should also work with a weight getter.', function () { 753 | var getter = function (item) { 754 | return item.weight; 755 | }; 756 | 757 | var mapper = function (value, index) { 758 | return { 759 | weight: value, 760 | index: index 761 | }; 762 | }; 763 | 764 | var relativeWeights = RELATIVE_WEIGHTS.map(mapper), 765 | absoluteWeights = ABSOLUTE_WEIGHTS.map(mapper); 766 | 767 | var relativeWeightedRandomIndex = createCachedWeightedRandomIndex( 768 | {rng: rng(), getWeight: getter}, 769 | relativeWeights 770 | ), 771 | absoluteWeightedRandomIndex = createCachedWeightedRandomIndex( 772 | {rng: rng(), getWeight: getter}, 773 | absoluteWeights 774 | ); 775 | 776 | var relativeF = freq(relativeWeightedRandomIndex), 777 | absoluteF = freq(absoluteWeightedRandomIndex); 778 | 779 | assert.deepStrictEqual(relativeF.relative, { 780 | 0: 0.1012, 781 | 1: 0.0999, 782 | 2: 0.7989 783 | }); 784 | 785 | assert.deepStrictEqual(absoluteF.relative, { 786 | 0: 0.1012, 787 | 1: 0.0999, 788 | 2: 0.7989 789 | }); 790 | }); 791 | }); 792 | 793 | describe('#.createWeightedChoice', function () { 794 | var createWeightedChoice = lib.createWeightedChoice; 795 | 796 | function freq(fn, target) { 797 | var sample = 10 * 1000, 798 | af = {}, 799 | rf = {}, 800 | r; 801 | 802 | for (var i = 0; i < sample; i++) { 803 | r = fn(target)[0]; 804 | af[r] = af[r] || 0; 805 | af[r]++; 806 | } 807 | 808 | for (var k in af) { 809 | rf[k] = af[k] / sample; 810 | } 811 | 812 | return {absolute: af, relative: rf}; 813 | } 814 | 815 | it('should correctly return a random item from weighted list.', function () { 816 | var weightedChoice = createWeightedChoice({ 817 | rng: rng(), 818 | getWeight: function (item) { 819 | return item[1]; 820 | } 821 | }); 822 | 823 | var weights = [ 824 | ['pear', 0.1], 825 | ['apple', 0.1], 826 | ['cherry', 0.8] 827 | ]; 828 | 829 | var f = freq(weightedChoice, weights); 830 | 831 | assert.deepStrictEqual(f.relative, { 832 | pear: 0.0977, 833 | apple: 0.0947, 834 | cherry: 0.8076 835 | }); 836 | }); 837 | }); 838 | 839 | describe('#.createCachedWeightedChoice', function () { 840 | var createCachedWeightedChoice = lib.createCachedWeightedChoice; 841 | 842 | function freq(fn) { 843 | var sample = 10 * 1000, 844 | af = {}, 845 | rf = {}, 846 | r; 847 | 848 | for (var i = 0; i < sample; i++) { 849 | r = fn()[0]; 850 | af[r] = af[r] || 0; 851 | af[r]++; 852 | } 853 | 854 | for (var k in af) { 855 | rf[k] = af[k] / sample; 856 | } 857 | 858 | return {absolute: af, relative: rf}; 859 | } 860 | 861 | it('should correctly return a random item from weighted list.', function () { 862 | var weights = [ 863 | ['pear', 0.1], 864 | ['apple', 0.1], 865 | ['cherry', 0.8] 866 | ]; 867 | 868 | var weightedChoice = createCachedWeightedChoice( 869 | { 870 | rng: rng(), 871 | getWeight: function (item) { 872 | return item[1]; 873 | } 874 | }, 875 | weights 876 | ); 877 | 878 | var f = freq(weightedChoice); 879 | 880 | assert.deepStrictEqual(f.relative, { 881 | pear: 0.1012, 882 | apple: 0.0999, 883 | cherry: 0.7989 884 | }); 885 | }); 886 | }); 887 | 888 | describe('#.createRandomString', function () { 889 | var createRandomString = lib.createRandomString; 890 | 891 | it('should be able to produce random string of fixed length.', function () { 892 | var randomString = createRandomString(rng()); 893 | 894 | var tests = [ 895 | '', 896 | 'w', 897 | 'os', 898 | 'CFq', 899 | 'DRw', 900 | '9TLYZ', 901 | 'QD02aP', 902 | 'VXNLajS', 903 | 'l07VLFXK' 904 | ]; 905 | 906 | var results = tests.map(function (s) { 907 | return randomString(s.length); 908 | }); 909 | 910 | assert.deepStrictEqual(results, tests); 911 | }); 912 | 913 | it('should be able to produce random string of variable length.', function () { 914 | var randomString = createRandomString(rng()); 915 | 916 | var tests = [ 917 | [0, 0, ''], 918 | [0, 2, ''], 919 | [0, 3, 'C'], 920 | [0, 3, 'qD'], 921 | [0, 3, 'w9'], 922 | [0, 10, 'LYZQD02a'], 923 | [4, 7, 'VXNLaj'], 924 | [5, 5, 'l07VL'], 925 | [3, 14, 'XKuVG2i18'] 926 | ]; 927 | 928 | var results = tests.map(function (s) { 929 | return randomString(s[0], s[1]); 930 | }); 931 | 932 | assert.deepStrictEqual( 933 | results, 934 | tests.map(function (s) { 935 | return s[2]; 936 | }) 937 | ); 938 | 939 | var batch = new Array(1000); 940 | 941 | for (var i = 0; i < 1000; i++) batch[i] = randomString(3, 14); 942 | 943 | assert( 944 | batch.every(function (s) { 945 | return s.length >= 3 && s.length <= 14; 946 | }) 947 | ); 948 | }); 949 | 950 | it('should produce random strings based on a custom alphabet.', function () { 951 | var randomString = createRandomString(rng(), 'ATGC'); 952 | 953 | var tests = [ 954 | '', 955 | 'T', 956 | 'AT', 957 | 'TGT', 958 | 'TGT', 959 | 'CGGCC', 960 | 'GTCCAG', 961 | 'CCGGAAG', 962 | 'ACCCGGCG' 963 | ]; 964 | 965 | var results = tests.map(function (s) { 966 | return randomString(s.length); 967 | }); 968 | 969 | assert.deepStrictEqual(results, tests); 970 | }); 971 | }); 972 | 973 | describe('#.createRandomPair', function () { 974 | it('should throw when working on length < 2.', function () { 975 | var randomPair = lib.createRandomPair(rng()); 976 | 977 | assert.throws(function () { 978 | randomPair([0]); 979 | }); 980 | 981 | assert.throws(function () { 982 | randomPair(0); 983 | }); 984 | }); 985 | 986 | it('should properly return valid pairs.', function () { 987 | var randomPair = lib.createRandomPair(rng()); 988 | 989 | var target = ['apple', 'pear', 'tomato', 'olive']; 990 | 991 | var test = [ 992 | ['pear', 'tomato'], 993 | ['pear', 'olive'], 994 | ['tomato', 'olive'], 995 | ['apple', 'pear'], 996 | ['apple', 'pear'], 997 | ['apple', 'tomato'], 998 | ['tomato', 'olive'], 999 | ['apple', 'tomato'], 1000 | ['tomato', 'olive'], 1001 | ['apple', 'olive'] 1002 | ]; 1003 | 1004 | var pairs = vec(10).map(function () { 1005 | return randomPair(target); 1006 | }); 1007 | 1008 | assert.deepStrictEqual(pairs, test); 1009 | 1010 | randomPair = lib.createRandomPair(rng()); 1011 | 1012 | pairs = vec(10).map(function () { 1013 | return randomPair(4); 1014 | }); 1015 | 1016 | assert.deepStrictEqual(pairs, [ 1017 | [1, 2], 1018 | [1, 3], 1019 | [2, 3], 1020 | [0, 1], 1021 | [0, 1], 1022 | [0, 2], 1023 | [2, 3], 1024 | [0, 2], 1025 | [2, 3], 1026 | [0, 3] 1027 | ]); 1028 | 1029 | assert.deepStrictEqual( 1030 | pairs.map(function (pair) { 1031 | return [target[pair[0]], target[pair[1]]]; 1032 | }), 1033 | test 1034 | ); 1035 | }); 1036 | }); 1037 | 1038 | describe('#.createOrderedRandomPair', function () { 1039 | it('should throw when working on length < 2.', function () { 1040 | var randomOrderedPair = lib.createRandomOrderedPair(rng()); 1041 | 1042 | assert.throws(function () { 1043 | randomOrderedPair([0]); 1044 | }); 1045 | 1046 | assert.throws(function () { 1047 | randomOrderedPair(0); 1048 | }); 1049 | }); 1050 | 1051 | it('should properly return valid ordered pairs.', function () { 1052 | var randomOrderedPair = lib.createRandomOrderedPair(rng()); 1053 | 1054 | var target = ['apple', 'pear', 'tomato', 'olive']; 1055 | 1056 | var test = [ 1057 | ['pear', 'tomato'], 1058 | ['pear', 'olive'], 1059 | ['tomato', 'olive'], 1060 | ['pear', 'apple'], 1061 | ['pear', 'apple'], 1062 | ['tomato', 'apple'], 1063 | ['olive', 'tomato'], 1064 | ['tomato', 'apple'], 1065 | ['olive', 'tomato'], 1066 | ['apple', 'olive'] 1067 | ]; 1068 | 1069 | var pairs = vec(10).map(function () { 1070 | return randomOrderedPair(target); 1071 | }); 1072 | 1073 | assert.deepStrictEqual(pairs, test); 1074 | 1075 | randomOrderedPair = lib.createRandomOrderedPair(rng()); 1076 | 1077 | pairs = vec(10).map(function () { 1078 | return randomOrderedPair(4); 1079 | }); 1080 | 1081 | assert.deepStrictEqual(pairs, [ 1082 | [1, 2], 1083 | [1, 3], 1084 | [2, 3], 1085 | [1, 0], 1086 | [1, 0], 1087 | [2, 0], 1088 | [3, 2], 1089 | [2, 0], 1090 | [3, 2], 1091 | [0, 3] 1092 | ]); 1093 | 1094 | assert.deepStrictEqual( 1095 | pairs.map(function (pair) { 1096 | return [target[pair[0]], target[pair[1]]]; 1097 | }), 1098 | test 1099 | ); 1100 | }); 1101 | }); 1102 | 1103 | describe('#.createSamplePairs', function () { 1104 | it('should return a correct sample.', function () { 1105 | var samplePairs = lib.createSamplePairs(rng()); 1106 | 1107 | var target = [ 1108 | 'apple', 1109 | 'pear', 1110 | 'tomato', 1111 | 'olive', 1112 | 'watermelon', 1113 | 'orange', 1114 | 'strawberry' 1115 | ]; 1116 | 1117 | var expected = [ 1118 | ['tomato', 'watermelon'], 1119 | ['tomato', 'orange'], 1120 | ['olive', 'orange'], 1121 | ['pear', 'olive'], 1122 | ['pear', 'tomato'] 1123 | ]; 1124 | 1125 | var sample = samplePairs(5, target); 1126 | 1127 | assert.deepStrictEqual(sample, expected); 1128 | 1129 | samplePairs = lib.createSamplePairs(rng()); 1130 | 1131 | sample = samplePairs(5, target.length); 1132 | 1133 | assert.deepStrictEqual( 1134 | sample.map(function (pair) { 1135 | return [target[pair[0]], target[pair[1]]]; 1136 | }), 1137 | expected 1138 | ); 1139 | }); 1140 | 1141 | it('should return unordered pairs.', function () { 1142 | var pairs = lib.samplePairs(25, 1000); 1143 | 1144 | pairs.forEach(function (pair) { 1145 | assert(pair[0] < pair[1]); 1146 | }); 1147 | }); 1148 | }); 1149 | 1150 | describe('#.createSampleOrderedPairs', function () { 1151 | it('should return a correct sample.', function () { 1152 | var samplePairs = lib.createSampleOrderedPairs(rng()); 1153 | 1154 | var target = [ 1155 | 'apple', 1156 | 'pear', 1157 | 'tomato', 1158 | 'olive', 1159 | 'watermelon', 1160 | 'orange', 1161 | 'strawberry' 1162 | ]; 1163 | 1164 | var expected = [ 1165 | ['tomato', 'watermelon'], 1166 | ['tomato', 'orange'], 1167 | ['olive', 'orange'], 1168 | ['olive', 'pear'], 1169 | ['tomato', 'pear'] 1170 | ]; 1171 | 1172 | var sample = samplePairs(5, target); 1173 | 1174 | assert.deepStrictEqual(sample, expected); 1175 | 1176 | samplePairs = lib.createSampleOrderedPairs(rng()); 1177 | 1178 | sample = samplePairs(5, target.length); 1179 | 1180 | assert.deepStrictEqual( 1181 | sample.map(function (pair) { 1182 | return [target[pair[0]], target[pair[1]]]; 1183 | }), 1184 | expected 1185 | ); 1186 | }); 1187 | }); 1188 | 1189 | describe('#.createWeightedReservoirSample', function () { 1190 | var items = [0.4, 0.4, 0.1, 0.001, 0.187, 0.3, 0.25, 0.5, 92]; 1191 | 1192 | it('should work properly.', function () { 1193 | var weightedReservoirSample = lib.createWeightedReservoirSample(rng()); 1194 | 1195 | var sample = weightedReservoirSample(3, items); 1196 | 1197 | assert.deepStrictEqual(sample, [92, 0.5, 0.4]); 1198 | 1199 | assert.deepStrictEqual( 1200 | new Set(weightedReservoirSample(100, items)), 1201 | new Set(items) 1202 | ); 1203 | }); 1204 | 1205 | it('should be possible to sample arbitrary items.', function () { 1206 | var weightedReservoirSample = lib.createWeightedReservoirSample({ 1207 | rng: rng(), 1208 | getWeight: function (n) { 1209 | return 1 / n; 1210 | } 1211 | }); 1212 | 1213 | var sample = weightedReservoirSample(3, items); 1214 | 1215 | assert.deepStrictEqual(sample, [0.001, 0.1, 0.187]); 1216 | }); 1217 | 1218 | it('should be possible to use a sampler.', function () { 1219 | var sampler = new lib.WeightedReservoirSampler(3, rng()); 1220 | 1221 | items.forEach(function (n) { 1222 | sampler.process(n); 1223 | }); 1224 | 1225 | assert.deepStrictEqual(sampler.end(), [92, 0.5, 0.4]); 1226 | }); 1227 | }); 1228 | 1229 | describe('#.createRandomUint32', function () { 1230 | it('should return uint32 numbers.', function () { 1231 | var i; 1232 | 1233 | for (i = 0; i < 1000; i++) { 1234 | assert(lib.randomUint32() < Math.pow(2, 32) - 1); 1235 | } 1236 | 1237 | var randomUint32 = lib.createRandomUint32(rng()); 1238 | 1239 | var result = []; 1240 | 1241 | for (i = 0; i < 10; i++) { 1242 | result.push(randomUint32()); 1243 | } 1244 | 1245 | assert.deepStrictEqual( 1246 | result, 1247 | [ 1248 | 1551229437, 989934901, 1266845448, 1941618487, 2202774849, 1138438012, 1249 | 2072530664, 3018165983, 1541435361, 4235357373 1250 | ] 1251 | ); 1252 | }); 1253 | }); 1254 | 1255 | describe('FisherYatesPermutation', function () { 1256 | it('should throw when exhausted.', function () { 1257 | var p = new lib.FisherYatesPermutation(2, rng()); 1258 | 1259 | p.permute(); 1260 | p.permute(); 1261 | 1262 | assert.throws(function () { 1263 | p.permute(); 1264 | }, /exhaust/); 1265 | }); 1266 | 1267 | it('should work on a basic case.', function () { 1268 | var p = new lib.FisherYatesPermutation(5, rng()); 1269 | 1270 | var result = []; 1271 | 1272 | for (var i = 0; i < 5; i++) { 1273 | result.push(p.permute()); 1274 | } 1275 | 1276 | assert.deepStrictEqual(result, [1, 0, 3, 2, 4]); 1277 | }); 1278 | 1279 | it('should always give good permutations.', function () { 1280 | for (var i = 0; i < 100; i++) { 1281 | var p = new lib.FisherYatesPermutation(10); 1282 | 1283 | var result = new Set(); 1284 | 1285 | for (var j = 0; j < 10; j++) { 1286 | result.add(p.permute()); 1287 | } 1288 | 1289 | assert.strictEqual(result.size, 10); 1290 | } 1291 | }); 1292 | }); 1293 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export type RNGFunction = () => number; 2 | -------------------------------------------------------------------------------- /utils.d.ts: -------------------------------------------------------------------------------- 1 | export function indices(length: number): Array; 2 | export function triuLinearLength(n: number): number; 3 | export function linearIndexToTriuCoords( 4 | n: number, 5 | k: number 6 | ): [i: number, j: number]; 7 | export function triuCoordsToLinearIndex( 8 | n: number, 9 | i: number, 10 | j: number 11 | ): number; 12 | export function linearIndexToTriuCoordsFast(k: number); 13 | export function createPairKeyFunction( 14 | n: number 15 | ): (i: number, j: number) => string | number; 16 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Utils 3 | * ================== 4 | * 5 | * Miscellaneous helper functions. 6 | */ 7 | 8 | /** 9 | * Function returning an array of indices. 10 | * 11 | * @param {number} n - Length of the array to create. 12 | * @return {Array} - The created array. 13 | */ 14 | exports.indices = function (n) { 15 | var a = new Array(n); 16 | var i; 17 | 18 | for (i = 0; i < n; i++) { 19 | a[i] = i; 20 | } 21 | 22 | return a; 23 | }; 24 | 25 | /** 26 | * Function related to conversion between coordinates in an upper triangular 27 | * square matrices and the equivalent linear indices (we don't consider the diagonal). 28 | * 29 | * [References]: 30 | * https://stackoverflow.com/questions/27086195/linear-index-upper-triangular-matrix 31 | * https://gist.github.com/PhDP/2358809 32 | */ 33 | exports.triuLinearLength = function (n) { 34 | return (n * (n - 1)) / 2; 35 | }; 36 | exports.linearIndexToTriuCoords = function (n, k) { 37 | var i = n - 2 - Math.floor(Math.sqrt(-8 * k + 4 * n * (n - 1) - 7) / 2 - 0.5); 38 | var j = k + i + 1 - (n * (n - 1)) / 2 + ((n - i) * (n - i - 1)) / 2; 39 | 40 | return [i, j]; 41 | }; 42 | exports.triuCoordsToLinearIndex = function (n, i, j) { 43 | return (n * (n - 1)) / 2 - ((n - i) * (n - i - 1)) / 2 + j - i - 1; 44 | }; 45 | exports.linearIndexToTriuCoordsFast = function (k) { 46 | var i = Math.floor(-0.5 + 0.5 * Math.sqrt(1 + 8 * k)) + 2; 47 | var j = (i * (3 - i)) / 2 + k; 48 | 49 | return [i - 1, j - 1]; 50 | }; 51 | 52 | /** 53 | * Function returning a unique key for a pair of numbers. 54 | */ 55 | var MAX_SAFE_ROOT = Math.floor(Math.sqrt(Number.MAX_SAFE_INTEGER)); 56 | 57 | exports.createPairKeyFunction = function (n) { 58 | if (n <= MAX_SAFE_ROOT) 59 | return function (i, j) { 60 | return j * n + i; 61 | }; 62 | 63 | return function (i, j) { 64 | return i + ',' + j; 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /weighted-choice.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type WeightedChoiceOptions = { 4 | rng: RNGFunction; 5 | getWeight: (item: T, index?: number) => number; 6 | }; 7 | 8 | type WeightedChoiceFunction = (array: Array) => T; 9 | type CachedWeightedChoiceFunction = () => T; 10 | 11 | declare const weightedChoice: { 12 | (array: Array): T; 13 | 14 | createWeightedChoice(rng: RNGFunction): WeightedChoiceFunction; 15 | createWeightedChoice( 16 | options: WeightedChoiceOptions 17 | ): WeightedChoiceFunction; 18 | 19 | createCachedWeightedChoice( 20 | rng: RNGFunction, 21 | array: Array 22 | ): CachedWeightedChoiceFunction; 23 | createCachedWeightedChoice( 24 | options: WeightedChoiceOptions, 25 | array: Array 26 | ): CachedWeightedChoiceFunction; 27 | }; 28 | 29 | export function createWeightedChoice( 30 | rng: RNGFunction 31 | ): WeightedChoiceFunction; 32 | export function createWeightedChoice( 33 | options: WeightedChoiceFunction 34 | ): WeightedChoiceFunction; 35 | 36 | export function createCachedWeightedChoice( 37 | rng: RNGFunction, 38 | array: Array 39 | ): CachedWeightedChoiceFunction; 40 | export function createCachedWeightedChoice( 41 | options: WeightedChoiceOptions, 42 | array: Array 43 | ): CachedWeightedChoiceFunction; 44 | 45 | export default weightedChoice; 46 | -------------------------------------------------------------------------------- /weighted-choice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Weighted Choice 3 | * ============================ 4 | * 5 | * Function returning a random item from a weighted list. 6 | */ 7 | var lib = require('./weighted-random-index.js'); 8 | 9 | /** 10 | * Creating a function returning a weighted random item from a cached 11 | * cumulative density function. 12 | * 13 | * @param {object|function} rngOrOptions - Either RNG function or options: 14 | * @param {function} rng - Custom RNG. 15 | * @param {function} getWeight - Weight getter. 16 | * @return {function} 17 | */ 18 | function createCachedWeightedChoice(rngOrOptions, sequence) { 19 | var randomIndex = lib.createCachedWeightedRandomIndex(rngOrOptions, sequence); 20 | 21 | /** 22 | * Weighted random item from the given sequence. 23 | * 24 | * @return {number} 25 | */ 26 | return function () { 27 | var index = randomIndex(); 28 | 29 | return sequence[index]; 30 | }; 31 | } 32 | 33 | /** 34 | * Creating a function returning a weighted random item. 35 | * 36 | * @param {object|function} rngOrOptions - Either RNG function or options: 37 | * @param {function} rng - Custom RNG. 38 | * @param {function} getWeight - Weight getter. 39 | * @return {function} 40 | */ 41 | function createWeightedChoice(rngOrOptions) { 42 | var randomIndex = lib.createWeightedRandomIndex(rngOrOptions); 43 | 44 | /** 45 | * Weighted random item from the given sequence. 46 | * 47 | * @param {array} sequence - Target sequence. 48 | * @return {number} 49 | */ 50 | return function (sequence) { 51 | var index = randomIndex(sequence); 52 | 53 | return sequence[index]; 54 | }; 55 | } 56 | 57 | /** 58 | * Default weighted choice using `Math.random`. 59 | */ 60 | var weightedChoice = createWeightedChoice(Math.random); 61 | 62 | /** 63 | * Exporting. 64 | */ 65 | weightedChoice.createCachedWeightedChoice = createCachedWeightedChoice; 66 | weightedChoice.createWeightedChoice = createWeightedChoice; 67 | module.exports = weightedChoice; 68 | -------------------------------------------------------------------------------- /weighted-random-index.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type WeightedRandomIndexOptions = { 4 | rng: RNGFunction; 5 | getWeight: (item: T, index?: number) => number; 6 | }; 7 | 8 | type WeightedRandomIndexFunction = (array: Array) => number; 9 | type CachedWeightedRandomIndexFunction = () => number; 10 | 11 | declare const weightedRandomIndex: { 12 | (array: Array): number; 13 | 14 | createWeightedRandomIndex( 15 | rng: RNGFunction 16 | ): WeightedRandomIndexFunction; 17 | createWeightedRandomIndex( 18 | options: WeightedRandomIndexOptions 19 | ): WeightedRandomIndexFunction; 20 | 21 | createCachedWeightedRandomIndex( 22 | rng: RNGFunction, 23 | array: Array 24 | ): CachedWeightedRandomIndexFunction; 25 | createCachedWeightedRandomIndex( 26 | options: WeightedRandomIndexOptions, 27 | array: Array 28 | ): CachedWeightedRandomIndexFunction; 29 | }; 30 | 31 | export function createWeightedRandomIndex( 32 | rng: RNGFunction 33 | ): WeightedRandomIndexFunction; 34 | export function createWeightedRandomIndex( 35 | options: WeightedRandomIndexFunction 36 | ): WeightedRandomIndexFunction; 37 | 38 | export function createCachedWeightedRandomIndex( 39 | rng: RNGFunction, 40 | array: Array 41 | ): CachedWeightedRandomIndexFunction; 42 | export function createCachedWeightedRandomIndex( 43 | options: WeightedRandomIndexOptions, 44 | array: Array 45 | ): CachedWeightedRandomIndexFunction; 46 | 47 | export default weightedRandomIndex; 48 | -------------------------------------------------------------------------------- /weighted-random-index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Weighted Index 3 | * =========================== 4 | * 5 | * Function returning a random index from a weighted list of items. 6 | * 7 | * [Reference]: 8 | * http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python 9 | */ 10 | 11 | /** 12 | * Defaults. 13 | */ 14 | var DEFAULTS = { 15 | rng: Math.random, 16 | getWeight: null 17 | }; 18 | 19 | /** 20 | * Function returning upper bound of value in the given sorted array. This 21 | * is the equivalent of python's `bisect_right`. 22 | * 23 | * @param {array} array - Target array. 24 | * @param {number} value - Number to position. 25 | * @return {number} 26 | */ 27 | function upperBound(array, value) { 28 | var l = array.length, 29 | d, 30 | c, 31 | i = 0; 32 | 33 | while (l) { 34 | d = l >>> 1; 35 | c = i + d; 36 | 37 | if (value < array[c]) { 38 | l = d; 39 | } else { 40 | i = c + 1; 41 | l -= d + 1; 42 | } 43 | } 44 | 45 | return i; 46 | } 47 | 48 | /** 49 | * Creating a function returning a weighted random index from a cached 50 | * cumulative density function. 51 | * 52 | * This algorithm is more costly in space because it needs to store O(n) 53 | * items to cache the CDF but is way faster if one is going to need a random 54 | * weighted index several times from the same sequence. 55 | * 56 | * @param {object|function} rngOrOptions - Either RNG function or options: 57 | * @param {function} rng - Custom RNG. 58 | * @param {function} getWeight - Weight getter. 59 | * @return {function} 60 | */ 61 | function createCachedWeightedRandomIndex(rngOrOptions, sequence) { 62 | var rng, options; 63 | 64 | if (typeof rngOrOptions === 'function') { 65 | rng = rngOrOptions; 66 | options = {}; 67 | } else { 68 | rng = rngOrOptions.rng || DEFAULTS.rng; 69 | options = rngOrOptions; 70 | } 71 | 72 | var getWeight = 73 | typeof options.getWeight === 'function' ? options.getWeight : null; 74 | 75 | // Computing the cumulative density function of the sequence (CDF) 76 | var l = sequence.length; 77 | 78 | var CDF = new Float64Array(l); 79 | var total = 0; 80 | var weight; 81 | 82 | for (var i = 0; i < l; i++) { 83 | weight = getWeight ? getWeight(sequence[i], i) : sequence[i]; 84 | total += weight; 85 | CDF[i] = total; 86 | } 87 | 88 | /** 89 | * Weighted random index from the given sequence. 90 | * 91 | * @return {number} 92 | */ 93 | return function () { 94 | var random = rng() * total; 95 | 96 | return upperBound(CDF, random); 97 | }; 98 | } 99 | 100 | /** 101 | * Creating a function returning a weighted random index. 102 | * 103 | * Note that this function uses the "King of the hill" algorithm which runs in 104 | * linear time O(n) and has some advantages, one being that one does not need 105 | * to know the weight's sum in advance. However, it may be slower that some 106 | * other methods because we have to run the RNG function n times. 107 | * 108 | * @param {object|function} rngOrOptions - Either RNG function or options: 109 | * @param {function} rng - Custom RNG. 110 | * @param {function} getWeight - Weight getter. 111 | * @return {function} 112 | */ 113 | function createWeightedRandomIndex(rngOrOptions) { 114 | var rng, options; 115 | 116 | if (typeof rngOrOptions === 'function') { 117 | rng = rngOrOptions; 118 | options = {}; 119 | } else { 120 | rng = rngOrOptions.rng || DEFAULTS.rng; 121 | options = rngOrOptions; 122 | } 123 | 124 | var getWeight = 125 | typeof options.getWeight === 'function' ? options.getWeight : null; 126 | 127 | /** 128 | * Weighted random index from the given sequence. 129 | * 130 | * @param {array} sequence - Target sequence. 131 | * @return {number} 132 | */ 133 | return function (sequence) { 134 | var total = 0; 135 | var winner = 0; 136 | var weight; 137 | 138 | for (var i = 0, l = sequence.length; i < l; i++) { 139 | weight = getWeight ? getWeight(sequence[i], i) : sequence[i]; 140 | 141 | total += weight; 142 | 143 | if (rng() * total < weight) winner = i; 144 | } 145 | 146 | return winner; 147 | }; 148 | } 149 | 150 | /** 151 | * Default weighted index using `Math.random`. 152 | */ 153 | var weightedRandomIndex = createWeightedRandomIndex(Math.random); 154 | 155 | /** 156 | * Exporting. 157 | */ 158 | weightedRandomIndex.createCachedWeightedRandomIndex = 159 | createCachedWeightedRandomIndex; 160 | weightedRandomIndex.createWeightedRandomIndex = createWeightedRandomIndex; 161 | module.exports = weightedRandomIndex; 162 | -------------------------------------------------------------------------------- /weighted-reservoir-sample.d.ts: -------------------------------------------------------------------------------- 1 | import {RNGFunction} from './types'; 2 | 3 | type WeightedRerservoirSampleOptions = { 4 | rng: RNGFunction; 5 | getWeight: (item: T, index?: number) => number; 6 | }; 7 | 8 | type WeightedReservoirSampleFunction = ( 9 | k: number, 10 | array: Array 11 | ) => Array; 12 | 13 | export class WeightedReservoirSampler { 14 | constructor(k: number, rng?: RNGFunction); 15 | constructor(k: number, options?: WeightedRerservoirSampleOptions); 16 | process(item: T): void; 17 | end(): Array; 18 | } 19 | 20 | declare const weightedReservoirSample: { 21 | (k: number, array: Array): Array; 22 | createWeightedReservoirSample( 23 | rng: RNGFunction 24 | ): WeightedReservoirSampleFunction; 25 | createWeightedReservoirSample( 26 | options: WeightedRerservoirSampleOptions 27 | ): WeightedReservoirSampleFunction; 28 | WeightedReservoirSampler: typeof WeightedReservoirSampler; 29 | }; 30 | 31 | export function createWeightedReservoirSample( 32 | rng: RNGFunction 33 | ): WeightedReservoirSampleFunction; 34 | 35 | export function createWeightedReservoirSample( 36 | options: WeightedRerservoirSampleOptions 37 | ): WeightedReservoirSampleFunction; 38 | 39 | export default weightedReservoirSample; 40 | -------------------------------------------------------------------------------- /weighted-reservoir-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pandemonium Weighted Reservoir Sample 3 | * ====================================== 4 | * 5 | * Weighted variant of reservoir sampling, using algorithm "A-ES" from the 6 | * following papers: 7 | * 8 | * 1. Pavlos S. Efraimidis, Paul G. Spirakis. "Weighted random sampling with a reservoir." 9 | * 2. Pavlos S. Efraimidis. "Weighted Random Sampling over Data Streams." 10 | * 11 | * Note that this algorithm picks a sample of weighted items without replacement, 12 | * i.e. we subtract to the total sum of weights the weight of already selected 13 | * items for subsequent picks. 14 | * 15 | * [Reference]: 16 | * https://doi.org/10.1016/j.ipl.2005.11.003 17 | * https://arxiv.org/pdf/1012.0256.pdf 18 | */ 19 | var FixedReverseHeap = require('mnemonist/fixed-reverse-heap'); 20 | 21 | /** 22 | * Helpers. 23 | */ 24 | 25 | function comparator(a, b) { 26 | a = a[0]; 27 | b = b[0]; 28 | 29 | if (a > b) return -1; 30 | if (a < b) return 1; 31 | 32 | return 0; 33 | } 34 | 35 | /** 36 | * Helper class. 37 | */ 38 | function WeightedReservoirSampler(k, rngOrOptions) { 39 | var rng, options; 40 | 41 | if (typeof rngOrOptions === 'function') { 42 | rng = rngOrOptions; 43 | options = {}; 44 | } else { 45 | rng = rngOrOptions.rng || Math.random; 46 | options = rngOrOptions; 47 | } 48 | 49 | var getWeight = 50 | typeof options.getWeight === 'function' ? options.getWeight : null; 51 | 52 | this.size = 0; 53 | this.rng = rng; 54 | this.getWeight = getWeight; 55 | this.heap = new FixedReverseHeap(Array, comparator, k); 56 | this.done = false; 57 | } 58 | 59 | WeightedReservoirSampler.prototype.process = function (item) { 60 | if (this.done) { 61 | throw new Error( 62 | 'pandemonium/WeightedReservoirSampler: this sampler has returned its result and cannot be used anymore.' 63 | ); 64 | } 65 | 66 | var u = this.rng(); 67 | var w = item; 68 | 69 | if (this.getWeight) w = this.getWeight(item, this.size); 70 | 71 | var ki = Math.pow(u, 1 / w); 72 | 73 | this.heap.push([ki, item]); 74 | 75 | this.size++; 76 | }; 77 | 78 | WeightedReservoirSampler.prototype.end = function () { 79 | this.done = true; 80 | 81 | var consumed = this.heap.consume(); 82 | var l = consumed.length; 83 | 84 | var result = new Array(l); 85 | 86 | for (var i = 0; i < l; i++) result[i] = consumed[i][1]; 87 | 88 | return result; 89 | }; 90 | 91 | /** 92 | * Creating a function returning a weighted sample of size n using the provided RNG. 93 | * 94 | * @param {function} rng - The RNG to use. 95 | * @return {function} - The created function. 96 | */ 97 | function createWeightedReservoirSample(rngOrOptions) { 98 | var rng, options; 99 | 100 | if (typeof rngOrOptions === 'function') { 101 | rng = rngOrOptions; 102 | options = {}; 103 | } else { 104 | rng = rngOrOptions.rng || Math.random; 105 | options = rngOrOptions; 106 | } 107 | 108 | var getWeight = 109 | typeof options.getWeight === 'function' ? options.getWeight : null; 110 | 111 | /** 112 | * Function returning weighted sample of size n from array. 113 | * 114 | * @param {number} k - Size of the sample. 115 | * @param {array|number} sequence - Target sequence or its length. 116 | * @return {array} - The random sample. 117 | */ 118 | return function (k, sequence) { 119 | var heap = new FixedReverseHeap(Array, comparator, k); 120 | 121 | var l = sequence.length; 122 | var item, i, u, w, ki; 123 | 124 | for (i = 0; i < l; i++) { 125 | u = rng(); 126 | item = sequence[i]; 127 | w = item; 128 | 129 | if (getWeight) w = getWeight(w, i); 130 | 131 | ki = Math.pow(u, 1 / w); 132 | 133 | heap.push([ki, item]); 134 | } 135 | 136 | var consumed = heap.consume(); 137 | l = consumed.length; 138 | 139 | var result = new Array(l); 140 | 141 | for (i = 0; i < l; i++) result[i] = consumed[i][1]; 142 | 143 | return result; 144 | }; 145 | } 146 | 147 | /** 148 | * Default reservoir sample using `Math.random`. 149 | */ 150 | var weightedReservoirSample = createWeightedReservoirSample(Math.random); 151 | 152 | /** 153 | * Exporting. 154 | */ 155 | weightedReservoirSample.createWeightedReservoirSample = 156 | createWeightedReservoirSample; 157 | weightedReservoirSample.WeightedReservoirSampler = WeightedReservoirSampler; 158 | module.exports = weightedReservoirSample; 159 | --------------------------------------------------------------------------------