Skip to content
wannacode

Regular expressions

Problem

Sixteen lines for one date

The booking form wants a date in the form dd.mm.yyyy. You check it character by character:

function isDate(s) {
if (s.length !== 10) {
return false;
}
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (i === 2 || i === 5) {
if (c !== ".") {
return false;
}
} else if (c < "0" || c > "9") {
return false;
}
}
return true;
}
console.log(isDate("05.03.2026"));
console.log(isDate("5.3.2026"));
console.log(isDate("05/03/2026"));
Output
true
false
false

Sixteen lines, and the function checks only the format: two digits, a dot, two digits, a dot, four digits. If you also want to accept / instead of a dot, or a day with one digit, you will add as many lines again. A phone number, an email or a license plate will cost you one more function like this each.

The idea

A pattern instead of code

You describe what the text looks like, and JavaScript does the checking:

function isDate(s) {
return /^\d{2}\.\d{2}\.\d{4}$/.test(s);
}
console.log(isDate("05.03.2026"));
console.log(isDate("5.3.2026"));
console.log(isDate("05/03/2026"));
Output
true
false
false

You read the pattern from left to right: \d is a digit, {2} means "two times", \. is a dot. A pattern like this is called a regular expression (regex).

The pattern sits between two slashes: /.../. This is a regular expression literal, just as "..." is a string literal. test(text) returns true if it finds the pattern anywhere in the text. That is why the pattern starts with ^ ("the start of the text") and ends with $ ("the end"): this way it has to cover the whole text, from the first character to the last.

See it

How the pattern searches

replace does not check the whole text. It searches for the pieces that fit the pattern and replaces them. Here is how it goes along the line:

const line = "€120 on 05.03.2026, due 15.04.2026";
const masked = line.replace(/\d{2}\.\d{2}\.\d{4}/g, "[date]");
console.log(masked);
Step by step, in words
  1. The line is a sequence of characters, each with its own position.
  2. From position 0: "€" is not a digit. From position 1: "1" and "2" are digits, but then comes "0" instead of a dot. The try fails.
  3. A new try from each next character: "20 ", "0 ", " o"… all failures.
  4. From position 8 all ten parts fit: the first match.
  5. The search carries on after the match, from the comma, and finds the second one.
  6. replace with the g flag replaces both matches with "[date]".

The pattern is tried from each position in turn. As soon as some part does not match, the try fails and a new one starts from the next character. That is why 120 did not pass: two digits, and then 0 instead of a dot. When it finds a match, the search carries on after it.

The g (global) flag after the pattern, /.../g, says "all matches". Without it, replace replaces only the first one.

The idea

Character classes

In square brackets you list which characters are allowed in one place. A car license plate is one or two letters, four digits and two letters:

const plates = ["CA1234BX", "B7788KK", "CA12345BX", "ca1234bx"];
for (const plate of plates) {
console.log(plate, /^[A-Z]{1,2}\d{4}[A-Z]{2}$/.test(plate));
}
Output
CA1234BX true
B7788KK true
CA12345BX false
ca1234bx false
Pattern Matches one character
[ABE] A, B or E
[0-9], [A-Z] from the range: a digit, a capital Latin letter
[^0-9] anything except a digit (^ at the start of the brackets)
\d a digit, the same as [0-9]
\s a space, a tab, a new line
\w a Latin letter, a digit or _
. any character at all (except a new line)

Capital letters flip the meaning: \D is "not a digit", \S is "not a space", \W is "not \w".

Watch out for \w: in JavaScript it is Latin letters only, without accents.

console.log(/^\w+$/.test("Sofia"));
console.log(/^\w+$/.test("Zürich"));
console.log(/^\p{L}+$/u.test("Zürich"));
console.log(/^[A-Za-zü]+$/.test("Zürich"));
Output
true
false
true
true

For text with letters like "ü" or "é", list them in the class, [A-Za-zü], or use \p{L} with the u flag: \p{L} is a letter from any language, and u (unicode) tells the pattern to understand classes like this.

More precisely, if you are curious
  • [A-Z] is a range by the codes of the characters. It has only the 26 Latin letters, without "É" or "Ü", which sit elsewhere in the table. For English text [A-Za-z] is enough. If you need letters with accents too, add them: [A-Za-zÉéÜü].
  • Real Bulgarian plates use only 12 letters, the ones that look the same in Cyrillic and Latin: [ABEKMHOPCTYX].

Try it

The dot

Someone forgot the backslash before the dots:

What will the program print?

const pattern = /^\d{2}.\d{2}.\d{4}$/;
console.log(pattern.test("05.03.2026"));
console.log(pattern.test("05-03-2026"));
console.log(pattern.test("05.03.26"));
Answer

