0

I'm working on the FreeCodeCamp 'Random Quote Machine' front end library project using React JSX, which was working fine with the exception that it frequently produced the same quote two or three times in a row. This was not a good thing for a user to exprience.

Looking into Math.random() I quickly realisded that this was quite normal - two or three repeats of the same number in a row. This is the code I had in a React method called getQuote:

// get new quote from array
 getQuote() {
   let x = Math.floor(Math.random() * quotes.length);
   {console.log("random num = ", x)};
   
   this.setState(quotes[x]
   );

This meant that I had to write a method of producing a random number that wasn't the same as the last. Which, thanks to the quality content on this site and input from @Etienne Martin I found pretty quickly:

Most of the answers in this thread are over complicated.

Here's a concise example of how I would do that:

function getNumber(){
    return (getNumber.number = Math.floor(Math.random() * (4 + 1))) === getNumber.lastNumber ? getNumber() : getNumber.lastNumber = getNumber.number; }

console.log(getNumber()); // Generates a random number between 0 and 4

Live example: https://jsfiddle.net/menv0tur/3/ share edit flag edited Apr 22 '17 at 16:25 answered Apr 20 '17 at 19:35

This allowed me to write my own method:

getQuote() {
  const randNum = () => {
    return (randNum.x = Math.floor(Math.random() * (quotes.length))) === randNum.y ? randNum() : randNum.y = randNum.x;    
}
  this.setState(quotes[randNum()])
}

My problem is; I don't fully understand it. Specifically getNumber.number it looks as though it is using dot notation to access a key value of our function getNumber(). I have never seen this before. As you can see in my re-write I defined the function name randNum() and I'm using randNum.x and randNum.y instead of getNumber.number and getNumber.lastNumber.

Could someone kindly enlighten me as to exactly what we are doing here?

Many thanks in advance.

Fishbite
  • 144
  • 12

3 Answers3

1

The first thing you should know is that a function is an object at the same time in javascript. So I can do this:

const randNum = () => {}
randNum.property = "value";

and that's exactly what your function is doing. Probably it will be better to write it as:

const randNum = () => {
  randNum.x = Math.floor(Math.random() * (quotes.length));
  // I can access randNum.x and randNum.y because randNum is a function and an object
  if (randNum.x === randNum.y) {
    // current value is the same as older value, run the function again
    return randNum();
  } else {
    // current value is not the same as the older value
    randNum.y = randNum.x;
    return randNum.y;
  }
};

or, even better:

let oldValue;

const randNum = () => {
  const currentValue = Math.floor(Math.random() * (quotes.length));  
  if (currentValue === oldValue) {
    return randNum();
  } else {
    oldValue = currentValue;
    return currentValue;
  }
};

I know this is not a one line solution but is easier to understand what it does (in my opinion)

Ariel
  • 1,366
  • 7
  • 16
  • First class answer. Thank you for the clarification. It makes perfect sense now. In the first re-mix, we are assign a new property and value to the object `randNum.x` & `randNum.y`. In the first comparison of `randNum.x` === `randNum.y` `randNum.y` will be `undefined` and therefore, is assigned the value of randNum.x. In your sencond remix, you have defined a vairiable `currentValue` rather than an object `property:value` as you did in the first remix. I think the thing niggling my brain is the fact that an error is not thrown comparing `currentValue` to `oldValue` as old value is undefined? – Fishbite Jul 24 '20 at 07:48
  • 1
    You can compare null or undefined. What you can't do is access a property from a null or undefined object. – Ariel Jul 24 '20 at 20:02
1

Let's rewrite getNumber function so that it's more readable:

function getNumber() {
  let result;
  if (
    (getNumber.number = Math.floor(Math.random() * (4 + 1))) ===
    getNumber.lastNumber
  ) {
    result = getNumber(); // recursive call
  } else {
    result = getNumber.lastNumber = getNumber.number; // assignment expression
  }
  return result;
}

As you can see the function uses recursion. Also in this line: result = getNumber.lastNumber = getNumber.number; the code getNumber.lastNumber = getNumber.number is an assignment expression so result is the same as getNumber.number. Lastly it's possible to write getNumber.number because getNumber is an object. In Javascript even functions are objects. So when you call getNumber() you invoke the function, when you call getNumber.number you accessing object field.

After you define the function getNumber you can then add as many fields to it as you want for example getNumber.anotherProperty = 'whatever' then console.log(getNumber.anotherProperty) // logs 'whatever'.

Yos
  • 1,276
  • 1
  • 20
  • 40
  • Excellent answer. Thanks for taking the time to explain this. As I've said below, I'm still surprised that `(getNumber.number = Math.floor(Math.random() * (4 + 1))) === getNumber.lastNumber )` doesn't throw an error because `lastNumber` is undefined at this point? Am I missing an important point here? – Fishbite Jul 24 '20 at 07:56
  • 1
    In the first call of the function lastNumber is indeed undefined but we’re just checking if lastNumber equals number. Checking if something equals undefined diesn’t result in error. – Yos Jul 24 '20 at 08:01
  • I can see clearly now the rain has gone - goes the first line of a great song! Your answer cleared the last of the fog from this. Many thanks. – Fishbite Jul 24 '20 at 08:12
0

You may populate your state with a shuffled version of array of quotes, popping one at a time upon nextQuote until all of them are shown, then repeat, once your shuffled array is empty, thus, the quote will not appear until all the rest are shown.

However, it does not eliminate probability of the following outcome:

quote1, quote3, quote5, quote4 | quote5, quote1, quote3..

Pushing the distance between quotes further ultimately leads you to the point where the interval of repeating for each quote is source array length, i.e. your initial array going round:

quote1, quote2... , quote5| quote1, quote2..., quote5 | quote1, quote2...

But then it won't be random quotes any longer

Live-demo of my approach, you may find as follows:

const { useState } = React,
      { render } = ReactDOM,
      rootNode = document.getElementById('root')
      
const quotes = ['quote1', 'quote2', 'quote3', 'quote4', 'quote5'],

      shuffle = arr => [...arr].reduceRight((r,_,__,s) => 
        (r.push(s.splice(0|Math.random()*s.length,1)[0]), r),[])

const App = () => {
  const [currentQuote, setCurrentQuote] = useState(quotes[0|Math.random()*quotes.length]),
        [quoteList, setQuoteList] = useState(shuffle(quotes).filter(q => q!= currentQuote)),
        nextQuote = () => {
          const [newQuote, ...rest] = quoteList
          !rest.length ?
          setQuoteList(shuffle(quotes).filter(q => q != newQuote)) :
          setQuoteList(rest)
          setCurrentQuote(newQuote)
        }
  return (
    <div>
      <div>{currentQuote}</div>
      <button onClick={() => nextQuote()}>Next</button>
    </div>
  )
  
}

render (
  <App />,
  rootNode
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yevhen Horbunkov
  • 14,965
  • 3
  • 20
  • 42
  • You can do that, but the underlying problem is that `Math.random()` does generate two or three consecutive numbers that are the same. It was that problem that really need to be resolved. – Fishbite Jul 22 '20 at 13:55
  • I think you may have posted before I edited my post as I forgot to include my solution. I'm just looking for clarification of `getNumber.number` – Fishbite Jul 22 '20 at 14:05
  • Sorry, I didn't use your solution. The method I was using solved the problem that `Math.random()` frequently returns the same value consecutively. Whilst the method I was using worked, I just needed clarification of exactly what it was doing - I really don't like to use code snippets in my apps that I don't understand 100%. @Ariel Alvarado wrote a very concise explanation as did @Yos with a shorter version. Thanks for your input though, it's very much appreciated. – Fishbite Jul 24 '20 at 08:07
  • @Fishbite : it looks like the answer you've accepted does exactly what the first edit of my answer did, except for I excluded repetitions with `.filter()`, while that answer suggests doing guesswork until you pick non-repeating value. However, you should bear in mind that my solution has one major advantage: it won't show quote that was shown couple iterations back (until all the quotes in random order are shown), whereas solution you've accepted may show repeatedly with small interval all the same quotes. So, if you'll change your mind at some point I will gladly give my further support. – Yevhen Horbunkov Jul 24 '20 at 08:31