ES6 get random elements from object with no repeat

Viewed 637

I have a function that's supposed to render random colors, but without repeating the colors.

Meaning if blue is randomly selected, it can't be selected again. Of course this means there needs to be a default value. I was thinking of using a switch statement.

Here is my current code:

const colors = {
      grey: '#BDC8D1',
      blue: '#0500FF',
      pink: '#FF00C7',
      orange: '#FF7A00'
    }

    const randomColor = () => {
      let keys = Object.keys(colors)
      return colors[keys[keys.length * Math.random() << 0]]
    }
3 Answers

You could "consume" an array of valid values to return. By consuming I mean, removing them from an array.

For instance:

// List of all valid values (those that can be returned)
const colors = [ 'red', 'green', 'blue' ];

// The default color (when all others have been "consumed")
const defaultColor = 'black'; 

// The function that returns a random color
const getRandomColor = () => {
    // At least we return a color
    let color = defaultColor;

    // If all colors were previously consumed, we won't need
    // to pick one randomly
    if (colors.length > 0) {
        // We select randomly an index from the colors array
        const index = Math.floor(colors.length * Math.random());

        // We store the color to return
        color = colors[index];

        // We remove it from the array
        colors.splice(index, 1);
    }
    
    return color;
};

console.log(getRandomColor());
console.log(getRandomColor());
console.log(getRandomColor());
console.log(getRandomColor());
console.log(getRandomColor());
console.log(getRandomColor());

The obvious problem with this solution is that you can't reuse your function for several times. A better solution would be creating an iterator. Each time some part of your application needs to generate a random series of colors, you create a new iterator, and use its next method to get a new value. Check the following:

// The default color (when all others have been "consumed")
const defaultColor = 'black'; 

const RandomColorIterator = () => {
    // List of all valid values (those that can be returned)
    const colors = [ 'red', 'green', 'blue' ];
    
    return {
        next: () => {
            // At least we return a color
            let color = defaultColor;

            // If all colors were previously consumed, we won't need
            // to pick one randomly
            if (colors.length > 0) {
                // We select randomly an index from the colors array
                const index = Math.floor(colors.length * Math.random());

                // We store the color to return
                color = colors[index];

                // We remove it from the array
                colors.splice(index, 1);
            }

            return color;
        },
    };
};

const iterator1 = RandomColorIterator();
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());
console.log('1:', iterator1.next());

const iterator2 = RandomColorIterator();
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());
console.log('2:', iterator2.next());

I have been using arrow function to profit from the parent scope. This allows to access colors on a per call basis.

Just to be different and use some functional programming:

const colors = {
      grey: '#BDC8D1',
      blue: '#0500FF',
      pink: '#FF00C7',
      orange: '#FF7A00'
    }


const randomIndex = arr => (Math.random() * arr.length) >> 0


const getColors = (keys = [], times = 0, colors = []) => {
  if (!keys.length || times <= 0) {
    return colors
  }

  const randIndex = randomIndex(keys)

  colors.push(keys[randIndex])
  keys.splice(randIndex, 1)
  times--


  return getColors(keys, times, colors)
}

// select 2 colors
console.log(getColors(Object.keys(colors), 2))

Note: It would be better if you didn't mutate the arguments i.e keys.splice

Also, here's the .sort method someone mentioned - cool idea.

Object.keys(colors).sort((a, b) => {
  const order = [ -1, 0, 1 ]
  return order[(Math.random() * 3) >> 0]
})

Here is an actual implementation:

const colors = {
  grey: '#BDC8D1',
  blue: '#0500FF',
  pink: '#FF00C7',
  orange: '#FF7A00'
}



let keysArr = Object.keys(colors);
let keyArrLength = keysArr.length


for (let i = 0; i < keyArrLength; i++) {
  let el = keysArr[Math.floor(Math.random() * keysArr.length)];
  console.log(el);
  let index = keysArr.indexOf(el);
  keysArr.splice(index, 1);
}

Hopefully this is helpful, if you have any questions you can leave a comment ;).

Related