Warm-up · Activity 1 of 7
Warm-up from lesson B5.4: what does this print?
const items = JSON.parse('[{"qty": 2}, {"qty": 3}]');
console.log(items.length, items[1].qty);// B5.5 · ~38 min · Beginner
After this build you load records from JSON, total and filter them, work out a discount and VAT to the cent, and save the result as JSON again.
Lesson 5 of 5 in B5 Arrays, objects and JSON
You will be able to
Warm-up · Activity 1 of 7
const items = JSON.parse('[{"qty": 2}, {"qty": 3}]');
console.log(items.length, items[1].qty);Predict · Activity 2 of 7
console.log(0.1 + 0.2 === 0.3, 10 + 20 === 30);Practice · Activity 3 of 7
const subtotal = lines.reduce((sum, line) => sum + line.totalCents, ____);Practice · Activity 4 of 7
Practice · Activity 5 of 7
const items = [{ cat: "food", cents: 300 }, { cat: "home", cents: 500 }, { cat: "food", cents: 250 }];
const food = items.filter((i) => i.cat === "food").reduce((s, i) => s + i.cents, 0);
console.log(food);Brain teaser · Activity 6 of 7
const prices = [0.1, 0.2, 0.3];
const total = prices.reduce((s, p) => s + p, 0);
console.log(total === 0.6, total.toFixed(2));Apply · Activity 7 of 7
Check your work against this list
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 basket arrives as JSON text with four items, each priced in cents. map makes the line totals, reduce the subtotal, filter the food items with a 15% offer, and Math.round keeps the offer and the 19% VAT in whole cents. euro formats cents only for printing. The line with 0.1 + 0.2 shows why: euros as decimals drift, cents do not. The receipt is saved back as JSON text.
main.js
// Shopping-basket totals, worked in whole cents.
const text = `[
{"name": "Coffee", "category": "food", "priceCents": 899, "qty": 2},
{"name": "Milk", "category": "food", "priceCents": 119, "qty": 3},
{"name": "Soap", "category": "home", "priceCents": 249, "qty": 1},
{"name": "Sponges", "category": "home", "priceCents": 175, "qty": 4}
]`;
const euro = (cents) => `${(cents / 100).toFixed(2)} EUR`;
const basket = JSON.parse(text);
console.log(Array.isArray(basket), basket.length, "items");
const lines = basket.map(({ name, priceCents, qty }) => ({ name, qty, totalCents: priceCents * qty }));
for (const { name, qty, totalCents } of lines) {
console.log(`${qty} x ${name}: ${euro(totalCents)}`);
}
const subtotal = lines.reduce((sum, line) => sum + line.totalCents, 0);
console.log(`Subtotal: ${euro(subtotal)}`);
const food = basket.filter((item) => item.category === "food");
const foodCents = food.reduce((sum, item) => sum + item.priceCents * item.qty, 0);
const foodOffer = Math.round(foodCents * 85 / 100);
console.log(`Food: ${euro(foodCents)}, with 15% off: ${euro(foodOffer)}`);
const vat = Math.round(subtotal * 19 / 100);
console.log(`VAT 19%: ${euro(vat)}, total: ${euro(subtotal + vat)}`);
console.log(0.1 + 0.2, 10 + 20);
const receipt = { subtotalCents: subtotal, vatCents: vat, totalCents: subtotal + vat };
console.log(JSON.stringify(receipt, null, 2));
Run it with
node main.jsOutput
true 4 items
2 x Coffee: 17.98 EUR
3 x Milk: 3.57 EUR
1 x Soap: 2.49 EUR
4 x Sponges: 7.00 EUR
Subtotal: 31.04 EUR
Food: 21.55 EUR, with 15% off: 18.32 EUR
VAT 19%: 5.90 EUR, total: 36.94 EUR
0.30000000000000004 30
{
"subtotalCents": 3104,
"vatCents": 590,
"totalCents": 3694
}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.
Exercise 1 of 3
loadBasket(text) should parse JSON text and return the array of items, or [] when the text holds no array. lineTotal(item) should return the price in cents times the quantity. subtotal(items) should add every line total, and give 0 for an empty basket. The starter trusts the text, adds the quantity to the price, and calls reduce without a start value. Fix all three.
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.
Run the tests: lineTotal gives 901 for 2 x 899, and subtotal([]) stops with a TypeError.
Check the parsed value with Array.isArray(data) ? data : [], and multiply in lineTotal.
Give reduce its start value: .reduce((sum, cents) => sum + cents, 0).
One way to solve it. Yours can look different and still pass the checks.
// The items in the JSON text, or [] when it holds no array.
export function loadBasket(text) {
const data = JSON.parse(text);
return Array.isArray(data) ? data : [];
}
// One line: the price in cents times the quantity.
export function lineTotal(item) {
return item.priceCents * item.qty;
}
// Every line total added up, in cents; 0 for an empty basket.
export function subtotal(items) {
return items.map(lineTotal).reduce((sum, cents) => sum + cents, 0);
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// The items in the JSON text, or [] when it holds no array.
export function loadBasket(text) {
return JSON.parse(text);
}
// One line: the price in cents times the quantity.
export function lineTotal(item) {
return item.priceCents + item.qty;
}
// Every line total added up, in cents; 0 for an empty basket.
export function subtotal(items) {
return items.map(lineTotal).reduce((sum, cents) => sum + cents);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadBasket, lineTotal, subtotal} from './main.js';
const TEXT = '[{"name": "Coffee", "category": "food", "priceCents": 899, "qty": 2}, {"name": "Milk", "category": "food", "priceCents": 119, "qty": 3}, {"name": "Soap", "category": "home", "priceCents": 249, "qty": 1}, {"name": "Sponges", "category": "home", "priceCents": 175, "qty": 4}]';
test('loadBasket returns the four items', () => {
const got = loadBasket(TEXT);
assert.equal(Array.isArray(got) && got.length, 4, 'loadBasket should return an array of 4 items, got ' + JSON.stringify(got));
});
test('loadBasket returns [] when the text holds an object', () => {
const got = loadBasket('{"items": []}');
assert.deepEqual(got, [], 'loadBasket of an object should be [], got ' + JSON.stringify(got));
});
test('lineTotal multiplies the price by the quantity', () => {
const got = lineTotal({ name: 'Coffee', priceCents: 899, qty: 2 });
assert.equal(got, 1798, 'lineTotal of 2 x 899 cents should be 1798, got ' + got);
});
test('subtotal adds every line', () => {
const got = subtotal(JSON.parse(TEXT));
assert.equal(got, 3104, 'subtotal of the basket should be 3104 cents, got ' + got);
});
test('subtotal of an empty basket is 0', () => {
const got = subtotal([]);
assert.equal(got, 0, 'subtotal([]) should be 0, got ' + 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.jsExercise 2 of 3
categoryTotal(items, category) should add up the lines of one category only, in cents. discounted(cents, percent) should return the amount after a discount, and vatOf(netCents, rate) the VAT at rate percent; both must be whole cents, rounded to the nearest one. The starter totals every category, and its discount and VAT have fractions of a cent. Fix them with filter and Math.round.
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.
Run the tests: the food total includes the home lines, and vatOf(1049, 19) gives 199.31.
Put .filter((item) => item.category === category) before the reduce.
Wrap each calculation in Math.round(…), so the result is a whole number of cents.
One way to solve it. Yours can look different and still pass the checks.
// The total in cents of the lines in one category.
export function categoryTotal(items, category) {
return items
.filter((item) => item.category === category)
.reduce((sum, item) => sum + item.priceCents * item.qty, 0);
}
// cents after a discount of percent, in whole cents.
export function discounted(cents, percent) {
return Math.round(cents * (100 - percent) / 100);
}
// The VAT on netCents at rate percent, in whole cents.
export function vatOf(netCents, rate) {
return Math.round(netCents * rate / 100);
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// The total in cents of the lines in one category.
export function categoryTotal(items, category) {
return items.reduce((sum, item) => sum + item.priceCents * item.qty, 0);
}
// cents after a discount of percent, in whole cents.
export function discounted(cents, percent) {
return cents * (100 - percent) / 100;
}
// The VAT on netCents at rate percent, in whole cents.
export function vatOf(netCents, rate) {
return netCents * rate / 100;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {categoryTotal, discounted, vatOf} from './main.js';
const TEXT = '[{"name": "Coffee", "category": "food", "priceCents": 899, "qty": 2}, {"name": "Milk", "category": "food", "priceCents": 119, "qty": 3}, {"name": "Soap", "category": "home", "priceCents": 249, "qty": 1}, {"name": "Sponges", "category": "home", "priceCents": 175, "qty": 4}]';
test('categoryTotal adds the food lines only', () => {
const got = categoryTotal(JSON.parse(TEXT), 'food');
assert.equal(got, 2155, 'the food total should be 2155 cents, got ' + got);
});
test('categoryTotal adds the home lines only', () => {
const got = categoryTotal(JSON.parse(TEXT), 'home');
assert.equal(got, 949, 'the home total should be 949 cents, got ' + got);
});
test('discounted rounds to a whole cent', () => {
const got = discounted(1999, 15);
assert.equal(got, 1699, 'discounted(1999, 15) should be 1699, got ' + got);
});
test('vatOf rounds to a whole cent', () => {
assert.equal(vatOf(1049, 19), 199, 'vatOf(1049, 19) should be 199, got ' + vatOf(1049, 19));
assert.equal(vatOf(3104, 19), 590, 'vatOf(3104, 19) should be 590, got ' + vatOf(3104, 19));
});
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
formatEuro(cents) should return the amount in euros with two decimals and the currency, such as 12.50 EUR. toReceipt(items) should return JSON text, indented by two spaces, of an object with lines, each { name, totalCents }, and the totalCents of all lines. The starter prints 12.5 EUR and saves the raw items. Fix it with toFixed, a template literal, map, reduce and JSON.stringify.
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.
Run the tests: formatEuro(1250) gives 12.5 EUR, because a number does not keep its trailing zero.
Format with a template literal: `${(cents / 100).toFixed(2)} EUR`.
Build lines with map, add their totalCents with reduce from 0, then JSON.stringify({ lines, totalCents }, null, 2).
One way to solve it. Yours can look different and still pass the checks.
// "12.50 EUR" for 1250 cents.
export function formatEuro(cents) {
return `${(cents / 100).toFixed(2)} EUR`;
}
// JSON text, indented by two spaces, of { lines: [{ name, totalCents }], totalCents }.
export function toReceipt(items) {
const lines = items.map(({ name, priceCents, qty }) => ({ name, totalCents: priceCents * qty }));
const totalCents = lines.reduce((sum, line) => sum + line.totalCents, 0);
return JSON.stringify({ lines, totalCents }, null, 2);
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// "12.50 EUR" for 1250 cents.
export function formatEuro(cents) {
return cents / 100 + " EUR";
}
// JSON text, indented by two spaces, of { lines: [{ name, totalCents }], totalCents }.
export function toReceipt(items) {
return JSON.stringify(items);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatEuro, toReceipt} from './main.js';
test('formatEuro shows two decimals and the currency', () => {
assert.equal(formatEuro(1250), '12.50 EUR', 'formatEuro(1250) should be "12.50 EUR", got ' + JSON.stringify(formatEuro(1250)));
assert.equal(formatEuro(3104), '31.04 EUR', 'formatEuro(3104) should be "31.04 EUR", got ' + JSON.stringify(formatEuro(3104)));
});
test('formatEuro shows 5 cents as 0.05 EUR', () => {
assert.equal(formatEuro(5), '0.05 EUR', 'formatEuro(5) should be "0.05 EUR", got ' + JSON.stringify(formatEuro(5)));
});
test('toReceipt holds the line totals and the total', () => {
const items = [{ name: 'Tea', category: 'food', priceCents: 349, qty: 2 }, { name: 'Soap', category: 'home', priceCents: 249, qty: 1 }];
const got = JSON.parse(toReceipt(items));
const expected = { lines: [{ name: 'Tea', totalCents: 698 }, { name: 'Soap', totalCents: 249 }], totalCents: 947 };
assert.deepEqual(got, expected, 'toReceipt should hold ' + JSON.stringify(expected) + ', got ' + JSON.stringify(got));
});
test('toReceipt is indented by two spaces', () => {
const got = toReceipt([{ name: 'Tea', category: 'food', priceCents: 349, qty: 2 }]);
assert.match(got, /^[{]\n "lines": /, 'toReceipt should start with {, a new line and two spaces before "lines", got ' + 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.jsconst basket = [];
const subtotal = basket.reduce((sum, cents) => sum + cents);
console.log(subtotal);
What Node.js prints
TypeError: Reduce of empty array with no initial valueWhy, and the fix
Without a start value, reduce begins with the first item, and an empty array has none. A basket can be empty, so always pass the start value: basket.reduce((sum, cents) => sum + cents, 0) gives 0. The start value also keeps the sum a number when the items are objects.
const text = '[{"name": "Tea", "priceCents": 349, "qty": 2}]';
const lines = text.map((item) => item.priceCents * item.qty);
console.log(lines);
What Node.js prints
TypeError: text.map is not a functionWhy, and the fix
text is a string that holds JSON, and strings have no map method. Parse it first: const basket = JSON.parse(text);, check it with Array.isArray(basket), then call basket.map(…).
const item = JSON.parse('{"name": "Tea", "price": "3.49"}');
console.log(item.price.toFixed(2));
What Node.js prints
TypeError: item.price.toFixed is not a functionWhy, and the fix
In the JSON, "3.49" has quotes, so JSON.parse gives a string, and toFixed exists only on numbers. Store amounts as JSON numbers, and in whole cents: {"priceCents": 349}. For data you cannot change, convert it first with Number(item.price), and check the result is not NaN.
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.