PHP DateTime Birthday Puzzle - Solve Date Challenges Like a Pro

PHP DateTime Birthday Puzzle - Solve Date Challenges Like a Pro

  1. PHP 🐘
  2. 2023-01-20 21:00
  3. 17 min read

Date handling in PHP is one of those things that seems trivial until it isn’t. You’ve probably written something like strtotime() or time() plus a few magic numbers, convinced yourself it worked, and only discovered the bug months later when daylight saving time or a leap year quietly destroyed your calculations.

The truth is that date and time arithmetic is surprisingly complex. Time zones shift. Governments change DST policies. Leap seconds exist. Months have different lengths. A day is not always 86,400 seconds, and assuming otherwise will eventually break your application.

This article takes a different approach to learning PHP date handling. Instead of a dry reference manual, we’ll work through a concrete puzzle — comparing two people’s birthdays — that touches every major feature of PHP’s modern DateTime API. By the time you finish, you’ll understand why DateTimeImmutable should be your default choice, how to compare dates safely, and how to avoid the traps that catch even experienced developers.

What You’ll Learn

  • How to create and manipulate dates safely using DateTimeImmutable
  • How to extract day-of-week, season, and ordinal day information using format()
  • How to calculate precise age differences with diff()
  • Why UNIX timestamp arithmetic is dangerous for date calculations
  • How to determine meteorological seasons from month and day
  • How to calculate days between two dates in the same calendar year
  • How to compute mean, median, mode, and a text-based histogram in PHP

The Birthday Puzzle

Here’s the challenge. You have two people with known birth dates. Given those two dates, you need to answer five questions:

  1. Were they born on the same day of the week?
  2. Are they born in the same meteorological season?
  3. Were they both born on an even-numbered day or an odd-numbered day?
  4. What is their exact age difference in years, months, and days?
  5. In any given calendar year, how many days fall between their birthdays?

Let’s define our two subjects. Alice was born on March 15, 1990. Bob was born on July 22, 1992.

Setting Up DateTimeImmutable

The first decision you need to make is which DateTime class to use. PHP offers two: DateTime and DateTimeImmutable. The difference is subtle but critical.

DateTime is mutable. Every method that modifies it — setDate(), modify(), add(), sub() — changes the original object. This sounds innocent, but it leads to bugs where a date changes unexpectedly because some other part of your code modified the same object reference.

DateTimeImmutable returns a new instance for every modification, leaving the original unchanged. This is the safer, more predictable choice and should be your default in almost every situation.

<?php
 
$alice = new DateTimeImmutable('1990-03-15');
$bob   = new DateTimeImmutable('1992-07-22');
 
echo $alice->format('F j, Y'); // March 15, 1990
echo $bob->format('F j, Y');   // July 22, 1992

Creating DateTimeImmutable instances is straightforward. PHP accepts a wide range of date string formats. The YYYY-MM-DD format is unambiguous and recommended. Avoid formats like 03/15/1990 which can be interpreted differently depending on locale.

Were They Born on the Same Day of the Week?

This is the simplest question. PHP’s format() method accepts a wide range of format characters. The lowercase l — that’s a lowercase L, not the number one — returns the full textual day of the week. The uppercase D returns the three-letter abbreviation.

<?php
 
$aliceDay = $alice->format('l'); // Thursday
$bobDay   = $bob->format('l');   // Wednesday
 
$sameDayOfWeek = $aliceDay === $bobDay;
 
echo "Alice was born on a $aliceDay.\n";
echo "Bob was born on a $bobDay.\n";
echo $sameDayOfWeek ? "Same day of week!" : "Different day of week.";

In this case, Alice was born on a Thursday and Bob on a Wednesday, so the answer is no.

The N format character returns the ISO-8601 numeric representation (1 for Monday through 7 for Sunday), which is useful for programmatic comparisons. The w format returns a zero-based numeric (0 for Sunday through 6 for Saturday). Which one you use depends on whether you follow ISO standards or the American Sunday-first convention.

<?php
 
$aliceNumeric = (int) $alice->format('N'); // 4 for Thursday
$bobNumeric   = (int) $bob->format('N');   // 3 for Wednesday
 
var_dump($aliceNumeric === $bobNumeric); // false

Are They Born in the Same Meteorological Season?

Meteorological seasons divide the year into three-month blocks based on the calendar, unlike astronomical seasons which are determined by solstices and equinoxes. Meteorologists use this system because it aligns with our monthly calendar and makes seasonal statistics more consistent.

