3

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]]
    }
Dileet
  • 2,034
  • 3
  • 33
  • 53
  • Please show us what you tried in order to solve the problem here? You mentioned a switch, how have you tried one, and how does it not solve your problem? – Luca Kiebel Sep 16 '18 at 16:17
  • If you don't want repeats, then put the colours in an array, shuffle it and start taking off elements from the array. You'd have to figure out what to do if you have no more colours left and still need more, though. – VLAZ Sep 16 '18 at 16:17
  • Is it possible you could use an Array? Then you could use Array.sort() to randomize the order and then use each color once. – Brendon Shaw Sep 16 '18 at 16:18
  • 1
    @BrendonShaw `.sort` is not a good idea for randomising an array. It won't be very...random. Maybe. It depends on the sort implementation and also on chance but overall, it's easier to use [a more established method of shuffling](https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array) – VLAZ Sep 16 '18 at 16:20
  • @vlaz I see what your saying. That just popped into my head so I put it here. Thanks for pointing it out! – Brendon Shaw Sep 17 '18 at 14:29
  • 1
    @BrendonShaw we are all here to learn :) For the record, the reason why `.sort` [might not be suitable for randomizing if the sorting function is unstable](https://stackoverflow.com/questions/24080785/sorting-in-javascript-shouldnt-returning-a-boolean-be-enough-for-a-comparison). The post actually talks about comparison only returns two value - `true` or `false` as opposed to three: `-1`, `0`, and `1`. However, the underlying reason is the same - the sorting algorithm may be optimised to take as few steps as possible. Unstable comparison could result just few swaps instead of shuffling. – VLAZ Sep 17 '18 at 14:52

3 Answers3

5

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.

Léopold Houdin
  • 1,515
  • 13
  • 18
1

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]
})
CodeDraken
  • 1,163
  • 6
  • 12
  • So this works, but I should of been more clear with my question. I'm rendering a dynamic list where I'm trying to set the random color that hasn't been chosen before. So for example ```
    ```
    – Dileet Sep 16 '18 at 17:23
  • This won't select the same color twice. Do you mean you need to grab the elements and figure out what colors already exist? @Dileet Or where you pass in colors you already have? – CodeDraken Sep 16 '18 at 17:27
  • Ya exactly, making sure each list item has a different color – Dileet Sep 16 '18 at 17:28
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 ;).

Willem van der Veen
  • 33,665
  • 16
  • 190
  • 155