The idea

How many times

After a character or a class you say how many times it can repeat. This is a quantifier:

Quantifier Means
? zero or one (it may be there, it may not)
* zero or more
+ one or more
{4} exactly 4
{1,2} from 1 to 2
{2,} 2 or more

A Bulgarian mobile number starts with 0 or +359, then 87, 88 or 89, and 7 more digits:

const mobile = /^(\+359|0)8[789]\d{7}$/;
console.log(mobile.test("0888123456"));
console.log(mobile.test("+359877654321"));
console.log(mobile.test("0888 123 456"));
console.log(mobile.test("088812345"));
Output
true
true
false
false

| means "or". The parentheses keep \+359|0 together: without them, "or" would split the whole pattern in two. Parentheses do more things, which we will see in the next lesson. The + at the start has a \, because otherwise it is a quantifier.

A quantifier also works on a whole group in parentheses. Here is a price with or without a fractional part:

const price = /^€\d+([.,]\d{1,2})?$/;
console.log(price.test("€12"));
console.log(price.test("€12.5"));
console.log(price.test("€12,50"));
console.log(price.test("€.50"));
Output
true
true
true
false

([.,]\d{1,2})? means "a dot or a comma and one or two digits", and this whole block may be missing.

The idea

Literal or string

In the /.../ literal the backslash is only for the pattern: \d is a digit, \. is a dot, with no doubling.

Sometimes you build the pattern from text, for example when part of it comes from a variable. Then it is a string in quotes for new RegExp(...), and it goes through two layers: first JavaScript reads the string, then the pattern. In a string, \ starts a special character: \n is a new line, \" is a quote. \d is not one of them, and JavaScript just drops the backslash, with no error and no warning:

const date = "05.03.2026";
const pattern = new RegExp("^\d{2}\.\d{2}\.\d{4}$");
console.log(pattern.source);
console.log(pattern.test(date));
Output
^d{2}.d{2}.d{4}$
false

source shows the pattern that RegExp got: d{2} means "two letters d". So in a string every \ of the pattern is written \\:

const date = "05.03.2026";
console.log(/^\d{2}\.\d{2}\.\d{4}$/.test(date));
console.log(new RegExp("^\\d{2}\\.\\d{2}\\.\\d{4}$").test(date));
console.log(/^C:\\Temp$/.test("C:\\Temp"));
Output
true
true
true
You want in the pattern Literal String for new RegExp
\d (a digit) /\d/ "\\d"
\. (a real dot) /\./ "\\."
\\ (a real backslash) /\\/ "\\\\"

In the literal, / also takes a \: /\//. Otherwise it would close the pattern.

The characters that need a \ when you want them literally are . * + ? ( ) [ ] { } | ^ $ \.

Try it

Find the bug

The program should print "12 euros and 50 cents", but it prints this:
Output
0 euros and 0 cents

Pick the line with the bug.

const price = "12.50";
console.log(price.split("."));
console.log(price.split(/./));
Output
[ '12', '50' ]
[ '', '', '', '', '', '' ]

The idea

Replacing and splitting by a pattern

replace and split take a pattern. This is how you clean up and break apart text that people type any way they like:

const address = "Delivery: 12 Shipka St., entrance B";
console.log(address.replace(/\s+/g, " "));
const items = "bread, milk;coffee tea".split(/[,;\s]+/);
console.log(`${items.length}: ${items.join(" | ")}`);
console.log("Code SUMMER10, code summer25".replace(/summer\d+/gi, "***"));
Output
Delivery: 12 Shipka St., entrance B
4: bread | milk | coffee | tea
Code ***, code ***
  • \s+ is "one or more spaces": every group of spaces becomes one.
  • [,;\s]+ is "commas, semicolons or spaces, however many there are": the separator can be any of them.
  • i after the pattern is a flag: the pattern ignores upper and lower case. You write flags next to each other: gi is "all matches, ignoring case".

replace with text instead of a pattern replaces only the first occurrence. For all of them it is replaceAll("old", "new"). If you do not need a pattern, use text: you do not have to think about dots and backslashes.

\b is a word boundary: the place between a letter and something that is not a letter. With it you replace only whole words. But in JavaScript \b, like \w, knows only Latin letters without accents, even with the u flag:

const text = "éclair and éclairs";
console.log(text.replace(/éclair/g, "donut"));
console.log(text.replace(/\béclair\b/g, "donut"));
console.log(text.replace(/(?<!\p{L})éclair(?!\p{L})/gu, "donut"));
Output
donut and donuts
éclair and éclairs
donut and éclairs

