Newsletter

Date regex: validate DD/MM/YYYY or YYYY-MM-DD (JavaScript, PHP, HTML)

A date regex that rejects 30 February and 31 April, with the ISO and American variants, the JavaScript and PHP functions, the HTML attribute, and the moment when it is better to leave the regex aside.

Regex to validate a date: JavaScript, PHP and HTML
Quick answer

The regex below validates a French DD/MM/YYYY date (separator /, - or .) and rejects impossible dates such as 30 February or 31 April, leap years included. For the ISO YYYY-MM-DD format, a short regex is enough if you then confirm with the Date object in JavaScript or checkdate() in PHP.

Validating a date typed by hand is trickier than it looks: “30/02/2026” has the right shape and does not exist. The date regex below checks the DD/MM/YYYY format, with /, - or . as the separator, and rejects impossible days, including 29 February in non-leap years. You will then find the JavaScript, PHP and HTML versions, the ISO variant, and the case where the Date object does better than a regex.

The date regex, ready to copy

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

It is long because it reasons like a calendar. Three branches, separated by |:

  1. The 31st only exists in 31-day months (01, 03, 05, 07, 08, 10, 12), and the 29th and 30th in every month except February.
  2. 29 February is only accepted if the year is a leap year: divisible by 4 (0[48]|[2468][048]|[13579][26]), or a century divisible by 400 ((?:16|[2468][048]|[3579][26])00). 2000 and 2024 pass, 1900 and 2023 are rejected.
  3. From the 1st to the 28th, every month accepts the date.

The back-references \1, \2, \3 and \4 require the second separator to be identical to the first: 07/09/2026 and 07-09-2026 pass, 07/09-2026 does not.

A single separator?

To accept the slash only, replace each (\/|-|\.) with (\/). The parentheses are still needed: the back-references \1 to \4 point to those groups.

The dates tested

Actual result of the regex under Node.js 22 and PHP 8.5, identical in both engines:

Input Result Reason
31/12/2026 accepted 31 in a 31-day month
29/02/2024 accepted 2024 is a leap year
29/02/2000 accepted 2000 is divisible by 400
01/01/2026 and 1/1/2026 accepted the leading zero is optional
07-09-2026 and 07.09.2026 accepted other permitted separators
29/02/2023 rejected 2023 is not a leap year
29/02/1900 rejected century not divisible by 400
30/02/2026 rejected February never has 30 days
31/04/2026 rejected April has 30 days
00/01/2026 rejected day 0 does not exist
12/13/2026 rejected month 13: the regex really is DD/MM, not MM/DD
07/09-2026 rejected mismatched separators
2026-09-07 rejected ISO format, see below
07/09/26 and 31/12/1899 accepted two-digit year, or a year before 1900: bound it in your code if need be

JavaScript version

The function anchors the regex with ^ and $, strips the whitespace around the input and returns a boolean.

validateDate.js
/**
 * Checks a date in the DD/MM/YYYY, DD-MM-YYYY or DD.MM.YYYY format.
 * @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

In a form, the pattern is the same as for validating an email address: listen for submit, call preventDefault() if the date is invalid, and show the message next to the field.

PHP version

The server-side check is mandatory: JavaScript can be disabled. The same pattern works with preg_match(); the back-references are written \\1 in a single-quoted PHP string.

isValidDate.php
/**
 * Checks a date in the DD/MM/YYYY, DD-MM-YYYY or DD.MM.YYYY format.
 */
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)

The regex-free alternative: checkdate()

PHP already knows whether a day, month and year triplet exists. A small regex to split the input, then checkdate() to judge it: shorter and more readable.

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]); // month, day, year
}

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

Beware of DateTime::createFromFormat('d/m/Y', '29/02/2023'): it does not return false but 1st March 2023, reporting the discrepancy in DateTime::getLastErrors(). For a strict check, checkdate() is safer.

HTML version

If the user types the date as free text, the pattern attribute applies the regex before submission. It is implicitly anchored: ^ and $ are removed.

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
>

But the best HTML solution is not the regex: it is type="date". The browser displays a picker suited to the user’s language, prevents 30 February, and always sends the value in the ISO YYYY-MM-DD format, whatever it displays.

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

The consequence on the server: with type="date", it is the ISO format you have to validate, not the French one.

ISO variant: YYYY-MM-DD

For an ISO date, a short regex checks the shape and the Date object confirms that the day exists. Together, the two replace the big regex.

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) returns 2 March: we check that nothing has shifted
  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

The same technique applies to the French format: split with /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/, then check with Date. It is the JavaScript equivalent of PHP’s checkdate().

What about the American MM/DD/YYYY format?

This article’s regex is French: day first, then month. For an American audience, swap the first two blocks or, simpler still, normalise everything to ISO as soon as it is entered, with type="date". An application that accepts both orders without telling them apart gets it wrong half the time for every date from the 1st to the 12th.

Comparing two dates after validation

Once the date has been validated and converted into a Date object, comparisons are straightforward: the < and > operators compare instants, and subtraction gives a difference in milliseconds.

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 days

Formatting for display (toLocaleDateString, Intl.DateTimeFormat) is another subject, as are date and time in PHP and Carbon in Laravel.

Regex or Date object: which to choose?

  • The big regex when you want a single rule, identical in JavaScript, PHP and HTML, with no dependency on a library or on a Date object.
  • Split + Date or checkdate() when readability matters, or when you then need to work with the date: you already have it as an object.
  • type="date" for any modern form: input is guided, the value is normalised, and all that is left is the ISO check on the server.

The other validations in the series: email address, IP address, postcode and password.

Common errors

Mixing separators The regex captures the first separator and requires the same one afterwards: “07/09-2026” is rejected. That is intentional, but surprising if a form accepts several ways of typing a date.
Forgetting that new Date silently corrects new Date(2026, 1, 30) does not throw: it returns 2 March 2026. Always compare year, month and day after construction.
Validating an ISO date with the French regex 2026-09-07 is rejected by the DD/MM/YYYY regex. Use this article's ISO variant or, better, input type="date", which already returns that format.
Trusting the browser's display format A type="date" field displays “07/09/2026” in French but the value it sends is “2026-09-07”. Server-side validation has to expect the ISO format.

HTMLJavaScriptPHP

Damien Flandrin Web developer since 2010, creator of Gekkode and Email Impact. Every article is tested on a real project before publication. Contact
Newsletter

New tests, tutorials and projects, by e-mail.

Reproducible tests, versioned code, dated results. Never any spam.