2

I want to run a function that each time randomly chooses an element from an array that wasn't chosen before. And if all elements were chosen, I want to reset the used elements and start from the beginning.

Hope this makes sense.

I already have a function that chooses a random element from an array. But I also don't want it to choose elements that were chosen before unless all elements were already chosen.

Here is what I have got so far (credit to @Kelly):

var item = items[Math.floor(Math.random() * items.length)]
Anatol
  • 3,720
  • 2
  • 20
  • 40
  • 2
    The easiest is to shuffle the array -> keep an index of the current element -> increment it every time you get the "next random". On completion, just wrap around. – VLAZ Mar 23 '20 at 10:46
  • @VLAZ do you mind giving an example with code? – Anatol Mar 23 '20 at 10:48
  • @VLAZ just imagine if your random number comes as 3 5 times, this will have to be increment again and again and build logic for that – Rajesh Mar 23 '20 at 10:49
  • @Rajesh why would you be generating random numbers, after you've shuffled the array? – VLAZ Mar 23 '20 at 10:55
  • In case you have values inside, you can set them to undefined after reading that item and then check while (item == undefined) repeat selection - in case there is anything you can select still ;-) But mind undefined cannnot be compared opposite (!= undefined). – Jan Mar 23 '20 at 11:04
  • @VLAZ My point was, if `Math.random()` gives similar number, it will point to same number and you will have to keep a track of indexes as we need to move to next non-chosen item – Rajesh Mar 23 '20 at 11:05
  • 1
    @Rajesh again, non-issue after shuffling the array. It's already random order, no need to fetch random item from it, you can progress linearly. – VLAZ Mar 23 '20 at 11:06
  • If you're going to shuffle the array anyway, why not just pop values off the end of it? You'd need to reshuffle every time you repopulated it, but a small price to pay if you ask me. – Psychemaster Mar 23 '20 at 11:09
  • @Psychemaster what does popping give you? You'd need to store the popped elements somewhere or make a copy of the array before every shuffle. It seems like a lot more work than a single numeric variable. – VLAZ Mar 23 '20 at 11:13
  • You can still store the popped elements in a temporary array and then use it to overwrite the original one when it is empty (you could throw the shuffle logic in at that point too). It just seems a waste to have randomization occuring both during shuffling and during retrieval. – Psychemaster Mar 23 '20 at 11:16
  • @Psychemaster *What randomization during retrieval*? Again, I proposed *only* shuffling the array, not fetching random elements from it after. The next random item is literally the next index *from the already randomized array*. [Have a look at my answer](https://stackoverflow.com/a/60812248/) – VLAZ Mar 23 '20 at 11:20
  • Guess I misread the 'next random' bit in the original comment. Oh well, no harm done. – Psychemaster Mar 23 '20 at 11:26

4 Answers4

3

You can try something like this:

Idea

  • Create a utility function that takes an array and returns you a random value.
  • Inside this Array, maintain 2 array, choices and data.
  • In every iteration, remove 1 item from data and put it in chosenItems
  • Once the length of data reaches 0, set chosenItems or originalArray as data and repeat process.

Benefit of this approach would be,

  • You dont need to maintain and pass array variable.
  • It can be made generic and used multiple times.

function randomize(arr) {
  let data = [...arr];
  let chosenItems = [];

  function getRandomValue() {
    if (data.length === 0) {
      data = chosenItems;
      chosenItems = [];
    }
    const index = Math.floor(Math.random() * data.length);
    const choice = data.splice(index, 1)[0];

    chosenItems.push(choice);
    return choice;
  }
  
  return {
    randomItem: getRandomValue
  }
}

const dummyData = [ 1,2,3,4,5 ];

const randomizeData = randomize(dummyData);

for (let i = 0; i< 10; i++) {
  console.log(randomizeData.randomItem())
}
Community
  • 1
  • 1
Rajesh
  • 24,354
  • 5
  • 48
  • 79
2

The easiest way to handle this is:

function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  while (0 !== currentIndex) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}


var items = ["alpha", "beta", "gamma", "delta", "epsilon"];
var index = Infinity;

function start() {
  console.log("----- shuffling -----")
  shuffle(items);
  index = 0;
}

function nextItem() {
  if (index >= items.length) {
    //re-start
    start()
  }
  
  //return current index and increment
  return items[index++];
}

document.getElementById("click_me")
  .addEventListener("click", function() {
    console.log(nextItem())
  })
<button id="click_me">Next random</button>

This can also be converted to a generator function

function shuffle(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  while (0 !== currentIndex) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

function* random(array) {
  let index = Infinity;
  const items = array.slice(); //take a copy of the array;
  
  while(true) {
    if (index >= array.length) {
      console.log("----- shuffling -----")
      shuffle(items);
      index = 0;
    }
    
    yield items[index++];
  }
}


var items = ["alpha", "beta", "gamma", "delta", "epsilon"];

//start the generator
const generateRandom = random(items);

document.getElementById("click_me")
  .addEventListener("click", function() {
    console.log(generateRandom.next().value)
  })
<button id="click_me">Next random</button>
VLAZ
  • 26,331
  • 9
  • 49
  • 67
2

One possible way is prime walk.

First have the list of e.g. 500 items. Get the next prime number that is greater than 500. Here 503. Select random seed. This seed is any number that is constant for an user.

var prime = 503;
var list = ["item1", "item2", ... "item500"];

function pick_nth(seed, n, p, l) {
    if(!n) return l[seed % p];
    return pick_nth((seed + l.length) % p, n - 1, l);
}

Picking up n:th item from list is easy. For example:

pick_nth(seed, 0, prime, list);  // first item
pick_nth(seed, 1, prime, list);  // second item
...
pick_nth(seed, 499, prime, list);  // 500th item

The order of the items returned are permutated by the seed.

Teemu Ikonen
  • 11,861
  • 4
  • 22
  • 35
0

Here's a solution that's pretty short. It uses randojs.com to simplify the randomness and make it more readable, though I kinda threw a wrench in the "easy to read" part by making it super short. If you're having trouble understanding, here are some resources that will help explain: declaring variables on one line, the ternary operator, different ways to declare a function, the pop() method, and randojs

You can actually make the JavaScript one line if you want by just removing the line break, but I chose to make it two for the sake of clarity and shorter lines.

var arr = ["one", "two", "three", "four"], shuffled = randoSequence(arr), 
uniqueRandom = () => (shuffled.length ? shuffled : shuffled = randoSequence(arr)).pop().value;
<script src="https://randojs.com/1.0.0.js"></script>
<button onclick="console.log(uniqueRandom());">Log a unique random</button>

NOTE: this code won't work if you forget to import randojs with the script tag, so make sure to paste that in the head of your HTML document if you want to use this code.

Aaron Plocharczyk
  • 2,776
  • 2
  • 7
  • 15