0

I need to randomize only some elements of an array.

Array example:

let array = [
    // index 0
    {
      randomize: true,
      value: 'value_1'
    },
    // index 1    
    {
      randomize: false,
      value: 'value_2'
    },
    // index 2
    {
      randomize: false,
      value: 'value_3'
    },
    // index 3
    {
      randomize: true,
      value: 'value_4'
    },
    // index 4
    {
      randomize: false,
      value: 'value_5'
    },
  ]

The array is dynamically created, so it can have a lot of elements with randomize: true on any position. I want to randomize only elements with randomize: true, and others should remain in their current positions. What is the best solution to do it?

EDIT:

Answers from @georg, @Nina Scholz, @TKoL and @Neveen Atik all works. I implemented @georg solution since it is the most elegant.

NeNaD
  • 18,172
  • 8
  • 47
  • 89
  • 1
    Shuffling an array is usually done by selecting two elements randomly and swapping them (multiple times). Instead of picking any two elements, pick two where `.randomize === true` –  Apr 16 '21 at 11:43
  • The array is dynamically created, so it can have a lot of elements with `randomize: true` and I don't know their positions. I will add that information in question. – NeNaD Apr 16 '21 at 11:45
  • 2
    That doesn't matter at all; you pick a random index using `Math.floor(Math.random() * array.length)` until the picked element's `randomize` is `true`. Then you pick a second index using the same method. Then you swap the two elements. It doesn't matter how the array is created or what the positions are. –  Apr 16 '21 at 11:46
  • I added index comments, so we know what to talk about – yunzen Apr 16 '21 at 11:47
  • @NenadMilosavljevic You mean, you want to have 01234 to be randomly shuffled to e.g 21430 or 01432 or 41032 with index 1 and 3 (as denoted by the randomize value) should stay in place? – yunzen Apr 16 '21 at 11:49
  • If the item has `randomize: false`, it needs to stay in his current position. – NeNaD Apr 16 '21 at 11:50
  • @ChrisG it would need more detail to figure out how to swap random elements more than one time. – TKoL Apr 16 '21 at 11:52
  • do you need the same object reference, or is a new assignment of the array fine? – Nina Scholz Apr 16 '21 at 11:56
  • Whatever. I can just assign the value of a new_array to my_array, after the new_array is created. – NeNaD Apr 16 '21 at 11:58

4 Answers4

2

I think one idea is to make a new array with only the randomizable things, shuffle it, and then place them back into the old array. Check this out:

var array = [
    {
      randomize: true,
      value: 'value_1'
    },
    {
      randomize: false,
      value: 'value_2'
    },
    {
      randomize: false,
      value: 'value_3'
    },
    {
      randomize: true,
      value: 'value_4'
    },
    {
      randomize: false,
      value: 'value_5'
    },
    {
      randomize: true,
      value: 'value_6'
    },
    {
      randomize: true,
      value: 'value_7'
    },
  ];

// https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    // And swap it with the current element.
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

// make a list of all randomizable elements
var randomizable = array.filter(o => o.randomize);
// shuffle their order
shuffle(randomizable);
// to make this easy to reason about, i think i'll make all the randomizeable elements in the first array null
array.forEach(function(el, index) {
    if (el.randomize) array[index] = null;
});

console.log(array);

// now, we can iterate over the null slots and place in elements from randomizable
array.forEach(function(el, index) {
    if (el === null) {
        array[index] = randomizable.pop();
    }
});

console.log(array);
TKoL
  • 13,158
  • 3
  • 39
  • 73
  • 1
    I guess you could do `if (el.randomize) array[index] = randomizable.pop();` instead of setting the index to null and skip the second loop – Nick Parsons Apr 16 '21 at 11:53
  • @NickParsons I did think that, the only reason i did it this way was because the intermediate step is easier to visualize -- if you set a breakpoint before i start putting the shuffled elements back in, you can clearly see which elements of the array are available to have something put into them. – TKoL Apr 16 '21 at 11:55
  • 1
    But you're right, it's not a necessary step at all, and is only useful for the purpose I stated. – TKoL Apr 16 '21 at 11:56
  • sure no worries. Yeah, it's a good way to visualize how your code is working :) – Nick Parsons Apr 16 '21 at 11:56
  • Thank you for your answer. I tested it and it works. I implemented @georg's answer though, since it’s a little more elegant. – NeNaD Apr 17 '21 at 05:31
