Criterios:
Cada año divisible por 4 es un año bisiesto, excepto cuando es divisible por 100 a menos que sea divisible por 400. Entonces:
2004 - leap year - divisible by 4
1900 - not a leap year - divisible by 4, but also divisible by 100
2000 - leap year - divisible by 4, also divisible by 100, but divisible by 400
Febrero tiene 29 días en un año bisiesto y 28 cuando no es un año bisiesto
30 días en abril, junio, septiembre y noviembre
31 días en enero, marzo, mayo, julio, agosto, octubre y diciembre
Prueba:
Las siguientes fechas deben pasar la validación:
1976-02-29
2000-02-29
2004-02-29
1999-01-31
Las siguientes fechas deberían fallar en la validación:
2015-02-29
2015-04-31
1900-02-29
1999-01-32
2015-02-00
Rango:
Probaremos fechas desde el 1 de enero de 1000 hasta el 31 de diciembre de 2999. Técnicamente, el calendario gregoriano que se usa actualmente solo entró en uso en 1753 para el Imperio Británico y en varios años en el siglo XVII para países de Europa, pero no voy a preocuparse por eso.
Regex para probar durante un año bisiesto:
Los años divisibles por 400:
1200|1600|2000|2400|2800
can be shortened to:
(1[26]|2[048])00
if you wanted all years from 1AD to 9999 then this would do it:
(0[48]|[13579][26]|[2468][048])00
if you're happy with accepting 0000 as a valid year then it can be shortened:
([13579][26]|[02468][048])00
Los años divisibles por 4:
[12]\d([02468][048]|[13579][26])
Los años divisibles por 100:
[12]\d00
No divisible por 100:
[12]\d([1-9]\d|\d[1-9])
Los años divisibles por 100 pero no por 400:
((1[1345789])|(2[1235679]))00
Divisible por 4 pero no por 100:
[12]\d([2468][048]|[13579][26]|0[48])
Los años bisiestos:
divisible by 400 or (divisible by 4 and not divisible by 100)
((1[26]|2[048])00)|[12]\d([2468][048]|[13579][26]|0[48])
No divisible por 4:
[12]\d([02468][1235679]|[13579][01345789])
No es un año bisiesto:
Not divisible by 4 OR is divisible by 100 but not by 400
([12]\d([02468][1235679]|[13579][01345789]))|(((1[1345789])|(2[1235679]))00)
Mes y día válidos excepto febrero (MM-DD):
((01|03|05|07|08|10|12)-(0[1-9]|[12]\d|3[01]))|((04|06|09|11)-(0[1-9]|[12]\d|30))
shortened to:
((0[13578]|1[02])-(0[1-9]|[12]\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\d|30))
Febrero con 28 días:
02-(0[1-9]|1\d|2[0-8])
Febrero con 29 días:
02-(0[1-9]|[12]\d)
Fecha válida:
(leap year followed by (valid month-day-excluding-february OR 29-day-february))
OR
(non leap year followed by (valid month-day-excluding-february OR 28-day-february))
((((1[26]|2[048])00)|[12]\d([2468][048]|[13579][26]|0[48]))-((((0[13578]|1[02])-(0[1-9]|[12]\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\d|30)))|(02-(0[1-9]|[12]\d))))|((([12]\d([02468][1235679]|[13579][01345789]))|((1[1345789]|2[1235679])00))-((((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\d|2[0-8]))))
Ahí lo tiene una expresión regular para las fechas entre el 1 de enero de 1000 y el 31 de diciembre de 2999 en formato AAAA-MM-DD.
Sospecho que se puede acortar un poco, pero se lo dejo a otra persona.
Eso coincidirá con todas las fechas válidas. Si desea que solo sea válido cuando contenga solo una fecha y nada más, envuélvalo ^( )$
así:
^(((((1[26]|2[048])00)|[12]\d([2468][048]|[13579][26]|0[48]))-((((0[13578]|1[02])-(0[1-9]|[12]\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\d|30)))|(02-(0[1-9]|[12]\d))))|((([12]\d([02468][1235679]|[13579][01345789]))|((1[1345789]|2[1235679])00))-((((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\d|2[0-8])))))$
Si lo desea para una entrada de fecha opcional (es decir, puede estar en blanco o una fecha válida), agregue ^$|
al principio, así:
^$|^(((((1[26]|2[048])00)|[12]\d([2468][048]|[13579][26]|0[48]))-((((0[13578]|1[02])-(0[1-9]|[12]\d|3[01]))|((0[469]|11)-(0[1-9]|[12]\d|30)))|(02-(0[1-9]|[12]\d))))|((([12]\d([02468][1235679]|[13579][01345789]))|((1[1345789]|2[1235679])00))-((((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\d|2[0-8])))))$
date("Y-m-d", strtotime("2012-09-12"))=="2012-09-12";
o PHPcheckdate ( int $month , int $day , int $year )
.