Warm-up · Activity 1 of 7
Warm-up from lesson I5.1. A string is iterable, letter by letter. What does this print?
console.log([...new Set("banana")].join(""));// I5.2 · ~35 min · Intermediate
After this lesson you check text against a pattern, pull out the parts you need with named groups, and replace or split text with a regular expression.
You will be able to
Warm-up · Activity 1 of 7
console.log([...new Set("banana")].join(""));Predict · Activity 2 of 7
console.log("cat bat".replace("at", "og"));Practice · Activity 3 of 7
console.log(/hello/____.test("Hello World"));Practice · Activity 4 of 7
Practice · Activity 5 of 7
const iso = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/;
console.log("2026-10-05".replace(iso, "$<d>.$<m>.$<y>"));Brain teaser · Activity 6 of 7
const re = /o/g;
console.log(re.test("foo"), re.test("foo"), re.test("foo"));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
One text, seven questions. test answers whether there is a date, match with g lists the dates, and matchAll with named groups takes each date apart. replace with $<day> rewrites the dates in German order. The i flag finds an address written in capitals, and the m flag lets ^ match at the start of the second line. Finally, the same typed text is used as a pattern, once raw and once escaped with RegExp.escape.
main.js
const text = `Meeting on 2026-10-05 with ada@example.com.
Follow-up on 2026-10-12 with BO@Example.com.`;
// 1. test: is there a date at all?
console.log("has a date:", /\d{4}-\d{2}-\d{2}/.test(text));
// 2. match with the g flag: every date, as strings.
console.log("dates:", text.match(/\d{4}-\d{2}-\d{2}/g).join(" "));
// 3. matchAll with named groups: the parts of each date.
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
for (const match of text.matchAll(datePattern)) {
const { day, month, year } = match.groups;
console.log(`at ${match.index}: day ${day}, month ${month}, year ${year}`);
}
// 4. replace with $<name>: German date order.
const german = text.replace(datePattern, "$<day>.$<month>.$<year>");
console.log(german.split("\n")[0]);
// 5. The i flag: addresses in any case, made lower case.
const emails = text.match(/[a-z]+@example\.com/gi).map((e) => e.toLowerCase());
console.log("emails:", emails.join(", "));
// 6. The m flag: ^ matches at the start of every line.
console.log("lines starting with Follow:", text.match(/^Follow/gm).length);
// 7. RegExp.escape: search for text a user typed.
const typed = "example.com";
const plain = new RegExp(typed);
const escaped = new RegExp(RegExp.escape(typed));
console.log("exampleXcom:", plain.test("exampleXcom"), escaped.test("exampleXcom"));
Run it with
node main.jsOutput
has a date: true
dates: 2026-10-05 2026-10-12
at 11: day 05, month 10, year 2026
at 57: day 12, month 10, year 2026
Meeting on 05.10.2026 with ada@example.com.
emails: ada@example.com, bo@example.com
lines starting with Follow: 1
exampleXcom: true falseTab 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
isPostcode(text) should return true only when the whole text is a German postcode: exactly five digits, such as "10115". The starter returns true for "123456" and "PLZ 10115", because its pattern may match anywhere in the text. Anchor the pattern so that it must cover the whole text.
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: "123456" passes. Where in the text may /\d{5}/ match?
^ means the start of the text and $ the end.
return /^\d{5}$/.test(text);
One way to solve it. Yours can look different and still pass the checks.
export function isPostcode(text) {
return /^\d{5}$/.test(text);
}
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 isPostcode(text) {
return /\d{5}/.test(text);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {isPostcode} from './main.js';
test('Five digits are a postcode', () => {
assert.equal(isPostcode('10115'), true, 'isPostcode("10115") should be true');
});
test('Four digits are too short', () => {
assert.equal(isPostcode('1011'), false, 'isPostcode("1011") should be false');
});
test('Six digits are too long', () => {
assert.equal(isPostcode('123456'), false, 'isPostcode("123456") should be false');
});
test('Other text around the digits is not a postcode', () => {
assert.equal(isPostcode('PLZ 10115'), false, 'isPostcode("PLZ 10115") should be false');
});
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
parseTime(text) should turn "14:05" or "9:30" into {hours: 14, minutes: 5} with numbers, and return null for anything else, such as "noon" or "14:05pm". The starter crashes on text without a match and returns the groups as strings. Anchor the pattern, return null when exec finds nothing, and convert the groups with Number.
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.
exec returns null when there is no match, and null.groups throws. Check for null first.
Groups are always strings: Number(match.groups.hours) gives a number.
Put ^ before the hours and $ after the minutes, so "14:05pm" does not match.
One way to solve it. Yours can look different and still pass the checks.
export function parseTime(text) {
const match = /^(?<hours>\d{1,2}):(?<minutes>\d{2})$/.exec(text);
if (match === null) return null;
return { hours: Number(match.groups.hours), minutes: Number(match.groups.minutes) };
}
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 parseTime(text) {
const match = /(?<hours>\d{1,2}):(?<minutes>\d{2})/.exec(text);
return { hours: match.groups.hours, minutes: match.groups.minutes };
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseTime} from './main.js';
test('14:05 gives the numbers 14 and 5', () => {
const got = parseTime('14:05');
assert.deepEqual(got, {hours: 14, minutes: 5}, `parseTime('14:05') gave ${JSON.stringify(got)}`);
});
test('One-digit hours work', () => {
const got = parseTime('9:30');
assert.deepEqual(got, {hours: 9, minutes: 30}, `parseTime('9:30') gave ${JSON.stringify(got)}`);
});
test('Text without a time gives null', () => {
assert.equal(parseTime('noon'), null, 'parseTime("noon") should be null');
});
test('Extra text after the time gives null', () => {
assert.equal(parseTime('14:05pm'), null, 'parseTime("14:05pm") should be null');
});
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 date = "2026-10-05";
console.log(date.replaceAll(/-/, "/"));
What Node.js prints
TypeError: String.prototype.replaceAll called with a non-global RegExp argumentWhy, and the fix
replaceAll is meant to replace every match, so a regex without the g flag is refused. Add the flag: date.replaceAll(/-/g, "/"). With g, date.replace(/-/g, "/") does the same. matchAll has the same rule.
const digits = new RegExp("\d+");
const total = "Total: 42 EUR".match(digits)[0];
console.log(Number(total));
What Node.js prints
TypeError: Cannot read properties of null (reading '0')Why, and the fix
In a string, "\d" is just the letter d, so the pattern is d+, which does not occur in the text, and match returns null. Double the backslash in a string, new RegExp("\\d+"), or use the literal /\d+/. And check for null before you read [0] or groups.
const typed = "(1+1";
const search = new RegExp(typed);
console.log(search.test("Is (1+1 = 2) true?"));
What Node.js prints
SyntaxError: Invalid regular expression: /(1+1/: Unterminated groupWhy, and the fix
The user meant the characters ( and +, but in a pattern ( opens a group and + repeats. An open group without ) is not a valid pattern, so new RegExp throws. Escape the text first: new RegExp(RegExp.escape(typed)). Then every character only stands for itself.
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.