Skip to content
wannacode

Search and replace

Problem

The bank statement

Your bank sends you text with the payments for the month. You want each amount separately, so you can add them up:

const statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10";
console.log(/€\d+\.\d{2}/.test(statement));
Output
true
test only says "yes, there is an amount somewhere". replace would change the amounts, but it does not give them to you. You need a way to find each match and take it.

The idea

matchAll

For searching, a string has the matchAll method:

const statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10";
const amount = /€\d+\.\d{2}/g;
for (const m of statement.matchAll(amount)) {
console.log(m[0]);
}
Output
€450.00
€87.35
€23.10
  • The pattern has the g flag: "all matches". Without it, matchAll stops with a TypeError.
  • statement.matchAll(amount) gives the matches one by one, from left to right. That is why it is in a for…of: each iteration gets the next one, until they run out.
  • m is one match that was found: an array in which m[0] is its text.
More precisely, if you are curious
  • match with the g flag gives you an array with the texts of the matches right away. Without g it gives only the first match, with its groups (null if there is none).
const statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10";
console.log(statement.match(/€\d+\.\d{2}/g));
console.log(statement.match(/€\d+\.\d{2}/)[0]);
Output
[ '€450.00', '€87.35', '€23.10' ]
€450.00

See it

Where the match is

Besides the text, the match also knows where it was found:

const log = "ERROR disk; INFO start; ERROR network";
let count = 0;
for (const m of log.matchAll(/ERROR/g)) {
count++;
console.log(`${m[0]} from ${m.index} to ${m.index + m[0].length}`);
}
console.log(`Total: ${count}`);
Output
ERROR from 0 to 5
ERROR from 24 to 29
Total: 2
m.index is the index of the first character of the match. The end is m.index + m[0].length: the index after the last one, as with slice. That is why log.slice(m.index, m.index + m[0].length) is the same as m[0].

Try it

A digit or a number?

What will the program print?

let result = "";
for (const m of "Table 12, 4 people, 2 bottles".matchAll(/\d+/g)) {
result += `[${m[0]}]`;
}
console.log(result);
Answer

The idea

Groups: parts of the match

Parentheses in the pattern make a group. The match remembers what matched each group separately:

const date = /(\d{2})\.(\d{2})\.(\d{4})/;
const m = "Due: 15.11.2026".match(date);
if (m) {
console.log(m[0]);
console.log(m[1] + " / " + m[2] + " / " + m[3]);
console.log(m[3] + "-" + m[2] + "-" + m[1]);
}
Output
15.11.2026
15 / 11 / 2026
2026-11-15
  • Groups are numbered by their opening parentheses, from left to right, starting at 1.
  • m[1] is group 1. m[0] is the whole match.
  • match without the g flag is for when you need only the first match. If there is none, m is null and if (m) skips it.

This way you take the day, the month and the year of a date separately, and arrange them the way you need.

const date = /(\d{2})\.(\d{2})\.(\d{4})/;
const m = "Due: 15.11.2026".match(date);
if (m) {
console.log(m[0]);
console.log(m[1] + " / " + m[2] + " / " + m[3]);
console.log(m[3] + "-" + m[2] + "-" + m[1]);
}
Step by step, in words
  1. The date pattern has three pairs of parentheses: three groups.
  2. match without the g flag looks for the first match. m is an array with it and the groups, or null.
  3. match finds the date: from index 5 to 15.
  4. Each group remembers its part: group 1 is 15, group 2 is 11, group 3 is 2026.
  5. The parts sit in the array m, each under its own index. m[0] is the whole match.
  6. m[0] is the whole match: 15.11.2026.
  7. The three groups, one by one.
  8. In reverse order, the parts give 2026-11-15.
More precisely, if you are curious
  • (?:...) is a group that only holds the parts together, without remembering them. For example, (?:\+359|0)8[789]\d{7} does not make a group 1.
  • m.length - 1 is the number of groups: the array has the whole match and one place for each group.
  • If a group did not take part in the match (for example, (\.\d{2})? was not there), its place is undefined.
const m = "Tip €5".match(/€(\d+)(\.\d{2})?/);
console.log(m.length - 1);
console.log(m[1], m[2]);
Output
2
5 undefined

The idea

Named groups and replacing

When there are many groups, the numbers get mixed up. You can name them: (?<name>...), and then m.groups.name. Here is a line from a café bill:
const item = /(?<qty>\d+) x (?<name>[A-Za-z ]+) at (?<price>\d+\.\d{2})/g;
for (const m of "2 x Coffee with milk at 3.20; 1 x Croissant at 2.90".matchAll(item)) {
const qty = Number(m.groups.qty);
const price = Number(m.groups.price);
console.log(`${m.groups.name}: €${qty * price}`);
}
Output
Coffee with milk: €6.4
Croissant: €2.9
Groups work in replace too: in the replacement text, $1 is group 1, and $<name> is the group with that name. Here are the dates, from the Bulgarian form to ISO and to American:
const text = "Deadline: 15.11.2026, reminder: 08.11.2026";
console.log(text.replace(/(\d{2})\.(\d{2})\.(\d{4})/g, "$3-$2-$1"));
console.log(text.replace(/(?<d>\d{2})\.(?<m>\d{2})\.(?<y>\d{4})/g, "$<m>/$<d>/$<y>"));
Output
Deadline: 2026-11-15, reminder: 2026-11-08
Deadline: 11/15/2026, reminder: 11/08/2026
Here you do not need matchAll: replace understands groups by itself.
More precisely, if you are curious
  • $ in the replacement text is special. A real dollar sign is $$:
