Expressão regular para validar uma data em JavaScript, PHP e HTML

Uma regex data que recusa o 30 de fevereiro e o 31 de abril, com as variantes ISO e americana, as funções JavaScript e PHP, o atributo HTML e o momento em que é melhor deixar a regex de lado.

Expressão regular para validar uma data: JavaScript, PHP e HTML
Resposta rápida

A regex abaixo valida uma data francesa DD/MM/AAAA (separador /, - ou .) e recusa as datas impossíveis como o 30 de fevereiro ou o 31 de abril, anos bissextos incluídos. Para um formato ISO AAAA-MM-DD, uma regex curta chega se confirmares a seguir com o objeto Date em JavaScript ou checkdate() em PHP.

Validar uma data escrita à mão é mais delicado do que parece: «30/02/2026» tem a forma certa e não existe. A regex data abaixo verifica o formato DD/MM/AAAA, com /, - ou . como separador, e recusa os dias impossíveis, incluindo o 29 de fevereiro dos anos não bissextos. A seguir vêm as versões JavaScript, PHP e HTML, a variante ISO e o caso em que o objeto Date faz melhor do que uma regex.

A regex data pronta a 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})$

É longa porque raciocina como um calendário. Três ramos, separados por |:

  1. O dia 31 só existe nos meses de 31 dias (01, 03, 05, 07, 08, 10, 12), e o 29 e o 30 em todos os meses exceto fevereiro.
  2. O 29 de fevereiro só é aceite se o ano for bissexto: divisível por 4 (0[48]|[2468][048]|[13579][26]), ou século divisível por 400 ((?:16|[2468][048]|[3579][26])00). 2000 e 2024 passam, 1900 e 2023 são recusados.
  3. Do dia 1 ao 28, todos os meses aceitam a data.

As retrorreferências \1, \2, \3 e \4 impõem que o segundo separador seja idêntico ao primeiro: 07/09/2026 e 07-09-2026 passam, 07/09-2026 não.

Um único separador?

Para aceitares apenas a barra, substitui cada (\/|-|\.) por (\/). Os parênteses continuam a ser necessários: as retrorreferências \1 a \4 apontam para esses grupos.

As datas testadas

Resultado real da regex em Node.js 22 e PHP 8.5, idêntico nos dois motores:

Valor introduzido Resultado Razão
31/12/2026 aceite 31 num mês de 31 dias
29/02/2024 aceite 2024 é bissexto
29/02/2000 aceite 2000 é divisível por 400
01/01/2026 e 1/1/2026 aceites o zero inicial é opcional
07-09-2026 e 07.09.2026 aceites outros separadores permitidos
29/02/2023 recusada 2023 não é bissexto
29/02/1900 recusada século não divisível por 400
30/02/2026 recusada fevereiro nunca tem 30 dias
31/04/2026 recusada abril tem 30 dias
00/01/2026 recusada o dia 0 não existe
12/13/2026 recusada mês 13: a regex é mesmo DD/MM e não MM/DD
07/09-2026 recusada separadores diferentes
2026-09-07 recusada formato ISO, ver mais abaixo
07/09/26 e 31/12/1899 aceites ano com dois dígitos ou anterior a 1900: limita-o no teu código se for preciso

Versão JavaScript

A função ancora a regex com ^ e $, retira os espaços à volta do valor introduzido e devolve um booleano.

