Check whether your own dates match ISO 8601

Paste the dates you actually have — from an API response, a spreadsheet, a form field — into the tester below and see which match the YYYY-MM-DD shape and which don’t. The sample already loaded mixes three genuinely valid dates with a wrong separator and a bad month that correctly fail, and one date — 2026-02-30 — that this pattern accepts even though it does not exist on any calendar. Why, is explained underneath, along with the copy-paste pattern itself.

/ / gm
Flags

    Your pattern and test text never leave this tab. Matching runs in a same-origin Web Worker with a timeout, so a date pasted in from a real record is never sent anywhere. That protection matters more than it looks: a single regular expression once caused a real, global outage — the timeout above is what keeps this page safe from the same failure mode.

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

    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

    TokenMeaning
    ^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 0109, or 1012 — together, exactly 01 through 12, zero-padded
    -Another literal hyphen
    (0[1-9]|[12]\d|3[01])The day: 0109, or 1029 (a leading 1 or 2 followed by any digit), or 3031
    $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 with 1 or 2, so it passes for the wrong reason.

    What it deliberately does not match, and why

    InputVerdictWhy
    2026-13-01RejectedMonth 13 does not exist — the month group only reaches 12
    26-07-22RejectedTwo-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/22RejectedSlashes instead of hyphens — a different, non-ISO separator convention common in US-formatted dates
    2026-00-15RejectedMonth 00 does not exist — the month group starts at 01
    2026-2-9RejectedNot 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-30AcceptedDoes 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