Regex for Numbers, Phone Numbers and IP Addresses
Copy-ready patterns for numeric input, phone numbers and IP addresses, each tested, with notes on where a parser or library does the job better.
Numbers, phone numbers and IP addresses look like easy regex targets because they are mostly digits. Each has a trap: \d means different things in different languages, phone numbers follow national rules no pattern can capture, and an IP address is only valid if every part is in range. The patterns below were tested in Node.js unless another engine is named, and every accepted or rejected example is one we ran.
Numbers only
| What you want | Pattern | Accepts | Rejects |
|---|---|---|---|
| Digits only | ^[0-9]+$ | 123, 007 | -42, 3.14, a leading space, empty input |
| Integer, optional sign | ^[+-]?[0-9]+$ | -42, +42, 007 | 3.14, 1e3 |
| Integer, no leading zeros | ^-?(?:0|[1-9][0-9]*)$ | 0, -42 | 007, +42 |
| Decimal | ^[+-]?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$ | 3.14, -0.5, .5 | 5., 1e3, 1,000 |
| JSON-style number | ^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$ | 1.5E-7, -0 | .5, 01.5, +42 |
| Thousands separators | ^[0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]+)?$ | 1,000, 12,345,678.90, 100 | 1000, 1,00 |
A regex checks the shape, not the value. For a range such as 1 to 100, match the digits and then compare the number in code. Do not skip the regex and rely on conversion alone: in JavaScript Number('') is 0, Number(' 42 ') is 42, Number('0x1A') is 26, and parseInt('12abc') is 12.
Does \d only match 0 to 9?
Not everywhere. We tested ^\d+$ against the Arabic-Indic digits ٣٤:
| Engine | Matched non-ASCII digits? | To restrict to 0–9 |
|---|---|---|
| JavaScript | No, even with the u flag | Nothing needed; \p{Nd} with u matches all decimal digits |
| Python 3 (str patterns) | Yes, also fullwidth 12 | [0-9] or the re.ASCII flag |
| PHP (PCRE) | No; yes with the u modifier | [0-9] |
| .NET | Yes | [0-9] or RegexOptions.ECMAScript |
| Perl 5 | Yes | [0-9] or the /a modifier |
That matters when a value passes the regex and then reaches code that reads it differently. Python's int('٣٤') returns 34, while a database column or another service may reject it. Write [0-9] when you mean ASCII digits; it means the same thing in every engine.
Phone numbers
The one format with a simple, international definition is E.164: a plus sign, a country code that does not start with 0, and at most 15 digits in total.
^\+[1-9]\d{1,14}$ E.164, plus sign required (best for storage)
^\+?[1-9]\d{1,14}$ plus sign optional
With the plus required, +14155552671 and +442071838750 pass, while +0123456789, a 16-digit number and 004914155552671 (written with the 00 international prefix) fail. People type spaces, dots, dashes and brackets, so strip those first with value.replace(/[\s().-]/g, ''); that turns +1 415 555 2671 into +14155552671.
The limits show up quickly. The pattern checks shape only, so +12 passes even though no real number is that short. Worse, +49 (0)30 1234567 becomes +490301234567 after cleaning and passes, but it is the wrong number: the 0 in brackets is a domestic trunk prefix that must be dropped when dialling from abroad.
National formats are no better. The common US pattern ^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$ accepted (415-555-2671 with an unclosed bracket and 000-000-0000, which cannot be a real number. Every country has its own lengths, prefixes and mobile ranges, and they change. For real validation and formatting, use Google's libphonenumber or one of its ports: libphonenumber-js for JavaScript, phonenumbers for Python, giggsey/libphonenumber-for-php for PHP. Store the E.164 result, and confirm ownership with an SMS code if it matters.
IPv4 addresses
The popular shortcut ^\d{1,3}(?:\.\d{1,3}){3}$ accepted 256.1.1.1 and 999.999.999.999. To limit each part to 0–255, spell out the ranges:
^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$
We checked the octet part against every number from 0 to 999 written with one, two and three digits, and the full pattern against 200,000 random addresses; it agreed with Node's net.isIPv4() every time. It accepts 0.0.0.0, 192.168.1.1 and 255.255.255.255, and rejects 192.168.1, 192.168.1.1.1 and leading zeros such as 192.168.01.1.
Rejecting leading zeros is deliberate. Some parsers read them as octal: Python's socket.inet_aton('010.0.0.1') gave us the address 8.0.0.1. Node's net.isIPv4(), Python's ipaddress.ip_address() and PHP's filter_var($ip, FILTER_VALIDATE_IP) all rejected 192.168.01.1 too.
To find addresses inside a log line, drop ^ and $, add \b on both sides and use the g flag. On from 10.0.0.5 to 192.168.1.300 via 8.8.8.8 that returned 10.0.0.5 and 8.8.8.8. Without \b, the text 1192.168.1.300x produced a false match of 192.168.1.30. One limitation remains: in 1.2.3.4.5, the \b version still matches 1.2.3.4.
IPv6 addresses: use a parser
IPv6 allows :: to replace one run of zero groups, optional leading zeros, embedded IPv4 such as ::ffff:192.0.2.1 and zone IDs such as fe80::1%eth0. A regex for all of that is long and easy to get subtly wrong. A parser is shorter and better tested:
- Node.js:
net.isIPv6(value), ornet.isIP(value), which returns 4, 6 or 0. Both accepted embedded IPv4 and zone IDs. - Python:
ipaddress.ip_address(value)raisesValueErrorwhen invalid. It accepted embedded IPv4 and zone IDs. - PHP:
filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6). It accepted embedded IPv4 but rejectedfe80::1%eth0. - Browsers: there is no built-in IP validator, so use a library such as
ipaddr.js.
If you still need a regex, for example in a schema or configuration file that only accepts one, this 408-character pattern covers the hexadecimal forms and deliberately rejects embedded IPv4 and zone IDs. On 500,000 generated and mutated strings without dots or percent signs, it agreed with net.isIPv6() on every one.
^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))$
Mistakes that let bad input through
- Missing anchors.
/\d+/.test('abc123')istruebecause it finds digits anywhere. Validation needs^and$. - Alternation outside a group.
^cat|dog$means "starts with cat, or ends with dog", so it matchedcats and dogs!. Write^(?:cat|dog)$. - The
mflag. With it,^and$match at every line, so/^\d+$/maccepted the two-line string"abc\n123". Never usemwhen validating a single value. $before a final newline. In Python, PHP and .NET,^\d+$accepted"42\n". Usere.fullmatch()in Python, theDmodifier in PHP, or\zin PHP and .NET. JavaScript rejected it.- The
gflag withtest(). A regex withgrememberslastIndex, so calling/^\d+$/g.test('42')three times on the same object returned true, false, true. - Unanchored alternatives in the wrong order. Searching
255.255.255.255with the octet written as(?:\d|1\d\d|2[0-4]\d|25[0-5])matched only255.255.255.2, because the engine takes the first alternative that works. Put longer alternatives first, or anchor the pattern.
Test your pattern before shipping it
Paste a pattern into the Regex Tester without the slashes, set the flags to gm and enter your samples one per line: valid ones, invalid ones and the awkward cases from this page. Each line that matches is highlighted. The tester runs your browser's JavaScript engine, so recheck the \d and $ differences above if the pattern will run in Python, PHP or .NET. Validating email addresses instead? See Regex for Email Validation.