
La regex de abajo valida una fecha en formato DD/MM/AAAA (separador /, - o .) y rechaza las fechas imposibles como el 30 de febrero o el 31 de abril, años bisiestos incluidos. Para el formato ISO AAAA-MM-DD basta con una regex corta si después lo confirmas con el objeto Date en JavaScript o con checkdate() en PHP.
Validar una fecha escrita a mano es más delicado de lo que parece: «30/02/2026» tiene la forma correcta y no existe. La regex de fecha de abajo comprueba el formato DD/MM/AAAA, con /, - o . como separador, y rechaza los días imposibles, incluido el 29 de febrero de los años no bisiestos. Después vienen las versiones en JavaScript, PHP y HTML, la variante ISO y el caso en el que el objeto Date lo hace mejor que una regex.
La regex de fecha lista para copiar
^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$Es larga porque razona como un calendario. Tres ramas, separadas por |:
- El 31 solo existe en los meses de 31 días (
01, 03, 05, 07, 08, 10, 12), y el 29 y el 30 en todos los meses menos febrero. - El 29 de febrero solo se acepta si el año es bisiesto: divisible entre 4 (
0[48]|[2468][048]|[13579][26]) o siglo divisible entre 400 ((?:16|[2468][048]|[3579][26])00). 2000 y 2024 pasan; 1900 y 2023 se rechazan. - Del 1 al 28, todos los meses aceptan la fecha.
Las referencias \1, \2, \3 y \4 obligan a que el segundo separador sea idéntico al primero: 07/09/2026 y 07-09-2026 pasan; 07/09-2026, no.
Para aceptar solo la barra inclinada, sustituye cada (\/|-|\.) por (\/). Los paréntesis siguen siendo necesarios: las retrorreferencias \1 a \4 apuntan a esos grupos.
Las fechas probadas
Resultado real de la regex en Node.js 22 y PHP 8.5, idéntico en los dos motores:
| Entrada | Resultado | Motivo |
|---|---|---|
31/12/2026 | aceptada | 31 en un mes de 31 días |
29/02/2024 | aceptada | 2024 es bisiesto |
29/02/2000 | aceptada | 2000 es divisible entre 400 |
01/01/2026 y 1/1/2026 | aceptadas | el cero inicial es opcional |
07-09-2026 y 07.09.2026 | aceptadas | otros separadores permitidos |
29/02/2023 | rechazada | 2023 no es bisiesto |
29/02/1900 | rechazada | siglo no divisible entre 400 |
30/02/2026 | rechazada | febrero nunca tiene 30 días |
31/04/2026 | rechazada | abril tiene 30 días |
00/01/2026 | rechazada | el día 0 no existe |
12/13/2026 | rechazada | mes 13: la regex es DD/MM y no MM/DD |
07/09-2026 | rechazada | separadores distintos |
2026-09-07 | rechazada | formato ISO, más abajo |
07/09/26 y 31/12/1899 | aceptadas | año de dos cifras o anterior a 1900: acótalo en tu código si lo necesitas |
Versión JavaScript
La función ancla la regex con ^ y $, quita los espacios de alrededor de la entrada y devuelve un booleano.
/**
* Comprueba una fecha con formato DD/MM/AAAA, DD-MM-AAAA o DD.MM.AAAA.
* @param {string} date
* @returns {boolean}
*/
function validateDate(date) {
const reg = /^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$/;
return reg.test(String(date).trim());
}
console.log(validateDate('29/02/2024')); // true
console.log(validateDate('29/02/2023')); // false
console.log(validateDate('31/04/2026')); // falseEn un formulario, el esquema es el mismo que para la validación de un correo: escuchar submit, llamar a preventDefault() si la fecha no es válida y mostrar el mensaje junto al campo.
Versión PHP
La comprobación en el servidor es obligatoria: el JavaScript se puede desactivar. El mismo patrón funciona con preg_match(); las retrorreferencias se escriben \\1 dentro de una cadena PHP entre comillas simples.
/**
* Comprueba una fecha con formato DD/MM/AAAA, DD-MM-AAAA o DD.MM.AAAA.
*/
function isValidDate(string $date): bool
{
$pattern = '/^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$/';
return preg_match($pattern, trim($date)) === 1;
}
var_dump(isValidDate('29/02/2024')); // bool(true)
var_dump(isValidDate('30/02/2026')); // bool(false)La alternativa sin regex: checkdate()
PHP ya sabe decir si un trío de día, mes y año existe. Una regex pequeña para trocear y luego checkdate() para juzgar: es más corto y más legible.
function isValidDateFr(string $date): bool
{
if (!preg_match('#^(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})$#', trim($date), $m)) {
return false;
}
return checkdate((int) $m[2], (int) $m[1], (int) $m[3]); // mes, día, año
}
var_dump(isValidDateFr('29/02/2024')); // bool(true)
var_dump(isValidDateFr('29/02/2023')); // bool(false)
var_dump(isValidDateFr('31/04/2026')); // bool(false)Cuidado con DateTime::createFromFormat('d/m/Y', '29/02/2023'): no devuelve false, sino el 1.o de marzo de 2023, y señala la discrepancia en DateTime::getLastErrors(). Para una comprobación estricta, checkdate() es más seguro.
Versión HTML
Si el usuario escribe la fecha en texto libre, el atributo pattern aplica la regex antes del envío. Lleva anclas implícitas: se quitan ^ y $.
<label for="naissance">Date de naissance (JJ/MM/AAAA)</label>
<input
id="naissance"
name="naissance"
type="text"
inputmode="numeric"
placeholder="JJ/MM/AAAA"
pattern="(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})|(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))|(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})"
title="Une date valide au format JJ/MM/AAAA"
required
>Pero la mejor solución HTML no es la regex: es type="date". El navegador muestra un selector adaptado al idioma del usuario, impide el 30 de febrero y envía siempre el valor en formato ISO AAAA-MM-DD, sea cual sea la presentación.
<input id="naissance" name="naissance" type="date" min="1900-01-01" max="2026-12-31" required>Consecuencia en el servidor: con type="date" hay que validar el formato ISO, no el formato con el día delante.
Variante ISO AAAA-MM-DD
Para una fecha ISO, una regex corta comprueba la forma y el objeto Date confirma que el día existe. Los dos juntos sustituyen a la regex grande.
function isValidIsoDate(value) {
const m = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/.exec(value);
if (!m) return false;
const [, y, mo, d] = m.map(Number);
const date = new Date(y, mo - 1, d);
// new Date(2026, 1, 30) devuelve el 2 de marzo: comprobamos que nada se ha desplazado
return date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d;
}
console.log(isValidIsoDate('2026-09-07')); // true
console.log(isValidIsoDate('2026-02-30')); // false
console.log(isValidIsoDate('2026-13-01')); // falseLa misma técnica sirve para el formato con el día delante: trocear con /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/ y comprobar después con Date. Es el equivalente en JavaScript del checkdate() de PHP.
¿Y el formato estadounidense MM/DD/AAAA?
La regex del artículo sigue el orden europeo: primero el día, después el mes. Para un público estadounidense, invierte los dos primeros bloques o, más sencillo, normaliza todo a ISO desde la entrada con type="date". Una aplicación que acepta los dos órdenes sin distinguirlos se equivoca una de cada dos veces con todas las fechas del 1 al 12.
Comparar dos fechas después de validarlas
Una vez validada la fecha y convertida en objeto Date, las comparaciones son directas: los operadores < y > comparan los instantes y la resta da la diferencia en milisegundos.
const debut = new Date('2026-09-07');
const fin = new Date('2026-12-25');
console.log(debut < fin); // true
console.log(Math.round((fin - debut) / 86_400_000)); // 109 díasEl formateo para mostrarla (toLocaleDateString, Intl.DateTimeFormat) es otro tema, igual que la fecha y la hora en PHP o Carbon en Laravel.
Regex u objeto Date: ¿cuál elegir?
- La regex grande cuando quieres una sola regla, idéntica en JavaScript, PHP y HTML, sin depender de una biblioteca ni de un objeto
Date. - Trocear +
Dateocheckdate()cuando importa la legibilidad, o cuando después tienes que manipular la fecha: ya la tienes como objeto. type="date"para cualquier formulario moderno: la entrada está guiada, el valor normalizado, y solo queda la comprobación ISO en el servidor.
Las demás validaciones de la serie: dirección de correo, dirección IP, código postal y contraseña.
Errores frecuentes
new Date(2026, 1, 30) no lanza ningún error: devuelve el 2 de marzo de 2026. Compara siempre año, mes y día después de construir la fecha.2026-09-07 lo rechaza la regex DD/MM/AAAA. Usa la variante ISO del artículo o, mejor, input type="date", que ya devuelve ese formato.type="date" muestra «07/09/2026» en español, pero el valor que envía es «2026-09-07». La validación en el servidor debe esperar el formato ISO.

