
La regex ci-dessous valide une date française JJ/MM/AAAA (séparateur /, - ou .) et refuse les dates impossibles comme le 30 février ou le 31 avril, années bissextiles comprises. Pour un format ISO AAAA-MM-JJ, une regex courte suffit si vous confirmez ensuite avec l’objet Date en JavaScript ou checkdate() en PHP.
Valider une date saisie à la main est plus délicat qu’il n’y paraît : « 30/02/2026 » a la bonne forme et n’existe pas. La regex date ci-dessous vérifie le format JJ/MM/AAAA, avec /, - ou . comme séparateur, et refuse les jours impossibles, y compris le 29 février des années non bissextiles. Vous trouverez ensuite les versions JavaScript, PHP et HTML, la variante ISO et le cas où l’objet Date fait mieux qu’une regex.
La regex date prête à copier
^(?:(?: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})$Elle est longue parce qu’elle raisonne comme un calendrier. Trois branches, séparées par | :
- Le 31 n’existe que dans les mois de 31 jours (
01, 03, 05, 07, 08, 10, 12), et le 29 et le 30 dans tous les mois sauf février. - Le 29 février n’est accepté que si l’année est bissextile : divisible par 4 (
0[48]|[2468][048]|[13579][26]), ou siècle divisible par 400 ((?:16|[2468][048]|[3579][26])00). 2000 et 2024 passent, 1900 et 2023 sont refusées. - Du 1 au 28, tous les mois acceptent la date.
Les références \1, \2, \3 et \4 imposent que le second séparateur soit identique au premier : 07/09/2026 et 07-09-2026 passent, 07/09-2026 non.
Pour n’accepter que la barre oblique, remplacez chaque (\/|-|\.) par (\/). Les parenthèses restent nécessaires : les références arrière \1 à \4 pointent vers ces groupes.
Les dates testées
Résultat réel de la regex sous Node.js 22 et PHP 8.5, identique dans les deux moteurs :
| Saisie | Résultat | Raison |
|---|---|---|
31/12/2026 | acceptée | 31 dans un mois de 31 jours |
29/02/2024 | acceptée | 2024 est bissextile |
29/02/2000 | acceptée | 2000 est divisible par 400 |
01/01/2026 et 1/1/2026 | acceptées | le zéro initial est optionnel |
07-09-2026 et 07.09.2026 | acceptées | autres séparateurs autorisés |
29/02/2023 | refusée | 2023 n’est pas bissextile |
29/02/1900 | refusée | siècle non divisible par 400 |
30/02/2026 | refusée | février n’a jamais 30 jours |
31/04/2026 | refusée | avril a 30 jours |
00/01/2026 | refusée | le jour 0 n’existe pas |
12/13/2026 | refusée | mois 13 : la regex est bien JJ/MM et non MM/JJ |
07/09-2026 | refusée | séparateurs différents |
2026-09-07 | refusée | format ISO, voir plus bas |
07/09/26 et 31/12/1899 | acceptées | année sur deux chiffres ou avant 1900 : à borner dans votre code si besoin |
Version JavaScript
La fonction ancre la regex avec ^ et $, retire les espaces autour de la saisie et renvoie un booléen.
/**
* Vérifie une date au format JJ/MM/AAAA, JJ-MM-AAAA ou JJ.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')); // falseDans un formulaire, le schéma est le même que pour la validation d’un e-mail : écouter submit, appeler preventDefault() si la date est invalide, afficher le message à côté du champ.
Version PHP
Le contrôle côté serveur est obligatoire : le JavaScript se désactive. Le même motif fonctionne avec preg_match(), les références arrière s’écrivent \\1 dans une chaîne PHP entre apostrophes.
/**
* Vérifie une date au format JJ/MM/AAAA, JJ-MM-AAAA ou JJ.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)L’alternative sans regex : checkdate()
PHP sait déjà dire si un triplet jour, mois, année existe. Une petite regex pour découper, puis checkdate() pour juger : c’est plus court et plus lisible.
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]); // mois, jour, année
}
var_dump(isValidDateFr('29/02/2024')); // bool(true)
var_dump(isValidDateFr('29/02/2023')); // bool(false)
var_dump(isValidDateFr('31/04/2026')); // bool(false)Attention à DateTime::createFromFormat('d/m/Y', '29/02/2023') : il ne renvoie pas false mais le 1er mars 2023, en signalant l’écart dans DateTime::getLastErrors(). Pour un contrôle strict, checkdate() est plus sûr.
Version HTML
Si l’utilisateur saisit la date en texte libre, l’attribut pattern applique la regex avant l’envoi. Il est implicitement ancré : on retire ^ et $.
<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
>Mais la meilleure solution HTML n’est pas la regex : c’est type="date". Le navigateur affiche un sélecteur adapté à la langue de l’utilisateur, empêche le 30 février, et transmet toujours la valeur au format ISO AAAA-MM-JJ, quel que soit l’affichage.
<input id="naissance" name="naissance" type="date" min="1900-01-01" max="2026-12-31" required>Conséquence côté serveur : avec type="date", c’est le format ISO qu’il faut valider, pas le format français.
Variante ISO AAAA-MM-JJ
Pour une date ISO, une regex courte contrôle la forme, et l’objet Date confirme que le jour existe. Les deux ensemble remplacent la grande regex.
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) renvoie le 2 mars : on vérifie que rien n'a glissé
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 même technique s’applique au format français : découper avec /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/, puis vérifier avec Date. C’est l’équivalent JavaScript du checkdate() de PHP.
Et le format américain MM/JJ/AAAA ?
La regex de l’article est française : jour puis mois. Pour un public américain, inversez les deux premiers blocs ou, plus simple, normalisez tout en ISO dès la saisie avec type="date". Une application qui accepte les deux ordres sans les distinguer se trompe une fois sur deux pour toutes les dates du 1 au 12.
Comparer deux dates après validation
Une fois la date validée et convertie en objet Date, les comparaisons sont directes : les opérateurs < et > comparent les instants, et la soustraction donne un écart en millisecondes.
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 joursLe formatage pour l’affichage (toLocaleDateString, Intl.DateTimeFormat) est un autre sujet, comme la date et l’heure en PHP ou Carbon dans Laravel.
Regex ou objet Date : que choisir ?
- La grande regex quand vous voulez une seule règle, identique en JavaScript, PHP et HTML, sans dépendre d’une bibliothèque ni d’un objet
Date. - Découpage +
Dateoucheckdate()quand la lisibilité compte, ou quand vous devez ensuite manipuler la date : vous l’avez déjà sous forme d’objet. type="date"pour tout formulaire moderne : la saisie est guidée, la valeur normalisée, et il ne reste que le contrôle ISO côté serveur.
Les autres validations de la série : adresse e-mail, adresse IP, code postal et mot de passe.
Erreurs fréquentes
new Date(2026, 1, 30) ne lève pas d’erreur : elle renvoie le 2 mars 2026. Comparez toujours année, mois et jour après construction.2026-09-07 est refusé par la regex JJ/MM/AAAA. Utilisez la variante ISO de l’article, ou mieux, input type="date" qui renvoie déjà ce format.type="date" affiche « 07/09/2026 » en français mais sa valeur transmise est « 2026-09-07 ». La validation côté serveur doit attendre le format ISO.