validateDate.js
/**
 * Verifica uma data no formato DD/MM/AAAA, DD-MM-AAAA ou 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

Num formulário, o esquema é o mesmo da validação de um email: ouvir submit, chamar preventDefault() se a data for inválida, mostrar a mensagem ao lado do campo.

Versão PHP

A verificação do lado do servidor é obrigatória: o JavaScript desativa-se. O mesmo padrão funciona com preg_match(); as retrorreferências escrevem-se \\1 numa string PHP entre aspas simples.

isValidDate.php
/**
 * Verifica uma data no formato DD/MM/AAAA, DD-MM-AAAA ou 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)

A alternativa sem regex: checkdate()

O PHP já sabe dizer se um trio dia, mês, ano existe. Uma pequena regex para separar os blocos, depois checkdate() para decidir: é mais curto e mais legível.

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]); // mês, dia, ano
}

var_dump(isValidDateFr('29/02/2024')); // bool(true)
var_dump(isValidDateFr('29/02/2023')); // bool(false)
var_dump(isValidDateFr('31/04/2026')); // bool(false)

Atenção a DateTime::createFromFormat('d/m/Y', '29/02/2023'): não devolve false mas o dia 1.º de março de 2023, e assinala a diferença em DateTime::getLastErrors(). Para uma verificação estrita, checkdate() é mais seguro.

Versão HTML

Se o utilizador escrever a data em texto livre, o atributo pattern aplica a regex antes do envio. Está implicitamente ancorado: retiram-se ^ e $.

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
>

Mas a melhor solução HTML não é a regex: é type="date". O navegador mostra um seletor adaptado à língua do utilizador, impede o 30 de fevereiro e transmite sempre o valor no formato ISO AAAA-MM-DD, seja qual for a apresentação.

html
<input id="naissance" name="naissance" type="date" min="1900-01-01" max="2026-12-31" required>

Consequência do lado do servidor: com type="date", é o formato ISO que tens de validar, não o formato francês.

Variante ISO AAAA-MM-DD

Para uma data ISO, uma regex curta controla a forma e o objeto Date confirma que o dia existe. Os dois juntos substituem a 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) devolve 2 de março: verificamos que nada escorregou
  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

A mesma técnica aplica-se ao formato francês: separar com /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/, depois verificar com Date. É o equivalente JavaScript do checkdate() do PHP.

E o formato americano MM/DD/AAAA?

A regex do artigo é francesa: primeiro o dia, depois o mês. Para um público americano, inverte os dois primeiros blocos ou, mais simples, normaliza tudo em ISO logo na introdução dos dados com type="date". Uma aplicação que aceita as duas ordens sem as distinguir engana-se uma vez em cada duas em todas as datas do dia 1 ao 12.

Comparar duas datas depois da validação

Assim que a data está validada e convertida em objeto Date, as comparações são diretas: os operadores < e > comparam os instantes e a subtração dá uma diferença em milissegundos.

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 dias

A formatação para apresentação (toLocaleDateString, Intl.DateTimeFormat) é outro assunto, tal como a data e a hora em PHP ou o Carbon no Laravel.

Regex ou objeto Date: o que escolher?

  • A regex grande quando queres uma regra única, idêntica em JavaScript, PHP e HTML, sem depender de uma biblioteca nem de um objeto Date.
  • Separar os blocos + Date ou checkdate() quando a legibilidade conta, ou quando tens de manipular a data a seguir: já a tens sob a forma de objeto.
  • type="date" para qualquer formulário moderno: a introdução é guiada, o valor é normalizado e só resta a verificação ISO do lado do servidor.

As outras validações da série: endereço de email, endereço IP, código postal e palavra-passe.

Erros frequentes

Misturar os separadores A regex captura o primeiro separador e exige o mesmo a seguir: «07/09-2026» é recusado. É intencional, mas surpreende se um formulário aceitar várias formas de escrita.
Esquecer que new Date corrige em silêncio new Date(2026, 1, 30) não lança erro: devolve o 2 de março de 2026. Compara sempre ano, mês e dia depois da construção.
Validar uma data ISO com a regex francesa 2026-09-07 é recusado pela regex DD/MM/AAAA. Usa a variante ISO do artigo ou, melhor ainda, input type="date", que já devolve esse formato.
Confiar no formato de apresentação do navegador Um campo type="date" mostra «07/09/2026» em francês, mas o valor transmitido é «2026-09-07». A validação do lado do servidor tem de esperar o formato ISO.

HTMLJavaScriptPHP

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.