Regex de fecha: validar DD/MM/AAAA o AAAA-MM-DD en JavaScript y PHP

Una regex de fecha que rechaza el 30 de febrero y el 31 de abril, con las variantes ISO y estadounidense, las funciones JavaScript y PHP, el atributo HTML y el momento en el que conviene dejar la regex de lado.

Expresión regular para validar una fecha: JavaScript, PHP y HTML
Respuesta rápida

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

javascript
^(?:(?: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 |:

  1. 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.
  2. 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.
  3. 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.

¿Un solo separador?

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.

validateDate.js
/**
 * 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')); // false

En 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.

isValidDate.php
/**
 * 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.

isValidDateFr.php
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 $.

formulaire.html
<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.

html
<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.

isValidIsoDate.js
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')); // false

La 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.

javascript
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ías

El 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 + Date o checkdate() 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

Mezclar los separadores La regex captura el primer separador y exige el mismo después: «07/09-2026» se rechaza. Es intencionado, pero sorprende si un formulario admite varias formas de escribir la fecha.
Olvidar que new Date corrige en silencio 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.
Validar una fecha ISO con la regex de DD/MM/AAAA 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.
Fiarse del formato que muestra el navegador Un campo 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.

HTMLJavaScriptPHP

Damien Flandrin Desarrollador web desde 2010, creador de Gekkode y de Email Impact. Cada artículo se prueba en un proyecto real antes de publicarse. Contacto
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.