TechEarl

How to Match a Date with Regex (Multiple Formats)

Match a date with regex in ISO 8601, US, EU, and relaxed formats. Month/day validation, leap-year notes, JavaScript / Python / PHP examples, engine notes, common mistakes.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
Match a date with regex in ISO 8601, US (MM/DD/YYYY), EU (DD/MM/YYYY), and relaxed forms. Month/day validation, leap-year note. JavaScript / Python / PHP examples, engine notes, common mistakes.

The practical regex for ISO 8601 dates (YYYY-MM-DD): ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$. It rejects month 13, day 32, and the all-zeros date. There are equivalent patterns for US-style MM/DD/YYYY, EU-style DD/MM/YYYY, relaxed forms that accept single-digit months, and a common ISO-style timestamp with time. Below I walk all of them, with runnable code in JavaScript, Python, and PHP, engine notes, common bugs, and calendar validation after the format check.

The compact patterns here do not reject 2025-02-30 or enforce leap years. A more complex regex can encode those rules, but a strict date parser is easier to maintain. Regex narrows the input to "date-shaped"; the parser confirms it is a real day.

The executable validators below use years 0001–9999. The short shape regex alone also accepts year 0000; whether that year is meaningful depends on the calendar convention and parser.

Quick reference

ISO 8601 date:

code
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$

US (MM/DD/YYYY):

code
^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$

EU (DD/MM/YYYY):

code
^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$

Common ISO-style datetime shape with timezone:

code
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$

Use one explicit input format. 09/10/2026 is ambiguous without knowing whether the first field is month or day. A date format regex should not guess the user's locale. For a timestamp, validate the offset, calendar date, and application-supported precision after the shape check; the example is not a complete implementation of every ISO 8601 representation.

ISO 8601 date (YYYY-MM-DD)

code
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$
  • \d{4} is a 4-digit year. To restrict to a sensible range, use (19|20)\d{2} for 1900-2099.
  • (0[1-9]|1[0-2]) is the month: 01-09 or 10-12.
  • (0[1-9]|[12][0-9]|3[01]) is the day: 01-09, 10-29, or 30-31.

This pattern accepts 2025-02-30 and 2025-04-31 (days that do not exist in those months). Use a date parser for maintainable calendar validation.

US format (MM/DD/YYYY)

code
^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$

Same month and day sub-patterns, year on the right, separator is /. To allow - or . as alternative separators: replace \/ with [\/\-.] (and use the same separator both times by capturing it: ([\/\-.]) ... \1).

EU format (DD/MM/YYYY)

code
^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$

Same components as US, day-first instead of month-first. The classic ambiguity: 01/02/2025 could be January 2nd (US) or February 1st (EU). Always know which format your input is in before validating.

Relaxed: single-digit months and days

If the input might be 2025-2-9 (not zero-padded):

code
^\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])$

The ? after the leading zero makes it optional, so both 02 and 2 match.

ISO 8601 datetime with time and timezone

This pattern covers the timestamp shape used by examples like 2025-10-29T14:32:07Z or 2025-10-29T14:32:07-05:00:

code
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$

The added parts:

  • T separator between date and time.
  • (?:[01]\d|2[0-3]) for hour (00-23).
  • :[0-5]\d:[0-5]\d for minutes and seconds (00-59).
  • (?:\.\d+)? for optional fractional seconds.
  • (?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d) for the timezone: either Z for UTC or a +HH:MM / -HH:MM offset.

Examples in JavaScript, Python, and PHP

JavaScript:

javascript
const isoDate = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

function isValidIsoDate(input) {
  if (typeof input !== "string" || !isoDate.test(input) || input.startsWith("0000-")) return false;
  // Belt-and-braces: ensure it parses as a real date too
  const d = new Date(input);
  return !isNaN(d.getTime()) && input === d.toISOString().slice(0, 10);
}

isValidIsoDate("2025-10-29");  // true
isValidIsoDate("2025-02-30");  // false (regex passes, Date check fails)
isValidIsoDate("2025-13-01");  // false (regex fails)

Python:

python
import re
from datetime import datetime

ISO_DATE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$")

def is_valid_iso_date(value: str) -> bool:
    if not ISO_DATE.fullmatch(value):
        return False
    try:
        datetime.strptime(value, "%Y-%m-%d")
        return True
    except ValueError:
        return False

is_valid_iso_date("2024-02-29")  # True (leap year)
is_valid_iso_date("2023-02-29")  # False (not a leap year)

PHP:

php
function isValidIsoDate(string $value): bool {
    if (str_starts_with($value, '0000-') || preg_match('/\A[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])\z/', $value) !== 1) {
        return false;
    }
    $d = DateTime::createFromFormat('Y-m-d', $value);
    return $d !== false && $d->format('Y-m-d') === $value;
}

The pattern catches obvious format errors instantly; the date parser catches "valid format, impossible date" cases like February 30 or 2023-02-29.

Engine compatibility

Character classes and alternation are widely supported, but \d, delimiter escaping, and ending anchors differ across engines. Use [0-9] when the format requires ASCII digits, and a whole-string check before parsing.

