Skip to content

Commit

Permalink
Merge pull request #2944 from jgonggrijp/fix-sample-string
Browse files Browse the repository at this point in the history
Properly convert _.sample input collection to array
  • Loading branch information
jgonggrijp authored Dec 16, 2021
2 parents aec10f9 + 72173ba commit d9e0091
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 47 deletions.
4 changes: 2 additions & 2 deletions modules/sample.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import isArrayLike from './_isArrayLike.js';
import clone from './clone.js';
import values from './values.js';
import getLength from './_getLength.js';
import random from './random.js';
import toArray from './toArray.js';

// Sample **n** random values from a collection using the modern version of the
// [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
Expand All @@ -13,7 +13,7 @@ export default function sample(obj, n, guard) {
if (!isArrayLike(obj)) obj = values(obj);
return obj[random(obj.length - 1)];
}
var sample = isArrayLike(obj) ? clone(obj) : values(obj);
var sample = toArray(obj);
var length = getLength(sample);
n = Math.max(Math.min(n, length), 0);
var last = length - 1;
Expand Down
22 changes: 22 additions & 0 deletions test/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,28 @@
var partialSample = _.sample(_.range(1000), 10);
var partialSampleSorted = partialSample.sort();
assert.notDeepEqual(partialSampleSorted, _.range(10), 'samples from the whole array, not just the beginning');
// The next few lines (up to END) are a regression test for #2927.
var alphabet = 'abcdefghijklmnopqrstuvwxyz';
var prefixLength = 5;
var prefix = _.toArray(alphabet.slice(0, prefixLength));
// We're going to take three random samples from the alphabet and count how
// many of them are exact prefixes of the alphabet ('abcde').
var verbatimPrefixes = 0;
_.times(3, function() {
var sample = _.toArray(_.sample(alphabet, prefixLength));
if (_.isEqual(sample, prefix)) ++verbatimPrefixes;
});
// The probability of a sample of length N being a prefix is 1/(A!/(A-N)!),
// with A being the length of the alphabet. That amounts to roughly 1 in
// 7.9e6 when N=5 and A=26. Most of the time, therefore, we should find that
// verbatimPrefixes=0. We will however accept the occasional hit. Only when
// it happens twice, does it start to look really suspicious; the
// probability of this happening is roughly 1 in 21e12. If you are lucky
// enough to witness this, you should be fine when you run the test again.
// However, if you can reliably make the test fail again, you can be sure
// that the code is not working as intended.
assert.ok(verbatimPrefixes < 2, 'sampling a string should not just return a prefix');
// END of regression test for #2927.
});

QUnit.test('toArray', function(assert) {
Expand Down
28 changes: 14 additions & 14 deletions underscore-esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion underscore-esm.js.map

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions underscore-node-f.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,19 @@ function min(obj, iteratee, context) {
return result;
}

// Safely create a real, live array from anything iterable.
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
function toArray(obj) {
if (!obj) return [];
if (isArray(obj)) return slice.call(obj);
if (isString(obj)) {
// Keep surrogate pair characters together.
return obj.match(reStrSymbol);
}
if (isArrayLike(obj)) return map(obj, identity);
return values(obj);
}

// Sample **n** random values from a collection using the modern version of the
// [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
// If **n** is not specified, returns a single random element.
Expand All @@ -1515,7 +1528,7 @@ function sample(obj, n, guard) {
if (!isArrayLike(obj)) obj = values(obj);
return obj[random(obj.length - 1)];
}
var sample = isArrayLike(obj) ? clone(obj) : values(obj);
var sample = toArray(obj);
var length = getLength(sample);
n = Math.max(Math.min(n, length), 0);
var last = length - 1;
Expand Down Expand Up @@ -1592,19 +1605,6 @@ var partition = group(function(result, value, pass) {
result[pass ? 0 : 1].push(value);
}, true);

// Safely create a real, live array from anything iterable.
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
function toArray(obj) {
if (!obj) return [];
if (isArray(obj)) return slice.call(obj);
if (isString(obj)) {
// Keep surrogate pair characters together.
return obj.match(reStrSymbol);
}
if (isArrayLike(obj)) return map(obj, identity);
return values(obj);
}

// Return the number of elements in a collection.
function size(obj) {
if (obj == null) return 0;
Expand Down
2 changes: 1 addition & 1 deletion underscore-node-f.cjs.map

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions underscore-umd.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion underscore-umd.js.map

Large diffs are not rendered by default.

0 comments on commit d9e0091

Please sign in to comment.