factory.js (8477B)
1 /** 2 * @license Apache-2.0 3 * 4 * Copyright (c) 2018 The Stdlib Authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 'use strict'; 20 21 // MODULES // 22 23 var setReadOnly = require( '@stdlib/utils/define-nonenumerable-read-only-property' ); 24 var isArrayLike = require( '@stdlib/assert/is-array-like' ); 25 var isTypedArrayLike = require( '@stdlib/assert/is-typed-array-like' ); 26 var isString = require( '@stdlib/assert/is-string' ).isPrimitive; 27 var randu = require( './../../base/mt19937' ).factory; 28 var copy = require( '@stdlib/utils/copy' ); 29 var discreteUniform = require( './discrete_uniform.js' ); 30 var renormalizing = require( './renormalizing.js' ); 31 var fisherYates = require( './fisher_yates.js' ); 32 var vose = require( './vose.js' ); 33 var defaults = require( './defaults.json' ); 34 var validate = require( './validate.js' ); 35 36 37 // FUNCTIONS // 38 39 var slice = Array.prototype.slice; 40 41 42 // MAIN // 43 44 /** 45 * Returns a function to sample elements from an array-like object. 46 * 47 * @param {(ArrayLike|TypedArrayLike)} [pool] - array-like object from which to sample 48 * @param {Options} [options] - function options 49 * @param {PositiveInteger} [options.seed] - integer-valued seed 50 * @param {NonNegativeInteger} [options.size] - sample size 51 * @param {boolean} [options.replace=true] - boolean indicating whether to sample with replacement 52 * @param {boolean} [options.mutate=false] - boolean indicating whether to mutate the `pool` when sampling without replacement 53 * @throws {TypeError} `pool` must be an array-like object 54 * @throws {TypeError} options argument must be an object 55 * @throws {TypeError} must provide valid options 56 * @returns {Function} function to sample elements from an array-like object 57 * 58 * @example 59 * var sample = factory({ 60 * 'seed': 232 61 * }); 62 * var out = sample( 'abcdefg' ); 63 * // e.g., returns [ 'g', 'd', 'g', 'f', 'c', 'e', 'f' ] 64 * 65 * @example 66 * var sample = factory( [ 1, 2, 3, 4, 5, 6 ], { 67 * 'seed': 232, 68 * 'size': 2 69 * }); 70 * var out = sample(); 71 * // e.g., returns [ 6, 4 ] 72 * 73 * out = sample(); 74 * // e.g., returns [ 6, 5 ] 75 * 76 * @example 77 * var sample = factory( [ 1, 2, 3, 4, 5, 6 ], { 78 * 'seed': 474, 79 * 'size': 3, 80 * 'mutate': true, 81 * 'replace': false 82 * }); 83 * var out = sample(); 84 * // e.g., returns [ 4, 3, 6 ] 85 * 86 * out = sample(); 87 * // e.g., returns [ 1, 5, 2 ] 88 * 89 * out = sample(); 90 * // returns null 91 * 92 * @example 93 * var sample = factory( [ 0, 1 ], { 94 * 'size': 2 95 * }); 96 * 97 * var out = sample(); 98 * // e.g., returns [ 1, 1 ] 99 * 100 * out = sample({ 101 * 'size': 10 102 * }); 103 * // e.g., returns [ 0, 1, 1, 1, 0, 1, 0, 0, 1, 1 ] 104 * 105 * @example 106 * var sample = factory( [ 0, 1 ], { 107 * 'size': 2 108 * }); 109 * 110 * var out = sample(); 111 * // e.g., returns [ 1, 1 ] 112 * 113 * out = sample({ 114 * 'replace': false 115 * }); 116 * // e.g., returns [ 0, 1 ] or [ 1, 0 ] 117 * 118 * out = sample(); 119 * // e.g., returns [ 1, 1 ] 120 * 121 * @example 122 * var sample = factory( [ 0, 1 ], { 123 * 'size': 2, 124 * 'mutate': true 125 * }); 126 * 127 * var out = sample(); 128 * // e.g., returns [ 1, 1 ] 129 * 130 * out = sample({ 131 * 'replace': false 132 * }); 133 * // e.g., returns [ 0, 1 ] or [ 1, 0 ] 134 * 135 * out = sample(); 136 * // returns null 137 */ 138 function factory() { 139 var config; 140 var pool; 141 var conf; 142 var rand; 143 var err; 144 var fcn; 145 146 conf = copy( defaults ); 147 if ( arguments.length === 1 ) { 148 if ( isArrayLike( arguments[ 0 ] ) || isTypedArrayLike( arguments[ 0 ] ) ) { // eslint-disable-line max-len 149 pool = arguments[ 0 ]; 150 } else { 151 config = arguments[ 0 ]; 152 err = validate( conf, config ); 153 } 154 } else if ( arguments.length > 1 ) { 155 pool = arguments[ 0 ]; 156 config = arguments[ 1 ]; 157 if ( !( isArrayLike( pool ) || isTypedArrayLike( pool ) ) ) { 158 throw new TypeError( 'invalid argument. `pool` argument must be array-like. Value: `' + pool + '`.' ); 159 } 160 err = validate( conf, config ); 161 } 162 if ( err ) { 163 throw err; 164 } 165 if ( config && config.seed ) { 166 rand = randu({ 167 'seed': config.seed 168 }); 169 } else { 170 rand = randu(); 171 } 172 if ( pool === void 0 ) { 173 fcn = sample1; 174 } else { 175 if ( isString( pool ) ) { 176 pool = pool.split( '' ); 177 } else { 178 pool = copy( pool ); 179 } 180 fcn = sample2; 181 } 182 setReadOnly( fcn, 'seed', rand.seed ); 183 setReadOnly( fcn, 'PRNG', rand ); 184 185 rand = rand.normalized; 186 187 return fcn; 188 189 /** 190 * Samples elements from an array-like object. 191 * 192 * @private 193 * @param {(ArrayLike|TypedArrayLike)} x - array-like object from which to sample elements 194 * @param {Options} [options] - function options 195 * @param {NonNegativeInteger} [options.size] - sample size 196 * @param {ProbabilityArray} [options.probs] - element probabilities 197 * @param {boolean} [options.replace=true] - boolean indicating whether to sample with replacement 198 * @throws {TypeError} first argument must be array-like 199 * @throws {TypeError} options argument must be an object 200 * @throws {TypeError} must provide valid options 201 * @throws {RangeError} `size` option must be less than or equal to the length of `x` when the `replace` option is `false` 202 * @returns {Array} sample 203 */ 204 function sample1( x, options ) { 205 var replace; 206 var xcopy; 207 var probs; 208 var opts; 209 var size; 210 var err; 211 212 if ( !( isArrayLike( x ) || isTypedArrayLike( x ) ) ) { 213 throw new TypeError( 'invalid argument. First argument must be array-like. Value: `' + x + '`.' ); 214 } 215 if ( isString( x ) ) { 216 x = x.split( '' ); 217 } 218 opts = {}; 219 if ( arguments.length > 1 ) { 220 err = validate( opts, options ); 221 if ( err ) { 222 throw err; 223 } 224 } 225 if ( opts.replace === void 0 ) { 226 replace = conf.replace; 227 } else { 228 replace = opts.replace; 229 } 230 if ( opts.probs !== void 0 ) { 231 probs = opts.probs; 232 } 233 if ( opts.size ) { 234 size = opts.size; 235 } else if ( conf.size ) { 236 size = conf.size; 237 } else { 238 size = x.length; 239 } 240 if ( 241 replace === false && 242 size > x.length 243 ) { 244 throw new RangeError( 'invalid input option. `size` option must be less than or equal to the length of `x` when `replace` is `false`. Option: `' + size + '`.' ); 245 } 246 // Custom probabilities... 247 if ( probs ) { 248 if ( replace ) { 249 return vose( x, size, rand, probs ); 250 } 251 return renormalizing( x, size, rand, probs ); 252 } 253 // All elements equally likely... 254 if ( replace ) { 255 return discreteUniform( x, size, rand ); 256 } 257 xcopy = slice.call( x ); 258 return fisherYates( xcopy, size, rand ); 259 } 260 261 /** 262 * Samples elements from a population. 263 * 264 * @private 265 * @param {Options} [options] - function options 266 * @param {NonNegativeInteger} [options.size] - sample size 267 * @param {boolean} [options.replace=true] - boolean indicating whether to sample with replacement 268 * @param {boolean} [options.mutate=false] - boolean indicating whether to mutate the `pool` when sampling without replacement 269 * @throws {TypeError} options argument must be an object 270 * @throws {TypeError} must provide valid options 271 * @throws {RangeError} `size` option must be less than or equal to the population when the `replace` option is `false` 272 * @returns {Array} sample 273 */ 274 function sample2( options ) { 275 var replace; 276 var mutate; 277 var opts; 278 var size; 279 var err; 280 var out; 281 282 if ( pool.length === 0 ) { 283 return null; 284 } 285 opts = {}; 286 if ( arguments.length ) { 287 err = validate( opts, options ); 288 if ( err ) { 289 throw err; 290 } 291 } 292 if ( opts.mutate === void 0 ) { 293 mutate = conf.mutate; 294 } else { 295 mutate = opts.mutate; 296 } 297 if ( opts.replace === void 0 ) { 298 replace = conf.replace; 299 } else { 300 replace = opts.replace; 301 } 302 if ( opts.size ) { 303 size = opts.size; 304 } else if ( conf.size ) { 305 size = conf.size; 306 } else { 307 size = pool.length; 308 } 309 if ( 310 replace === false && 311 size > pool.length 312 ) { 313 throw new RangeError( 'invalid input option. `size` option must be less than or equal to the population size when `replace` is `false`. Option: `' + size + '`.' ); 314 } 315 if ( replace ) { 316 return discreteUniform( pool, size, rand ); 317 } 318 out = fisherYates( pool, size, rand ); 319 if ( mutate ) { 320 // Remove the sample observations: 321 pool = pool.slice( size, pool.length ); 322 } 323 return out; 324 } 325 } 326 327 328 // EXPORTS // 329 330 module.exports = factory;