Without \b, "éclairs" becomes "donuts". With \b, JavaScript does not see "é" as a letter and replaces nothing. For letters like this, you write the boundary with a lookaround: (?<!\p{L}) is "there is no letter before it", and (?!\p{L}) is "there is no letter after it". They do not take any characters. They only check.

More precisely, if you are curious
  • \B is the opposite of \b: a place inside a word. It also knows only Latin letters without accents, so with "é" it gets it backwards: it skips the "é" in "café" and finds the one in "éclair". Only inside a word is (?<=\p{L})é with the u flag: "there is a letter before it".
  • ^ is the start of the text, and $ is the end: text.replace(/^0+/, "") removes the leading zeros.
  • Flags are letters after the pattern: g (all), i (ignore case), u (unicode), m and s (in the next lesson). (?i) at the start of the pattern is an error in Node 22.
  • "a|b".split(/|/) is a trap like the dot: | is "or", and the pattern is empty, so it splits between every two characters. "a|b".split("|") searches for plain text and does not have this problem.
console.log("café éclair".replace(/\Bé/g, "e"));
console.log("café éclair".replace(/(?<=\p{L})é/gu, "e"));
console.log("a|b".split(/|/));
console.log("a|b".split("|"));
console.log("a a".replace("a", "b"), "a a".replaceAll("a", "b"));
Output
café eclair
cafe éclair
[ 'a', '|', 'b' ]
[ 'a', 'b' ]
b a b b

Try it

Postal code

A Bulgarian postal code is exactly four digits. What is missing for the program to print true for "1000" and false for "100" or "10000"?

const code = "1000";
console.log(/^$/.test(code));

Exercise

Checks with a pattern

Solve the tasks in your editor. Read lines while there are any, like this (for now, just copy it):

import { createInterface } from "node:readline";
const rl = createInterface({ input: process.stdin });
for await (const line of rl) {
// line is the next line, without "\n"
}

In the lesson "Files" you will read the same lines from a file.

1. Date. Check whether each date is in the form dd/mm/yyyy. The day is from 01 to 31, and the month from 01 to 12. Hint: the day is 0[1-9] or [12]\d, or 3[01]. In the literal, / is written \/.

Input
01/01/2020
32/01/2020
15/05/2021
Output
01/01/2020 - Valid
32/01/2020 - Invalid
15/05/2021 - Valid

2. Mobile number. People write numbers with spaces and dashes. Remove them with replace and check what is left with the mobile number pattern from the lesson.

Input
0888 123 456
+359 87 765 4321
02 812 3456
0899-12-345
Output
0888 123 456 - valid
+359 87 765 4321 - valid
02 812 3456 - invalid
0899-12-345 - invalid

3. Email. Check whether each line looks like an email: one or more Latin letters, digits, ., + or -, then @, then a domain name with at least one dot (company.com, mail.company.com).

Input
elena.koleva@company.com
elena@company
@company.com
elena koleva@company.com
Output
elena.koleva@company.com - yes
elena@company - no
@company.com - no
elena koleva@company.com - no

4. Shopping list. The products are separated with commas, semicolons, vertical bars or spaces, sometimes several at once. Print them numbered.

Input
bread, milk;coffee tea|cheese
Output
1. bread
2. milk
3. coffee
4. tea
5. cheese
More precisely, if you are curious
  • The date pattern also accepts 31/02/2026. A regex cannot sensibly tell whether the day exists in the month. The pattern is only the first sieve. The built-in Date does not help here: it does not reject February 31. It moves it to March 3 (months in Date are counted from 0, so February is 1):
console.log(new Date(2026, 1, 31).toDateString());
Output
Tue Mar 03 2026

So check the day yourself: build the date and see whether the month stayed the same.

  • The full rule for an email address (RFC 5322) allows quotes, comments and spaces in certain places. A pattern that covers all of it is hundreds of characters long, and nobody maintains it. The real check is to send an email with a confirmation link.

More tasks

  • Password. Check whether the password is at least 8 characters long and has at least one digit. Do it with two test: /^.{8,}$/ and /\d/. Why do you not need ^ and $ in the second one?
  • Time. Check a time in the form hh:mm, from 00:00 to 23:59.
  • Digits only. From "Tel.: +359 (88) 812-34-56" keep only the digits and the +, with one replace and [^...].

Test

Check yourself

1. What does /\d{4}/.test("12026") give?
2. What does . (a dot) match in a pattern?
3. Which one means "one or more"?
4. Does "Zürich" match /^\w+$/ in JavaScript?
5. How many parts does "bread, milk;coffee".split(/[,;]\s*/) give?

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