TechEarl

How to Match Numbers with Regex

Match integers, decimals, signed, scientific, thousands-separated, currency, and percent numbers with regex. JavaScript / Python / PHP examples, engine notes, common mistakes, test table.

Ishan KarunaratneIshan Karunaratne⏱️ 8 min readUpdated
Match integers, decimals, signed, scientific, thousands-separated, currency, and percent numbers with regex. JavaScript / Python / PHP examples, engine notes, common mistakes, test table.

The shortest regex for a positive integer: ^\d+$. For a signed integer (positive, negative, or zero): ^-?\d+$. For a decimal that may or may not have a fractional part: ^-?\d+(\.\d+)?$. The patterns get more specific from there: scientific notation, thousands-separated, percent-suffixed, currency-prefixed. Below I walk all of them with runnable code in JavaScript, Python, and PHP, engine notes, the bugs I see most often, and the case where regex stops being the right tool.

The fact you can have a different regex for "number" depending on context (form input vs CSV column vs log line) is why this question keeps coming up. The right pattern is the one that matches the exact subset your input might contain.

Quick reference

code
Positive integer:                ^\d+$
Signed integer:                  ^-?\d+$
Signed (explicit +/- allowed):   ^[+-]?\d+$
Non-zero positive integer:       ^[1-9]\d*$
Positive decimal:                ^\d+(\.\d+)?$
Signed decimal:                  ^-?\d+(\.\d+)?$
Scientific:                      ^-?\d+(\.\d+)?([eE][-+]?\d+)?$
US thousands-separated:          ^-?\d{1,3}(,\d{3})*(\.\d+)?$
Currency (USD):                  ^\$?-?\$?\d+(,\d{3})*(\.\d{2})?$
Percent:                         ^-?\d+(\.\d+)?%$

The basic patterns

code
Positive integer:        ^\d+$
Signed integer:          ^-?\d+$
Signed-or-unsigned:      ^[+-]?\d+$
Integer with leading 0:  ^0\d+$
Non-zero positive:       ^[1-9]\d*$
  • \d matches a digit. \d+ requires at least one. ^ and $ anchor to the full string.
  • -? makes the minus sign optional.
  • [+-]? allows either explicit sign.
  • [1-9]\d* enforces no leading zero (the first digit cannot be 0).

The non-zero-leading version is useful when you don't want 0123 interpreted as 123 (which would be the case if the application parsed it as an integer).

Decimal numbers

code
Positive decimal:                     ^\d+(\.\d+)?$
Decimal with optional integer part:   ^(\d+\.\d+|\.\d+|\d+\.?)$
Signed decimal:                       ^-?\d+(\.\d+)?$
Decimal with fixed precision (2dp):   ^-?\d+\.\d{2}$

The bare positive form accepts integers too because the fractional part is optional. The "optional integer part" form additionally accepts .5 (no leading digit) and 5. (trailing dot). Most form fields use the first form.

The fixed-precision form is useful for currency: ^-?\d+\.\d{2}$ requires exactly two decimal places.

Scientific notation

code
^-?\d+(\.\d+)?([eE][-+]?\d+)?$

Adds an optional exponent part: e or E, optional sign, one or more digits. Matches 1.5e10, -2.7E-9, and plain 42 (the exponent is optional).

Thousands-separated numbers (1,234,567.89)

The pattern depends on the locale's separator. For US-style commas:

code
^-?\d{1,3}(,\d{3})*(\.\d+)?$
  • \d{1,3} for the first group (1-3 digits before any comma).
  • (,\d{3})* for zero or more comma-prefixed three-digit groups.
  • (\.\d+)? for the optional decimal part.

This matches 1,234, 1,234,567.89, and rejects 1,2345 (wrong grouping) and ,123 (leading comma). For European locales that use . as thousands and , as decimal, swap them.

Currency and percent

code
USD currency:    ^\$?-?\$?\d+(,\d{3})*(\.\d{2})?$
Percent:         ^-?\d+(\.\d+)?%$

The currency pattern allows the $ either before the sign or after, since both $-50 and -$50 are seen in the wild.

Examples in JavaScript, Python, and PHP

JavaScript:

javascript
const patterns = {
  integer:    /^-?\d+$/,
  decimal:    /^-?\d+(\.\d+)?$/,
  scientific: /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/,
  currency:   /^\$?-?\$?\d+(,\d{3})*(\.\d{2})?$/,
};

function classify(input) {
  for (const [name, pattern] of Object.entries(patterns)) {
    if (pattern.test(input)) return name;
  }
  return null;
}

classify("42");          // 'integer'
classify("-3.14");       // 'decimal'
classify("1.5e10");      // 'scientific'
classify("$1,234.56");   // 'currency'

Python:

python
import re