2

I'd do it like this:

  • extract indexes of random elements
  • shuffle these indexes
  • map the array, and, for a random element, return an element from the shuffled index:

function rnd(a) {
    let indexes = a
        .map((x, n) => x.randomize ? n : -1)
        .filter(x => x >= 0)
        .map(x => [Math.random(), x])
        .sort()
        .map(x => x[1]);

    return a.map(x => x.randomize ? a[indexes.pop()] : x);
}


let arr = [
    { randomize: true, value: 'a'},
    { randomize: false, value: '2'},
    { randomize: true, value: 'b'},
    { randomize: false, value: '4'},
    { randomize: true, value: 'c'},
    { randomize: false, value: '6'},
    { randomize: true, value: 'd'},
    { randomize: false, value: '8'},
    { randomize: false, value: '9'},
    { randomize: true, value: 'e'},
  ]


console.log(...rnd(arr).map(x => x.value))
console.log(...rnd(arr).map(x => x.value))
console.log(...rnd(arr).map(x => x.value))
console.log(...rnd(arr).map(x => x.value))
console.log(...rnd(arr).map(x => x.value))
console.log(...rnd(arr).map(x => x.value))
georg
  • 211,518
  • 52
  • 313
  • 390
1

You could get the indices shuffle them and apply the random values back to a new array.

const
    array = [{ randomize: true, value: 'value_1' }, { randomize: false, value: 'value_2' }, { randomize: false, value: 'value_3' }, { randomize: true, value: 'value_4' }, { randomize: false, value: 'value_5' }],
    indices = [...array.keys()]
        .filter(i => array[i].randomize)
        .map((v, i, a) => {
            const
                j = (Math.random() * (a.length - i) | 0) + i,
                t = a[j];
            a[j] = v;
            return t;
        }),
    result = array.map((o, i, a) => o.randomize ? a[indices.shift()]: o);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thank you for your answer. I tested it and it works. I implemented @georg's answer though, since it’s a little more elegant. – NeNaD Apr 17 '21 at 05:32
1

You can do something like

// inspired by this answer https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array

function shuffle(array) {
  let currentIndex = array.length;
  let randomIndex = Math.floor(Math.random() * currentIndex);

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {
    let currentElement = array[currentIndex -1];

    //if currentElement is randomizable
    if(currentElement.randomize){

      // Pick another element that is randomizable
      while(!array[randomIndex].randomize){
        randomIndex = Math.floor(Math.random() * currentIndex);
      }
      currentIndex -= 1;
      
      // And swap it with the current element.
      array[currentIndex] = array[randomIndex];
      array[randomIndex] = currentElement;

    // otherwise go to previous element
    } else {
      currentIndex -= 1;
    }
  }

  return array;
}

// Used like so
const array = [
    // index 0
    {
      randomize: true,
      value: 'value_1'
    },
    // index 1    
    {
      randomize: false,
      value: 'value_2'
    },
    // index 2
    {
      randomize: false,
      value: 'value_3'
    },
    // index 3
    {
      randomize: true,
      value: 'value_4'
    },
    // index 4
    {
      randomize: false,
      value: 'value_5'
    },
  ]
shuffle(array);
console.log(array);
Neveen Atik
  • 141
  • 4
  • Thank you for your answer. I tested it and it works. I implemented @georg's answer though, since it’s a little more elegant. – NeNaD Apr 17 '21 at 05:32
  • I agree my function can use some grooming :) However I was more concerned with performance since @georg's answer is like 6n and this would be 2n if I'm not mistaken. Happy you found your answer – Neveen Atik Apr 19 '21 at 08:30