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⏱️ 8 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 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

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though 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 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.