javascriptarraysfisher-yates-shuffle

How to save the old state of an array before shuffling


I have an array of 50 objects as elements.

Each object contains an array of 4 elements:

var all = [{
    question: "question 1 goes here",
    options: ["A", "B", "C", "D"]
}, ... {
    question: "question 50",
    options: ["A", "B", "C", "D"]
}]

I want to select 10 elements randomly and save to two other arrays, one of the array I want to shuffle options. but when shuffling both arrays are affected.

var selected = [];
var shuffled = [];

for(let i = 0; i < 10; i++) {
    let rand = Math.floor(Math.random() * all.length);
    selected.push(all[rand]);
    shuffled.push(all[rand]);
    all.splice(rand, 1);

    for(let j = 3; j > 0; j--) {
        let rand2 = Math.floor(Math.random() * j);
        [
             shuffled[i].options[j], 
             shuffled[i].options[rand2]
        ] = [
             shuffled[i].options[rand2],
             shuffled[i].options[j]
        ];
    }
}
console.log(selected); // is shuffled too
console.log(shuffled);

How do I prevent that?

I feel like I'm missing something pretty simple, but I can't spot it.


Solution

  • You need to create new instances for the chosen objects and their options arrays:

    // shuffle array a in place (Fisher-Yates)
    // optional argument len (=a.length):
    //   reduced shuffling: shuffling is done for the first len returned elements only, 
    //   array a will be shortened to length len.
    function shuffle(a, len=a.length){
     for(let m=a.length,n=Math.min(len,m-1),i=0,j;i<n;i++){
      j=Math.floor(Math.random()*(m-i)+i);
      if (j-i) [ a[i],a[j] ] = [ a[j],a[i] ]; // swap 2 array elements
     }
     a.length=len;
     return a;
    }
    
    const all=[...new Array(50)].map((_,i)=>({question:"Question "+(i+1), options:["A","B","C","D"]}));
    
    const selected = shuffle([...all],10), // return first 10 shuffled elements only!
          shuffled = selected.map(o=>({...o,options:shuffle([...o.options])}));
    
    console.log(selected) // is no longer shuffled!
    console.log(shuffled);

    I consigned the shuffle algorithm to a separate function (shuffle()) and applied it twice: first to the all array to make sure we don't get any duplicates in our "random" selection and then to the options arrays contained within their sliced-off objects. The function shuffle(a,len) sorts array a in place. I made it return the array reference again purely out of convenience, as it helps me keep my code more compact. The optional argument len will cause the array a to be shortened to len shuffled elements (still "in place": the input array will be affected too!).

    So, in order to preserve my "input" arrays I created new array instances each time I called the function by applying the ... operator:

    shuffled = shuffle([...all],10);
    ...
    shuffle([...o.options])