Skip to content
aviral gupta

// I5.2 · ~35 min · Intermediate

Regular expressions: find, check and replace text

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.

Lesson 2 of 5 in I5 Working with data

You will be able to

  • Write a pattern as a literal or with new RegExp, use the flags g, i, m and u, and check text with test
  • Get matches and their groups with match, matchAll and exec, including named groups
  • Replace and split text with a pattern, and escape user text with RegExp.escape
  1. 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(""));
  2. Predict · Activity 2 of 7

    Predict before you read on. What does this print?

    console.log("cat bat".replace("at", "og"));
  3. Practice · Activity 3 of 7

    Fill in the flag that makes the pattern ignore upper and lower case.

    console.log(/hello/____.test("Hello World"));
    console.log(/hello/.test("Hello World"));
  4. Practice · Activity 4 of 7

    Match each call to what it returns.

  5. Practice · Activity 5 of 7

    Named groups in the replacement. What does this print?

    const iso = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/;
    console.log("2026-10-05".replace(iso, "$<d>.$<m>.$<y>"));
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The same test, three times, with the g flag. What does this print?

    const re = /o/g;
    console.log(re.test("foo"), re.test("foo"), re.test("foo"));
  7. Apply · Activity 7 of 7

    Mini-task: text = "Tea 3.50 EUR, cake 4.20 EUR, coffee 2.80 EUR". Write one pattern with the named groups item and amount, loop over every match with matchAll, print item: amount for each, and print the total with two decimals.

    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

Dates and addresses in a short text

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

Output

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 false
  • match.index is where the match starts, counted from 0.
  • Without the m flag, /^Follow/g would find nothing: ^ would only mean the start of the whole text.
  • The raw pattern example.com matched exampleXcom, because . means any character. The escaped one did not.
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 2

A German postcode

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.

Hints
  1. Hint 1

    Run the tests: "123456" passes. Where in the text may /\d{5}/ match?

  2. Hint 2

    ^ means the start of the text and $ the end.

  3. Hint 3

    return /^\d{5}$/.test(text);

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function isPostcode(text) {
  return /^\d{5}$/.test(text);
}
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

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

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

node --test
Download learnrun.js

Exercise 2 of 2

Read a time with named groups

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.

Hints
  1. Hint 1

    exec returns null when there is no match, and null.groups throws. Check for null first.

  2. Hint 2

    Groups are always strings: Number(match.groups.hours) gives a number.

  3. Hint 3

    Put ^ before the hours and $ after the minutes, so "14:05pm" does not match.

Show a solution

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

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

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

node --test
Download learnrun.js

Common mistakes

replaceAll with a regex without g

const date = "2026-10-05";
console.log(date.replaceAll(/-/, "/"));

What Node.js prints

TypeError: String.prototype.replaceAll called with a non-global RegExp argument

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

A single backslash in a RegExp string

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.

Using typed text as a pattern

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 group

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

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

Patterns and flags

A regular expression is a pattern for text. Write it as a literal, /\d{4}/, or build it from a string with new RegExp("\\d{4}"). In a string every backslash must be doubled, because "\d" in a string is just d. Flags go after the closing slash or as the second argument: g finds every match, i ignores case, m makes ^ and $ match at each line, and u reads the text as Unicode code points, so an emoji counts as one character. ^ and $ anchor the pattern to the start and the end; without them, test is true when the pattern occurs anywhere. pattern.test(text) answers true or false.

Matches and groups

text.match(pattern) without g gives the first match as an array, with index and groups, or null when nothing matches; always handle null. With g, match gives an array of all matched strings and no groups. text.matchAll(pattern) needs g and gives every match with its groups, ideal for a for...of loop. Parentheses capture a part: match[1] is the first group. A named group, (?<year>\d{4}), is read as match.groups.year, which stays right when you add groups later. pattern.exec(text) works like match without g; with g, a regex remembers lastIndex between calls, and so does test.

Replace, split and escape

text.replace(pattern, replacement) returns a new string. With a string pattern or a regex without g, only the first match is replaced; with g, all of them. replaceAll does the same but throws a TypeError for a regex without g. In the replacement, $1 or $<name> insert a group, and a function receives each match and returns its replacement. text.split(/\s*,\s*/) splits at commas with any spaces around them. Text a user typed may contain . ( + or ?, which mean something in a pattern. RegExp.escape(text) escapes them, so new RegExp(RegExp.escape(text)) finds that exact text.

Sources

Last reviewed October 5, 2026