console.log("Price: 5".replace(/(\d+)/, "$$$1"));
Output
Price: $5
  • You can also use a group in the pattern itself: \1 means "the same as group 1". This is how you catch repeated words. See the card on flags.

Try it

How much does + take?

You want all the HTML tags from the line:

What will the program print?

const html = "<b>Price:</b> <i>€12</i>";
for (const m of html.matchAll(/<.+>/g)) {
console.log(m[0]);
}
Answer

The idea

Greedy and lazy

A ? after a quantifier makes it lazy: it takes as little as it can and stops at the first chance.

const html = "<b>Price:</b> <i>€12</i>";
for (const m of html.matchAll(/<.+?>/g)) {
console.log(m[0]);
}
console.log(html.replace(/<[^>]+>/g, ""));
Output
<b>
</b>
<i>
</i>
Price: €12
  • <.+?> is "<, at least one character, and the first > after it".
  • <[^>]+> does the same in another way: "<, characters that are not >, and >". It cannot skip past a >, so it is also faster. With it you remove the tags, and only the text is left.
Greedy Lazy
* *?
+ +?
? ??
{2,5} {2,5}?

Try it

Find the bug

The program should print the three order numbers, but it prints only #1042. Which line has the bug?

Pick the line with the bug.

The idea

Flags and lines

Flags are letters after the pattern. From a log file with several lines, you take the error messages, written in upper or lower case:

const log = "INFO: Start\nERROR: No connection to the database\nINFO: Retrying\nerror: Disk 95%";
const p = /^error: (.+)$/gim;
for (const m of log.matchAll(p)) {
console.log(m[1]);
}
Output
No connection to the database
Disk 95%
  • i: upper and lower case letters are the same.
  • m (multiline) makes ^ and $ the start and end of each line. Without it, they are only the start and end of the whole text, and the pattern will find nothing.
  • s lets . catch a new line too.
  • You already know g: without it, matchAll does not work.
More precisely, if you are curious

Two things you will run into in other people's patterns:

  • A backreference \1: "the same text as group 1". This is how you catch repeated words.
  • A lookahead (?=...): "somewhere from here on there is ...", without taking any characters. Several of them in a row are handy for passwords: "has a digit" and "has a capital letter", and at least 8 characters.
const twice = /(?<!\p{L})(\p{L}+) \1(?!\p{L})/gu;
for (const m of "Meet me at the café café, please please be on time.".matchAll(twice)) {
console.log("Repeated: " + m[1]);
}
const strong = /^(?=.*\d)(?=.*[A-Z]).{8,}$/;
console.log(strong.test("secret12"), strong.test("Secret12"));
Output
Repeated: café
Repeated: please
false true

(?!...) is the opposite: "from here on there is no ...". There is also lookbehind: (?<=...) and (?<!...). They also give you the word boundary for letters like "é" above: \b in JavaScript knows only Latin letters without accents.

Exercise

Find, extract, replace

Solve the tasks in your editor. The text is one line from the keyboard. In the lesson "Files" you will do them with a file too.

1. Find and replace. Replace the word "old" with "new" everywhere it is a whole word, and say how many replacements you made. Parts of other words ("oldest") are left alone.

Input
This is old content. More old content, not the oldest.
Output
This is new content. More new content, not the oldest.
Replaced: 2

2. Words with a prefix. The first line is the prefix, the second is the text. Print each word that starts with the prefix. The pattern is built from text, so it is new RegExp(...).

Input
app
apple application banana appreciate snapple
Output
apple
application
appreciate

3. Addresses in text. Extract all web addresses that start with http:// or https://. The dot at the end of a sentence is not part of the address.

Input
See the site https://www.example.com and this one: http://www.test.com. The documents are at https://docs.example.com/guide/start.
Output
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Dates to ISO. Turn every date dd.mm.yyyy in the text into the form yyyy-mm-dd with one replace.

Input
Contract of 03.10.2026, payment by 15.10.2026.
Output
Contract of 2026-10-03, payment by 2026-10-15.

More tasks

  • Statement total. Take the amounts from the first card with a group for the euros and a group for the cents, add them up and print "Total: €560.45".
  • Hashtags. Extract all words that start with # from a post that mixes plain Latin letters and letters like "é". Think about why you need \p{L} and the u flag.
  • No tags. Remove the HTML tags from <p>The deadline is <b>15.11</b>.</p> and print the clean text.
  • Phone numbers in text. Find all mobile numbers in a text and print them in the form +359 88 812 3456, with groups and replace.

Test

Check yourself

1. What does text.matchAll(/…/g) give?
2. The pattern is /(\d{2})\.(\d{2})\.(\d{4})/, and the text is 15.11.2026. What is m[2]?
3. What does "05.03.2026".replace(/(\d+)\.(\d+)\.(\d+)/, "$2/$1") give?
4. Why does "<.+>" catch the whole text "<b>Price</b>" and not just "<b>"?
5. What does the m flag do?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·