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"));truefalsefalseSixteen 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"));truefalsefalseYou 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
- The line is a sequence of characters, each with its own position.
- 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.
- A new try from each next character: "20 ", "0 ", " o"… all failures.
- From position 8 all ten parts fit: the first match.
- The search carries on after the match, from the comma, and finds the second one.
- 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}"));}CA1234BX trueB7788KK trueCA12345BX falseca1234bx 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ü]+"));truefalsetruetrueFor 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));Without \ the dot means "any character at all", so the dash passes too, and 05-03-2026 is true. The third text has only two digits for the year, and the pattern wants four. The check looks like it works until wrong input comes along. So test the pattern with texts that should not pass too.
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));truetruefalsefalse| 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));truetruetruefalse([.,]\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}"));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"));truetrue| 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
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.
split takes a regular expression. "." is "any character", so every character is a separator and there is nothing between them. The empty parts at the end are dropped, so the array is left with length 0, and the error shows up only on the next line, at parts[0]. The fix is split("\\.").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+", "***"));Delivery: 12 Shipka St., entrance B4: bread | milk | coffee | teaCode ***, 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"));donut and donutséclair and éclairsdonut and éclairsWithout \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
\Bis 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 withmatches, because it looks at the whole text anyway.- You can also give the flags as constants of the
Patternclass:Pattern.CASE_INSENSITIVEis the same as(?i), andPattern.UNICODE_CHARACTER_CLASSas(?U). We will use thePatternclass in the next lesson. "a|b".split("|")is a trap like the dot:|is "or", and the pattern is empty. Writesplit("\\|").
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(""));\\d{4}: exactly four digits. Because matches checks the whole text, three or five digits do not pass.Exercise
Checks with a pattern
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].
01/01/202032/01/202015/05/202101/01/2020 - Valid32/01/2020 - Invalid15/05/2021 - Valid2. 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.
0888 123 456+359 87 765 432102 812 34560899-12-3450888 123 456 - valid+359 87 765 4321 - valid02 812 3456 - invalid0899-12-345 - invalid3. 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).
elena.koleva@company.comelena@company@company.comelena koleva@company.comelena.koleva@company.com - yeselena@company - no@company.com - noelena koleva@company.com - no4. Shopping list. The products are separated with commas, semicolons, vertical bars or spaces, sometimes several at once. Print them numbered.
bread, milk;coffee tea|cheese1. bread2. milk3. coffee4. tea5. cheeseMore 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 isjava.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, from00:00to23:59. - Digits only. From
"Tel.: +359 (88) 812-34-56"keep only the digits and the+, with onereplaceAlland[^...].
Test