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));truetest 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]);}€450.00€87.35€23.10- The pattern has the
gflag: "all matches". Without it,matchAllstops with aTypeError. statement.matchAll(amount)gives the matches one by one, from left to right. That is why it is in afor…of: each iteration gets the next one, until they run out.mis one match that was found: an array in whichm[0]is its text.
More precisely, if you are curious
matchwith thegflag gives you an array with the texts of the matches right away. Withoutgit gives only the first match, with its groups (nullif 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]);[ '€450.00', '€87.35', '€23.10' ]€450.00See 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}`);ERROR from 0 to 5ERROR from 24 to 29Total: 2m.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);\d+ is "one or more digits", so "12" is one match. With \d and no +, each digit would be separate: [1][2][4][2]. The space and the comma break the run, so 12 and 4 do not merge.
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]);}15.11.202615 / 11 / 20262026-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.matchwithout thegflag is for when you need only the first match. If there is none,misnullandif (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
- The date pattern has three pairs of parentheses: three groups.
- match without the g flag looks for the first match. m is an array with it and the groups, or null.
- match finds the date: from index 5 to 15.
- Each group remembers its part: group 1 is 15, group 2 is 11, group 3 is 2026.
- The parts sit in the array m, each under its own index. m[0] is the whole match.
- m[0] is the whole match: 15.11.2026.
- The three groups, one by one.
- 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 - 1is 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 isundefined.
const m = "Tip €5".match(/€(\d+)(\.\d{2})?/);console.log(m.length - 1);console.log(m[1], m[2]);25 undefinedThe idea
Named groups and replacing
(?<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}`);}Coffee with milk: €6.4Croissant: €2.9replace 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>"));Deadline: 2026-11-15, reminder: 2026-11-08Deadline: 11/15/2026, reminder: 11/08/2026matchAll: 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"));Price: $5- You can also use a group in the pattern itself:
\1means "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]);}One match, the whole line. .+ takes as much as it can: it goes to the end of the text, then gives back character by character until it finds a >. The first > from the back is the last one in the line. Quantifiers like this are called greedy.
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, ""));<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.
g flag, so match gives only the first match. With /#\d+/g, match gives an array with the texts of all of them.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]);}No connection to the databaseDisk 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.slets.catch a new line too.- You already know
g: without it,matchAlldoes 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"));Repeated: caféRepeated: pleasefalse 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.
This is old content. More old content, not the oldest.This is new content. More new content, not the oldest.Replaced: 22. 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(...).
appapple application banana appreciate snappleappleapplicationappreciate3. 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.
See the site https://www.example.com and this one: http://www.test.com. The documents are at https://docs.example.com/guide/start.https://www.example.comhttp://www.test.comhttps://docs.example.com/guide/start4. Dates to ISO. Turn every date dd.mm.yyyy in the text into the form yyyy-mm-dd with one replace.
Contract of 03.10.2026, payment by 15.10.2026.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 theuflag. - 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 andreplace.
Test