Skip to content

Commit d9e0091

Browse files
authored
Merge pull request #2944 from jgonggrijp/fix-sample-string
Properly convert _.sample input collection to array
2 parents aec10f9 + 72173ba commit d9e0091

File tree

8 files changed

+69
-47
lines changed

8 files changed

+69
-47
lines changed

modules/sample.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import isArrayLike from './_isArrayLike.js';
2-
import clone from './clone.js';
32
import values from './values.js';
43
import getLength from './_getLength.js';
54
import random from './random.js';
5+
import toArray from './toArray.js';
66

77
// Sample **n** random values from a collection using the modern version of the
88
// [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
@@ -13,7 +13,7 @@ export default function sample(obj, n, guard) {
1313
if (!isArrayLike(obj)) obj = values(obj);
1414
return obj[random(obj.length - 1)];
1515
}
16-
var sample = isArrayLike(obj) ? clone(obj) : values(obj);
16+
var sample = toArray(obj);
1717
var length = getLength(sample);
1818
n = Math.max(Math.min(n, length), 0);
1919
var last = length - 1;

test/collections.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,28 @@
843843
var partialSample = _.sample(_.range(1000), 10);
844844
var partialSampleSorted = partialSample.sort();
845845
assert.notDeepEqual(partialSampleSorted, _.range(10), 'samples from the whole array, not just the beginning');
846+
// The next few lines (up to END) are a regression test for #2927.
847+
var alphabet = 'abcdefghijklmnopqrstuvwxyz';
848+
var prefixLength = 5;
849+
var prefix = _.toArray(alphabet.slice(0, prefixLength));
850+
// We're going to take three random samples from the alphabet and count how
851+
// many of them are exact prefixes of the alphabet ('abcde').
852+
var verbatimPrefixes = 0;
853+
_.times(3, function() {
854+
var sample = _.toArray(_.sample(alphabet, prefixLength));
855+
if (_.isEqual(sample, prefix)) ++verbatimPrefixes;
856+
});
857+
// The probability of a sample of length N being a prefix is 1/(A!/(A-N)!),
858+
// with A being the length of the alphabet. That amounts to roughly 1 in
859+
// 7.9e6 when N=5 and A=26. Most of the time, therefore, we should find that
860+
// verbatimPrefixes=0. We will however accept the occasional hit. Only when
861+
// it happens twice, does it start to look really suspicious; the
862+
// probability of this happening is roughly 1 in 21e12. If you are lucky
863+
// enough to witness this, you should be fine when you run the test again.
864+
// However, if you can reliably make the test fail again, you can be sure
865+
// that the code is not working as intended.
866+
assert.ok(verbatimPrefixes < 2, 'sampling a string should not just return a prefix');
867+
// END of regression test for #2927.
846868
});
847869

848870
QUnit.test('toArray', function(assert) {

underscore-esm.js

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-esm.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-node-f.cjs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,19 @@ function min(obj, iteratee, context) {
15061506
return result;
15071507
}
15081508

1509+
// Safely create a real, live array from anything iterable.
1510+
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
1511+
function toArray(obj) {
1512+
if (!obj) return [];
1513+
if (isArray(obj)) return slice.call(obj);
1514+
if (isString(obj)) {
1515+
// Keep surrogate pair characters together.
1516+
return obj.match(reStrSymbol);
1517+
}
1518+
if (isArrayLike(obj)) return map(obj, identity);
1519+
return values(obj);
1520+
}
1521+
15091522
// Sample **n** random values from a collection using the modern version of the
15101523
// [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
15111524
// If **n** is not specified, returns a single random element.
@@ -1515,7 +1528,7 @@ function sample(obj, n, guard) {
15151528
if (!isArrayLike(obj)) obj = values(obj);
15161529
return obj[random(obj.length - 1)];
15171530
}
1518-
var sample = isArrayLike(obj) ? clone(obj) : values(obj);
1531+
var sample = toArray(obj);
15191532
var length = getLength(sample);
15201533
n = Math.max(Math.min(n, length), 0);
15211534
var last = length - 1;
@@ -1592,19 +1605,6 @@ var partition = group(function(result, value, pass) {
15921605
result[pass ? 0 : 1].push(value);
15931606
}, true);
15941607

1595-
// Safely create a real, live array from anything iterable.
1596-
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
1597-
function toArray(obj) {
1598-
if (!obj) return [];
1599-
if (isArray(obj)) return slice.call(obj);
1600-
if (isString(obj)) {
1601-
// Keep surrogate pair characters together.
1602-
return obj.match(reStrSymbol);
1603-
}
1604-
if (isArrayLike(obj)) return map(obj, identity);
1605-
return values(obj);
1606-
}
1607-
16081608
// Return the number of elements in a collection.
16091609
function size(obj) {
16101610
if (obj == null) return 0;

underscore-node-f.cjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-umd.js

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

underscore-umd.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)