Skip to content

Commit

Permalink
feat: Add 'rest' option to the shuffle method. (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
misteroneill committed Jan 25, 2018
1 parent eb80503 commit 57d5f0c
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 13 deletions.
68 changes: 55 additions & 13 deletions src/playlist-maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,31 @@ const indexInSources = (arr, src) => {
return -1;
};

/**
* Randomize the contents of an array.
*
* @private
* @param {Array} arr
* An array.
*
* @return {Array}
* The same array that was passed in.
*/
const randomize = (arr) => {
let index = -1;
const lastIndex = arr.length - 1;

while (++index < arr.length) {
const rand = index + Math.floor(Math.random() * (lastIndex - index + 1));
const value = arr[rand];

arr[rand] = arr[index];
arr[index] = value;
}

return arr;
};

/**
* Factory function for creating new playlist implementation on the given player.
*
Expand Down Expand Up @@ -486,26 +511,43 @@ export default function factory(player, initialList, initialIndex = 0) {
/**
* Shuffle the contents of the list randomly.
*
* @see {@link https://github.com/lodash/lodash/blob/40e096b6d5291a025e365a0f4c010d9a0efb9a69/shuffle.js}
* @see {@link https://github.com/lodash/lodash/blob/40e096b6d5291a025e365a0f4c010d9a0efb9a69/shuffle.js}
* @fires playlistsorted
* @todo Make the `rest` option default to `true` in v5.0.0.
* @param {Object} [options]
* An object containing shuffle options.
*
* @param {boolean} [options.rest = false]
* By default, the entire playlist is randomized. However, this may
* not be desirable in all cases, such as when a user is already
* watching a video.
*
* When `true` is passed for this option, it will only shuffle
* playlist items after the current item. For example, when on the
* first item, will shuffle the second item and beyond.
*/
playlist.shuffle = () => {
let index = -1;
const length = list.length;
playlist.shuffle = ({rest} = {}) => {
let index = 0;
let arr = list;

// When options.rest is true, start randomization at the item after the
// current item.
if (rest) {
index = playlist.currentIndex_ + 1;
arr = list.slice(index);
}

// Bail if the array is empty.
if (!length) {
// Bail if the array is empty or too short to shuffle.
if (arr.length <= 1) {
return;
}

const lastIndex = length - 1;

while (++index < length) {
const rand = index + Math.floor(Math.random() * (lastIndex - index + 1));
const value = list[rand];
randomize(arr);

list[rand] = list[index];
list[index] = value;
// When options.rest is true, splice the randomized sub-array back into
// the original array.
if (rest) {
list.splice(...[index, arr.length].concat(arr));
}

// If the playlist is changing, don't trigger events.
Expand Down
55 changes: 55 additions & 0 deletions test/playlist-maker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -788,3 +788,58 @@ QUnit.test('playlist.shuffle() works as expected', function(assert) {
assert.notStrictEqual(list.indexOf(4), -1, '4 is in the list');
assert.strictEqual(spy.callCount, 1, 'the "playlistsorted" event triggered');
});

QUnit.test('playlist.shuffle({rest: true}) works as expected', function(assert) {
const player = playerProxyMaker();
const spy = sinon.spy();

player.on('playlistsorted', spy);
const playlist = playlistMaker(player, [1, 2, 3, 4]);

playlist.currentIndex_ = 3;
playlist.shuffle({rest: true});
let list = playlist();

assert.deepEqual(list, [1, 2, 3, 4], 'playlist is unchanged because the last item is selected');
assert.strictEqual(spy.callCount, 0, 'the "playlistsorted" event was not triggered');

playlist.currentIndex_ = 2;
playlist.shuffle({rest: true});
list = playlist();

assert.deepEqual(list, [1, 2, 3, 4], 'playlist is unchanged because the second-to-last item is selected');
assert.strictEqual(spy.callCount, 0, 'the "playlistsorted" event was not triggered');

playlist.currentIndex_ = 1;
playlist.shuffle({rest: true});
list = playlist();

assert.strictEqual(list.length, 4, 'playlist is the correct length');
assert.strictEqual(list.indexOf(1), 0, '1 is the first item in the list');
assert.strictEqual(list.indexOf(2), 1, '2 is the second item in the list');
assert.notStrictEqual(list.indexOf(3), -1, '3 is in the list');
assert.notStrictEqual(list.indexOf(4), -1, '4 is in the list');
assert.strictEqual(spy.callCount, 1, 'the "playlistsorted" event triggered');

playlist.currentIndex_ = 0;
playlist.shuffle({rest: true});
list = playlist();

assert.strictEqual(list.length, 4, 'playlist is the correct length');
assert.strictEqual(list.indexOf(1), 0, '1 is the first item in the list');
assert.notStrictEqual(list.indexOf(2), -1, '2 is in the list');
assert.notStrictEqual(list.indexOf(3), -1, '3 is in the list');
assert.notStrictEqual(list.indexOf(4), -1, '4 is in the list');
assert.strictEqual(spy.callCount, 2, 'the "playlistsorted" event triggered');

playlist.currentIndex_ = -1;
playlist.shuffle({rest: true});
list = playlist();

assert.strictEqual(list.length, 4, 'playlist is the correct length');
assert.notStrictEqual(list.indexOf(1), -1, '1 is in the list');
assert.notStrictEqual(list.indexOf(2), -1, '2 is in the list');
assert.notStrictEqual(list.indexOf(3), -1, '3 is in the list');
assert.notStrictEqual(list.indexOf(4), -1, '4 is in the list');
assert.strictEqual(spy.callCount, 3, 'the "playlistsorted" event triggered');
});

0 comments on commit 57d5f0c

Please sign in to comment.