Skip to content
aviral gupta

// B5.5 · ~38 min · Beginner

Build: shopping-basket totals

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

End of the module

You will be able to

  • Load basket items from JSON and work out line totals with map and a subtotal with reduce
  • Filter by category and work out a discount or VAT in whole cents, rounded with Math.round
  • Format amounts with toFixed and template literals, and save the basket with JSON.stringify
  1. 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);
  2. Predict · Activity 2 of 7

    Predict before you read on: the same sum in euros and in cents. What does this print?

    console.log(0.1 + 0.2 === 0.3, 10 + 20 === 30);
  3. Practice · Activity 3 of 7

    Fill in the start value, so that the subtotal is a number of cents, and 0 for an empty basket.

    const subtotal = lines.reduce((sum, line) => sum + line.totalCents, ____);
    const subtotal = lines.reduce((sum, line) => sum + line.totalCents, );
  4. Practice · Activity 4 of 7

    Match each expression to its value.

  5. Practice · Activity 5 of 7

    What does this print?

    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);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Three prices in euros are added up. What does this print?

    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));
  7. Apply · Activity 7 of 7

    Mini-task, a first step towards Pocket budget. Write three expenses as JSON text, each with a description, a category and an amount in cents. Parse it, print the total in euros, and print the total and the number of expenses for one category. Add an expense with push, and print the descriptions as JSON.

    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 basket, start to finish

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.js

Output

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
}
  • The map callback unpacks each item and returns a new object with its line total.
  • toFixed(2) pads 700 cents to 7.00, as a string, only when printing.
  • 2155 * 85 / 100 is 1831.75 cents, and Math.round makes it 1832.
  • 3104 * 19 / 100 is 589.76, so the VAT is 590 whole cents.
  • The receipt holds whole cents, which JSON saves and loads unchanged.
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

Load the basket and total it

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.

Hints
  1. Hint 1

    Run the tests: lineTotal gives 901 for 2 x 899, and subtotal([]) stops with a TypeError.

  2. Hint 2

    Check the parsed value with Array.isArray(data) ? data : [], and multiply in lineTotal.

  3. Hint 3

    Give reduce its start value: .reduce((sum, cents) => sum + cents, 0).

Show a solution

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);
}
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

// 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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 3

One category, a discount and VAT, to the cent

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.

Hints
  1. Hint 1

    Run the tests: the food total includes the home lines, and vatOf(1049, 19) gives 199.31.

  2. Hint 2

    Put .filter((item) => item.category === category) before the reduce.

  3. Hint 3

    Wrap each calculation in Math.round(…), so the result is a whole number of cents.

Show a solution

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);
}
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

// 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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 3 of 3

Format the amounts and save the receipt

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.

Hints
  1. Hint 1

    Run the tests: formatEuro(1250) gives 12.5 EUR, because a number does not keep its trailing zero.

  2. Hint 2

    Format with a template literal: `${(cents / 100).toFixed(2)} EUR`.

  3. Hint 3

    Build lines with map, add their totalCents with reduce from 0, then JSON.stringify({ lines, totalCents }, null, 2).

Show a solution

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);
}
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

// "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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

reduce on an empty basket, with no start value

const basket = [];
const subtotal = basket.reduce((sum, cents) => sum + cents);
console.log(subtotal);

What Node.js prints

TypeError: Reduce of empty array with no initial value

Why, 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.

Mapping over JSON text that was never parsed

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 function

Why, 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(…).

A price stored as a string in JSON

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 function

Why, 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

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Records in, totals out

A basket is an array of objects, one per line: {"name": "Coffee", "category": "food", "priceCents": 899, "qty": 2}. It often arrives as JSON text, so JSON.parse comes first, and Array.isArray checks you got an array. map turns each item into its line total: basket.map((item) => item.priceCents * item.qty). reduce adds the totals into one number: lines.reduce((sum, cents) => sum + cents, 0). Always give reduce its start value, 0: without it, an empty basket stops with TypeError: Reduce of empty array with no initial value, and a basket of objects starts the sum with an object, which turns the sum into text.

Money in whole cents

Lesson B2.2 showed that 0.1 + 0.2 prints 0.30000000000000004: most decimals cannot be stored exactly, so sums of euros drift. Whole numbers are exact, so keep money in cents: 10 + 20 is exactly 30. Store 899, not 8.99, and multiply by whole quantities. A percentage gives a fraction of a cent, so round it once, straight away, with Math.round: the VAT at 19% on 1049 cents is Math.round(1049 * 19 / 100), 199. A discount works the same way: Math.round(cents * (100 - percent) / 100). filter picks the lines of one category before you total them: basket.filter((item) => item.category === "food").

Format at the end, save as JSON

Turn cents into euros only to show them: (cents / 100).toFixed(2) gives "17.98", padded to two decimals, so 5 cents is "0.05". Put it into a template literal with the currency: `${(cents / 100).toFixed(2)} EUR`. toFixed returns a string, so do every calculation before it, never after: "19.99" + 1 is "19.991". To save the basket or a receipt, JSON.stringify(receipt, null, 2) turns it into JSON text with two spaces of indent. Whole cents travel through JSON unchanged, and JSON.parse gives back numbers you can keep adding. This is the core of the Beginner project, Pocket budget.

Sources

Last reviewed October 3, 2026