The four meteorological seasons are:

  • Spring: March, April, May
  • Summer: June, July, August
  • Fall: September, October, November
  • Winter: December, January, February

To determine the season, we extract the month from each birthday and compare the three-month groupings.

<?php
 
function getMeteorologicalSeason(DateTimeImmutable $date): string
{
    $month = (int) $date->format('n');
 
    return match (true) {
        $month >= 3 && $month <= 5  => 'Spring',
        $month >= 6 && $month <= 8  => 'Summer',
        $month >= 9 && $month <= 11 => 'Fall',
        default                     => 'Winter',
    };
}
 
$aliceSeason = getMeteorologicalSeason($alice); // Spring
$bobSeason   = getMeteorologicalSeason($bob);   // Summer
 
echo $sameSeason = $aliceSeason === $bobSeason
    ? "Both born in $aliceSeason"
    : "Alice was born in $aliceSeason, Bob in $bobSeason";

The match expression — available since PHP 8.0 — is cleaner than a chain of if-elseif statements. It evaluates conditions in order and returns the first match. The default case catches Winter, which spans December through February.

Note that meteorological Winter wraps around the calendar year boundary (December to February). The match block handles this naturally since December (month 12) doesn’t match any of the first three conditions and falls through to default.

Were They Both Born on an Even or Odd Day?

This check uses the modulo operator on the day of the month. The j format character returns the day of the month without leading zeros, which is ideal for integer conversion.

<?php
 
function isEvenDay(DateTimeImmutable $date): bool
{
    return ((int) $date->format('j')) % 2 === 0;
}
 
$aliceEven = isEvenDay($alice); // 15 -> odd
$bobEven   = isEvenDay($bob);   // 22 -> even
 
echo "Alice born on an " . ($aliceEven ? "even" : "odd") . " day.\n";
echo "Bob born on an " . ($bobEven ? "even" : "odd") . " day.\n";
echo ($aliceEven === $bobEven) ? "Same parity!" : "Different parity.";

This is a straightforward check, but it demonstrates an important principle: always use PHP’s date formatting functions to extract date components. Never parse date strings manually. Never use explode() on a date string. The formatting API handles localization, calendar systems, and edge cases that your manual parsing probably doesn’t.

What Is Their Exact Age Difference?

This is where DateTimeImmutable::diff() shines. The diff() method returns a DateInterval object that contains the exact difference between two dates broken down into years, months, days, hours, minutes, and seconds.

<?php
 
$ageDifference = $alice->diff($bob);
 
echo $ageDifference->y . " years, "
     . $ageDifference->m . " months, "
     . $ageDifference->d . " days\n";
// Outputs: 2 years, 4 months, 7 days

The DateInterval object has public properties: y, m, d, h, i, s, days, invert, and more. The days property is particularly useful — it gives you the total number of days between the two dates, accounting for all calendar irregularities.

The invert property is 1 if the difference is negative (i.e., the first date is after the second). This lets you determine who is older without comparing timestamps.

<?php
 
if ($ageDifference->invert) {
    echo "Bob is older by {$ageDifference->y} years, {$ageDifference->m} months, {$ageDifference->d} days.";
} else {
    echo "Alice is older by {$ageDifference->y} years, {$ageDifference->m} months, {$ageDifference->d} days.";
}

The precision of diff() comes from the fact that it operates at the calendar level, not the timestamp level. It understands that months have different lengths, that February has 28 or 29 days depending on leap years, and that the Gregorian calendar has a complex structure.

How Many Days Between Their Birthdays in a Given Year?

This question is trickier than it looks. You need to calculate the number of days between March 15 and July 22 within the same calendar year. The naive approach would be to pick a specific year and calculate the difference between those two dates. But which year?

If you pick a non-leap year, you get one answer. If you pick a leap year, the answer might change because February 29 shifts the ordinal day numbering. Actually, in this specific case, both dates fall after February 29, so the leap day doesn’t affect the interval between them. But in general, you need to be careful about which year you choose.

The safest approach uses PHP’s format('z') method, which returns the zero-based day of the year (0 through 365). In a leap year, days after February 28 are shifted by one compared to a non-leap year.

<?php
 
function daysBetweenBirthdaysInYear(
    DateTimeImmutable $birthday1,
    DateTimeImmutable $birthday2,
    int $year
): int {
    $date1 = new DateTimeImmutable("$year-{$birthday1->format('m-d')}");
    $date2 = new DateTimeImmutable("$year-{$birthday2->format('m-d')}");
 
    $diff = $date1->diff($date2);
 
    return (int) $diff->format('%r%a');
}
 
