Chapter 15. Handling Dates and Times

Dates and times are all over the place in a web application. In a shopping cart, you need to handle shipping dates of products. In a forum, you need to keep track of when messages are posted. In all sorts of applications, you need to keep track of the last time each user logged in so that you can tell them things like “15 new messages were posted since you last logged in.”

Handling dates and times properly in your programs is more complicated than handling strings or numbers. A date or a time is not a single value but a collection of values—month, day, and year, for example, or hour, minute, and second. Because of this, doing math with them can be tricky. Instead of just adding or subtracting entire dates and times, you have to consider their component parts and what the allowable values for each part are. Hours go up to 12 (or 24), minutes and seconds go up to 59, and not all months have the same number of days.

To ease this burden, PHP provides you with a class, DateTime, that encapsulates all the information about a specific point in time. With the methods of this class, you can print out a date or time in the format of your choice, add or subtract two dates, and work with time intervals.

In this book, the phrase time parts (or date parts or time-and-date parts) means an array or group of time-and-date components such as day, month, year, hour, minute, and second. Formatted time string (or formatted date string, etc.) means a string that contains some particular grouping of time-and-date parts—for example “Thursday, October 20, 2016” or “3:54 p.m.”

Displaying the Date or Time

The simplest display of date or time is telling your users what time it is. For this, use the format() method of a DateTime object, as shown in Example 15-1.

Example 15-1. What time is it?
$d = new DateTime();
print 'It is now: ';
print $d->format('r');
print "\n";
Warning
If you get a warning that “It is not safe to rely on the system’s timezone settings” from the PHP engine when you run the code in Example 15-1, peek ahead at “Working with Timezones” to find out what that means and how to make it go away.

At noon on October 20, 2016, Example 15-1 would print:

It is now: Thu, 20 Oct 2016 12:00:00 +0000

When you create a DateTime object, the time or date to store inside it is provided to the object constructor. If no argument is provided, as in Example 15-1, the current date and time are used. The format string passed to the format() method controls how the date and time are formatted for printing.

Individual letters in the format string translate into certain time values. Example 15-2 prints out a month, day, and year.

Example 15-2. Printing a formatted date string
$d = new DateTime();
print $d->format('m/d/y');

At noon on October 20, 2016, Example 15-2 would print:

10/20/16

In Example 15-2, the m becomes the month (10), the d becomes the day of the month (20), and the y becomes the two-digit year (04). Because the slash is not a format character that format() understands, it is left alone in the string that format() returns.

Table 15-1 lists all of the special characters that DateTime::format() understands.

Table 15-1. Date/time formatting characters
Type Format character Description Range/example
DayjDay of the month; numeric131
DaydDay of the month; numeric; leading zeros0131
DaySEnglish ordinal suffix for day of month, textst, th, nd, rd
DayzDay of the year; numeric0365
DaywDay of the week; numeric; 0 == Sunday06
DayNDay of the week; numeric; 1 == Monday17
DayDAbbreviated weekday name; textMonSun
DaylFull weekday name; textMondaySunday
WeekWWeek number in the year (ISO-8601); numeric; leading zeros; week 01 is the first week that has at least four days in the current year; Monday is the first day of the week01-53
MonthMAbbreviated month name; textJanDec
MonthFFull month name; textJanuaryDecember
MonthnMonth; numeric112
MonthmMonth; numeric; leading zeros0112
MonthtMonth length in days; numeric2831
YearyYear; without century; numeric0099
YearYYear; including century; numeric00009999
YearoYear (ISO-8601); including century; numeric; year that the current week number (W) belongs to00009999
YearLLeap year flag; 1 == yes0,1
HourgHour; 12-hour clock; numeric112
HourhHour; 12-hour clock; numeric; leading zeros0112
HourGHour; 24-hour clock; numeric023
HourHHour; 24-hour clock; numeric; leading zeros0023
Houraa.m. or p.m. designationam, pm
HourAA.M. or P.M. designationAM, PM
MinuteiMinutes; numeric; leading zeros0059
SecondsSeconds; numeric; leading zeros0059
SeconduMicroseconds; numeric; leading zeros000000999999
TimezoneeTimezone identifier; textFrom supported timezones
TimezoneTTimezone abbreviation; text GMT, CEST, MDT, etc.
TimezoneOTimezone difference to UTC in hours with sign; text-1100+1400
TimezonePTimezone difference to UTC in hours with sign and colon; text-11:00+14:00
TimezoneZTimezone difference to UTC in seconds; numeric-3960050400
OtherIDaylight Saving Time flag; 1 == yes0,1
OtherBSwatch Internet Time; numeric000999
OthercISO-8601 formatted date; text2016-10-20T12:33:56+06:00
OtherrRFC-2822 formatted date; textThu, 20 Oct 2016 12:33:56 +0600
OtherUSeconds since 12:00:00 a.m. UTC on Jan 1, 19701476945236

