From 57d5f0ce7835edcca449314f6e6c5d432ab2049d Mon Sep 17 00:00:00 2001 From: Pat O'Neill Date: Thu, 25 Jan 2018 11:41:47 -0500 Subject: [PATCH] feat: Add 'rest' option to the shuffle method. (#91) --- src/playlist-maker.js | 68 ++++++++++++++++++++++++++++++------- test/playlist-maker.test.js | 55 ++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/src/playlist-maker.js b/src/playlist-maker.js index a5506d1..58ee623 100644 --- a/src/playlist-maker.js +++ b/src/playlist-maker.js @@ -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. * @@ -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. diff --git a/test/playlist-maker.test.js b/test/playlist-maker.test.js index b90b6aa..48a82e8 100644 --- a/test/playlist-maker.test.js +++ b/test/playlist-maker.test.js @@ -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'); +});