Warm-up · Activity 1 of 7
// B3.5 · ~38 min · Beginner
Build: a grade report
After this build you turn a list of test scores into a report: a grade per score, the average, the best and the worst, with invalid scores skipped.
Lesson 5 of 5 in B3 Control flow and loops
You will be able to
- Total a list of scores in one loop and work out the average, best and worst
- Map a score to a grade with an else if chain in the right order, or a fixed value with switch
- Skip invalid scores with continue and format the report with toFixed and template literals
Predict · Activity 2 of 7
Predict before you read on: the score is 95. What does this print?
const score = 95; let grade; if (score >= 60) { grade = "D"; } else if (score >= 90) { grade = "A"; } else { grade = "F"; } console.log(grade);Practice · Activity 3 of 7
Fill in the operator so that best ends as the highest score, 95.
const scores = [72, 95, 58]; let best; for (const score of scores) { if (best === undefined || score ____ best) { best = score; } } console.log("best:", best);if (best === undefined || score best) {Practice · Activity 4 of 7
Six scores add up to 377. What does this print?
const total = 377; const count = 6; console.log(`Average: ${(total / count).toFixed(1)}`);Practice · Activity 5 of 7
This chain gives a grade for a score. Match each score to its grade.
let grade; if (score >= 90) { grade = "A"; } else if (score >= 80) { grade = "B"; } else if (score >= 70) { grade = "C"; } else { grade = "F"; }Brain teaser · Activity 6 of 7
Brain teaser. How many of these scores get through the check? What does this print?
const scores = [80, NaN, "90"]; let used = 0; for (const score of scores) { if (typeof score !== "number" || score < 0 || score > 100) { continue; } used++; } console.log(used);Apply · Activity 7 of 7
Mini-task. Write the whole grade report from scratch: the scores in an array literal, with some invalid entries; one loop that skips and counts the invalid ones with continue, prints each valid score with its grade, and updates total, count, best and worst; then three lines with the counts, the average to one decimal and the best and worst scores. Then add a score and predict the new output.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
The grade report
Eight scores of one test, out of 100, three of them unusable: a student was absent, one score was typed as 101, and one as -4. The program goes through them once. It skips and reports the invalid ones, prints each valid score with its grade on this program’s own A to F scale, and keeps a running total, count, best and worst. The summary comes after the loop. Change a score, or add one, and predict the output before you run it again.
main.js
// A grade report for one test. Scores are out of 100.
const scores = [72, 95, "absent", 58, 101, 88, -4, 64];
let total = 0;
let count = 0;
let skipped = 0;
let best;
let worst;
for (const score of scores) {
if (typeof score !== "number" || score < 0 || score > 100) {
console.log(`Skipped: ${score}`);
skipped++;
continue;
}
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
console.log(`${score} -> ${grade}`);
total += score;
count++;
if (best === undefined || score > best) {
best = score;
}
if (worst === undefined || score < worst) {
worst = score;
}
}
console.log(`Scores: ${count}, skipped: ${skipped}`);
console.log(`Average: ${(total / count).toFixed(1)}`);
console.log(`Best: ${best}, worst: ${worst}`);
Run it with
node main.jsOutput
72 -> C
95 -> A
Skipped: absent
58 -> F
Skipped: 101
88 -> B
Skipped: -4
64 -> D
Scores: 5, skipped: 3
Average: 75.4
Best: 95, worst: 58- Each invalid entry is reported and skipped by continue, so it never reaches the grade or the total.
- The chain tests 90 first, so 95 is an A and 64 falls through to the D branch.
- best and worst start undefined, so the first valid score, 72, sets both.
- The average is 377 / 5 = 75.4, worked out once, after the loop, and formatted with toFixed(1).
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Exercises
Exercise 1 of 3
Step 1: total, average, best and worst
The loop counts the scores but does nothing else yet. Inside the loop, add each score to total, and update best and worst, so that the program prints Scores: 5, then Average: 75.4, then Best: 95, worst: 58. Keep the three console.log lines as they are.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Hints
Hint 1
total += score; adds the score of this pass to the running total.
Hint 2
best starts undefined. if (best === undefined || score > best) { best = score; } stores the first score, then every higher one.
Hint 3
worst works the same way, with < instead of >.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
const scores = [72, 95, 58, 88, 64];
let total = 0;
let count = 0;
let best;
let worst;
for (const score of scores) {
count++;
total += score;
if (best === undefined || score > best) {
best = score;
}
if (worst === undefined || score < worst) {
worst = score;
}
}
console.log(`Scores: ${count}`);
console.log(`Average: ${(total / count).toFixed(1)}`);
console.log(`Best: ${best}, worst: ${worst}`);
Run it on your computer
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const scores = [72, 95, 58, 88, 64];
let total = 0;
let count = 0;
let best;
let worst;
for (const score of scores) {
count++;
// Add the score to total, and update best and worst.
}
console.log(`Scores: ${count}`);
console.log(`Average: ${(total / count).toFixed(1)}`);
console.log(`Best: ${best}, worst: ${worst}`);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const lines = async () => (await runMain()).trimEnd().split('\n');
test('counts five scores', async () => {
const got = await lines();
assert.equal(got[0], 'Scores: 5', `the first line is ${JSON.stringify(got[0])}, expected "Scores: 5"`);
});
test('works out the average', async () => {
const got = await lines();
assert.equal(got[1], 'Average: 75.4', `the second line is ${JSON.stringify(got[1])}, expected "Average: 75.4"`);
});
test('finds the best and the worst score', async () => {
const got = await lines();
assert.equal(got[2], 'Best: 95, worst: 58', `the third line is ${JSON.stringify(got[2])}, expected "Best: 95, worst: 58"`);
});
test('prints exactly three lines', async () => {
const got = await lines();
assert.equal(got.length, 3, `the program printed ${got.length} line(s), expected 3`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
Step 2: from score to grade
Each score should get its grade: A from 90, B from 80, C from 70, D from 60, and F below 60. The chain is in the wrong order, so almost everything gets a D. Reorder it so that the program prints 72 -> C, 95 -> A, 58 -> F, 88 -> B, 60 -> D and 90 -> A, one per line.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Hints
Hint 1
The chain runs the first branch whose condition is true. Which condition is true for 95 and comes first?
Hint 2
Test the highest bound first: score >= 90, then >= 80, >= 70 and >= 60.
Hint 3
Keep the final else for F: it runs only when every test above it is false.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
const scores = [72, 95, 58, 88, 60, 90];
for (const score of scores) {
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
console.log(`${score} -> ${grade}`);
}
Run it on your computer
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const scores = [72, 95, 58, 88, 60, 90];
for (const score of scores) {
let grade;
if (score >= 60) {
grade = "D";
} else if (score >= 70) {
grade = "C";
} else if (score >= 80) {
grade = "B";
} else if (score >= 90) {
grade = "A";
} else {
grade = "F";
}
console.log(`${score} -> ${grade}`);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const lines = async () => (await runMain()).trimEnd().split('\n');
test('gives each score its grade', async () => {
const got = await lines();
assert.deepEqual(got.slice(0, 4), ['72 -> C', '95 -> A', '58 -> F', '88 -> B'], `the first four lines are ${JSON.stringify(got.slice(0, 4))}`);
});
test('a score on a bound gets the higher grade', async () => {
const got = await lines();
assert.deepEqual(got.slice(4), ['60 -> D', '90 -> A'], `the last lines are ${JSON.stringify(got.slice(4))}, expected ["60 -> D","90 -> A"]`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
Step 3: skip the invalid scores
Now the list has invalid entries: "absent", 101 and -4, and the report breaks: the average is NaN and 101 is the best. At the top of the loop, skip every entry that is not a number from 0 to 100: print Skipped: absent (with the entry), add 1 to skipped and continue. The report should then end with Scores: 5, skipped: 3, Average: 75.4 and Best: 95, worst: 58.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Hints
Hint 1
An entry is invalid when typeof score !== "number" || score < 0 || score > 100.
Hint 2
Inside that if: console.log(`Skipped: ${score}`); then skipped++; then continue;.
Hint 3
The if goes first in the loop, so that nothing below it, grade or total, ever sees an invalid entry.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// A grade report for one test. Scores are out of 100.
const scores = [72, 95, "absent", 58, 101, 88, -4, 64];
let total = 0;
let count = 0;
let skipped = 0;
let best;
let worst;
for (const score of scores) {
if (typeof score !== "number" || score < 0 || score > 100) {
console.log(`Skipped: ${score}`);
skipped++;
continue;
}
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
console.log(`${score} -> ${grade}`);
total += score;
count++;
if (best === undefined || score > best) {
best = score;
}
if (worst === undefined || score < worst) {
worst = score;
}
}
console.log(`Scores: ${count}, skipped: ${skipped}`);
console.log(`Average: ${(total / count).toFixed(1)}`);
console.log(`Best: ${best}, worst: ${worst}`);
Run it on your computer
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const scores = [72, 95, "absent", 58, 101, 88, -4, 64];
let total = 0;
let count = 0;
let skipped = 0;
let best;
let worst;
for (const score of scores) {
// Skip and count the invalid scores here.
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
console.log(`${score} -> ${grade}`);
total += score;
count++;
if (best === undefined || score > best) {
best = score;
}
if (worst === undefined || score < worst) {
worst = score;
}
}
console.log(`Scores: ${count}, skipped: ${skipped}`);
console.log(`Average: ${(total / count).toFixed(1)}`);
console.log(`Best: ${best}, worst: ${worst}`);
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
const lines = async () => (await runMain()).trimEnd().split('\n');
test('reports each invalid score', async () => {
const got = (await lines()).filter((line) => line.startsWith('Skipped'));
assert.deepEqual(got, ['Skipped: absent', 'Skipped: 101', 'Skipped: -4'], `the Skipped lines are ${JSON.stringify(got)}`);
});
test('grades only the valid scores', async () => {
const got = (await lines()).filter((line) => line.includes(' -> '));
assert.deepEqual(got, ['72 -> C', '95 -> A', '58 -> F', '88 -> B', '64 -> D'], `the grade lines are ${JSON.stringify(got)}`);
});
test('ends with the summary', async () => {
const got = (await lines()).slice(-3);
assert.deepEqual(got, ['Scores: 5, skipped: 3', 'Average: 75.4', 'Best: 95, worst: 58'], `the last three lines are ${JSON.stringify(got)}`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsCommon mistakes
Formatting a best score that was never set
const scores = ["absent", "ill"];
let best;
for (const score of scores) {
if (typeof score !== "number") {
continue;
}
if (best === undefined || score > best) {
best = score;
}
}
console.log(`Best: ${best.toFixed(1)}`);
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'toFixed')Why, and the fix
Every entry was skipped, so best is still undefined, and undefined has no toFixed. The same list would make the average 0 / 0, which is NaN. Before the summary, check that something was counted: if (count === 0) { console.log("No valid scores"); } else { … the summary … }.
A running total declared with const
const scores = [72, 95, 58];
const total = 0;
for (const score of scores) {
total += score;
}
console.log(total);
What Node.js prints
TypeError: Assignment to constant variable.Why, and the fix
total += score gives total a new value in every pass, and a const cannot get a new value. Declare running values with let: let total = 0;. const is right for the list itself and for the score of each pass, which is a fresh variable every time.
The grade declared inside the branches
const score = 72;
if (score >= 70) {
let grade = "C";
} else {
let grade = "F";
}
console.log(score, grade);
What Node.js prints
ReferenceError: grade is not definedWhy, and the fix
A let exists only inside the braces where it is declared, so each grade is gone when its block ends, and the console.log line finds no grade. Declare it once before the chain, let grade;, and only assign it in the branches: grade = "C";.
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.