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:

static boolean isDate(String s) {
if (s.length() != 10) {
return false;
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (i == 2 || i == 5) {
if (c != '.') {
return false;
}
} else if (c < '0' || c > '9') {
return false;
}
}
return true;
}
System.out.println(isDate("05.03.2026"));
System.out.println(isDate("5.3.2026"));
System.out.println(isDate("05/03/2026"));
Output
true
false
false

Sixteen lines, and the method 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 method like this each.

The idea

A pattern instead of code

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

static boolean isDate(String s) {
return s.matches("\\d{2}\\.\\d{2}\\.\\d{4}");
}
System.out.println(isDate("05.03.2026"));
System.out.println(isDate("5.3.2026"));
System.out.println(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).

matches returns true if the whole text fits the pattern, from the first character to the last.

Why does the code say \\d and not \d? The backslash is special for the Java string too, so you write it twice. We will look at this in detail in a moment. For now, read \\ in the code as one \ in the pattern.

See it

How the pattern searches

replaceAll 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:

String line = "€120 on 05.03.2026, due 15.04.2026";
String masked = line.replaceAll("\\d{2}\\.\\d{2}\\.\\d{4}", "[date]");
System.out.println(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. replaceAll 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 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:

String[] plates = { "CA1234BX", "B7788KK", "CA12345BX", "ca1234bx" };
for (String plate : plates) {
System.out.println(plate + " " + plate.matches("[A-Z]{1,2}\\d{4}[A-Z]{2}"));
}
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 Java, by default it is Latin letters only, without accents.

System.out.println("Sofia".matches("\\w+"));
System.out.println("Zürich".matches("\\w+"));
System.out.println("Zürich".matches("(?U)\\w+"));
System.out.println("Zürich".matches("[A-Za-zü]+"));
Output
true
false
true
true

For text with letters like "ü" or "é", list them in the class, [A-Za-zü], or put (?U) at the start of the pattern: then \w knows the letters of all languages.

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?

String pattern = "\\d{2}.\\d{2}.\\d{4}";
System.out.println("05.03.2026".matches(pattern));
System.out.println("05-03-2026".matches(pattern));
System.out.println("05.03.26".matches(pattern));
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:

String mobile = "(\\+359|0)8[789]\\d{7}";
System.out.println("0888123456".matches(mobile));
System.out.println("+359877654321".matches(mobile));
System.out.println("0888 123 456".matches(mobile));
System.out.println("088812345".matches(mobile));
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:

String price = "€\\d+([.,]\\d{1,2})?";
System.out.println("€12".matches(price));
System.out.println("€12.5".matches(price));
System.out.println("€12,50".matches(price));
System.out.println("€.50".matches(price));
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

Two backslashes

The pattern goes through two layers. First Java reads the string in the quotes, then the regex reads the pattern.

In a Java string, \ starts a special character: \n is a new line, \" is a quote. \d is not one of them, so the program does not compile:

String date = "05.03.2026";
System.out.println(date.matches("\d{2}\.\d{2}\.\d{4}"));
Compile error
Main.java:4: error: illegal escape character
System.out.println(date.matches("\d{2}\.\d{2}\.\d{4}"));
^

So every \ of the pattern is written \\ in Java:

String date = "05.03.2026";
System.out.println(date.matches("\\d{2}\\.\\d{2}\\.\\d{4}"));
System.out.println("C:\\Temp".matches("C:\\\\Temp"));
Output
true
true
You want in the pattern You write in Java
\d (a digit) "\\d"
\. (a real dot) "\\."
\\ (a real backslash) "\\\\"

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 stops like this:
Error
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at Main.main(Main.java:5)

Pick the line with the bug.

The idea

Replacing and splitting by a pattern

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

String address = "Delivery: 12 Shipka St., entrance B";
System.out.println(address.replaceAll("\\s+", " "));
String[] items = "bread, milk;coffee tea".split("[,;\\s]+");
System.out.println(items.length + ": " + String.join(" | ", items));
System.out.println("Code SUMMER10, code summer25".replaceAll("(?i)summer\\d+", "***"));
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) at the start is a flag: the pattern ignores upper and lower case.

replace (without All) also replaces every occurrence, but it searches for plain text, not a pattern. If you do not need a pattern, use it: 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. Here too, (?U) is a must for letters like "é":

String text = "éclair and éclairs";
System.out.println(text.replaceAll("éclair", "donut"));
System.out.println(text.replaceAll("\\béclair\\b", "donut"));
System.out.println(text.replaceAll("(?U)\\béclair\\b", "donut"));
Output
donut and donuts
éclair and éclairs
donut and éclairs

Without \b, "éclairs" becomes "donuts". With \b but without (?U), Java does not see "é" as a letter and replaces nothing.

More precisely, if you are curious
  • \B is the opposite of \b: a place inside a word. "(?U)\\Bé" finds "é" only when it is not at the start of a word: in "café", but not in "éclair".
  • ^ is the start of the text, and $ is the end: replaceAll("^0+", "") removes the leading zeros. You do not need them with matches, because it looks at the whole text anyway.
  • You can also give the flags as constants of the Pattern class: Pattern.CASE_INSENSITIVE is the same as (?i), and Pattern.UNICODE_CHARACTER_CLASS as (?U). We will use the Pattern class in the next lesson.
  • "a|b".split("|") is a trap like the dot: | is "or", and the pattern is empty. Write split("\\|").

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"?

String code = "1000";
System.out.println(code.matches(""));

Exercise

Checks with a pattern

Solve the tasks in your editor. Read lines while there are any: while (in.hasNextLine()). 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].

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 replaceAll 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. For that there is java.time.LocalDate, and the pattern is only the first sieve.
  • 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 matches: ".{8,}" and ".*\\d.*".
  • 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 replaceAll and [^...].

Test

Check yourself

1. What does "2026".matches("\\d{4}") check?
2. What does . (a dot) match in a pattern?
3. Which one means "one or more"?
4. Does "Zürich" match \w+ in Java?
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 ·