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 KarunaratneIshan Karunaratne⏱️ 8 min readUpdated
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 the full ISO 8601 with time. Below I walk all of them, with runnable code in JavaScript, Python, and PHP, engine notes, common bugs, and the leap-year case regex cannot handle.

The thing regex genuinely cannot do is reject 2025-02-30 (February 30, which never exists) or determine whether 2024-02-29 is valid (leap year) versus 2023-02-29 (not). Those checks belong in a date parser. Regex narrows the input to "date-shaped"; the parser confirms it is a real day.

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}$

ISO 8601 datetime 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)$

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). Stricter day-validation requires a date parser.

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

For full timestamps 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 (!isoDate.test(input)) 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.match(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 (!preg_match('/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/', $value)) {
        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

These patterns use only universal features (anchors, character classes, alternation). They run unmodified everywhere. The per-engine notes are about the parser you call after the regex passes.

EnginePractical patternsNative date parser
JavaScriptWorksnew Date(s) for ISO 8601. Returns Invalid Date on bad input; check !isNaN(d).
Python (re)Worksdatetime.strptime(s, fmt). Raises ValueError. Or datetime.fromisoformat(s) for ISO.
PHP (PCRE)WorksDateTime::createFromFormat('Y-m-d', $s). Returns false on parse failure.
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 the regex to validate the actual day. It can't. 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.

What regex cannot validate (leap years, days-in-month)

Regex has no arithmetic. It can confirm that a string is in the right shape but cannot determine:

  • 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

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.

TagsRegexDate ValidationISO 8601Regular 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 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 a hex color code with regex. 3-digit, 6-digit, and 8-digit (alpha) forms. Case-insensitive. JavaScript / Python / PHP examples, engine notes, common mistakes, test cases.

How to Match a Hex Color Code with Regex

Match a hex color code with regex. 3-digit, 6-digit, and 8-digit (alpha) forms. JavaScript / Python / PHP examples, engine notes, common mistakes, a stripped-hash variant.