Parsing a Date or Time

To create a DateTime object that represents a specific time, pass that time as a first argument to the constructor. This argument is a string indicating the date and time you want the object to represent. A DateTime object understands a very wide variety of format strings. Whatever you are dealing with probably works, but an exhaustive list of all the possible formats is available at http://www.php.net/datetime.formats.

Example 15-3 shows a few date-and-time formats that the DateTime constructor understands.

Example 15-3. Formatted date/time strings that DateTime understands
// If only a time is supplied, the current date is used for day/month/year
$a = new DateTime('10:36 am');
// If only a date is supplied, the current time is used for hour/minute/second
$b = new DateTime('5/11');
$c = new DateTime('March 5th 2017');
$d = new DateTime('3/10/2018');
$e = new DateTime('2015-03-10 17:34:45');
// DateTime understands microseconds
$f = new DateTime('2015-03-10 17:34:45.326425');
// Epoch timestamp must be prefixed with @
$g = new DateTime('@381718923');
// Common log format
$h = new DateTime('3/Mar/2015:17:34:45 +0400');

// Relative formats, too!
$i = new DateTime('next Tuesday');
$j = new DateTime("last day of April 2015");
$k = new DateTime("November 1, 2012 + 2 weeks");

At noon on October 20, 2016, the full dates and times that would end up in the variables in Example 15-3 would be:

Thu, 20 Oct 2016 10:36:00 +0000
Wed, 11 May 2016 00:00:00 +0000
Sun, 05 Mar 2017 00:00:00 +0000
Sat, 10 Mar 2018 00:00:00 +0000
Tue, 10 Mar 2015 17:34:45 +0000
Tue, 10 Mar 2015 17:34:45 +0000
Fri, 05 Feb 1982 01:02:03 +0000
Tue, 03 Mar 2015 17:34:45 +0400
Tue, 25 Oct 2016 00:00:00 +0000
Thu, 30 Apr 2015 00:00:00 +0000
Thu, 15 Nov 2012 00:00:00 +0000

If you have discrete date-and-time parts, such as those submitted from form elements in which a user can specify month, day, and year or hour, minute, and second, you can also pass them to the the setTime() and setDate() methods to adjust the time and date stored inside the DateTime object.

Example 15-4 shows setTime() and setDate() at work.

Example 15-4. Setting date or time parts
// $_POST['mo'], $_POST['dy'], and $_POST['yr']
// contain month number, day, and year submitted
// from a form
//
// $_POST['hr'], $_POST['mn'] contain
// hour and minute submitted from a form

// $d contains the current time, but soon that will
// be overridden
$d = new DateTime();

$d->setDate($_POST['yr'], $_POST['mo'], $_POST['dy']);
$d->setTime($_POST['hr'], $_POST['mn']);

print $d->format('r');

If $_POST['yr'] is 2016, $_POST['mo'] is 5, $_POST['dy'] is 12, $_POST['hr'] is 4, and $_POST['mn'] is 15, then Example 15-4 prints:

Thu, 12 May 2016 04:15:00 +0000

Even though $d is initialized to the current date and time when Example 15-4 is run, the calls to setDate() and setTime() change what’s stored inside the object.

The DateTime object tries to be as accommodating as possible when parsing incoming data. Sometimes this is helpful, but sometimes it is not. For example, consider what you think should happen if, in Example 15-4, $_POST['mo'] is 3 and $_POST['dy'] is 35. It can never be the 35th of March. That doesn’t bother DateTime, though. It considers March 35 to be the same as April 4 (March 31 is the last day of March, so March 32 is the next day (April 1), March 33 is April 2, March 34 is April 3, and March 35 is April 4). Calling $d->setDate(2016, 3, 35) gives you a DateTime object set to April 4, 2016.

