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:
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$
US (MM/DD/YYYY):
^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$
EU (DD/MM/YYYY):
^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}$
ISO 8601 datetime with timezone:
^\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)
^\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)
^(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)
^(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):
^\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:
^\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:
Tseparator between date and time.(?:[01]\d|2[0-3])for hour (00-23).:[0-5]\d:[0-5]\dfor minutes and seconds (00-59).(?:\.\d+)?for optional fractional seconds.(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)for the timezone: eitherZfor UTC or a+HH:MM/-HH:MMoffset.
Examples in JavaScript, Python, and PHP
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:
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:
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.
| Engine | Practical patterns | Native date parser |
|---|---|---|
| JavaScript | Works | new Date(s) for ISO 8601. Returns Invalid Date on bad input; check !isNaN(d). |
Python (re) | Works | datetime.strptime(s, fmt). Raises ValueError. Or datetime.fromisoformat(s) for ISO. |
| PHP (PCRE) | Works | DateTime::createFromFormat('Y-m-d', $s). Returns false on parse failure. |
| Java | Works | LocalDate.parse(s) for ISO; DateTimeFormatter.ofPattern(fmt).parse(s) for custom. |
| .NET | Works | DateTime.TryParseExact(s, fmt, CultureInfo.InvariantCulture, DateTimeStyles.None, out var d). |
| Go (RE2) | Works | time.Parse("2006-01-02", s). The reference time is a Go quirk. |
Rust (regex crate) | Works | chrono::NaiveDate::parse_from_str from the chrono crate. |
| Ruby | Works | Date.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
| Input | ISO regex | After Date parse |
|---|---|---|
2025-10-29 | Match | Valid |
2024-02-29 | Match | Valid (leap year) |
2023-02-29 | Match | Invalid (not a leap year) |
2025-02-30 | Match | Invalid (never exists) |
2025-13-01 | No match | Not reached |
2025-00-15 | No match | Not reached |
2025-04-31 | Match | Invalid (April has 30 days) |
25-10-29 | No match | Not reached (2-digit year) |
FAQ
See also
- How to Match Numbers with Regex: year, month, and day are all bounded-range numbers
- How to Match an Email Address with Regex: the cousin field on every signup form
- Regex Anchors: why
^and$matter so much for date validation - Regex Lookaheads and Lookbehinds: composing constraints like "must precede a time"
- Regex Capturing Groups and Backreferences: the
\1trick for "same separator on both sides" - Regex Cheat Sheet: the wider syntax and engine compatibility reference
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.





