Dates7 min readExamples run on PHP 8.5.10

checkdate()

Learn how PHP 8.4 checkdate() validates a month, day and year, including leap years, range limits and the mistakes that cause wrong results.

Signature

checkdate(int $month, int $day, int $year): bool

Purpose

checkdate() answers one question: do these three integers form a real calendar date in the Gregorian calendar? It checks that the month exists, that the day fits inside that month, and that the year is in the supported range. Leap years are taken into account, so February 29 is accepted only in years that have one.

The function does not parse text, create timestamps or objects, or adjust bad values. It only returns a yes or no answer. That makes it a good gatekeeper before you build a date from user input.

Signature

The argument order is month, day, year. This is easy to mix up if you are used to ISO order (year, month, day) or to day-first formats.

Parameters and defaults

None of the parameters is optional, so there are no defaults.

  • $month: must be between 1 and 12 inclusive.
  • $day: must be within the number of days in the given month. The year decides whether February has 28 or 29 days.
  • $year: must be between 1 and 32767 inclusive.

Return value and failure cases

The function returns true when the date is valid and false otherwise. An invalid date is not an error: no exception or warning is produced, you just get false.

Because the parameters are typed as int, passing a value that cannot be used as an integer, such as a non-numeric string, raises a TypeError. Under declare(strict_types=1), even a numeric string like "12" is rejected, so cast values deliberately before the call.

Example 1: leap years

A year is a leap year when it is divisible by 4, except century years, which must be divisible by 400. checkdate() applies that rule for you.

Example
<?php
echo json_encode([
    checkdate(2, 29, 2000),
    checkdate(2, 29, 1900),
    checkdate(2, 29, 2024),
    checkdate(2, 29, 2023),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
[true,false,true,false]

The year 2000 and 2024 are leap years, while 1900 and 2023 are not.

Example 2: validating an ISO-style string

User input usually arrives as text. A common pattern is to split the text with a regular expression, cast the parts to integers, and then ask checkdate() whether the combination is real. The pattern alone cannot reject 2023-02-29; the calendar check does that.

Example
<?php
function isValidIsoDate(string $value): bool
{
    if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m) !== 1) {
        return false;
    }
    return checkdate((int) $m[2], (int) $m[3], (int) $m[1]);
}

$inputs = ['2024-02-29', '2023-02-29', '2024-13-01', '2024-04-31', '24-01-01'];
$results = [];
foreach ($inputs as $input) {
    $results[$input] = isValidIsoDate($input);
}
echo json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"2024-02-29":true,"2023-02-29":false,"2024-13-01":false,"2024-04-31":false,"24-01-01":false}

Note the order in the call: the captured month is $m[2], the day is $m[3], and the year is $m[1].

Example 3: range boundaries

The documented limits are inclusive. Values just outside them fail.

Example
<?php
$checks = [
    'year 1' => checkdate(1, 1, 1),
    'year 32767' => checkdate(12, 31, 32767),
    'year 0' => checkdate(1, 1, 0),
    'year 32768' => checkdate(1, 1, 32768),
    'month 0' => checkdate(0, 1, 2024),
    'day 0' => checkdate(1, 0, 2024),
];
echo json_encode($checks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"year 1":true,"year 32767":true,"year 0":false,"year 32768":false,"month 0":false,"day 0":false}

Edge cases

  • Month lengths: April, June, September and November have 30 days, so checkdate(4, 31, 2024) is false.
  • Day zero and month zero: neither is valid. The function does not roll them over to the previous month or year the way some timestamp functions do.
  • Proleptic calendar: the check uses the Gregorian rules for every year in range, including years before the calendar was historically adopted.
  • Date only: there is no time component, so hours, minutes and time zones are outside its scope.

Common mistakes

  1. Swapping the argument order. checkdate(2024, 2, 29) is not a way to check February 29, 2024. Always write month, day, year.
  2. Assuming it parses strings. It takes three integers. Split and cast the text yourself, and validate the format first.
  3. Skipping format validation. Casting "abc" with (int) gives 0, which then fails as a month, but loose input such as "2024-2-9" may be accepted or rejected depending on your own pattern. Decide what formats you allow.
  4. Treating it as a normalizer. It never fixes 2023-02-30 into March. If you want normalization, that is a different task.
  5. Passing non-numeric values under strict types. This raises a TypeError rather than returning false.
  • mktime() builds a Unix timestamp from date parts and will roll over out-of-range values, unlike checkdate().
  • strtotime() parses English date text into a timestamp.

Recap

checkdate(int $month, int $day, int $year): bool returns true only for real Gregorian dates, with months 1 to 12, a day that fits the month and leap-year rules, and a year from 1 to 32767. It returns false for anything else, never adjusts values, and needs integer arguments in month, day, year order.

Tested examples

Leap year handling
<?php
echo json_encode([
    checkdate(2, 29, 2000),
    checkdate(2, 29, 1900),
    checkdate(2, 29, 2024),
    checkdate(2, 29, 2023),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
echo "\n";
Output
[true,false,true,false]
Validating ISO-style date strings
<?php
function isValidIsoDate(string $value): bool
{
    if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m) !== 1) {
        return false;
    }
    return checkdate((int) $m[2], (int) $m[3], (int) $m[1]);
}

$inputs = ['2024-02-29', '2023-02-29', '2024-13-01', '2024-04-31', '24-01-01'];
$results = [];
foreach ($inputs as $input) {
    $results[$input] = isValidIsoDate($input);
}
echo json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"2024-02-29":true,"2023-02-29":false,"2024-13-01":false,"2024-04-31":false,"24-01-01":false}
Range boundaries
<?php
$checks = [
    'year 1' => checkdate(1, 1, 1),
    'year 32767' => checkdate(12, 31, 32767),
    'year 0' => checkdate(1, 1, 0),
    'year 32768' => checkdate(1, 1, 32768),
    'month 0' => checkdate(0, 1, 2024),
    'day 0' => checkdate(1, 0, 2024),
];
echo json_encode($checks, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
Output
{"year 1":true,"year 32767":true,"year 0":false,"year 32768":false,"month 0":false,"day 0":false}