From 528bec0bb12d8b98752abf73287267e391092075 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 27 Sep 2015 18:05:05 -0700 Subject: [PATCH] Add `_.sampleSize`. --- lodash.js | 83 +++++++++++------------ test/test.js | 154 ++++++++++++++----------------------------- test/underscore.html | 4 ++ 3 files changed, 95 insertions(+), 146 deletions(-) diff --git a/lodash.js b/lodash.js index c330b5172..bf47906f1 100644 --- a/lodash.js +++ b/lodash.js @@ -1432,12 +1432,13 @@ * `modArgsSet', 'negate`, `omit`, `omitBy`, `once`, `pairs`, `partial`, * `partialRight`, `partition`, `pick`, `pickBy`, `plant`, `property`, * `propertyOf`, `pull`, `pullAll`, `pullAt`, `push`, `range`, `rearg`, - * `reject`, `remove`, `rest`, `restParam`, `reverse`, `set`, `setWith`, - * `shuffle`, `slice`, `sort`, `sortBy`, `sortByOrder`, `splice`, `spread`, - * `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, `throttle`, - * `thru`, `times`, `toArray`, `toPath`, `toPlainObject`, `transform`, `union`, - * `uniq`, `uniqBy`, `unset`, `unshift`, `unzip`, `unzipWith`, `values`, - * `valuesIn`, `without`, `wrap`, `xor`, `zip`, `zipObject`, and `zipWith` + * `reject`, `remove`, `rest`, `restParam`, `reverse`, `sampleSize`, `set`, + * `setWith`, `shuffle`, `slice`, `sort`, `sortBy`, `sortByOrder`, `splice`, + * `spread`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, + * `throttle`, `thru`, `times`, `toArray`, `toPath`, `toPlainObject`, + * `transform`, `union`, `uniq`, `uniqBy`, `unset`, `unshift`, `unzip`, + * `unzipWith`, `values`, `valuesIn`, `without`, `wrap`, `xor`, `zip`, + * `zipObject`, and `zipWith` * * The wrapper methods that are **not** chainable by default are: * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clone`, `cloneDeep`, @@ -1450,16 +1451,13 @@ * `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, `isNumber`, * `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, `isSafeInteger`, * `isString`, `isUndefined`, `isTypedArray`, `join`, `kebabCase`, `last`, - * `lastIndexOf`, `lt`, `lte`, `max`, `min`, `noConflict`, `noop`, `now`, - * `pad`, `padLeft`, `padRight`, `parseInt`, `pop`, `random`, `reduce`, - * `reduceRight`, `repeat`, `result`, `round`, `runInContext`, `shift`, `size`, + * `lastIndexOf`, `lt`, `lte`, `max`, `min`, `noConflict`, `noop`, `now`, `pad`, + * `padLeft`, `padRight`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, + * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`, * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, * `sortedLastIndexBy`, `startCase`, `startsWith`, `sum`, `sumBy`, `template`, - * `toInteger`, `toString`, `trim`, `trimLeft`, `trimRight`, `trunc`, `unescape`, - * `uniqueId`, `value`, and `words` - * - * The wrapper method `sample` will return a wrapped value when `n` is provided, - * otherwise an unwrapped value is returned. + * `toInteger`, `toString`, `trim`, `trimLeft`, `trimRight`, `trunc`, + * `unescape`, `uniqueId`, `value`, and `words` * * @name _ * @constructor @@ -6629,9 +6627,9 @@ * * The guarded methods are: * `ary`, `callback`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, - * `fill`, `invert`, `parseInt`, `random`, `range`, `sample`, `slice`, `some`, - * `sortBy`, `take`, `takeRight`, `template`, `trim`, `trimLeft`, `trimRight`, - * `uniq`, and `words` + * `fill`, `invert`, `parseInt`, `random`, `range`, `slice`, `some`, `sortBy`, + * `take`, `takeRight`, `template`, `trim`, `trimLeft`, `trimRight`, `uniq`, + * and `words` * * @static * @memberOf _ @@ -6820,29 +6818,40 @@ } /** - * Gets a random element or `n` random elements from a collection. + * Gets a random element from `collection`. * * @static * @memberOf _ * @category Collection * @param {Array|Object} collection The collection to sample. - * @param {number} [n] The number of elements to sample. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {*} Returns the random sample(s). + * @returns {*} Returns the random element. * @example * * _.sample([1, 2, 3, 4]); * // => 2 + */ + function sample(collection) { + var array = isArrayLike(collection) ? collection : values(collection), + length = array.length; + + return length > 0 ? array[baseRandom(0, length - 1)] : undefined; + } + + /** + * Gets `n` random elements from `collection`. * - * _.sample([1, 2, 3, 4], 2); + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object} collection The collection to sample. + * @param {number} [n=0] The number of elements to sample. + * @returns {Array} Returns the random elements. + * @example + * + * _.sampleSize([1, 2, 3, 4], 2); * // => [3, 1] */ - function sample(collection, n, guard) { - if (guard || n == null) { - collection = isArrayLike(collection) ? collection : values(collection); - length = collection.length; - return length > 0 ? collection[baseRandom(0, length - 1)] : undefined; - } + function sampleSize(collection, n) { var index = -1, result = toArray(collection), length = result.length, @@ -6875,7 +6884,7 @@ * // => [4, 1, 3, 2] */ function shuffle(collection) { - return sample(collection, INFINITY); + return sampleSize(collection, MAX_ARRAY_LENGTH); } /** @@ -12218,6 +12227,7 @@ lodash.remove = remove; lodash.rest = rest; lodash.restParam = restParam; + lodash.sampleSize = sampleSize; lodash.set = set; lodash.setWith = setWith; lodash.shuffle = shuffle; @@ -12354,6 +12364,7 @@ lodash.result = result; lodash.round = round; lodash.runInContext = runInContext; + lodash.sample = sample; lodash.size = size; lodash.snakeCase = snakeCase; lodash.some = some; @@ -12389,20 +12400,6 @@ /*------------------------------------------------------------------------*/ - // Add functions capable of returning wrapped and unwrapped values when chaining. - lodash.sample = sample; - - lodash.prototype.sample = function(n) { - if (!this.__chain__ && n == null) { - return sample(this.value()); - } - return this.thru(function(value) { - return sample(value, n); - }); - }; - - /*------------------------------------------------------------------------*/ - /** * The semantic version number. * diff --git a/test/test.js b/test/test.js index 6ce0041a3..b450478ca 100644 --- a/test/test.js +++ b/test/test.js @@ -15907,29 +15907,59 @@ assert.ok(_.includes(array, actual)); }); - QUnit.test('should return two random elements', function(assert) { + QUnit.test('should return `undefined` when sampling empty collections', function(assert) { assert.expect(1); - var actual = _.sample(array, 2); - assert.ok(actual.length == 2 && actual[0] !== actual[1] && _.includes(array, actual[0]) && _.includes(array, actual[1])); + var expected = _.map(empties, _.constant(undefined)); + + var actual = _.transform(empties, function(result, value) { + try { + result.push(_.sample(value)); + } catch (e) {} + }); + + assert.deepEqual(actual, expected); + }); + + QUnit.test('should sample an object', function(assert) { + assert.expect(1); + + var object = { 'a': 1, 'b': 2, 'c': 3 }, + actual = _.sample(object); + + assert.ok(_.includes(array, actual)); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.sampleSize'); + + (function() { + var array = [1, 2, 3]; + + QUnit.test('should return an array of random elements', function(assert) { + assert.expect(2); + + var actual = _.sampleSize(array, 2); + assert.strictEqual(actual.length, 2); + assert.deepEqual(_.difference(actual, array), []); }); QUnit.test('should contain elements of the collection', function(assert) { assert.expect(1); - var actual = _.sample(array, array.length); + var actual = _.sampleSize(array, array.length); assert.deepEqual(actual.sort(), array); }); - QUnit.test('should treat falsey `n` values, except nullish, as `0`', function(assert) { + QUnit.test('should treat falsey `n` values as `0`', function(assert) { assert.expect(1); - var expected = _.map(falsey, function(value) { - return value == null ? 1 : []; - }); + var expected = _.map(falsey, _.constant([])); - var actual = _.map(falsey, function(n) { - return _.sample([1], n); + var actual = _.map(falsey, function(n, index) { + return index ? _.sampleSize([1], n) : _.sampleSize([1]); }); assert.deepEqual(actual, expected); @@ -15939,7 +15969,7 @@ assert.expect(3); _.each([0, -1, -Infinity], function(n) { - assert.deepEqual(_.sample(array, n), []); + assert.deepEqual(_.sampleSize(array, n), []); }); }); @@ -15947,34 +15977,25 @@ assert.expect(4); _.each([3, 4, Math.pow(2, 32), Infinity], function(n) { - assert.deepEqual(_.sample(array, n).sort(), array); + assert.deepEqual(_.sampleSize(array, n).sort(), array); }); }); QUnit.test('should coerce `n` to an integer', function(assert) { assert.expect(1); - var actual = _.sample(array, 1.6); + var actual = _.sampleSize(array, 1.6); assert.strictEqual(actual.length, 1); }); - QUnit.test('should return `undefined` when sampling an empty array', function(assert) { - assert.expect(1); - - assert.strictEqual(_.sample([]), undefined); - }); - QUnit.test('should return an empty array for empty collections', function(assert) { assert.expect(1); - var expected = _.transform(empties, function(result) { - result.push(undefined, []); - }); + var expected = _.map(empties, _.constant([])); - var actual = []; - _.each(empties, function(value) { + var actual = _.transform(empties, function(result, value) { try { - actual.push(_.sample(value), _.sample(value, 1)); + result.push(_.sampleSize(value, 1)); } catch (e) {} }); @@ -15985,83 +16006,10 @@ assert.expect(2); var object = { 'a': 1, 'b': 2, 'c': 3 }, - actual = _.sample(object); + actual = _.sampleSize(object, 2); - assert.ok(_.includes(array, actual)); - - actual = _.sample(object, 2); - assert.ok(actual.length == 2 && actual[0] !== actual[1] && _.includes(array, actual[0]) && _.includes(array, actual[1])); - }); - - QUnit.test('should work as an iteratee for methods like `_.map`', function(assert) { - assert.expect(2); - - var array1 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - array2 = ['abc', 'def', 'ghi']; - - _.each([array1, array2], function(values) { - var a = values[0], - b = values[1], - c = values[2], - actual = _.map(values, _.sample); - - assert.ok(_.includes(a, actual[0]) && _.includes(b, actual[1]) && _.includes(c, actual[2])); - }); - }); - - QUnit.test('should return a wrapped value when chaining and `n` is provided', function(assert) { - assert.expect(2); - - if (!isNpm) { - var wrapped = _(array).sample(2), - actual = wrapped.value(); - - assert.ok(wrapped instanceof _); - assert.ok(actual.length == 2 && actual[0] !== actual[1] && _.includes(array, actual[0]) && _.includes(array, actual[1])); - } - else { - skipTest(assert, 2); - } - }); - - QUnit.test('should return an unwrapped value when chaining and `n` is not provided', function(assert) { - assert.expect(1); - - if (!isNpm) { - var actual = _(array).sample(); - assert.ok(_.includes(array, actual)); - } - else { - skipTest(assert); - } - }); - - QUnit.test('should return a wrapped value when explicitly chaining', function(assert) { - assert.expect(1); - - if (!isNpm) { - assert.ok(_(array).chain().sample() instanceof _); - } - else { - skipTest(assert); - } - }); - - QUnit.test('should use a stored reference to `_.sample` when chaining', function(assert) { - assert.expect(2); - - if (!isNpm) { - var sample = _.sample; - _.sample = _.noop; - - var wrapped = _(array); - assert.notStrictEqual(wrapped.sample(), undefined); - assert.notStrictEqual(wrapped.sample(2).value(), undefined); - _.sample = sample; - } - else { - skipTest(assert, 2); - } + assert.strictEqual(actual.length, 2); + assert.deepEqual(_.difference(actual, _.values(object)), []); }); }()); @@ -21024,7 +20972,7 @@ 'reject', 'remove', 'rest', - 'sample', + 'sampleSize', 'shuffle', 'sortBy', 'sortByOrder', @@ -21042,7 +20990,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); QUnit.test('should accept falsey arguments', function(assert) { - assert.expect(243); + assert.expect(245); var emptyArrays = _.map(falsey, _.constant([])); diff --git a/test/underscore.html b/test/underscore.html index dfe3fc091..1357e23b7 100644 --- a/test/underscore.html +++ b/test/underscore.html @@ -199,6 +199,10 @@ 'reject': [ 'Returns empty list given empty array' ], + 'sample': [ + 'behaves correctly on negative n', + 'Died on test #3' + ], 'some': [ 'Can be called with object', 'Died on test #17',