EnginePractical patternsNative date parser
JavaScriptWorksnew Date(s) may normalize impossible dates; require a format round trip as shown above.
Python (re)Worksdatetime.strptime(s, fmt). Raises ValueError. Or datetime.fromisoformat(s) for ISO.
PHP (PCRE)WorksDateTime::createFromFormat('Y-m-d', $s) may normalize dates; compare the formatted result to the input.
JavaWorksLocalDate.parse(s) for ISO; DateTimeFormatter.ofPattern(fmt).parse(s) for custom.
.NETWorksDateTime.TryParseExact(s, fmt, CultureInfo.InvariantCulture, DateTimeStyles.None, out var d).
Go (RE2)Workstime.Parse("2006-01-02", s). The reference time is a Go quirk.
Rust (regex crate)Workschrono::NaiveDate::parse_from_str from the chrono crate.
RubyWorksDate.parse(s) (lenient) or Date.strptime(s, fmt) (strict).
POSIX ERE (grep -E)Works (replace \d with [0-9])None; use date -d for parsing

I prefer fromisoformat in Python 3.11+ and LocalDate.parse in Java: they both reject 2025-02-30 automatically without needing the round-trip check.

Common mistakes

The bugs I see most often.

Trusting this short regex to validate the actual day. It only checks the listed field ranges. 2025-02-30 and 2025-04-31 match the regex but those days do not exist. Always pair the regex with a date parser if the input has to be a real day.

Two-digit years. Patterns like ^\d{2}-\d{2}-\d{2}$ accept 25-10-29 but you cannot tell if "25" is 1925 or 2025. Always require a 4-digit year unless you have a hard reason not to.

Hardcoded separators. A pattern with literal / rejects 2025-10-29 (hyphens). Either pick one format per input field, or use a character class [\/\-.] and capture it with a backreference to require both sides match.

Forgetting the anchors. \d{4}-\d{2}-\d{2} matches 2025-10-29extra because nothing pins the end. Always anchor for validation.

Locale-ambiguous parsing. 01/02/2025 could be January 2nd or February 1st depending on locale. Never accept this format from international users without an explicit locale signal.

Treating a passing regex as a parsed value. The regex returns true/false. It does NOT give you a usable Date object. Run the parser after the regex to get the actual date for arithmetic and comparison.

Calendar validation: leap years and days in each month

A sufficiently detailed regex can encode calendar rules, but the short patterns above intentionally leave these checks to a date parser:

  • February 29 is only valid in years divisible by 4 (except century years not divisible by 400).
  • February 30 and February 31 never exist.
  • April, June, September, and November have 30 days; everything else has 31 except February.

To enforce these, run the regex first and then parse the string as a date in your language. If the parse succeeds and round-trips back to the same string, the date is real. This is the pattern shown in all three code examples above.

Test cases

InputISO regexAfter Date parse
2025-10-29MatchValid
2024-02-29MatchValid (leap year)
2023-02-29MatchInvalid (not a leap year)
2025-02-30MatchInvalid (never exists)
2025-13-01No matchNot reached
2025-00-15No matchNot reached
2025-04-31MatchInvalid (April has 30 days)
25-10-29No matchNot reached (2-digit year)

FAQ

A complex regex can, but these short patterns only confirm that the input looks like a date (right separators, year/month/day in valid ranges) but cannot enforce that February 29 is only valid in leap years or that April has 30 days, not 31.

The pattern: regex narrows to "date-shaped", then a date parser confirms it is a real day. All three language examples in this article follow that pattern.

Use a character class for the separator: [\/\-.]. To require the same separator on both sides, capture it: ([\/\-.])(0[1-9]|1[0-2])\1(0[1-9]|[12][0-9]|3[01]) where \1 backreferences the first separator.

Be aware that 01.02.2025 is unambiguous (EU dot format) but 01/02/2025 is ambiguous between US and EU.

Add a T separator and the time/timezone parts: ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$.

This matches 2025-10-29T14:32:07Z and 2025-10-29T14:32:07-05:00 but not the older YYYYMMDDTHHMMSS compact form.

Alternate the two patterns: ^((0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])|(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2]))\/\d{4}$.

But you cannot disambiguate them: 01/02/2025 matches both. If you accept either, you need an out-of-band signal (a locale, a form field saying which) to interpret the result.

Because regex day-validation only checks the 01-31 range; it does not know that February ends at 28 or 29. The bounded day part (0[1-9]|[12][0-9]|3[01]) happily accepts 30.

Validate with a date parser after the regex passes. new Date("2025-02-30") in JavaScript can normalize to a date in March; compare the formatted result back to the original input; strptime in Python raises ValueError. Parsing behavior differs, which is why the JavaScript example includes a round-trip check.

Year only: ^(19|20)\d{2}$. Month-year (e.g., credit card expiry): ^(0[1-9]|1[0-2])\/(?:[0-9]{2}|[0-9]{4})$ for "MM/YY" or "MM/YYYY".

For "YYYY-MM" use ^\d{4}-(0[1-9]|1[0-2])$. This is a reduced-precision calendar date, not an ordinal date.

Yes. The pattern in this article includes (?:\.\d+)? which allows an optional dot and one-or-more digits after the seconds. 2025-10-29T14:32:07.123Z and ...07.123456789Z both match.

If you need to cap the fractional precision (some databases reject more than 6 digits), tighten the quantifier: (?:\.\d{1,6})?.

See also

External reference: the ISO 8601 standard summary covers the full date-and-time formats. Try the patterns at regex101.com with your own input strings.

Sources

Authoritative references this article was fact-checked against.

TagsRegexDate ValidationISO 8601Regular ExpressionsJavaScriptPythonPHP

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

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 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 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.