PHP Time handling: the ultimate guide (Part 2/3)

THE DEFINITIVE GUIDE TO PHP TIME HANDLING

PART 2/3

 

In the first part of this guide we saw how web development can be affected by the complexity of time measurement, underlining the differences between second-based clocks like the International Atomic Time and astronomical-based clocks like the Universal Time.

We also introduced UTC, the standard civilian time clock used around the world, and one of its most common implementations called Unix Time, which is used by many different systems and programming languages including PHP.

 

In this second part of the guide you will see the most important time related PHP functions and how to use them properly. You will also learn how to solve the leap seconds problem introduced in part 1 (with a complete example).

 

 

 

World map

 

 

THE 4 ESSENTIAL TIME RELATED FUNCTIONS

 

 

The PHP library includes many time related functions and classes. Four of them are particularly relevant:

 

The reason these functions are so important is that they are both necessary and sufficient to properly perform all time related operations in PHP.

(An alternative to using these functions would be to use the DateTime class instead, but since in some contexts it’s much easier to use the above functions I prefer to focus on those).

 

In the first part of this guide we already introduced time(), a function that returns the current Unix Time. We also underlined the fact that Unix Time is transparent to leap seconds and that the Unix Time value returned by time() doesn’t contain any information about leap seconds.

When dealing with time intervals, using Unix Time can thus lead to wrong results if leap seconds are not properly considered.

 

Suppose you want to calculate a flight arrival time knowing the departure time and the flight length. One simple solution would be to add the flight time to the departure time and then get the result; that will usually work, but if one leap second occurs inside the flight timeframe then the result will be wrong (by 1 second).

In contexts where you need exact results you simply cannot ignore leap seconds.

Unfortunately, PHP itself doesn’t know anything about leap seconds, so you need to get the required data from somewhere else.

 

Some international organizations keep track of leap seconds. The IETF, for example, shares a file on its website with a list of all the past leap seconds and, when available, the upcoming one too. This file can be found here:

 

 

You can use that file (or any other file as long as it’s from an authoritative source) to properly insert leap seconds when needed.

 

Let’s see how you can do that with an exercise.

We want to make a PHP script that takes a UTC datetime as input and then prints the corresponding International Atomic Time (if you don’t remember the difference between UTC and Atomic Time, take a look at the infographic in part 1 before continuing).

The offset between UTC and Atomic Time increases by 1 second every time a leap second is added, so we are going to parse the IETF file to know exactly how many leap seconds we need to add when calculating the offset.

 

Here is the (fully working) code:

 

<?php

/* UTC datetime components are read from the request string */
$year = intval($_REQUEST['year'], 10);
$month = intval($_REQUEST['month'], 10);
$day = intval($_REQUEST['day'], 10);
$hour = intval($_REQUEST['hour'], 10);
$minute = intval($_REQUEST['minute'], 10);
$second = intval($_REQUEST['second'], 10);
/* Set UTC as the work time zone */
date_default_timezone_set('UTC');
/* Find the Unix Time relative to the UTC datetime */
$unix_t = mktime($hour, $minute, $second, $month, $day, $year);
/* Find the offset between UTC and Atomic Time (TAI) */
$tai_offset = tai_offset($unix_t);
/* Print the result. */
echo 'UTC datetime is: ' . date('Y-m-d H:i:s', $unix_t) . '<br>';
echo 'TAI datetime is: ' . date('Y-m-d H:i:s', $unix_t + $tai_offset) . '<br>';
echo 'Offset (in seconds): ' . strval($tai_offset);

/* Calculates the offset between UTC and TAI, including leap seconds */
function tai_offset($unix_t)
{   
   /* Array containing offset changes */
   $offset = offset_array();
   
   /* Offset between UTC and TAI (10 seconds is the base offset) */
   $ts_offset = 10;
   
   /* We iterate the offset array until we arrive at our datetime, adding leap seconds as needed */
   foreach ($offset as $key => $value)
   {
      /* $key here contains the Unix Time of the leap second datetime */
      if ($key <= $unix_t)
      {
         $ts_offset = $value;
      }
   }
   
   return $ts_offset;
}