$days = daysBetweenBirthdaysInYear($alice, $bob, 2023);
 
echo abs($days); // 129 days

For the year 2023, March 15 to July 22 is 129 days. If the first birthday comes after the second, the result will be negative, so we use abs() to get the absolute value.

An alternative approach uses the z format character directly:

<?php
 
function daysBetweenBirthdaysOrdinal(
    DateTimeImmutable $birthday1,
    DateTimeImmutable $birthday2,
    int $year
): int {
    $date1 = new DateTimeImmutable("$year-{$birthday1->format('m-d')}");
    $date2 = new DateTimeImmutable("$year-{$birthday2->format('m-d')}");
 
    $dayOfYear1 = (int) $date1->format('z');
    $dayOfYear2 = (int) $date2->format('z');
 
    return abs($dayOfYear1 - $dayOfYear2);
}

This ordinal approach is simpler and more readable, but be aware that in a leap year, the z value for dates after February 28 is one higher than in a non-leap year. This means the result can vary by one day depending on whether your chosen year is a leap year and whether either birthday falls after February 29.

Why You Should Never Use Timestamps for Date Calculations

Before PHP’s DateTime API matured, many developers used UNIX timestamps for date arithmetic. The pattern looked something like this:

<?php
 
// DANGEROUS: Never do this
$timestamp1 = strtotime('1990-03-15');
$timestamp2 = strtotime('1992-07-22');
 
$diffSeconds = abs($timestamp2 - $timestamp1);
$diffDays = $diffSeconds / 86400; // 86400 seconds in a day

This approach is fundamentally broken for several reasons.

Daylight Saving Time. A day with a DST transition is not 86,400 seconds long. In spring, a day can have 82,800 seconds. In fall, it can have 90,000 seconds. If your timestamp arithmetic crosses a DST boundary, your results will be off by an hour, which translates to fractional day errors.

Leap seconds. While rare, leap seconds are added to UTC to account for the Earth’s slowing rotation. The last one was added on December 31, 2016. A timestamp-based calculation that spans a leap second will be off by one second.

Leap years. February 29 exists. If you’re calculating across a leap year boundary, your day count will be wrong unless you manually account for it.

Large date ranges. Timestamps on 32-bit systems overflow on January 19, 2038 (the Year 2038 problem). Even on 64-bit systems, dates before 1901 or after 2038 can produce unexpected results.

Readability. $date1->diff($date2)->days communicates intent clearly. abs(strtotime($date1) - strtotime($date2)) / 86400 is a formula that every reader has to mentally parse.

The DateTime API exists specifically to solve these problems. Use it.

Real-World Use Cases

Event Scheduling Systems

Any application that schedules events — conference platforms, booking systems, calendar apps — needs to compare dates across days, weeks, months, and years. Determining whether two events fall on the same day of the week is a common requirement for recurring event logic. The meteorological season comparison is useful for event venues that charge different rates by season.

Age Verification and Compliance

Finance, healthcare, and e-commerce platforms need precise age calculations. A user must be 18 years and 0 days old to open an account, not 17 years and 364 days. Using diff() gives you exact year/month/day precision that timestamp arithmetic cannot reliably provide.

Billing and Subscription Cycles

Subscription platforms need to calculate prorated charges, trial periods, and renewal dates. The days-between-birthdays calculation maps directly to determining how many days fall between any two billing dates in a given month or year.

HR and Payroll Systems

Employee birthday tracking, anniversary reminders, and benefit eligibility dates all require safe date arithmetic. HR systems that depend on timestamp-based calculations inevitably produce bugs around DST transitions and leap years.

SaaS Analytics Platforms

Analytics dashboards often compare metrics across equivalent time periods — same day last week, same month last year, etc. The ordinal day-of-year calculation (z format) is useful for year-over-year comparisons that align with calendar position rather than weekday.

Best Practices

Always Use DateTimeImmutable

Make DateTimeImmutable your default. Only reach for mutable DateTime when you have a specific performance reason. The immutability guarantee eliminates an entire category of bugs where a date object is modified unintentionally through a shared reference.

Set the Default Timezone Explicitly

Always configure a default timezone in your application entry point or configuration:

<?php
 
date_default_timezone_set('UTC');

If you don’t, PHP uses the system timezone, which may differ between development, staging, and production environments. This inconsistency is a common source of “works on my machine” bugs.

Validate Date Inputs

Never trust user-supplied date strings. Use DateTimeImmutable::createFromFormat() with an explicit format to validate and parse user input:

<?php
 
