Say this plainly before anything else
This pattern will accept 2026-02-30 — a date that
does not exist, because February never has thirty days. It checks that the month is between
01 and 12 and the day is between 01 and 31, but a regex has no concept of which months have
30 days, which have 31, which have 28 or 29, or which years are leap years. Knowing that
requires actual calendar logic: construct a real date object from the three numbers and
check that it round-trips back to the same year, month and day. This page’s tester
does not do that extra step either — it is a plain regex tester, testing exactly the
pattern above and nothing more, which is precisely why this admission matters.
Token by token
| Token | Meaning |
|---|---|
^ | Start of the string |
\d{4} | Exactly four digits — the year. Accepts any four digits, including 0000; ISO 8601 permits years outside the common era with an extended syntax this pattern does not implement |
- | A literal hyphen separator |
(0[1-9]|1[0-2]) | The month: either 01–09, or 10–12 — together, exactly 01 through 12, zero-padded |
- | Another literal hyphen |
(0[1-9]|[12]\d|3[01]) | The day: 01–09, or 10–29 (a leading 1 or 2 followed by any digit), or 30–31 |
$ | End of the string |
Note what the day group does and does not encode: it allows every day from 01 to 31
regardless of which month precedes it. The month and day groups do not reference each other
at all — there is no way, in a single linear regex without lookahead tied to a specific
captured month value (which JavaScript regex cannot do; conditional patterns based on an
earlier capture’s value are not part of its feature set), to say “day ≤ 30 if
the month was 04, 06, 09, or 11, day
≤ 28 or 29 if the month was 02”. That cross-field logic is exactly what
constructing a real date and checking it round-trips gets you for free.
What it matches
2026-07-22— today, in this format.1999-12-31— the last day of a year.2000-02-29— a genuine leap day, and this pattern has no idea it is special; it just sees a day of 29, which is ≤ 29 for any month starting with1or2, so it passes for the wrong reason.
What it deliberately does not match, and why
| Input | Verdict | Why |
|---|---|---|
2026-13-01 | Rejected | Month 13 does not exist — the month group only reaches 12 |
26-07-22 | Rejected | Two-digit year — \d{4} requires exactly four digits, and ISO 8601’s basic and extended calendar-date forms both use a four-digit year |
2026/07/22 | Rejected | Slashes instead of hyphens — a different, non-ISO separator convention common in US-formatted dates |
2026-00-15 | Rejected | Month 00 does not exist — the month group starts at 01 |
2026-2-9 | Rejected | Not zero-padded — ISO 8601’s calendar-date form requires two digits for both month and day, so a single-digit month or day fails, correctly |
2026-02-30 | Accepted | Does not exist on any calendar — see above. This is the pattern’s central, unavoidable limitation, not a bug to be patched with a longer regex |
The three-line fix for the calendar problem
Regex can constrain digits; only a calendar library or a language’s own date type can confirm a date exists. In JavaScript, after this pattern matches, a short round-trip check catches every impossible date it lets through:
function isRealCalendarDate(y, m, d) {
const dt = new Date(Date.UTC(y, m - 1, d));
return dt.getUTCFullYear() === y && dt.getUTCMonth() === m - 1 && dt.getUTCDate() === d;
}
// isRealCalendarDate(2026, 2, 30) -> false (JS rolls it over to March 2nd)
// isRealCalendarDate(2000, 2, 29) -> true (2000 is a leap year)
// isRealCalendarDate(2100, 2, 29) -> false (2100 is not — divisible by 100 but not 400)
The reason this works: Date.UTC does not reject an out-of-range day, it
rolls it forward — February 30 quietly becomes March 2. Reading the resulting
year, month and day back out and comparing them to what you asked for turns that silent
rollover into an explicit, checkable failure. This is also why regex alone cannot replicate
it: the correction depends on knowing the exact length of every month in a specific year,
including the 2100-is-not-a-leap-year exception the third example shows, which is a rule
about divisibility by 4, then 100, then 400 — arithmetic a linear digit pattern has no
way to express.
Flavour: this is JavaScript’s regex
This pattern targets JavaScript’s built-in
RegExp,
per ECMA-262.
It uses only literals, character classes, {n} and alternation — all
identical in PCRE and Python’s re, so this exact text ports unchanged.
The date format itself is standardised separately, in
RFC 3339,
which profiles ISO 8601 for internet use and is the specification most APIs actually mean
when they say “ISO date”. RFC 3339 requires the same
YYYY-MM-DD calendar-date form this pattern matches, and additionally defines the
full YYYY-MM-DDThh:mm:ssZ timestamp form, which this pattern does not attempt
— it is scoped to the date portion only. See the
cookbook’s flavour-honesty section for constructs that
differ between JavaScript, PCRE and Python.
Worked examples
1. Rejecting an obviously wrong month or day before the round trip
Running the regex first and the calendar check second is not redundant: the regex
rejects 2026-13-45 instantly with zero date-object construction, while the
round-trip check alone would need extra range-checking code to reject a month of 13
cleanly (Date.UTC rolls an out-of-range month forward too, into the following
year, rather than throwing). Layering the two — regex for shape, then a real date
for calendar validity — is more precise than either alone.
2. Extracting a date from a longer timestamp string
Drop the ^/$ anchors and this same day-and-month logic can
pull the date portion out of a full timestamp like
2026-07-22T14:03:11Z — the pattern matches the first ten characters and
stops, leaving the time portion for a separate, dedicated timestamp pattern or parser.
3. Why leap-year logic belongs nowhere near a regex
The Gregorian leap-year rule is: divisible by 4, except centuries, except again every
400 years. 2000 is a leap year; 1900 and 2100 are
not. Expressing that as a regex over the digits of a year would require checking modular
arithmetic on an arbitrary four-digit number — something regular expressions, by
their formal definition, cannot do in general (this is precisely the kind of problem
regular languages are proven incapable of recognising). That is not a shortcoming of this
particular pattern; it is a hard boundary of what pattern matching can express at all,
which is why the round-trip check above hands the problem to real arithmetic instead.
4. Sorting dates as strings, safely
One quiet advantage of the zero-padded YYYY-MM-DD shape this pattern
enforces: strings in that exact format sort correctly as plain text, in the same order as
the dates they represent, with no parsing required —
"2026-02-05" < "2026-10-01" is true as a string comparison precisely
because the month is zero-padded to two digits. This pattern’s insistence on
0[1-9]|1[0-2] rather than accepting a bare 2-9 is what preserves
that property; a validator that allowed unpadded months would let through dates that sort
incorrectly as text even though they are perfectly readable to a person.
Using this pattern in your own code
The pattern uses only literals, character classes, alternation and {n}
quantifiers, so the text is identical across the environments most projects need it in.
// JavaScript
const ISO_DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
ISO_DATE_RE.test('2026-07-22'); // true
ISO_DATE_RE.test('2026-13-01'); // false
# Python's re module — identical pattern text
import re
ISO_DATE_RE = re.compile(r'^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$')
bool(ISO_DATE_RE.match('2000-02-29')) # True — shape passes; see the round-trip
# check above for whether it is real
# Python's datetime module for the calendar check the regex cannot do:
from datetime import date
try:
date(2026, 2, 30)
except ValueError as e:
print(e) # day is out of range for month
# grep -P, scanning a data export for lines that are not date-shaped at all
grep -Pv '^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$' export.csv
The Python example above is the same two-step idea as the JavaScript round-trip check
earlier in this page, just using datetime.date’s own validation instead
of reimplementing it: the regex narrows the input to the right shape cheaply, and the
language’s own date type, which already encodes the full Gregorian calendar
correctly, makes the final call on whether the date exists.
Frequently asked questions
Why does this pattern accept February 30th?
Because a regex constrains digits, not calendar semantics — it has no concept of how many days February has in a given year. See the “three-line fix” above for the actual check.
Does it support the full ISO 8601 timestamp with time and time zone?
No — it is scoped to the calendar-date portion,
YYYY-MM-DD. A full timestamp needs a separate pattern for the
Thh:mm:ss part and its time-zone offset or Z suffix, which
introduces its own set of edge cases (leap seconds, fractional seconds, +hh:mm
versus -hh:mm) better handled as its own pattern than folded into this
one.
Is RFC 3339 the same thing as ISO 8601?
Closely related but not identical. RFC 3339 is a stricter profile
of ISO 8601 written specifically for use on the internet — it requires the
T separator and a numeric time-zone offset or Z, where ISO 8601
itself permits a few additional format variations RFC 3339 deliberately excludes for
interoperability. For the plain calendar-date form this page covers, the two agree.
Can I make it accept a two-digit year?
You could, but think twice: a two-digit year is ambiguous (is
26 1926 or 2026?) in a way ISO 8601 was specifically designed to avoid by
requiring four digits. If you must accept legacy two-digit years, resolve the ambiguity
explicitly in your own code (a common convention is a fixed pivot year) rather than
guessing inside the pattern.
Why not just use new Date(str) and skip the regex?
Because new Date(str) is far more permissive about
input format than you probably want for validation — it accepts a wide range of
loosely-specified strings, some of them parsed differently between browsers for anything
outside the ISO format specifically. Using the regex first to pin down the exact
YYYY-MM-DD shape, then the round-trip check for calendar validity, is more
predictable than relying on Date’s general-purpose parser alone.
Does the pattern handle years before 1000 or after 9999?
Years before 1000 pad to four digits (0099) and are
accepted; years at or above 10000 are not, since \d{4} is exactly
four digits. ISO 8601 has an extended syntax with an explicit + or a leading
digit count for years outside that range, which this pattern does not implement — a
vanishingly rare need outside historical or astronomical data.
My data sometimes has single-digit months, like 2026-7-22. Should I loosen the pattern?
Better to normalise the data than loosen the pattern: pad single
digits to two characters (String(month).padStart(2, '0')) before validating,
so every value reaching this check is already in the strict form. Loosening the pattern
itself to accept both padded and unpadded months means downstream code has to handle both
shapes forever, and it quietly breaks the string-sort property described above, where a
zero-padded month is what keeps lexical order matching calendar order.
Is my date sent anywhere when I test it here?
No. Matching runs inside a same-origin Web Worker, and there is no upload endpoint anywhere on this page — nothing typed into the test-text box leaves your browser.
Limitations
- Accepts calendar-impossible dates like
2026-02-30; pair with the round-trip check above for real calendar validity. - Date portion only — no time, no time zone.
- Four-digit year only; no support for ISO 8601’s extended year syntax.
- Requires zero-padded month and day; a single-digit month or day is rejected, by design.
Related
- Regex for an IPv4 addressAnother range-correct digit pattern, with the same “syntax, not proof” caveat
- Regex for phone numbersDigit-group validation for a different real-world format
- Regex for a URL slugA much simpler shape — no numeric ranges to get wrong
- About this toolWhy it exists and how the tester is built
- JSON formatterAnother text tool that runs entirely in your browser
- All toolsSee everything on this site