Regex data: walidacja DD/MM/RRRR i RRRR-MM-DD (JavaScript, PHP)

Regex data, który odrzuca 30 lutego i 31 kwietnia, z wariantem ISO i amerykańskim, funkcjami w JavaScripcie i PHP, atrybutem HTML oraz momentem, w którym lepiej odłożyć wyrażenie regularne na bok.

Wyrażenie regularne do walidacji daty: JavaScript, PHP i HTML
Szybka odpowiedź

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

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})$

Jest długi, bo rozumuje jak kalendarz. Trzy gałęzie, rozdzielone znakiem |:

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

Tylko jeden separator?

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

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

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

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

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]); // 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 $.

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
>

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.

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

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

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

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 dni

Formatowanie 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 + Date lub checkdate(), 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

Mieszanie separatorów Wyrażenie zapamiętuje pierwszy separator i wymaga potem tego samego: „07/09-2026” jest odrzucane. To zamierzone, ale zaskakujące, jeśli formularz dopuszcza kilka sposobów zapisu.
Zapominanie, że new Date po cichu poprawia 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ń.
Walidacja daty ISO francuskim wyrażeniem 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.
Zaufanie do formatu wyświetlanego przez przeglądarkę Pole type="date" pokazuje po francusku „07/09/2026”, ale przesyłana wartość to „2026-09-07”. Walidacja po stronie serwera musi oczekiwać formatu ISO.

HTMLJavaScriptPHP

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty, e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.