$userInput = '03/15/1990';
$date = DateTimeImmutable::createFromFormat('m/d/Y', $userInput);
 
if (! $date) {
    // Handle invalid date
    $errors = DateTimeImmutable::getLastErrors();
}

This gives you control over the expected format and produces clear error messages when the input doesn’t match.

Prefer the c Format for ISO 8601

When storing or transmitting dates as strings, use ISO 8601 format:

<?php
 
$date->format('c'); // 1990-03-15T00:00:00+00:00

This format is unambiguous, sortable as a string, and understood by every modern programming language and database system.

Be Explicit About Time Zones

When creating dates, specify the timezone explicitly if it matters:

<?php
 
$tz = new DateTimeZone('America/New_York');
$date = new DateTimeImmutable('1990-03-15 09:30:00', $tz);

Default timezone handling is fine for simple date-only calculations, but any application that spans multiple time zones needs explicit timezone management.

Common Mistakes to Avoid

Using Timezone-Aware and Timezone-Unaware Dates Together

Mixing DateTimeImmutable instances with different timezones in comparisons can produce confusing results. Always normalize to the same timezone before comparing.

Assuming All Days Are 24 Hours

As discussed above, DST transitions make some days 23 or 25 hours long. If your application cares about hours, never calculate time intervals by multiplying days by 86,400 seconds.

Forgetting That Months Have Different Lengths

Adding “one month” to January 31 gives you March 3 (since February doesn’t have 31 days), not February 28. PHP’s DateInterval handles this by overflowing, but developers who don’t expect this behavior can get confusing results.

Using strtotime() for Complex Logic

strtotime() is fine for simple relative strings like “next Monday” or “+2 weeks”. It is not appropriate for precise date arithmetic. Use the DateTime API for anything that matters.

<?php
 
// Acceptable use of strtotime
$nextWeek = strtotime('+1 week');
 
// Use DateTime for anything precise
$date = new DateTimeImmutable('now');
$nextMonth = $date->modify('+1 month');

Ignoring the Year 2038 Problem

If your application runs on 32-bit systems or interacts with legacy databases that use 32-bit timestamps, plan for the Year 2038 problem. The DateTime API handles dates far beyond 2038, but time() and strtotime() do not.

Bonus Challenge: Stats 101 Grade Book

Now that you’ve mastered date handling, here’s a bonus puzzle that ties together PHP arrays, statistical functions, and algorithmic thinking.

You have an array of 40 exam grades. Your task is to calculate the mean, median, mode, and plot a simple text-based histogram.

<?php
 
$grades = [78, 85, 92, 67, 88, 91, 76, 84, 73, 69,
           95, 82, 79, 90, 88, 77, 83, 71, 86, 94,
           81, 75, 89, 72, 93, 80, 87, 70, 96, 68,
           79, 84, 91, 73, 82, 88, 76, 85, 90, 74];

Mean

The mean is the arithmetic average.

<?php
 
function calculateMean(array $grades): float
{
    return array_sum($grades) / count($grades);
}
 
echo calculateMean($grades); // Approximately 81.35

Median

The median is the middle value when the data is sorted. If there’s an even number of values, it’s the average of the two middle values.

<?php
 
function calculateMedian(array $grades): float
{
    sort($grades);
    $count = count($grades);
    $middle = intdiv($count, 2);
 
    if ($count % 2 === 0) {
        return ($grades[$middle - 1] + $grades[$middle]) / 2;
    }
 
    return $grades[$middle];
}
 
echo calculateMedian($grades); // Approximately 82

Mode

The mode is the value that appears most frequently. A dataset can have multiple modes.

<?php
 
function calculateMode(array $grades): array
{
    $counts = array_count_values($grades);
    $maxFrequency = max($counts);
 
    return array_keys($counts, $maxFrequency);
}
 
print_r(calculateMode($grades));

In our dataset, 79 and 88 appear three times each, making them the modes.

Text-Based Histogram

A histogram groups data into ranges and displays the frequency of each range as a bar. We’ll use a letter-grade scale.

<?php
 
function plotHistogram(array $grades): void
{
    $ranges = [
        'A (90-100)' => 0,
        'B (80-89)'  => 0,
        'C (70-79)'  => 0,
        'D (60-69)'  => 0,
        'F (0-59)'   => 0,
    ];
 
    foreach ($grades as $grade) {
        match (true) {
            $grade >= 90 => $ranges['A (90-100)']++,
            $grade >= 80 => $ranges['B (80-89)']++,
            $grade >= 70 => $ranges['C (70-79)']++,
            $grade >= 60 => $ranges['D (60-69)']++,
            default      => $ranges['F (0-59)']++,
        };
    }
 
    foreach ($ranges as $label => $count) {
        $bar = str_repeat('█', $count);
        echo sprintf("%-12s | %s (%d)\n", $label, $bar, $count);
    }
}
 