INTEGER    = re.compile(r"^-?\d+$")
DECIMAL    = re.compile(r"^-?\d+(\.\d+)?$")
SCIENTIFIC = re.compile(r"^-?\d+(\.\d+)?([eE][-+]?\d+)?$")

def is_number(value: str) -> bool:
    return bool(SCIENTIFIC.match(value))

is_number("42")       # True
is_number("-3.14")    # True
is_number("1.5e10")   # True
is_number("3.14.15")  # False

PHP:

php
function isNumber(string $value): bool {
    return (bool) preg_match('/^-?\d+(\.\d+)?([eE][-+]?\d+)?$/', $value);
}

// Or use the built-in:
is_numeric("42");       // true
is_numeric("-3.14");    // true
is_numeric("0x1A");     // true (also accepts hex, regex above does not)
is_numeric("1e10");     // true (scientific)

In PHP, is_numeric is broader than the regex (it accepts hex and binary in some versions). Use the regex when you want to lock down exactly which numeric forms are acceptable.

Engine compatibility

These patterns use only universal features. They run unmodified everywhere. The per-engine notes are about parse helpers when the regex passes and you want the actual numeric value.

EnginePractical patternNative parse helper
JavaScriptWorksNumber(s) and parseFloat(s). Number.isFinite(Number(s)) filters non-numbers.
Python (re)Worksint(s), float(s), decimal.Decimal(s). Raise ValueError on bad input.
PHP (PCRE)Worksis_numeric($v) is broader than the regex (accepts hex/octal in older versions).
JavaWorksInteger.parseInt, Double.parseDouble, BigDecimal.new.
.NETWorksint.TryParse, decimal.TryParse(s, NumberStyles.Currency, ...).
Go (RE2)Worksstrconv.Atoi, strconv.ParseFloat. No lookaround so the strict patterns below stay simple.
Rust (regex crate)Workss.parse::<i64>(), s.parse::<f64>().
RubyWorksInteger(s) and Float(s) raise on invalid. s.to_i silently accepts garbage.
POSIX ERE (grep -E)Works (replace \d with [0-9])None; combine with shell arithmetic

I usually run the regex first to confirm the input is shaped right, then Number(s) (JavaScript) or Decimal(s) (Python) for the actual numeric conversion. Skipping the regex and relying on Number(s) alone is also fine if you don't mind NaN for invalid input.

Common mistakes

The bugs I see most often.

Forgetting the second anchor. ^\d+ matches 42abc because nothing pins the end. For validation, anchor both ends.

Treating \d as Unicode-aware. In most engines, \d matches ASCII 0-9 only by default. In .NET it can match Unicode digits depending on options; in PCRE with (*UCP) mode it does too. If your input might include (full-width digit) or Arabic-Indic numerals, decide whether to accept them and choose the engine flag accordingly.

Not handling the explicit + sign. ^-?\d+$ rejects +42 because + is not in the pattern. Use ^[+-]?\d+$ if you want to accept either explicit sign.

Accepting leading zeros silently. Some applications interpret a leading zero as octal. ^[1-9]\d*$ or ^(0|[1-9]\d*)$ are the safer forms for inputs the application will later parse as an integer.

Locale confusion. US uses , for thousands and . for decimal. Most of Europe uses . for thousands and , for decimal. The pattern needs to match the locale of the input, not the locale of the developer.

Trusting the regex to validate the range. ^\d+$ matches 99999999999999999999999999 (way past 64-bit). If your downstream code is int(s), that throws; if it's Number(s), you get Infinity or a loss-of-precision float. Add a length cap or parse and check.

Test cases

InputIntegerDecimalScientific
42MatchMatchMatch
-42MatchMatchMatch
+42No matchNo matchNo match
3.14No matchMatchMatch
-3.14No matchMatchMatch
.5No matchNo matchNo match
5.No matchNo matchNo match
1.5e10No matchNo matchMatch
1,234No matchNo matchNo match
0123MatchMatchMatch
1eNo matchNo matchNo match

FAQ

See also

External reference: paste the patterns into regex101.com to test against your own inputs.

TagsRegexNumber ValidationIntegerDecimalRegular ExpressionsJavaScriptPythonPHP
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Match a URL with regex. http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments. JavaScript / Python / PHP examples, engine notes, parser alternative, common mistakes, test table.

How to Match a URL with Regex

Match a URL with regex. Covers http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments, runnable JavaScript / Python / PHP, engine notes, and the URL parser alternative.

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

How to Match a Domain Name with Regex

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

Match an email address with regex. Practical pattern, strict RFC 5321 pattern, JavaScript / Python / PHP examples, edge cases, engine compatibility, common mistakes, and a test table.

How to Match an Email Address with Regex

Match an email address with regex. The practical pattern, the strict RFC 5321 pattern, examples in JavaScript, Python, and PHP, edge cases, engine compatibility, common mistakes, and a validation test table.