For stricter validation of days and months, use checkdate() on the month, day, and year first. It tells you whether the provided month and day are valid for the provided year, as shown in Example 15-5.

Example 15-5. Verifying months and days
if (checkdate(3, 35, 2016)) {
    print "March 35, 2016 is OK";
}
if (checkdate(2, 29, 2016)) {
    print "February 29, 2016 is OK";
}
if (checkdate(2, 29, 2017)) {
    print "February 29, 2017 is OK";
}

In Example 15-5, only the second call to checkdate() returns true. The first fails because March always has fewer than 35 days, and the third fails because 2017 is not a leap year.

Calculating Dates and Times

Once you’ve got a DateTime object that represents a particular point in time, it’s straightforward to do date or time calculations. You might want to give a user a set of dates or times to choose from in a menu. Example 15-6 displays an HTML <select> menu where each choice is a day. The first choice is the date corresponding to the first Tuesday after the program is run. The subsequent choices are every other day after that.

Example 15-6. Displaying a range of days
$daysToPrint = 4;
$d = new DateTime('next Tuesday');
print "<select name='day'>\n";
for ($i = 0; $i < $daysToPrint; $i++) {
    print "  <option>" . $d->format('l F jS') . "</option>\n";
    // Add 2 days to the date
    $d->modify("+2 day");
}
print "</select>";

In Example 15-6, the modify() method changes the date inside the DateTime object at each pass through the loop. The modify() method accepts a string holding one of the relative date/time formats described at http://www.php.net/datetime.formats.relative and adjusts the object accordingly. In this case, +2 day bumps it forward two days each time.

On October 20, 2016, Example 15-6 would print:

<select name='day'>
  <option>Tuesday October 25th</option>
  <option>Thursday October 27th</option>
  <option>Saturday October 29th</option>
  <option>Monday October 31st</option>
</select>

The DateTime object’s diff() method tells you the difference between two dates. It returns a DateInterval object, which encapsulates the interval between the dates. Example 15-7 checks whether a given birthdate means someone is over 13 years old.

Example 15-7. Computing a date interval
$now = new DateTime();
$birthdate = new DateTime('1990-05-12');
$diff = $birthdate->diff($now);

if (($diff->y > 13) && ($diff->invert == 0)) {
    print "You are more than 13 years old.";
} else {
    print "Sorry, too young.";
}

In Example 15-7, the call to $birthdate->diff($now) returns a new DateInterval object. This object’s properties describe the interval between $birthdate and $now. The y property is the number of years and the invert property is 0 when the difference is a positive amount (the invert property would be 1 if $birthdate were after $now). The other properties are m (months), d (days in the month), h (hours), i (minutes), s (seconds), and days (total number of days between the two dates).

Working with Timezones

Dates and times are, unfortunately, not just collections of hours, minutes, seconds, months, days, and years. To be complete, they must also include a timezone. “Noon on October 20, 2016” is not the same instant in time in New York City as it is in London.

The PHP engine must be configured with a default timezone to use. The easiest way to do this is to set the date.timezone configuration parameter in your PHP configuration file.1 If you can’t adjust the file, call the date_default_timezone_set() function in your program before you do any date or time manipulation. In PHP 7, the engine defaults to the UTC timezone if you don’t specify your own default value.

There is a big list of possible timezone values that the PHP engine understands. Instead of using your local timezone, however, a convention that often makes software development easier is to set the timezone to UTC, the code for Coordinated Universal Time. This is the time at zero degrees longitude and doesn’t adjust in the summer for a Daylight Saving Time setting. Although you have to do a little mental math to convert a UTC timestamp that appears, say, in a log file to your local time, using UTC makes it easier to work with time data that could be coming from multiple servers located in different timezones. It also avoids confusion during the switch to and from Daylight Saving Time because the apparent “clock time” doesn’t change.

Chapter Summary

This chapter covered:

  • Defining some time- and date-handling vocabulary such as time-and-date parts and formatted time-and-date string
  • Getting the current time and date
  • Printing formatted time-and-date strings with the DateTime object’s format() method
  • Exploring the format characters that format() understands
  • Parsing a date or time from an absolute or relative format
  • Calculating a date or time relative to another date or time
  • Computing the difference between two dates
  • Understanding why UTC is a convenient default timezone

1 “Modifying PHP Configuration Directives” explains how to adjust configuration parameters.