plotHistogram($grades);

The output gives you a quick visual distribution of the grades:

A (90-100)   | ███████████ (11)
B (80-89)    | ██████████████ (14)
C (70-79)    | ██████████ (10)
D (60-69)    | █████ (5)
F (0-59)     | (0)

This bonus challenge demonstrates how PHP’s built-in functions — array_sum, count, sort, array_count_values, and str_repeat — can be composed into meaningful statistical analysis without any external libraries.

Frequently Asked Questions

What’s the difference between DateTime and DateTimeImmutable?

DateTime is mutable — modifying it changes the original object. DateTimeImmutable returns a new object for every modification, leaving the original unchanged. DateTimeImmutable prevents bugs caused by shared references and should be your default choice.

Can I still use strtotime() in modern PHP?

Yes, strtotime() is not deprecated, but it should only be used for simple relative date strings like “next Friday” or “+2 weeks”. For any precise date arithmetic or comparison, use the DateTime API.

How do I handle time zones correctly with DateTimeImmutable?

Pass a DateTimeZone object as the second argument to the constructor. Always store dates in UTC internally and convert to the user’s timezone only when displaying. Use setTimezone() on a DateTimeImmutable instance (which returns a new instance) for conversion.

Why does adding one month to January 31 give March 3?

PHP’s DateInterval handles month overflow by advancing to the next month. Since February has fewer than 31 days, PHP overflows into March. If you need last-day-of-month semantics, use modify('last day of next month') or check manually with format('t') for the days in a month.

Is the Year 2038 problem still relevant?

Yes, if your application runs on 32-bit systems or interacts with legacy systems that use 32-bit timestamps. The DateTime API itself is unaffected — it handles dates far beyond 2038 — but time(), strtotime(), and database timestamp columns may be affected.

How do I calculate age accurately in PHP?

Use DateTimeImmutable::diff() between the birth date and the current date. The y property of the resulting DateInterval gives the exact age. Never subtract timestamps to calculate age.

What’s the easiest way to format a date for display?

Use format() with the desired format characters. Common patterns: 'F j, Y' for “March 15, 1990”, 'Y-m-d' for ISO 8601 date, 'l, F j, Y' for “Thursday, March 15, 1990”. For localized formats, use IntlDateFormatter.

How do I find the number of days in a given month?

Use format('t') which returns the number of days in the month of the date object. For February 2024 (a leap year), new DateTimeImmutable('2024-02-01')->format('t') returns 29.

What’s a reliable way to parse user-supplied dates?

Use DateTimeImmutable::createFromFormat() with an explicit format string. This gives you control over the expected format and reliable error handling via getLastErrors().

Can I use Carbon instead of DateTimeImmutable?

Yes, Carbon extends DateTimeImmutable and adds a fluent API with convenience methods like addDays(), diffForHumans(), and isWeekend(). It’s a popular choice in the Laravel ecosystem. However, the native DateTimeImmutable API is sufficient for most applications and avoids an external dependency.

Conclusion

Date and time handling is one of those domains where small mistakes create subtle, hard-to-reproduce bugs. The birthday puzzle we worked through touches on every major feature of PHP’s modern DateTime API — creation, formatting, comparison, difference calculation, and the critical distinction between immutable and mutable date objects.

The key takeaways are straightforward. Always use DateTimeImmutable as your default. Never rely on UNIX timestamp arithmetic for calendar calculations. Extract date components using the format() method. Calculate differences with diff(). And always be explicit about time zones.

The bonus Stats 101 challenge shows that the same careful thinking applies beyond dates. PHP’s array functions, combined with algorithmic structure, can handle real statistical computation without external dependencies.

Now it’s your turn. Take the birthday puzzle code from this article and extend it. Add a feature that calculates how many birthdays each person has had in their lifetime. Or determine the day of the week for every birthday they’ll have for the next 20 years. The more you practice with the DateTime API, the more natural it becomes — and the fewer timestamp-related bugs you’ll introduce into your applications.

Open your editor, create a new PHP file, and start experimenting. Your future self — the one who won’t be debugging a DST-related production issue at 2 AM — will thank you.

PHP DateTime DateTimeImmutable Date Handling PHP Puzzles Coding Challenge Date Arithmetic PHP 8