/*    This function creates an array of offsets between UTC and TAI.
   It gets the leap seconds information from the IETF file */
function offset_array()
{
   /* This is the number of seconds from January 1st 1900 to 1970 (UTC). This is needed for parsing the IETF file */
   $base_t = '2208988800';
   /* Result array */
   $offset = array();
   /* Read the IETF file (note: here, for the sake of simplicity, we assume no errors occur) */
   $file = file('https://www.ietf.org/timezones/data/leap-seconds.list', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
   /* Read the file lines */
   foreach ($file as $line)
   {
      /* Check whether the line starts with a digit, meaning it actually contains offset information instead of comments */
      if (ctype_digit(mb_substr($line, 0, 1)))
      {
         /* Each file value is separated by a tab */
         $line_array = explode("\\\\\\\\t", $line);
         
         /* The offset, in seconds, starts from January 1st 1900 so we need to subtract $base_t to get the Unix Time.
            Note that numbers here can exceed the 32 bits integer's size, so we need to use the BC Math extension. */
         $offset_t = bcsub($line_array[0], $base_t);
         $offset_t = intval($offset_t, 10);
         
         /* Set the result array key => value, using the Unix Time as key and the offset as value */
         $offset[$offset_t] = intval($line_array[1], 10);
      }
   }
   return $offset;
}

 

 

You can try this script yourself simply passing the datetime components (year, month, day, hour, minute and second) you want to convert into Atomic Time. (You can download the source file from the form below or copy the code from the example above).

For example, the 2018-04-15 00:00:00 UTC datetime corresponds to the Atomic Time 2018-04-15 00:00:37, with an offset of 37 seconds.

 

 

 

 

 

For dates in the year 2016 the offset will be 36 seconds instead, because a new leap second was added at the end of 2016. You can have fun trying different datetimes (but dates before year 1972, the starting date of UTC, will probably give wrong results).

This script works for future datetimes as well but remember that leap seconds will be added sometime in the future. As I am writing this post the next leap second hasn’t been scheduled yet. As soon as it will be added in the IETF file, you will see that passing a datetime after the new leap second date will make the offset increase by 1 second.

 

 

date() AND mktime()

 

Date

 

 

The date() function is used to format and print a datetime from its Unix Time. The first argument sets the output format, while the second one is the Unix Time of the datetime we want to show. The default value for the second argument is the current Unix Time (the same that would be returned by the time() function).

You already saw how this function works in the previous example.

 

The interesting thing about date() is that it can also be used to retrieve a numeric component of a datetime. This functionality is very useful for time related operations and it’s often used in conjunction with another function: mktime().

 

In a sense, mktime() does the opposite of date(): it takes all the numeric components of a datetime (year, month, day, hour, minute and second) and then returns the corresponding Unix Time.

Here is a simple example:

 

$year = 2015;
$month = 10;
$day = 20;
$hour = 11;
$minute = 48;
$second = 47;
$unix_t = mktime($hour, $minute, $second, $month, $day, $year);
echo 'Unix Time is ' . strval($unix_t) . '<br>';
echo 'Datetime is ' . date('Y-m-d H:i:s', $unix_t);
/* 
Output:
Unix Time is 1445334527
Datetime is 2015-10-20 11:48:47
*/

 

 

It’s important to know that date() and mktime() output depends on which time zone your script is working on. By default, a PHP script uses the “system” time zone, which is usually the same time zone of the computer it runs on. For example, if you live in New Zealand then your default PHP time zone will be the one used in New Zealand.

 

Now suppose that two developers, one living in New Zealand and the other living in Los Angeles, run the following script on their computers at the same time:

echo 'Current Unix Time is ' . strval(time()) . '<br>';
echo 'Current datetime is ' . date('Y-m-d H:i:s');

 

 

The Unix Time returned by time() will be the same in both cases, because Unix Time is a representation of UTC which is the same all around the world. However, that same moment in time will be a different datetime depending on what time zone we consider, so the date()‘s output will be different.

 

 

mktime() can also be used to perform automatic datetime operations.

You can increment or decrement any of the arguments to make mktime() add or subtract datetime components. For example, a day argument set to 0 will be interpreted as the last day of the previous month; this is handy because it frees you from worrying about the different number of days a month can have.

This works with negative numbers too. For example, if the day argument is set to -2, it will be interpreted as three days before the first of the month. It also works with increments; for example, if the month value is set to 13 then it will be interpreted as the first month of the following year.

 

You may think of this as just a funny way to play with dates, but I actually use this functionality a lot in my work.

For example, in some of my scripts I need to get the Unix Time of the current time of the previous day.

A common mistake is to do that subtracting 86400 seconds (the number of seconds in a day) from the current Unix Time. That, however, will not work when a daylight-saving time change occurs.

The right (and simple) solution is to use mktime() simply subtracting one from the day component of the datetime. That will work flawlessly no matter the daylight-saving time changes.

 

Here is how you can do it:

 

$year = intval(date('Y'), 10);
$month = intval(date('n'), 10);
$day = intval(date('j'), 10);
$hour = intval(date('H'), 10);
$minute = intval(date('i'), 10);
$second = intval(date('s'), 10);
$prev_day_midnight = mktime($hour, $minute, $second, $month, $day - 1, $year);

 

 

 

TIME ZONES AND date_default_timezone_set()

 

 

date_default_timezone_set() is a function that sets the time zone used by the PHP script. Each time zone has its own parameters like the offset from the UTC and the daylight-saving time rules.

 

The time() function is not affected by time zone changes, because Unix Time is always the same regardless of which time zone the script works in.

 

date() and mktime(), on the other hand, are affected because their results change depending on the time zone. In fact, the same Unix Time is represented differently in different time zones, as you can see in the following example:

 

<?php
/* Set the time zone to UTC in order to print the UTC datetime */
date_default_timezone_set('UTC');
/* We print the current UTC datetime */
echo 'Current UTC datetime is ' . date('Y-m-d H:i:s') . '<br>';
/* We print the current Unix Time */
echo 'Current Unix Time is ' . strval(time()) . '<br>';
/* Note that the Unix Time does not depend on which time zone we are in */
date_default_timezone_set('Arctic/Longyearbyen');
echo 'Current Arctic datetime is ' . date('Y-m-d H:i:s') . '<br>';
echo 'Current Unix Time is ' . strval(time()) . '<br>';
/* Output:
Current UTC datetime is 2018-04-17 20:12:56
Current Unix Time is 1523995976
Current Arctic datetime is 2018-04-17 22:12:56
Current Unix Time is 1523995976
*/

 

 

The same applies to mktime(). The same set of numeric components of a datetime correspond to different Unix Times depending on which time zone those the components are referring to:

 

<?php
$year = 2018;
$month = 2;
$day = 15;
$hour = 0;
$minute = 40;
$second = 15;

date_default_timezone_set('UTC');
echo strval(mktime($hour, $minute, $second, $month, $day, $year)) . '<br>';
date_default_timezone_set('Arctic/Longyearbyen');
echo strval(mktime($hour, $minute, $second, $month, $day, $year)) . '<br>';
/*
Output:
1518655215
1518651615
*/

 

Handling time zone changes can be tricky, but don’t worry: in the last part of this guide you will see how to properly handle time zone changes (including the case where even leap seconds matter).

 

Continue to part 3.

 

 

If you have any questions, feel free to ask in the comments below or on my Facebook Group: Alex PHP café.

If this guide has been helpful to you, please spend a second of your time and share it using the buttons below… thanks!

 

Alex

Leave a Comment