
Poniższe wyrażenie regularne waliduje datę francuską DD/MM/RRRR (separator /, - lub .) i odrzuca daty niemożliwe, takie jak 30 lutego czy 31 kwietnia, wraz z latami przestępnymi. Dla formatu ISO RRRR-MM-DD wystarczy krótkie wyrażenie, jeśli potwierdzisz je potem obiektem Date w JavaScripcie albo funkcją checkdate() w PHP.
Sprawdzenie daty wpisanej ręcznie jest trudniejsze, niż się wydaje: „30/02/2026” ma poprawną postać, a nie istnieje. Poniższy regex data weryfikuje format DD/MM/RRRR, ze znakiem /, - lub . jako separatorem, i odrzuca niemożliwe dni, łącznie z 29 lutego w latach nieprzestępnych. Dalej znajdziesz wersje dla JavaScriptu, PHP i HTML-a, wariant ISO oraz przypadek, w którym obiekt Date radzi sobie lepiej niż wyrażenie regularne.
Regex data gotowy do skopiowania
^(?:(?: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})$Jest długi, bo rozumuje jak kalendarz. Trzy gałęzie, rozdzielone znakiem |:
- Dzień 31 istnieje tylko w miesiącach mających 31 dni (
01, 03, 05, 07, 08, 10, 12), a 29 i 30 we wszystkich miesiącach poza lutym. - 29 lutego jest przyjmowany tylko wtedy, gdy rok jest przestępny: podzielny przez 4 (
0[48]|[2468][048]|[13579][26]) albo stulecie podzielne przez 400 ((?:16|[2468][048]|[3579][26])00). Lata 2000 i 2024 przechodzą, 1900 i 2023 są odrzucane. - Od 1 do 28 datę przyjmuje każdy miesiąc.
Odwołania \1, \2, \3 i \4 wymuszają, by drugi separator był taki sam jak pierwszy: 07/09/2026 i 07-09-2026 przechodzą, 07/09-2026 nie.
Aby przyjmować wyłącznie ukośnik, zastąp każde (\/|-|\.) przez (\/). Nawiasy są nadal potrzebne: odwołania wsteczne od \1 do \4 wskazują właśnie na te grupy.
Przetestowane daty
Rzeczywisty wynik wyrażenia w Node.js 22 i PHP 8.5, identyczny w obu silnikach:
| Wpis | Wynik | Powód |
|---|---|---|
31/12/2026 | przyjęta | 31 w miesiącu mającym 31 dni |
29/02/2024 | przyjęta | 2024 jest rokiem przestępnym |
29/02/2000 | przyjęta | 2000 dzieli się przez 400 |
01/01/2026 i 1/1/2026 | przyjęte | początkowe zero jest opcjonalne |
07-09-2026 i 07.09.2026 | przyjęte | inne dozwolone separatory |
29/02/2023 | odrzucona | 2023 nie jest rokiem przestępnym |
29/02/1900 | odrzucona | stulecie niepodzielne przez 400 |
30/02/2026 | odrzucona | luty nigdy nie ma 30 dni |
31/04/2026 | odrzucona | kwiecień ma 30 dni |
00/01/2026 | odrzucona | dzień 0 nie istnieje |
12/13/2026 | odrzucona | miesiąc 13: wyrażenie oczekuje DD/MM, a nie MM/DD |
07/09-2026 | odrzucona | różne separatory |
2026-09-07 | odrzucona | format ISO, zobacz niżej |
07/09/26 i 31/12/1899 | przyjęte | rok dwucyfrowy albo sprzed 1900: w razie potrzeby ogranicz zakres w swoim kodzie |
Wersja JavaScript
Funkcja kotwiczy wyrażenie znakami ^ i $, usuwa spacje wokół wpisu i zwraca wartość logiczną.
/**
* Sprawdza datę w formacie DD/MM/RRRR, DD-MM-RRRR lub DD.MM.RRRR.
* @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')); // falseW formularzu schemat jest ten sam co przy walidacji adresu e-mail: nasłuchiwać zdarzenia submit, wywołać preventDefault(), gdy data jest nieprawidłowa, i wyświetlić komunikat obok pola.
Wersja PHP
Kontrola po stronie serwera jest obowiązkowa: JavaScript da się wyłączyć. Ten sam wzorzec działa z preg_match(); odwołania wsteczne zapisuje się jako \\1 w łańcuchu PHP w apostrofach.
/**
* Sprawdza datę w formacie DD/MM/RRRR, DD-MM-RRRR lub DD.MM.RRRR.
*/
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)Alternatywa bez wyrażenia regularnego: checkdate()
PHP potrafi już powiedzieć, czy trójka dzień, miesiąc, rok istnieje. Krótkie wyrażenie do rozbicia wpisu, a potem checkdate() do oceny: krócej i czytelniej.
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]); // miesiąc, dzień, rok
}
var_dump(isValidDateFr('29/02/2024')); // bool(true)
var_dump(isValidDateFr('29/02/2023')); // bool(false)
var_dump(isValidDateFr('31/04/2026')); // bool(false)Uwaga na DateTime::createFromFormat('d/m/Y', '29/02/2023'): nie zwraca false, tylko 1. marca 2023, sygnalizując rozbieżność w DateTime::getLastErrors(). Do ścisłej kontroli checkdate() jest pewniejsze.
Wersja HTML
Jeśli użytkownik wpisuje datę jako zwykły tekst, atrybut pattern stosuje wyrażenie przed wysłaniem formularza. Jest niejawnie zakotwiczony: usuwamy ^ i $.
<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
>Najlepszym rozwiązaniem HTML nie jest jednak wyrażenie regularne, tylko type="date". Przeglądarka pokazuje selektor dopasowany do języka użytkownika, blokuje 30 lutego i zawsze przesyła wartość w formacie ISO RRRR-MM-DD, niezależnie od tego, co wyświetla.
<input id="naissance" name="naissance" type="date" min="1900-01-01" max="2026-12-31" required>Konsekwencja po stronie serwera: przy type="date" walidować trzeba format ISO, a nie francuski.
Wariant ISO RRRR-MM-DD
Przy dacie ISO krótkie wyrażenie kontroluje postać, a obiekt Date potwierdza, że dzień istnieje. Razem zastępują to duże wyrażenie.
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) zwraca 2 marca: sprawdzamy, czy nic się nie przesunęło
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')); // falseTa sama technika działa dla formatu francuskiego: rozbić wpis wyrażeniem /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/, a potem sprawdzić obiektem Date. To javascriptowy odpowiednik checkdate() z PHP.
A format amerykański MM/DD/RRRR?
Wyrażenie z artykułu jest francuskie: najpierw dzień, potem miesiąc. Dla odbiorcy amerykańskiego zamień dwa pierwsze bloki miejscami albo, prościej, normalizuj wszystko do ISO już przy wpisywaniu, przez type="date". Aplikacja, która przyjmuje oba porządki i ich nie rozróżnia, myli się w połowie przypadków dla wszystkich dat od 1 do 12.
Porównywanie dwóch dat po walidacji
Gdy data jest już sprawdzona i przekształcona w obiekt Date, porównania są bezpośrednie: operatory < i > porównują chwile, a odejmowanie daje różnicę w milisekundach.
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 dniFormatowanie do wyświetlenia (toLocaleDateString, Intl.DateTimeFormat) to osobny temat, podobnie jak data i godzina w PHP czy Carbon w Laravelu.
Wyrażenie regularne czy obiekt Date: co wybrać?
- Duże wyrażenie, gdy chcesz mieć jedną regułę, identyczną w JavaScripcie, PHP i HTML-u, bez zależności od biblioteki ani od obiektu
Date. - Rozbicie wpisu +
Datelubcheckdate(), gdy liczy się czytelność albo gdy i tak musisz potem operować na dacie: masz ją już jako obiekt. type="date"w każdym nowoczesnym formularzu: wpisywanie jest prowadzone, wartość znormalizowana, a po stronie serwera zostaje tylko kontrola formatu ISO.
Pozostałe walidacje z tej serii: adres e-mail, adres IP, kod pocztowy i hasło.
Częste błędy
new Date(2026, 1, 30) nie zgłasza błędu: zwraca 2 marca 2026. Po utworzeniu obiektu zawsze porównuj rok, miesiąc i dzień.2026-09-07 jest odrzucane przez wyrażenie DD/MM/RRRR. Użyj wariantu ISO z artykułu albo, lepiej, input type="date", które zwraca już ten format.type="date" pokazuje po francusku „07/09/2026”, ale przesyłana wartość to „2026-09-07”. Walidacja po stronie serwera musi oczekiwać formatu ISO.

