
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
^(?:(?: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 |:
- 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. - 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. - 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.
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.
/**
* 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')); // falseNum 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.
/**
* 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.
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 $.
<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.
<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.
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')); // falseA 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.
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 diasA 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 +
Dateoucheckdate()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
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.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.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.

