Warm-up · Activity 1 of 7
Warm-up from lesson B5.3. What does this print?
const prices = { tea: 3, cake: 4 };
console.log(Object.entries(prices));// I5.1 · ~34 min · Intermediate
After this lesson you store and look up data by key with a Map, remove duplicates with a Set, and count or group a list in a few lines.
Lesson 1 of 5 in I5 Working with data
You will be able to
Warm-up · Activity 1 of 7
const prices = { tea: 3, cake: 4 };
console.log(Object.entries(prices));Predict · Activity 2 of 7
const seen = {};
seen[1] = "number one";
seen["1"] = "string one";
console.log(Object.keys(seen).length);
const map = new Map();
map.set(1, "number one");
map.set("1", "string one");
console.log(map.size);Practice · Activity 3 of 7
const unique = [...new ____(["de", "en", "de", "fr"])];Practice · Activity 4 of 7
Practice · Activity 5 of 7
const words = ["a", "b", "a", "c", "a"];
const counts = new Map();
for (const word of words) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
console.log([...counts]);Brain teaser · Activity 6 of 7
const settings = new Map();
settings["theme"] = "dark";
console.log(settings.size, settings.has("theme"));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
A list of website visits is turned into three answers. A Set gives the visitors, each once. A Map counts the views per page. A second Map groups the visitors by page: pushTo() uses ES2026 getOrInsert where the runtime has it and the classic has/set pattern elsewhere, so it prints the same in every runtime. An object key shows that a Map finds keys by identity, and Object.fromEntries prepares the counts for JSON.
main.js
// Visits to a small website: [visitor, page].
const visits = [
["ada", "/home"], ["bo", "/blog"], ["ada", "/blog"],
["cy", "/home"], ["bo", "/blog"], ["ada", "/home"],
];
// 1. A Set keeps each value once, in the order it first came.
const visitors = new Set(visits.map(([visitor]) => visitor));
console.log("visitors:", visitors.size, [...visitors].join(", "));
// 2. Counting: one Map entry per page.
const views = new Map();
for (const [, page] of visits) {
views.set(page, (views.get(page) ?? 0) + 1);
}
for (const [page, count] of views) console.log(page, count);
// 3. Grouping: page -> array of visitors.
function pushTo(map, key, value) {
if (typeof Map.prototype.getOrInsert === "function") {
map.getOrInsert(key, []).push(value); // ES2026 shortcut
} else {
if (!map.has(key)) map.set(key, []); // the classic way
map.get(key).push(value);
}
}
const byPage = new Map();
for (const [visitor, page] of visits) pushTo(byPage, page, visitor);
console.log("/blog:", byPage.get("/blog").join(" "));
// 4. Any value can be a key, even an object.
const ada = { name: "Ada" };
const lastSeen = new Map([[ada, "/home"]]);
console.log(lastSeen.get(ada), lastSeen.get({ name: "Ada" }));
// 5. Back to a plain object, for JSON.
console.log(JSON.stringify(Object.fromEntries(views)));
Run it with
node main.jsOutput
visitors: 3 ada, bo, cy
/home 3
/blog 3
/blog: bo ada bo
/home undefined
{"/home":3,"/blog":3}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 2
countWords(text) should return a Map from each word to how often it occurs, ignoring case: countWords("The cat and the hat").get("the") is 2. Words are separated by single spaces. The starter sets every count to 1. Fix it so that a word seen again adds 1 to its count.
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 is counted once. What does the starter do when it meets a word for the second time?
Read the old count first. counts.get(word) is undefined for a new word.
counts.set(word, (counts.get(word) ?? 0) + 1);
One way to solve it. Yours can look different and still pass the checks.
export function countWords(text) {
const counts = new Map();
for (const word of text.toLowerCase().split(" ")) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
return counts;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function countWords(text) {
const counts = new Map();
for (const word of text.toLowerCase().split(" ")) {
counts.set(word, 1);
}
return counts;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countWords} from './main.js';
test('countWords returns a Map', () => {
const counts = countWords('a b');
assert.ok(counts instanceof Map, `countWords returned ${counts}, not a Map`);
});
test('A repeated word is counted each time', () => {
const counts = countWords('the cat and the hat');
assert.equal(counts.get('the'), 2, `the was counted ${counts.get('the')} times`);
assert.equal(counts.get('cat'), 1, `cat was counted ${counts.get('cat')} times`);
});
test('Case is ignored and the order is first seen', () => {
const counts = countWords('Go go GO stop');
assert.deepEqual([...counts], [['go', 3], ['stop', 1]], `the entries are ${JSON.stringify([...counts])}`);
});
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 2
groupOrders(orders) gets objects like {id: 7, customer: "ada"} and should return a Map from each customer to the array of their order ids, in order. The starter makes a new array for every order, so each customer keeps only the last id. Create the array only for a new customer, then push. Use has and set; do not rely on getOrInsert.
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: ada has only [4]. When does byCustomer.set(order.customer, []) run?
Make the empty array only when the customer is not in the Map yet: byCustomer.has(order.customer).
if (!byCustomer.has(order.customer)) byCustomer.set(order.customer, []);
One way to solve it. Yours can look different and still pass the checks.
export function groupOrders(orders) {
const byCustomer = new Map();
for (const order of orders) {
if (!byCustomer.has(order.customer)) byCustomer.set(order.customer, []);
byCustomer.get(order.customer).push(order.id);
}
return byCustomer;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function groupOrders(orders) {
const byCustomer = new Map();
for (const order of orders) {
byCustomer.set(order.customer, []);
byCustomer.get(order.customer).push(order.id);
}
return byCustomer;
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {groupOrders} from './main.js';
const orders = [
{id: 1, customer: 'ada'},
{id: 2, customer: 'bo'},
{id: 3, customer: 'ada'},
{id: 4, customer: 'ada'}
];
test('Each customer keeps all their order ids', () => {
const groups = groupOrders(orders);
assert.deepEqual(groups.get('ada'), [1, 3, 4], `ada has ${JSON.stringify(groups.get('ada'))}`);
assert.deepEqual(groups.get('bo'), [2], `bo has ${JSON.stringify(groups.get('bo'))}`);
});
test('Customers come in the order of their first order', () => {
const groups = groupOrders(orders);
assert.deepEqual([...groups.keys()], ['ada', 'bo'], `the keys are ${JSON.stringify([...groups.keys()])}`);
});
test('No orders give an empty Map', () => {
const groups = groupOrders([]);
assert.equal(groups.size, 0, `the Map has ${groups.size} entries`);
});
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 prices = new Map();
prices["tea"] = 3;
console.log(prices.get("tea").toFixed(2));
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'toFixed')Why, and the fix
prices["tea"] = 3 adds an ordinary property to the Map object. The Map’s entries stay empty, so prices.get("tea") is undefined. Write with prices.set("tea", 3) and read with prices.get("tea"). The same goes for has, delete and size: they only see entries made with set.
const prices = new Map({ tea: 3, cake: 4 });
console.log(prices.size);
What Node.js prints
TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))Why, and the fix
new Map wants a list of [key, value] pairs, not a plain object. Turn the object into pairs first: new Map(Object.entries({ tea: 3, cake: 4 })). The way back is Object.fromEntries(map).
const byPage = new Map();
for (const [visitor, page] of [["ada", "/home"], ["bo", "/blog"]]) {
byPage.get(page).push(visitor);
}
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'push')Why, and the fix
The first time a page comes up, the Map has no array for it, so get returns undefined and push fails. Create the array for a new key first: if (!byPage.has(page)) byPage.set(page, []); then byPage.get(page).push(visitor). Where ES2026 getOrInsert exists, byPage.getOrInsert(page, []).push(visitor) does both.
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.