Score to Grade function

Hi, I’m struggling with this one. When the test is over I want to show grade to the user, but I got stuck here? Could you tell me what am I missing?

function endQuiz() {

    var grade = scoreGrade();

    if (myAnswers[(lengthofobject-1)] {

        var output = "<div class='output'>Резултат<br>";

        var questionResult = "NA";

        //console.log('Quiz Over');

        for (var i = 0; i < myAnswers.length; i++ || "ten-countdown"<1)) {

            if (data.quizcontent[i].correct == myAnswers[i]) {

                questionResult = '<span class="glyphicon glyphicon-ok-circle" aria-hidden="true"></span>';

                correct++;

            } else {

                questionResult = '<span class="glyphicon glyphicon-remove-circle" aria-hidden="true"></span>';

            }

            output = output + '<p>Питање ' + (i + 1) + ' ' + questionResult + '</p> ';

        }

        var grade = scoreGrade(correct, lengthofobject)

        document.getElementById("ocena").innerHTML = grade;

        function scoreGrade(){

            let score = (correct / lengthofobject) * 100;

            let grade;

        

            if (score>=85)  {grade ='5'}

            else if (score>=70) {grade ='4'}

            else if (score>=55) {grade ='3'}

            else if (score>=40) {grade ='2'}

            else {grade ='1'}

            return grade;

        }

        clearInterval(endTime);

        output = output + '<p>Имате ' + correct + ' од ' + lengthofobject + ' тачних одговора.</p></div> ';

        document.getElementById("quizContent").innerHTML = output;

    } else {

        //console.log('not answered');

    }

There are a lot of coding errors in the above code. There are unmatched parenthesis, there are incomplete braces, and what is lengthofobject?

lengthofobject is number of questions.

JavaScript doesn’t know what lengthofobject is supposed to be, according to the code that you’ve posted.

Sorry, I didn’t paste it here. I have it written in the code.
console.log(data.quizcontent);
var lengthofobject = Object(data.quizcontent).length;

Your code has syntax errors on lines 3,7, and16 (or 18, depending).

Code works perfectly without the function scoreGrade.

Hi @emilijastt, as already pointed out there are several syntax errors; you can see them in the console of your browser dev tools or using a linter, as for instance here:

After fixing those there are also structural problems with your code; from a quick look, why is scoreGrade() declared inside an if block, and called even before that?

1 Like

This might be an appropriate time to try and reverse-engineer what the code is supposed to do.

Fixing browser errors

Here’s a list of fixes that I’ve made to the code:

  • The last else clause need a closing brace to close the endQuiz function.
  • I’ve cleaned up the formatting of the code by passing it through beautifier.io
  • Uncaught SyntaxError: Unexpected token ‘{’ is fixed by completing the closing parenthesis around the condition.
  • Uncaught SyntaxError: Unexpected token ‘)’ is fixed by removing an extra closing parenthesis from the for loop.

Running the code

The code now loads with no browser errors, but does it do anything useful? I’m going to need a data.quizcontent object. I don’t know what it’s supposed to contain, but I’ll try and make something up based on what the code does.

Fixing scoreGrade

Running endQuiz() I’m told that scoreGrade() is not a function. That needs to be lifted up out of the if statement. I’ll out it before the endQuiz() function.

What is correct?

The next thing I’m told is that correct is not defined. The scoreGrade() function is called immediately at the start of the endQuiz function, as well as further down in the function. Removing that first call fixes that issue.

function endQuiz() {
    // var grade = scoreGrade();
    ...
}

Supplying the correct answers

With data.quizcontent[i].correct a correct property needs to be added to my faked quizcontent.

var data = {
    quizcontent: [
        {question: "A", correct: "Apple"},
        {question: "B", correct: "Banana"},
        {question: "C", correct: "Cherry"},
        {question: "D", correct: "Date"}
    ]
};
var myAnswers = ["Apple", "Banana", "Cake", "Date"];

Yes - the cake is a lie :stuck_out_tongue:

Correct variable not found

The next error is: Uncaught ReferenceError: correct is not defined which comes from the correct++ line. A variable called correct doesn’t yet exist, so I’m going to define one at the start of the endQuiz() function.

function endQuiz() {
    var correct = 0;

Function can’t access correct variable

The next error is Uncaught ReferenceError: correct is not defined which comes from the scoreGrade function.

function scoreGrade() {
    let score = (correct / lengthofobject) * 100;

Arguments are passed to the scoreGrade function with var grade = scoreGrade(correct, lengthofobject) but the scoreGrade function doesn’t have parameters to receive those arguments. So let’s add them.

// function scoreGrade() {
function scoreGrade(correct, lengthofobject) {

Adding referenced HTML elements

The next error is Uncaught TypeError: Cannot set property 'innerHTML' of null because of this line:

document.getElementById("ocena").innerHTML = grade;

My test page doesn’t have any elements yet, and google translate shows that ocena means grading in Slovenian. Things seem to be correct there so I’ll add a fake grading section to my test page.

<div id="ocena"></div>

Adding a fake timer

The next error is Uncaught ReferenceError: endTime is not defined which comes from a quiz timer. No test page or template code has been provided, so I’ll mock up a quick test endTime timer.

var endTime = setInterval(function timer() {}, 1000);

Adding more fake HTML elements

The next error is: Uncaught TypeError: Cannot set property 'innerHTML' of null which comes from the following code:

document.getElementById("quizContent").innerHTML = output;

So I’ll create another test HTML element for that.

<div id="quizContent"></div>

It’s working!

And the code is finally confirmed to be correctly working. :slight_smile:

image

Here is the updated code:

function scoreGrade(correct, lengthofobject) {
    const score = (correct / lengthofobject) * 100;
    let grade;

    if (score >= 85) {
        grade = "5";
    } else if (score >= 70) {
        grade = "4";
    } else if (score >= 55) {
        grade = "3";
    } else if (score >= 40) {
        grade = "2";
    } else {
        grade = "1";
    }
    return grade;
}
function endQuiz() {
    let correct = 0;
    if (myAnswers[(lengthofobject - 1)]) {
        let output = "<div class='output'>Резултат<br>";
        let questionResult = "NA";
        myAnswers.forEach(function (answer, i) {
            if (data.quizcontent[i].correct === answer) {
                questionResult = "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>";
                correct += 1;
            } else {
                questionResult = "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>";
            }
            output = output + "<p>Питање " + (i + 1) + " " + questionResult + "</p> ";
        });
        const grade = scoreGrade(correct, lengthofobject);
        document.getElementById("ocena").innerHTML = grade;

        clearInterval(endTime);
        output = output + "<p>Имате " + correct + " од " + lengthofobject + " тачних одговора.</p></div> ";
        document.getElementById("quizContent").innerHTML = output;
    }
}
1 Like

Cleaning up the working code

Now that the code is working, this is a good time to work through it again and make improvements to it.

The first thing that stands out is how lengthofobject is being used in several places. That’s a really bad variable name, and can be more usefully renamed to totalQuestions instead.

// const lengthofobject = data.quizcontent.length;
const totalQuestions = data.quizcontent.length;

// function scoreGrade(correct, lengthofobject) {
function scoreGrade(correct, totalQuestions) {
    // const score = (correct / lengthofobject) * 100;
    const score = (correct / totalQuestions) * 100;
    ...
}
...
    // if (myAnswers[(lengthofobject - 1)]) {
    if (myAnswers[(totalQuestions - 1)]) {
...
        // const grade = scoreGrade(correct, lengthofobject);
        const grade = scoreGrade(correct, totalQuestions);
...
        // output = output + "<p>Имате " + correct + " од " + lengthofobject + " тачних одговора.</p></div> ";
        output = output + "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p></div> ";

Working out the grades

Currently a series of if/else statements are used to give the grades.

    let grade;
    if (score >= 85) {
        grade = "5";
    } else if (score >= 70) {
        grade = "4";
    } else if (score >= 55) {
        grade = "3";
    } else if (score >= 40) {
        grade = "2";
    } else {
        grade = "1";
    }
    return grade;

Each score has a difference of 15, which means that you can easily get from 85 to 5 by subtracting 10 and dividing by 15. That works for all of the grades. 40-10 is 30, and divided by 15 gives 2.

The only trouble comes with 100%, which would give a grade of 5, and with less than 15% which gives a grade of zero.

How this is calculated is not my main focus here. That the let keyword is needed is the problem to be resolved. It’s always better to use const over let.

So instead of using a formula of (score-10)/15 I will instead remove the need for the let variable, by returning the grade value instead.

function scoreGrade(correct, totalQuestions) {
    const score = (correct / totalQuestions) * 100;
    // let grade = parseInt((score - 10) / 15);
    if (score >= 85) {
        // grade = "5";
        return "5";
    } else if (score >= 70) {
        // grade = "4";
        return "4";
    } else if (score >= 55) {
        // grade = "3";
        return "3";
    } else if (score >= 40) {
        // grade = "2";
        return "2";
    // } else {
        // grade = "1";
    }
    return "1";
    // return grade;
}

With the old lines removed that gives us the following code:

function scoreGrade(correct, totalQuestions) {
    const score = (correct / totalQuestions) * 100;
    if (score >= 85) {
        return "5";
    } else if (score >= 70) {
        return "4";
    } else if (score >= 55) {
        return "3";
    } else if (score >= 40) {
        return "2";
    }
    return "1";
}

Fixing the endQuiz condition

Currently the endQuiz function checks if there is a myAnswers array item at the same place a the last array index of the totalQuestions array.

    if (myAnswers[(totalQuestions - 1)]) {

That’s quite a complicated way of checking if they’re both the same length. Here’s a much easier way to do that:

    if (myAnswers.length === totalQuestions) {

Removing more let keywords

Variables that need to change tend to be few and far between. We also tend to end up with better code structure when variables don’t need to change too.

My next focus is on the correct variable. The condition (data.quizcontent[i].correct === answer) can be moved out to a separate function, so that it can be used from multiple places.

function isCorrect(answer, i) {
    return data.quizcontent[i].correct === answer;
}
...
            // if (data.quizcontent[i].correct === answer) {
            if (isCorrect(answer, i)) {

And we can now update the correct variable:

    // let correct = 0;
    const correct = myAnswers.filter(isCorrect).length;
    ...
                // correct += 1;

That also helps to remove the correct increment from the forEach function, which will get a separate improvement made to it.

Replacing forEach with map

Instead of using forEach to update information outside of it, it’s more appropriate to return information instead. We can use map to return an array of values.

        // let results = [];
        // myAnswers.forEach(function (answer, i) {
        const results = myAnswers.map(function (answer, i) {
            if (isCorrect(answer, i)) {
                questionResult = "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>";
            } else {
                questionResult = "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>";
            }
            // results.push("<p>Питање " + (i + 1) + " " + questionResult + "</p> ");
            return "<p>Питање " + (i + 1) + " " + questionResult + "</p> ";
        });
        output += results.join(""); 

The result variable can be moved in to the map function:

        // let questionResult = "NA";
        const results = myAnswers.map(function (answer, i) {
            // if (isCorrect(answer, i)) {
                // questionResult = "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>";
            // } else {
                // questionResult = "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>";
            // }
            const result = (
                isCorrect(answer, i)
                ? "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>"
                : "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>"
            );
            // return "<p>Питање " + (i + 1) + " " + questionResult + "</p> ";
            return "<p>Питање " + (i + 1) + " " + result + "</p> ";
        });
        output += results.join(""); 

And, the output variable can be declared after the myAnswers map.

        // let output = "<div class='output'>Резултат<br>";
        const results = myAnswers.map(function (answer, i) {
            ...
        });
        let output = "<div class='output'>Резултат<br>";
        output += results.join(""); 

And the output variable can now be changed into a const variable instead:

        // let output = "<div class='output'>Резултат<br>";
        // output += results.join(""); 
        ...
        // output = output + "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p></div> ";
        const output = "<div class='output'>Резултат<br>" + results.join("") +
            "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p></div> ";

Which for clarity, I’ll break apart into its separate parts:

        const output = "<div class='output'>" +
        	"Резултат<br>" + results.join("") +
            "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p>" +
            "</div> ";

Is the output variable even needed, now that it’s right beside where it’s used? Let’s remove that output variable and combine the code with the innerHTML statement.

        // const output = "<div class='output'>" +
        // document.getElementById("quizContent").innerHTML = output;
        document.getElementById("quizContent").innerHTML = "<div class='output'>" +
            "Резултат<br>" + results.join("") +
            "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p>" +
            "</div> ";

So the information is prepared beforehand, and then all brought together at the innerHTML statement. That has a better flow to it.

Triangle indenting

The endQuiz function now shows quite a lot of indenting, to the point where the left margin has a distinctive triangle shape to it.

function endQuiz() {
    if (myAnswers.length === totalQuestions) {
        const correct = myAnswers.filter(isCorrect).length;
        const results = myAnswers.map(function(answer, i) {
            const result = (
                isCorrect(answer, i) ?
                "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>" :
                "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>"
            );
            return "<p>Питање " + (i + 1) + " " + result + "</p> ";
        });
        const grade = scoreGrade(correct, totalQuestions);
        document.getElementById("ocena").innerHTML = grade;

        clearInterval(endTime);
        const output = "<div class='output'>" +
            "Резултат<br>" + results.join("") +
            "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p>" +
            "</div> ";
        document.getElementById("quizContent").innerHTML = output;
    }
}

As it can be tricky in the depths of that nesting to understand all of the different conditions that apply, it’s better to remove that indenting if possible.

First, the if statement at the top is a guard clause, preventing the rest of the function from being executed if the condition isn’t right. We can return immediately from there and remove one level of nesting.

function endQuiz() {
    if (myAnswers.length !== totalQuestions) {
    	return;
    }
    const correct = myAnswers.filter(isCorrect).length;
    ...
}

Next, because the map function doesn’t refer to anything outside of the function that hasn’t been directly given to it, it can be named and moved out of the endQuiz function.

function checkAnswer(answer, i) {
    const result = (
        isCorrect(answer, i) ?
        "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>" :
        "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>"
    );
    return "<p>Питање " + (i + 1) + " " + result + "</p> ";
}
function endQuiz() {
    ...
    const results = myAnswers.map(checkAnswer);
    ...
}

Rearranging the endQuiz function

After making those above changes, it’s now clear that the endQuiz function has several different things mixed in with each other.
Here, I’ve reorganised the endQuiz() function code.so that similar things are all grouped together.

function endQuiz() {
    if (myAnswers.length !== totalQuestions) {
    	return;
    }
    clearInterval(endTime);

    const correct = myAnswers.filter(isCorrect).length;
    const grade = scoreGrade(correct, totalQuestions);
    document.getElementById("ocena").innerHTML = grade;

    const results = myAnswers.map(checkAnswer);
    const output = "<div class='output'>" +
          "Резултат<br>" + results.join("") +
          "<p>Имате " + correct + " од " + totalQuestions + " тачних одговора.</p>" +
          "</div> ";
    document.getElementById("quizContent").innerHTML = output;
}

There are two clearly different parts to that function, that can be easily extracted out to separate functions. One is showGrade(), and the other is showResults()

Creating showGrade() function

The showGrade function only needs to receive the myAnswers array.

function showGrade(myAnswers) {
    const correct = myAnswers.filter(isCorrect).length;
    const grade = scoreGrade(correct, totalQuestions);
    document.getElementById("ocena").innerHTML = grade;
}

totalQuestions can be removed from there too, as the guard-clause in the endQuiz() function has already ensured that myAnswers is the same length as the total questions.

We can even move counting the number of answers out to a separate function, as it’s preferable to count them over again, instead of passing them as a separate function parameter.

function countCorrect(answers) {
    return myAnswers.filter(isCorrect).length;
}
function showGrade(myAnswers) {
    const correct = countCorrect(myAnswers);
    const grade = scoreGrade(correct, myAnswers.length);
    document.getElementById("ocena").innerHTML = grade;
}

Creating showResults() function

The showResults() function is pretty simple now too, only needing to be given the myAnswers function too.

function showResults(myAnswers) {
    const correct = countCorrect(myAnswers);
    const results = myAnswers.map(checkAnswer);
    const output = "<div class='output'>" +
          "Резултат<br>" + results.join("") +
          "<p>Имате " + correct + " од " + showAnswers.length + " тачних одговора.</p>" +
          "</div> ";
    document.getElementById("quizContent").innerHTML = output;
}

Removing totalQuestions

We can even remove that totalQuestions variable now, as it’s better to clearly indicate that it’s the length of quizcontent that we’re referring to.

// const totalQuestions = data.quizcontent.length;
...
// function scoreGrade(correct, totalQuestions) {
function scoreGrade(correct) {
    const score = (correct / data.quizcontent.length) * 100;
    ...
}
...
function showGrade(myAnswers) {
    const correct = countCorrect(myAnswers);
    // const grade = scoreGrade(correct, totalQuestions);
    const grade = scoreGrade(correct, data.quizcontent.length);
    document.getElementById("ocena").innerHTML = grade;
}
...
function endQuiz() {
    // if (myAnswers.length !== totalQuestions) {
    if (myAnswers.length !== data.quizcontent.length) {
    	return;
    }
    ...
}

The final endQuiz function

There’s not much left in the endQuiz function, and because of that it now clearly expresses exactly what needs to be done.

function endQuiz() {
    if (myAnswers.length !== data.quizcontent.length) {
    	return;
    }
    clearInterval(endTime);
	showGrade(myAnswers);
    showResults(myAnswers)
}

Here’s the final code after those improvements:

function isCorrect(answer, i) {
    return data.quizcontent[i].correct === answer;
}
function checkAnswer(answer, i) {
    const result = (
        isCorrect(answer, i) ?
        "<span class='glyphicon glyphicon-ok-circle' aria-hidden='true'></span>" :
        "<span class='glyphicon glyphicon-remove-circle' aria-hidden='true'></span>"
    );
    return "<p>Питање " + (i + 1) + " " + result + "</p> ";
}
function countCorrect(answers) {
    return myAnswers.filter(isCorrect).length;
}
function scoreGrade(myAnswers) {
    const correct = countCorrect(myAnswers);
    const score = (correct / data.quizcontent.length) * 100;
    if (score >= 85) {
        return "5";
    } else if (score >= 70) {
        return "4";
    } else if (score >= 55) {
        return "3";
    } else if (score >= 40) {
        return "2";
    }
    return "1";
}
function showGrade(myAnswers) {
    const grade = scoreGrade(myAnswers);
    document.getElementById("ocena").innerHTML = grade;
}
function showResults(myAnswers) {
    const correct = countCorrect(myAnswers);
    const results = myAnswers.map(checkAnswer);
    const output = "<div class='output'>" +
          "Резултат<br>" + results.join("") +
          "<p>Имате " + correct + " од " + myAnswers.length + " тачних одговора.</p>" +
          "</div> ";
    document.getElementById("quizContent").innerHTML = output;
}
function endQuiz() {
    if (myAnswers.length !== data.quizcontent.length) {
    	return;
    }
    clearInterval(endTime);
	showGrade(myAnswers);
    showResults(myAnswers)
}

There are still more improvements that could be made, but hopefully this has helped to demonstrate how the code can be easily refactored and molded like clay, to help it explains things more clearly.

1 Like

One possibility for scoreGrade, using the index of an array to workout the grade.

function scoreGrade( correct ) {

    const score = ( correct / data.quizcontent.length ) * 100;

    // use index + 1 to workout grade e.g. 85 is [4] + 1 = 5
    const grades = [ 0, 40, 55, 70, 85 ];

    // loop backwards starting with top grade 85
    for ( let i = grades.length; i > 0; i-- ) {

        if ( score > grades[i-1] ) return i;
    }
}

Just a thought.

2 Likes

Thanks @rpg_digital - that’s one of the directions I was hoping to explore, but got distracted by everything else that needed to be done.

I’ve tweaked your example so that a score of 84 gives 4 and 85 gives 5.

        // if ( score > grades[i-1] ) return i;
        if ( score >= grades[i-1] ) return i;

I’ve used some simple test code with 100 questions, to let me easily check that our functions work consistently across score boundaries.

const data = {
    quizcontent: Array(100).fill("")
};
[84, 85, 86].forEach(function (score) {
    console.log({score, initial: initial.scoreGrade(score), for: for.scoreGrade(score), filter: filter.scoreGrade(score)});
});

Which gives the following results:

{score: 84, initial: "4", for: 4, filter: 4}
{score: 85, initial: "5", for: 5, filter: 5}
{score: 86, initial: "5", for: 5, filter: 5}

Building on your example, I was wanting to use the Array filter method:

function scoreGrade(myAnswers) {
    const correct = countCorrect(myAnswers);
    const score = (correct / data.quizcontent.length) * 100;
    const grades = [ 0, 40, 55, 70, 85 ];
    return grades.filter(function (grade) {
        return score >= grade;
    }).length;
}
2 Likes

Thank you so so much for your time, I understand what I need to do. :blush:

Thank you :grin:

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.