Regex for ISO dates: matching YYYY-MM-DD without fake days
A date pattern can constrain the month to 01 to 12 and the day to 01 to 31 with alternation, but it cannot know which months are short or which years are leap years.
At a glance
- Built in preset
- Date (YYYY-MM-DD)
- Preset pattern
- \d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])
- Preset flags
- g
- Rejects
- month 00 and 13, day 00 and 32
- Still accepts
- 2026-02-31 and 2025-02-29
- Real check
- parse the value and compare the serialised result back
In detail
The month and day alternations are doing genuine work. Splitting the month into 01 to 09 and 10 to 12 rules out both 00 and 13, and splitting the day into three ranges rules out 00 and anything above 31. That removes the majority of malformed input, and it does so in a pattern short enough to read.
What it cannot do is know the calendar. February the thirty first passes, and so does the twenty ninth of February in a non leap year, because the day range has no way to depend on the month or the year. Encoding month lengths in a pattern is possible and produces something nobody can maintain; leap years are worse, since the rule involves three separate divisibility tests.
The reliable check is a round trip. Construct a date from the string, serialise it back to the same format and compare. A real date survives that unchanged, while the thirty first of February silently rolls forward into March and the comparison fails. Two lines of code replace an unmaintainable pattern.
The preset is also unanchored, which means it finds a valid looking date inside a longer run of digits. Against the malformed token 12026-03-089 it happily returns 2026-03-08, because nothing tells it the digits have to stop. For validation the pattern needs ^ and $, and for scanning it needs word boundaries.
Examples
const iso = /\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/g;
"Events on 2025-01-15, 2025-12-31, and 2026-03-08 are confirmed.".match(iso);
// ["2025-01-15", "2025-12-31", "2026-03-08"]
"12026-03-089".match(iso)[0]; // "2026-03-08", found inside a longer tokenconst iso = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
iso.test("2026-02-31"); // true, the pattern has no calendar
function isRealDate(s) {
return iso.test(s) && new Date(s).toISOString().slice(0, 10) === s;
}
isRealDate("2026-02-31"); // false
isRealDate("2026-03-08"); // trueThings that catch people out
- No date regex can validate a calendar. February the thirty first and the twenty ninth in a non leap year both pass every pattern of this shape.
- Without anchors the pattern extracts a plausible date from an implausible token, so a five digit year silently produces a four digit match.
- A full ISO 8601 timestamp is a much larger grammar than this. Matching the date part of one does not mean the rest of the value is well formed.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →Regex for phone numbers, and why strict ones lose customers
Phone formatting varies by country and by habit. The built in US preset does not even match the parenthesised form in its own sample, which is the lesson.
{n,m}: bounded repetition between two counts
A bounded count is inclusive at both ends and greedy toward the maximum. JavaScript has no {,m} shorthand, and writing one produces literal text.
Alternation: precedence and ordered choice
The pipe has the lowest precedence of any regex operator and the branches are tried in order, so ^cat|dog$ and /a|ab/ both surprise people.
Regex to trim whitespace, collapse runs and drop blank lines
There is no preset for this one. Four short patterns cover trimming, collapsing, per line trimming and removing blank lines, and each has a different failure.
Regex for HTML tags: what the preset matches and what breaks it
The tag preset is fine for finding tags in a known snippet and wrong for parsing a document. A greater than sign inside an attribute value is all it takes.
Regex for numbers: integers, decimals and negative signs
The integer preset splits 3.14 into two matches and the float preset ignores whole numbers. Here is the combined pattern and what it still leaves out.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links