Here you go, an unfailing regex that works on all valid YYYY/MM/DD (bonus for regex for YYYY-MM-DD inputs) (including only VALID leap days):
This lovely piece matches ONLY valid dates (and accounts for leap years - and discounts those leap years which occur on centennial years which are not wholly divisible by 400 (thus rendering them ineligible for a February 29th).
I have included non-delimited, as well as slash and dash delimited variants for the YYYYMMDD, MMDDYYYY, and DDMMYYYY variations that are commonly seen.
These have been thoroughly tested against all numeric combinations from "0000-00-00" through "9999-99-99" without issue. See the section "testing" below.
Non-Delimited Variants:
YYYYMMDD:
/^(?:(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26]))))0229)|(?:\d{4}(?:(?:(?:0[13578]|1[02])(?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)(?:0[1-9]|[12]\d|30))|(?:02(?:0[1-9]|1[0-9]|2[0-8]))))$/
MMDDYYYY:
/^(?:0229(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:(?:0[13578]|1[02])(?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)(?:0[1-9]|[12]\d|30))|(?:02(?:0[1-9]|1[0-9]|2[0-8])))\d{4})$/
DDMMYYYY:
/^(?:2902(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:0[1-9]|[12]\d|3[01])(?:(?:0[13578]|1[02]))|(?:(?:0[1-9]|[12]\d|30)(?:0[469]|11))|(?:(?:0[1-9]|1[0-9]|2[0-8])02))\d{4})$/
Forward Slash Delimited Variants:
YYYY/MM/DD:
/^(?:(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26]))))[/]02[/]29)|(?:\d{4}[/](?:(?:(?:0[13578]|1[02])[/](?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)[/](?:0[1-9]|[12]\d|30))|(?:02[/](?:0[1-9]|1[0-9]|2[0-8]))))$/
MM/DD/YYYY:
/^(?:02[/]29[/](?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:(?:0[13578]|1[02])[/](?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)[/](?:0[1-9]|[12]\d|30))|(?:02[/](?:0[1-9]|1[0-9]|2[0-8])))[/]\d{4})$/
DD/MM/YYYY:
/^(?:29[/]02[/](?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:0[1-9]|[12]\d|3[01])[/](?:(?:0[13578]|1[02]))|(?:(?:0[1-9]|[12]\d|30)[/](?:0[469]|11))|(?:(?:0[1-9]|1[0-9]|2[0-8])[/]02))[/]\d{4})$/
Dash Delimited Variants:
YYYY-MM-DD:
/^(?:(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26]))))[-]02[-]29)|(?:\d{4}[-](?:(?:(?:0[13578]|1[02])[-](?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)[-](?:0[1-9]|[12]\d|30))|(?:02[-](?:0[1-9]|1[0-9]|2[0-8]))))$/
MM-DD-YYYY:
/^(?:02[-]29[-](?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:(?:0[13578]|1[02])[-](?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)[-](?:0[1-9]|[12]\d|30))|(?:02[-](?:0[1-9]|1[0-9]|2[0-8])))[-]\d{4})$/
DD-MM-YYYY
/^(?:29[-]02[-](?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))))|(?:(?:(?:0[1-9]|[12]\d|3[01])[-](?:(?:0[13578]|1[02]))|(?:(?:0[1-9]|[12]\d|30)[-](?:0[469]|11))|(?:(?:0[1-9]|1[0-9]|2[0-8])[-]02))[-]\d{4})$/
Testing
It has also been thoroughly tested against all possible combinations of YYYY-MM-DD, from 0000-00-00 through 9999-99-99.
Crucially, it also aligns with JavaScript's produced Date objects when passed those dates. Thus, no Date object can exist between 0000-01-01 through 9999-12-31 which is not validated correctly by this regular expression.
This has been produced using non-capturing groups, as attempting to capture would (effectively) be pointless, due to the indexed nature of capturing groups. Don't bother, just separate the string by indexes (minding to skip the hyphens) if you need the "YYYY"/"MM"/"DD" strings for whatever reason. And, if we're not going to use them, we might as well save the compute on it.
Enjoy. Explanation below.
Note, the explanation is focused on the YYYY-MM-DD regex, but both work precisely the same way. The only difference is the delimiter.
Leap Day Matching:
The Regex:
/(?:(?:(?:(?:(?:[02468][048])|(?:[13579][26]))00)|(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26]))))-02-29)/
The Trick
Rationale: if a number's last two digits are evenly divisible by 4, the whole number is evenly divisible by 4.
If we look at all numbers (left-padded zero) between 00-99, we can separate them into two distinct groups
00 04 08 12 16 20 24 28 32 36 40 44 48 52 56 60 64 68 72 76 80 84 88 92 96
- Two digit pairs with an even leading digit, which will always be trailed by either 0, 4, or 8: /[02468][048]/
- Two digit pairs with an odd leading digit, which will always be trailed by either 2 or 6: /[13579][26]/
Centennial Years
We can use that to isolate those valid centennial years, whereby the year is wholly divisible by both 100 and 400. The last two digits are always double-zero, so we only care about the first two digits, which means they must fit the above trend of an even digit followed by [048] or an odd digit followed by [26]:
/(?:(?:(?:[02468][048])|(?:[13579][26]))00)/
Non-Centennial Years
Now we want to take the same logic and allow it to capture any combination of [0-9] for the first two digits of the year, and any combination of the above "trick" for the last two digits of the year: (/[02468][048]|[13579][26]/).
The only difference is that we DON'T capture years ending in double-zero for this part of the regex, which gives us the expression:
/(?:[0-9][0-9](?:(?:0[48])|(?:[2468][048])|(?:[13579][26])))/
All together: February 29th
Then, to put it all together, as we know leap days can ONLY be on February 29th, we just tack on /-02-29/ at the end of the group, giving us the full expression for only VALID leap days.
Matching Valid Non-Leap Days
/(?:\d{4}-(?:(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01]))|(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))|(?:(02)-(?:0[1-9]|1[0-9]|2[0-8]))))/
This is the easy part, now that leap-days are out of the equation.
The Year:
We literally just want any combination of 4 digits followed by a dash:
/\d{4}-/
Then we account for the different combinations of months with their maximum number of days.
31 Day Months
The months with 31 days are:
01 03 05 07 08 10 12
So, we make sure the month is one of those, followed by a hyphen:
/(?:0[13578]|1[02])-/
And that the days are anywhere between 01-31:
/(?:0[1-9]|[12]\d|3[01])/
Giving us:
/(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01]))/
30 Day Months:
Next up, we have the 30 day months:
04 06 09 11
So, basically the same tact, just with different targets. First the month followed by a dash:
/(?:0[469]|11)-/
Then the days, which must be between 01-30:
/(?:0[1-9]|[12]\d|30)/
Giving us:
/(?:(?:0[469]|11)-(?:0[1-9]|[12]\d|30))/
28 Day Months:
Finally, we have our last part, the only 28 day month there is. This one is much simpler, so I'm not going to separate it like I did the others:
/(?:02)-(?:0[1-9]|1[0-9]|2[0-8]))/
Finally, if we put it all together, we get the abomination at the top.
(((0[1-9]|[12][0-9]|3[01])([/])(0[13578]|10|12)([/])(\d{4}))|(([0][1-9]|[12][0-9]|30)([/])(0[469]|11)([/])(\d{4}))|((0[1-9]|1[0-9]|2[0-8])([/])(02)([/])(\d{4}))|((29)(\/)(02)([/])([02468][048]00))|((29)([/])(02)([/])([13579][26]00))|((29)([/])(02)([/])([0-9][0-9][0][48]))|((29)([/])(02)([/])([0-9][0-9][2468][048]))|((29)([/])(02)([/])([0-9][0-9][13579][26])))to validate date in DD/MM/YYYY format, Even this will works for Leay year validation such as, this will be validate 29/02/2020 but not 29/02/2019 and also not 29/02/2100