Java Regex to Validate and Format US Phone Numbers

Validate US and Canadian phone numbers typed in any common style with one Java regex that follows the NANP rules, then rewrite them into one display format or E.164 with capturing groups and replaceAll().

A North American phone number has 10 digits in three parts: a 3-digit area code, a 3-digit exchange code and a 4-digit line number, as in (202) 555-0123. With one Java regex we can validate US and Canadian numbers typed in any common style, such as “202.555.0123” or “+1 202 555 0123”, and capture the three parts in groups. A replacement string like ($1) $2-$3 then rewrites every valid input into one standard format.

This article shows how to validate and format US phone numbers with Java regex: the common 3-3-4 pattern, the North American Numbering Plan (NANP) digit rules, balanced parentheses, an optional +1, extensions, and reformatting numbers inside longer text with replaceAll(). All sample numbers use the 555-01xx range, which is reserved for fictional use.

The constants BASIC and NANP are compiled once; the helper methods format(), toE164() and reformatInText() wrap them and appear in full later.

Pattern NANP = Pattern.compile(
    "^(?:\\+?1[-. ]?)?(?:\\(([2-9]\\d{2})\\)|([2-9]\\d{2}))[-. ]?([2-9]\\d{2})[-. ]?(\\d{4})$");

BASIC.matcher("123-456-7890").matches();          // true
NANP.matcher("123-456-7890").matches();           // false, area code starts with 1
NANP.matcher("+1 (202) 555-0123").matches();      // true

format("202.555.0123");                           // "(202) 555-0123"
toE164("(202) 555-0123");                         // "+12025550123"
reformatInText("Call 202.555.0123 or 312 555 0199.");
                                                  // "Call (202) 555-0123 or (312) 555-0199."

1. The Common 3-3-4 Pattern

Most tutorials, including the earlier version of this one, use a regex that accepts three digit groups with optional parentheses and an optional separator between the groups:

Pattern BASIC = Pattern.compile("^\\(?(\\d{3})\\)?[-. ]?(\\d{3})[-. ]?(\\d{4})$");
PartMeaning
\(?An optional opening parenthesis
(\d{3})Group 1: three digits (area code)
\)?An optional closing parenthesis
[-. ]?An optional hyphen, dot or space
(\d{3})Group 2: three digits (exchange code)
(\d{4})Group 3: four digits (line number)

Parentheses in a regex create capturing groups. A capturing group stores the text it matched, and the replacement string refers to group 1 as $1, group 2 as $2 and so on. Matcher.replaceFirst() uses those references to build a new string:

BASIC.matcher("202.555.0123").replaceFirst("($1) $2-$3");   // "(202) 555-0123"
BASIC.matcher("hello").replaceFirst("($1) $2-$3");          // "hello", no match

When the pattern does not match, replaceFirst() returns the input unchanged, so we check matches() before formatting. Otherwise, invalid input passes through and looks like it was accepted.

2. What the Basic Pattern Gets Wrong

The basic pattern counts digits but knows nothing about real phone numbers. It has three gaps:

  • Each parenthesis is optional on its own, so “(202-555-0123” and “202) 555-0123” pass.
  • Any digit can start the area code or the exchange code, so “123-456-7890” passes, although no North American number starts with 1.
  • A leading country code, as in “+1 202 555 0123” or “1-202-555-0123”, fails.

The second gap comes from the numbering plan. The North American Numbering Plan covers 20 countries that share country code 1, including the US and Canada. NANPA, the plan administrator, writes the format as NXX-NXX-XXXX, where N is a digit from 2 to 9 and X is any digit.

3. A Regex That Follows the NANP Rules

The improved pattern fixes all three gaps. Each change is one small piece:

PartMeaning
(?:\+?1[-. ]?)?Optional country code: “+1”, “1”, “+1 ” or “1-“
\(([2-9]\d{2})\)Group 1: area code inside both parentheses
|([2-9]\d{2})or group 2: area code without parentheses
[-. ]?([2-9]\d{2})Group 3: exchange code, first digit 2 to 9
[-. ]?(\d{4})$Group 4: line number

The alternation (?:\(…\)|…) is the part that enforces balanced parentheses. Either both parentheses are present around the area code, or neither is. The outer (?:…) is a non-capturing group: it groups the two choices without storing a match of its own.

Pattern NANP = Pattern.compile(
    "^(?:\\+?1[-. ]?)?"
    + "(?:\\(([2-9]\\d{2})\\)|([2-9]\\d{2}))"
    + "[-. ]?([2-9]\\d{2})[-. ]?(\\d{4})$");

Running both patterns on the same inputs shows where they differ. The last column is the output of the format() method from section 4, where null means “not a valid number”.

InputBASICNANPFormatted
2025550123truetrue(202) 555-0123
202-555-0123truetrue(202) 555-0123
202.555.0123truetrue(202) 555-0123
202 555 0123truetrue(202) 555-0123
(202) 555-0123truetrue(202) 555-0123
(202)555-0123truetrue(202) 555-0123
+1 202 555 0123falsetrue(202) 555-0123
1-202-555-0123falsetrue(202) 555-0123
(202-555-0123truefalsenull
202) 555-0123truefalsenull
123-456-7890truefalsenull
202-155-0123truefalsenull
202-555-012falsefalsenull
202–555-0123falsefalsenull
+44 20 7946 0958falsefalsenull

The NANP regex checks the digit rules, not whether the area code is in service. Codes such as 911 (the N11 service codes) and unassigned area codes still have a valid shape. For a real check of assigned area codes, use libphonenumber, covered in the international phone numbers article.

4. Formatting a Valid Number

The area code ends up in group 1 or in group 2, depending on whether the input had parentheses. The other group did not take part in the match, so it holds null:

Matcher m = NANP.matcher("(202) 555-0123");
m.matches();        // true
m.group(1);         // "202"
m.group(2);         // null
m.group(3);         // "555"
m.group(4);         // "0123"

Matcher m2 = NANP.matcher("202-555-0123");
m2.matches();       // true
m2.group(1);        // null
m2.group(2);        // "202"

In a replacement string, a group that did not participate is replaced by an empty string. So $1$2 always produces the area code, whichever group holds it. That gives one format() method for every input style:

Matcher m = NANP.matcher(input.strip());
return m.matches() ? m.replaceFirst("($1$2) $3-$4") : null;

The same groups produce any other output style by changing only the replacement string:

NANP.matcher("202 555 0123").replaceFirst("($1$2) $3-$4");   // "(202) 555-0123"
NANP.matcher("202 555 0123").replaceFirst("$1$2-$3-$4");     // "202-555-0123"
NANP.matcher("202 555 0123").replaceFirst("$1$2.$3.$4");     // "202.555.0123"
NANP.matcher("202 555 0123").replaceFirst("+1$1$2$3$4");     // "+12025550123"

For storage, we prefer the last one. “+12025550123” is the E.164 form, which SMS and voice APIs expect and which compares correctly as a string. The display format can then change without touching stored data.

5. Accepting an Extension

Office numbers often carry an extension, typed as “x45”, “ext 45” or “ext. 45”. An optional group at the end of the pattern captures it, and the CASE_INSENSITIVE flag also accepts “EXT” and “X”.

Pattern NANP_EXT = Pattern.compile(
    "^(?:\\+?1[-. ]?)?(?:\\(([2-9]\\d{2})\\)|([2-9]\\d{2}))"
    + "[-. ]?([2-9]\\d{2})[-. ]?(\\d{4})"
    + "(?: ?(?:x|ext\\.?) ?(\\d{1,5}))?$", Pattern.CASE_INSENSITIVE);

NANP_EXT.matcher("202-555-0123 x45").matches();         // true, group(5) = "45"
NANP_EXT.matcher("202-555-0123 ext. 45").matches();     // true, group(5) = "45"
NANP_EXT.matcher("(202) 555-0123 EXT 45").matches();    // true, group(5) = "45"
NANP_EXT.matcher("202-555-0123 ext").matches();         // false, no digits
NANP_EXT.matcher("202-555-0123").matches();             // true, group(5) = null

The limit of 5 digits for the extension is our choice for this example; company phone systems differ, so adjust \d{1,5} to fit.

6. Reformatting Numbers Inside Text

To clean up numbers in a longer text, such as notes or an imported file, we cannot use ^ and $, because the number is only part of the input. Word boundaries, written \b, take their place: a word boundary matches the position between a word character (a letter, a digit or _) and a character that is not one, or the start or end of the input. The article on word boundaries explains them in detail.

Pattern IN_TEXT = Pattern.compile("\\b([2-9]\\d{2})[-. ]([2-9]\\d{2})[-. ](\\d{4})\\b");

String text = "Call 202.555.0123 or 312 555 0199. Order 1234567890 ships today.";

IN_TEXT.matcher(text).results().count();          // 2
IN_TEXT.matcher(text).replaceAll("($1) $2-$3");
// "Call (202) 555-0123 or (312) 555-0199. Order 1234567890 ships today."

In this pattern, the separators are required, not optional. Without them, any run of 10 digits, such as the order number “1234567890”, would also be treated as a phone number. Here it is left alone, both because it has no separators and because it starts with 1.

When the replacement needs logic, for example a lookup or a different format per area code, Matcher.replaceAll() also accepts a function that receives each match as a MatchResult:

IN_TEXT.matcher(text)
    .replaceAll(r -> "(" + r.group(1) + ") " + r.group(2) + "-" + r.group(3));

7. Trying the US Number Formatter

The example project on GitHub contains the four patterns and the three helper methods. Its main() method prints the comparison table from section 3, and the JUnit 6 tests assert every row of it, together with the group values, output formats, extensions and the text replacement. Java 25 and Maven are the only requirements.

mvn -q compile exec:java
mvn test

8. Conclusion

The common 3-3-4 phone regex accepts unbalanced parentheses and numbers that start with 1. Following the NANP rule NXX-NXX-XXXX, with an alternation for the parentheses and an optional \+?1, fixes both. Capturing groups and a replacement string such as ($1$2) $3-$4 then turn any accepted input into one display format, and +1$1$2$3$4 gives the E.164 form for storage.

9. References

The NANPA page defines the NXX-NXX-XXXX format; the JavaDocs cover the replacement syntax and group handling.

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.