Handling Time and Date in PHP and MySQL - OOP Version

Published:
Updated:

Introduction

Since I wrote the original article about Handling Date and Time in PHP and MySQL several years ago, it seemed like now was a good time to update it for object-oriented PHP.  This article does that, replacing as much as possible the principles and examples of the original article on a 1:1 basis.  As I created the examples for this article, I came across some anomalies in the PHP OOP DateTime implementation.  Here are some of my impressions and some notable gotcha's that I found.


It is possible to create an infinite loop with date/time arithmetic.  Your code will look right, but transitions at the boundaries of daylight savings time will change the values in your DateTime objects.  I hope PHP will fix this.

See: http://php.net/manual/en/datetime.sub.php#114780


When you create a new DateTime object, you can assign both a date/time string and a timezone.  If you create the DateTime object from a Unix timestamp, the timezone cannot be used in the constructor.  You must assign the timezone to the object separately, after the object is created.


The ISO-8601 format for DateTime::format() method is not 100% ISO-8601 compatible.  You can use DateTime::ATOM instead if you need perfect compatibility.


The designator for a Unix timestamp in DateTime::__construct() is the at-sign ("@") prepended to the numeric Unix timestamp.  The at-sign ("@") is also the PHP error-control operator, but it has no error-control meaning in the DateTime constructor.


There is no avoiding arithmetic - DateTime::getOffset() returns its answer in seconds, but functions that use this information may expect minutes or hours.


I found no convenient way to instantiate a DateTime object and get a formatted date/time string in a single line of code.  The marriage of PHP date() and strtotime() was a convenience that is not here any more.  I'll update this article if I find a good solution.


At PHP 7.1+ the DateTime constructor incorporates microseconds when the object is constructed from the current time.  This may increase the risk that two DateTime objects will be unequal.


Now the good news.  Most of what you learned about date formatting with the date() format parameters is still applicable.  You will write a little more code with the OOP notation, but that is common for OOP programming and may help to improve readability.  And none of the common tasks we use in procedural PHP is impeded by the OOP DateTime notation.  A list of the most useful references is included at the end of this Article.


The DATE/TIME Tower of Babel

Human beings can read dates and times in a variety of ways.  We readily understand such things as September 5th, 2010, and we know that it comes before November 11th, 2010,and after May 9th, 2010.  We have shorthand conventions that let us write things like 9/5/2010 or the military correspondence format 5 Sep 2010.  But when we try to process dates and times in a computer program, this variety of formats becomes a jumble of confusion.  In response to the confusion, the International Organization for Standards was moved to publish a standard in 1988.  The standard has been revised and extended somewhat since the original publication.  The only thing that mystifies students of history is why it took so long to promulgate a standard.


Toward a Universal DATETIME Notation

Date formats for computer processing are prescribed by the ISO-8601 standard.  The ISO-8601 standard places all the information in fixed-width data strings with leading zeros where needed.  When you read ISO-8601 standard information you notice immediately that everything lines up in columns.  The larger values are to the left and progressively smaller values are to the right, starting with Year, then Month, then Day, then Hour, etc.  Date/time information formatted according to the standard is very useful because it is both easy to read and easy to understand in comparisons and computations.  For example, the date '2010-09-05' is a valid ISO-8601 string meaning September 5th, 2010.  Imagine how difficult it would be to write programming that works with dates in the text formats, or dates that are formatted like this: 05.09.2010.  Does that mean May 9th, 2010 or September 5th, 2010?  Fortunately the ISO-8601 standard removes the ambiguity.


The ISO-8601 standard removes the ambiguity about the time of day, as well.  All of the hours are represented on a 24-hour clock, so there is no question about what "two o'clock" might mean.  The string 0200 or 02:00:00 refers to 2:00 a.m.  If you're thinking of 2:00 p.m. your ISO-8601 standard will use 14:00:00.


This link gives a good discussion and examples of permissible variations on the ISO-8601 format.

See http://en.wikipedia.org/wiki/ISO_8601


The Value of PHP time() is Always the Same

No matter where in the world you stand, the value of time() is always the same.  The PHP function time() returns the number of seconds since the Unix Epoch.  While local times may differ, time() ticks on second-by-second.  Run this script to see the effect.  Knowing this, we can do arithmetic with seconds, and then return human-readable date and time values that are sensible for different locations around the world.

<?php
/**
 * Different Timezones share the same Unix Timestamp
 */
error_reporting(E_ALL);
echo '<pre>';

// TIMEZONES AROUND THE GLOBE
$locations
= array
( 'Pacific/Auckland'
, 'Australia/Sydney'
, 'Australia/Perth'
, 'Asia/Tokyo'
, 'Asia/Calcutta'
, 'Asia/Tel_Aviv'
, 'Africa/Cairo'
, 'Europe/Rome'
, 'Europe/London'
, 'Atlantic/Bermuda'
, 'America/Chicago'
, 'America/Anchorage'
, 'Pacific/Honolulu'
, 'UTC'
)
;
// ITERATE OVER THE TIMEZONES
foreach ($locations as $location)
{
    // SET OUR ZONE AND LOCAL TIME
    $zoneobj = new DateTimeZone($location);
    $dateobj = new DateTime(NULL, $zoneobj);

    // SHOW THE LOCATION AND THE CURRENT DATE / TIME
    echo PHP_EOL;
    echo str_pad($location, 24, ' ');
    echo $dateobj->format(DateTime::RSS);

    // SHOW THE NUMBER OF SECONDS SINCE THE EPOCH, RECOMPUTED IN EACH TIMEZONE
    echo ' ';
    echo $dateobj->format('U');
}


Handling External Input

Whenever you accept a date/time string from an external data source, what is the first thing you should do?  Turn it (immediately) into an instance of the DateTime object.  The DateTime constructor can turn almost any English language human-readable date/time text into an object for use in DateTime computations and displays. Like the PHP function date(), the DateTime::format() method can turn DateTime information into a formatted date. 

<?php 
/**
 * The DateTime constructor may throw an Exception
 */
error_reporting(E_ALL);
echo '<pre>';

$external = 'Yesterday';
try
{
    $dateobj = new DateTime($external);
}
catch (Exception $e)
{
    // ON FAILURE, SHOW THE EXCEPTION
    var_dump($e);
}
echo PHP_EOL . $dateobj->format(Datetime::ATOM);
echo PHP_EOL;

Once you have done that internal date format conversion your programming will handle internal date/time computations easily.


When you are storing date/time values, you may want to store them in the ISO-8601 format strings or in the form of Unix timestamps.  I prefer to use the formatted strings - they are easier to understand when you're debugging.


Handling External Input that is Invalid

The PHP DateTime constructor can turn virtually any human-readable date / time text into a DateTime object - but it won't work on everything.  When it fails to make the conversion, it throws Exception.  You can and should test for this.


<?php
/**
 * Invalid date/time strings can be detected when you catch the Exception
 */
error_reporting(E_ALL);
echo '<pre>';

$external = 'Nonsense';
$dateobj  = FALSE;
try
{
    $dateobj = new DateTime($external);
}
catch (Exception $e)
{
    // ON FAILURE, SHOW THE EXCEPTION
    var_dump($e);
}
if ($dateobj)
{
    $iso_datetime = $dateobj->format(Datetime::ATOM);
    echo "SUCCESS: $external EQUALS ISO-8601 $iso_datetime";
}
else
{
    echo "ERROR: I DO NOT UNDERSTAND $external";
}


Handling External Input that is Formatted (contrary to the disambiguation rules)

The PHP DateTime Constructor works much the same way as strtotime(), making some assumptions and obeying certain rules about the format of the date / time input string.  But what if you have dates that are not formatted according to the rules?  One of the common issues arises when we have dates that are in European order d-m-y, but have been written with slashes, implying American order m/d/y.  This may result in incorrect output when the values for m and d  are both in the range from 1 - 12, and will cause an exception when m exceeds 12.  Fortunately PHP has a way around this issue, in the static constructor createFromFormat().  We can tell PHP what format to use as it interprets the date.  This example shows how.


<?php
/**
 * Date values separated by slash are assumed to be in American order: m/d/y
 * Date values separated by dash are assumed to be in European order: d-m-y
 * Exact formats for date/time strings can be injected with createFromFormat()
 *
 * http://php.net/manual/en/datetime.createfromformat.php
 */
error_reporting(E_ALL);
echo '<pre>';

// THIS WOULD IMPLY MONTH == 19, OBVIOUSLY INVALID
$external = "19/10/2016 14:48:21";

// HOWEVER WE CAN INJECT THE FORMATTING WHEN WE DECODE THE DATE
$format = "d/m/Y H:i:s";
$dateobj = DateTime::createFromFormat($format, $external);

$iso_datetime = $dateobj->format(Datetime::ATOM);
echo "SUCCESS: $external EQUALS ISO-8601 $iso_datetime";


Interesting Forms of External Input

All of these external inputs work correctly with the DateTime constructor and this gives you and your clients powerful ways of talking about dates and computing with dates.

- 3 hours

tomorrow

tomorrow midnight

tomorrow 1:35pm

March 15, 1986

yesterday

yesterday + 1 week

next year

now

now + 627 hours 15 minutes

last Tuesday

third Wednesday

3 minutes, 15 seconds


Producing "Pretty" Dates From DateTime Objects ISO-8601 Dates

When you are ready to present the dates to people and you want them to see nicely formatted dates, you can use the DateTime::format() method to do the reformatting.  Let's say you have this timestamp value: 1,284,593,400 (which equals the ISO-8601 date: '2010-09-15T18:30:00-0500'. )  Maybe you got it out of your data base or computed it in your script.  It's meaningful, but not what you expect when you open the envelope.  "Please join us for cocktails on Unix Timestamp 1,284,593,400."  How can we get something that would be appropriate on an invitation?  Here is how you might do the conversion from a timestamp or ISO-8601 date to the pretty date.

<?php 
/**
 * The DateTime::format() can change the rendering of date values and produce pretty dates
 */
error_reporting(E_ALL);
echo '<pre>';

$iso_date = '2010-09-15T18:30:00';
$formats  = 'l, F jS, Y \a\t g:ia';
$dateobj  = new DateTime($iso_date);
echo PHP_EOL . $dateobj->format($formats);

Outputs "Wednesday, September 15th, 2010 at 6:30pm" -- very civilized!


Breaking down the date formatting, we have the following (all documented on PHP.net)...

$pattern
= 'l, '    // LOWERCASE L - text name of the day of the week
. 'F '     // UPPERCASE F - text name of the month
. 'j'      // LOWERCASE J - day of the month
. 'S, '    // UPPERCASE S - ordinal like the letters in 1st, 2nd, etc 
. 'Y '     // UPPERCASE Y - 4-digit year
. '\a\t '  // ESCAPED LETTERS... More to follow
. 'g:'     // LOWERCASE G - hours on a 12-hour clock
. 'i'      // LOWERCASE I - minutes 
. 'a'      // LOWERCASE A - designator of "am" or "pm"
;


You might envision several patterns that you need in your application.  Most PHP sites and services are written with a central "common.php" file that gets included into all the specialized scripts.  You could prepare the formatting patterns in the common script and refer to them by a variable name, or define the patterns as constants.

define('MILITARY_CORRESPONDENCE', 'j M y');
define('SHORT_8601', 'Y-m-d');
define('COCKTAILS', 'l, F jS, Y \a\t g:ia');


What about the escaped letters?  Like the date() function, the DateTime::format() uses letter patterns as signals to describe the conversion into human-readable character strings, so some letters have special relevance and some do not.  Any character that is not used as a signal in the date pattern string is returned from the function unaltered.  Punctuation is one example - commas and hyphens go right through.  But if we want to get one of the signal letters back, we must tell PHP to ignore the signal and just return the letter.  We do that with the back-slash character, as shown here.  Try running these two lines of code to see how the escape can be useful:

echo PHP_EOL . date('The year is: Y');
echo PHP_EOL . date('\T\h\e \y\e\a\r\ \i\s\: Y');


Setting Your Own Clock Values

Since the values contained in the DateTime object are usable in many different time zones, you want to have control over the way PHP interprets the time zone.  You control this time with the DateTimeZone object.  You can set the DateTimeZone in the DateTime constructor, or you can inject a new DateTimeZone into a DateTime object via the setTimeZone() method.  Here is an example that I use, because my server is located in the midwest at ChiHost.com.


<?php
/**
 * DateTime objects are usually aware of time zones via the DateTimeZone object
 */
error_reporting(E_ALL);
echo '<pre>';

$string  = 'Now';
$zoneobj = new DateTimeZone("America/Chicago");
$formats = 'l, F jS, Y \a\t g:ia (e)';
$dateobj = new DateTime($string, $zoneobj);
echo PHP_EOL . $dateobj->format($formats);

// WHAT TIMEZONE SHOULD I USE?
$tz_ids = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, 'US');
foreach($tz_ids as $zone)
{
    echo PHP_EOL . $zone;
}


Special Meanings for NOW and TODAY

We know how to convert external date/time strings into DateTime objects, and we know how to return them to human-preferred formats with DateTime::format().  Now we can return to some of the special meanings of date/time strings.  NOW and TODAY are particularly useful.

<?php 
/**
 * NOW and TODAY have special meanings in the DateTime object; NOW includes time of day
 */
error_reporting(E_ALL);
echo '<pre>';

$date = new DateTime('Now');
echo PHP_EOL . $date->format(DateTime::RSS);

$date = new DateTime('Today');
echo PHP_EOL . $date->format(DateTime::RSS);

$date = new DateTime('Now', new DateTimeZone('UTC'));
echo PHP_EOL . $date->format(DateTime::RSS);

$date = new DateTime('Today', new DateTimeZone('UTC'));
echo PHP_EOL . $date->format(DateTime::RSS);

The principal difference between NOW and TODAY is the time of day.  TODAY is always today's local date at midnight, with hour, minute and second equal to zero.  NOW is today's date including the current time of day.  So in our September example, NOW and TODAY have UNIX timestamp values equal to 1,284,593,400 and 1,284,526,800 respectively.  The difference is 66,600.  This is the number of seconds from midnight to 6:30pm (18 hours plus 30 minutes).  


This script shows that the Unix timestamp is consistent, no matter what timezone you have specified.

<?php 
/**
 * The Unix Timestamp values for NOW and TODAY are the same, no matter what timezone
 */
error_reporting(E_ALL);
echo '<pre>';

$dateobj = new DateTime('Now');
echo PHP_EOL . $dateobj->format('U');

$dateobj = new DateTime('Today');
echo PHP_EOL . $dateobj->format('U');

$dateobj = new DateTime('Now', new DateTimeZone('UTC'));
echo PHP_EOL . $dateobj->format('U');

$dateobj = new DateTime('Today', new DateTimeZone('UTC'));
echo PHP_EOL . $dateobj->format('U');


Computing the Difference Between DateTime Objects

This script shows how to compute the difference between two DateTime objects, using the diff() method.


<?php 
/**
 * Computing the difference between NOW and TODAY in different timezones
 */
error_reporting(E_ALL);
echo '<pre>';

// TESTING UTC
$zoneobj    = new DateTimeZone('UTC');
$date_now   = new DateTime('Now',   $zoneobj);
$date_today = new DateTime('Today', $zoneobj);
$date_diff  = $date_now->diff($date_today);
$print_diff = $date_diff->format('%H:%i:%S');

echo PHP_EOL . $zoneobj->getName();
echo PHP_EOL . $print_diff;
echo PHP_EOL;

// TESTING ANOTHER ZONE
$zoneobj    = new DateTimeZone('America/New_York');
$date_now   = new DateTime('Now',   $zoneobj);
$date_today = new DateTime('Today', $zoneobj);
$date_diff  = $date_now->diff($date_today);
$print_diff = $date_diff->format('%H:%i:%S');

echo PHP_EOL . $zoneobj->getName();
echo PHP_EOL . $print_diff;
echo PHP_EOL;

// USING METHOD CHAINING FOR TIGHTER CODE
$print_diff  = $date_now->diff($date_today)->format('%H:%i:%S');

echo PHP_EOL . $zoneobj->getName();
echo PHP_EOL . $print_diff;
echo PHP_EOL;


As you can see, it is important to be consistent about your timezone.  You can deal with this phenomenon in one of two ways.  You can set your timezone to UTC.  Or you can set your timezone to a reasonable setting for the location of your server.  I prefer the latter.  See http://php.net/manual/en/timezones.php and http://php.net/manual/en/timezones.others.php for more.


Time Without Date

Let's say we want to know the elapsed time between two events, and we want to express it in notation like HH:MM:SS. DateTime::diff() and DateTime::format() can help with this.

<?php 
/**
 * Computing the difference between two times
 */
error_reporting(E_ALL);
echo '<pre>';

$alpha = "2:30:47pm";
$omega = "3:43:16pm";
$date_alpha = new DateTime($alpha);
$date_omega = new DateTime($omega);
$elapsed    = $date_omega->format('U') - $date_alpha->format('U');
$date_diff  = $date_omega->diff($date_alpha);;
$print_diff = $date_diff->format('%H:%i:%S');

echo PHP_EOL . "TWIXT $alpha AND $omega THERE ARE " . number_format($elapsed) . " SECONDS";
echo PHP_EOL . "RIGHT ELAPSED TIME IS $print_diff";


Leap Year and Daylight Savings Time

These are amazingly easy in PHP.  But beware of a bug in DateTime::add(), illustrated in this code.


<?php 
/**
 * Daylight Savings Time and Leap Year are easy, but...
 *
 * There is bug in the computations when using DateTime::add() across DST
 */
error_reporting(E_ALL);
echo '<pre>';

// USE THE SAME ZONE FOR ALL THESE TESTS
$zoneobj = new DateTimeZone('America/New_York');

// MAKE TESTS FOR THE CURRENT MOMENT
$dateobj = new DateTime('NOW', $zoneobj);
$leap    = $dateobj->format('L');
if ($leap) echo "IT IS A LEAP YEAR";
$dstt = $dateobj->format('I');
if ($dstt) echo "IT IS DAYLIGHT SAVINGS TIME";


// ILLUSTRATE A DST CHANGE IN OUR TIMEZONE
$alpha = new DateTime('November 2, 2014 01:57:00', $zoneobj);
$omega = new DateTime('November 2, 2014 02:02:00', $zoneobj);

echo PHP_EOL . $alpha->format('c');
if ($alpha->format('I')) echo ' IS DST';

echo PHP_EOL . $omega->format('c');
if ($omega->format('I')) echo ' IS DST';
echo PHP_EOL;

// YOU CAN ALSO FIND TIMEZONE INFORMATION IN "isdst" AND "abbr"
$tran = $zoneobj->getTransitions($alpha->format('U'), $alpha->format('U')+1);
var_dump($tran);


// GET THE TIMESTAMPS
$alpha_u = $alpha->format('U');
$omega_u = $omega->format('U');

// SHOW THE TRANSITION BETWEEN EDT AND EST
while ($alpha_u < $omega_u)
{
    // CREATE A NEW DATETIME OBJECT FROM THE TIMESTAMP
    $alpha = new DateTime("@$alpha_u");

    // ASSIGN THE ZONE AND DETERMINE THE STATUS OF DST
    $alpha->setTimeZone($zoneobj);
    echo PHP_EOL . $alpha->format('r');
    if ($alpha->format('I')) echo " IS DAYLIGHT SAVINGS TIME";

    // INCREMENT THE TIMESTAMP BY SIXTY SECONDS
    $alpha_u = $alpha_u + 60;
}

/**
 * DO NOT DO IT THIS WAY; THIS WILL CAUSE AN INFINITE LOOP
 *
$alpha = new DateTime('November 2, 2014 01:57:00', $zoneobj);
$omega = new DateTime('November 2, 2014 02:02:00', $zoneobj);
$minute = new DateInterval('PT1M'); // Pee-Tee-One-Em

// SHOW THE TRANSITION BETWEEN EDT AND EST
while ($alpha < $omega)
{
    // THIS RECASTS THE TIME TO THE LOCAL CLOCK TIME
    $alpha = $alpha->add($minute);

    echo PHP_EOL . $alpha->format('r');
    if ($alpha->format('I')) echo " IS DAYLIGHT SAVINGS TIME";
}
 */


Using mktime() in DATETIME Computations

The PHP mktime() function returns a Unix timestamp according to an argument list.  The arguments in order are hour, minute, second, month, day, year.  Because mktime() will work with out-of-range arguments, it can be useful for date calculations.  But in my experience it was better to use the DateInterval object.


<?php 
/**
 * Using mktime() is not as dependable as using DateInterval
 */
error_reporting(E_ALL);
echo '<pre>';

// WORKS NO MATTER WHAT MONTH IT IS
$last_month = mktime(0, 0, 0, date("m")-1, date("d"), date("Y"));
var_dump($last_month);

// USE THE SAME ZONE FOR ALL THESE TESTS
$zoneobj = new DateTimeZone('America/New_York');

// HIGH NOON THIRTY DAYS FROM NOW - NOT RELIABLE BECAUSE OF DST TRANSITION
$dateobj = new DateTime( '@' . mktime(12, 0, 0, date("m"), date("d")+30, date("Y")) );
$dateobj->setTimeZone($zoneobj);
var_dump($dateobj->format('g:ia T \o\n D, M j, Y'));

// WORKS BETTER
$interval_p30d = new DateInterval('P30D');
$dateobj  = new DateTime('TODAY 12:00', $zoneobj);
$dateobj->add($interval_p30d);
var_dump($dateobj->format('g:ia T \o\n D, M j, Y'));


Ambiguity of the term "Month"

PHP is very permissive in its language requirements when describing dates and this may lead to some confusion when it comes to months.  Consider the meaning of "next month" when used on January 5.  Obviously the answer is February 5.  Now consider what happens when it is January 30.  What should the correct answer be?  This code snippet shows how I handle this question.  Different business rules may apply for your work.  There may be ambiguity in leap year, too.  Consider "Feb 29, 2008 + 1 year."  You may want to install this script and let it inform your policy about the meaning of "month" in contracts and forecasts.

<?php 
/**
 * The term "month" may be ambiguous since months can have different numbers of days
 */
error_reporting(E_ALL);
echo '<pre>';

// USE THE SAME ZONE FOR ALL THESE TESTS
$zone = new DateTimeZone('America/New_York');

// TEST DATES INCLUDE SOME THAT MIGHT CAUSE AMBIGUITY WHEN YOU SAY "next month"
$my_dates
= array
( 'January 28, 2011'
, 'January 29, 2011'
, 'January 30, 2011'
, 'January 31, 2011'
, 'February 1, 2011'
, 'February 2, 2011'
, 'February 28, 2011'
, 'March 30, 2011'
, 'March 31, 2011'
)
;

$month = new DateInterval('P1M');

// TEST EACH OF THE DATES
foreach ($my_dates as $my_date)
{
    $date = new DateTime($my_date, $zone);
    $date->add($month);
    echo PHP_EOL . "$my_date PLUS ONE MONTH = " . $date->format('r');
}


// TEST EACH OF THE DATES
foreach ($my_dates as $my_date)
{
    list
    ( $safe_date
    , $requested_day
    , $actual_day
    ) = nextMonth($my_date, 'r');
    echo PHP_EOL . "MY_DATE $my_date";
    echo PHP_EOL . "NEXTMONTH $safe_date";
    echo PHP_EOL . "REQUESTED DAY $requested_day";
    echo PHP_EOL . "ACTUAL DAY $actual_day";
    echo PHP_EOL;
}

function nextMonth($date, $format='c')
{
    $timestamp  = strtotime($date);
    $start_Y    = date('Y', $timestamp);
    $start_m    = date('m', $timestamp);
    $start_d    = date('d', $timestamp);

    // MAKE A TIMESTAMP FOR THE FIRST, LAST AND REQUESTED DAY OF NEXT MONTH
    $timestamp_first = mktime(0,0,0, $start_m+1,  1, $start_Y);
    $timestamp_last  = mktime(0,0,0, $start_m+1, date('t', $timestamp_first), $start_Y);
    $timestamp_try   = mktime(0,0,0, $start_m+1, $start_d, $start_Y);

    // USE THE LESSER OF THE REQUESTED DAY AND THE LAST OF THE MONTH
    if ($timestamp_try > $timestamp_last) $timestamp_try = $timestamp_last;
    $good_date = date($format, $timestamp_try);

    return array
    ( $good_date
    , $start_d
    , date('d', $timestamp_try)
    )
    ;
}


Storing Date and Time in Your Database

In MySQL, date columns are usually carried in the format DATE and DATETIME.  These are very "relaxed" fields.  You can insert invalid dates (such as February 31) and MySQL will accept these.  More on point, you can store and recall ISO-8601 datetime strings.  MySQL has a large complement of internal date and time functions.  You may sometimes find it practical to perform date / time calculations in your query strings.  See http://dev.mysql.com/doc/refman/5.5/en/datetime.html and http://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html for references and examples.


PHP and MySQL Have Different Time Zone Settings

Note that the time zone values for PHP and for MySQL are completely independent of each other.  You may want to adjust the time zones for one or both of these systems.  Use date_default_timezone_set() to adjust the value in PHP.  Use a query that says something like SET time_zone = '+00:00' to adjust the value in MySQL.  See http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html for more.  The code snippet illustrates this independence.


<?php 
/**
 * PHP and MySQL have different and independent time settings
 */
error_reporting(E_ALL);
echo '<pre>';

// DATABASE CONNECTION AND SELECTION VARIABLES - GET THESE FROM YOUR HOSTING COMPANY
$db_host = "localhost"; // PROBABLY THIS IS OK
$db_name = "??";
$db_user = "??";
$db_word = "??";

// OPEN A CONNECTION TO THE DATA BASE SERVER AND SELECT THE DB
$mysqli = new mysqli($db_host, $db_user, $db_word, $db_name);

// DID THE CONNECT/SELECT WORK OR FAIL?
if ($mysqli->connect_errno)
{
    trigger_error("CONNECT FAIL: $mysqli->connect_errno $mysqli->connect_error", E_USER_ERROR);
}


// GET THE PHP TIME
$zoneobj = new DateTimeZone('GMT');
$dateobj = new DateTime('NOW', $zoneobj);
echo PHP_EOL . 'IN PHP, THE GMT TIME IS: ' . $dateobj->format('c');
echo PHP_EOL;


// GET THE DEFAULT MYSQL TIME SETTINGS
$res = $mysqli->query("SELECT CURRENT_TIMESTAMP AS t, NOW() AS n");
$row = $res->fetch_object();
echo PHP_EOL . 'IN MySQL, THE CURRENT TIMES ARE: ';
print_r($row);
echo PHP_EOL;


// SET THE MYSQL TIMEZONE TO GMT
$sql = "SET time_zone = '+00:00' ";
$res = $mysqli->query($sql);

echo PHP_EOL . "<b>$sql</b>";

// GET THE NEW MYSQL VALUES
$res = $mysqli->query("SELECT CURRENT_TIMESTAMP AS t, NOW() AS n");
$row = $res->fetch_object();
echo PHP_EOL . 'IN MySQL, THE CURRENT TIMES HAVE BEEN CHANGED TO: ';
print_r($row);
echo PHP_EOL;


Practical Application #1

How old are you today? Given your birthday, this function can return your age. This is easier than the procedural way!

<?php 
/**
 * How old are you?  DateTime::diff() knows
 */
error_reporting(E_ALL);
echo '<pre>';

// A FUNCTION TO RETURN THE AGE ON A GIVEN DATE
function your_age($birth_day, $test_date='Today')
{
    $alpha_date = new DateTime($birth_day);
    $omega_date = new DateTime($test_date);
    $diff = $omega_date->diff($alpha_date);
    return $diff->y;
}

var_dump( your_age('May 12, 1993') );


Practical Application #2

How many days between two dates?  DateTime::diff() knows. 

<?php 
/**
 * Computing the difference between two dates
 */
error_reporting(E_ALL);
echo '<pre>';

function days_between($alpha='TODAY', $omega='TODAY')
{
    $alpha_date = new DateTime($alpha);
    $omega_date = new DateTime($omega);
    $diff = $omega_date->diff($alpha_date);
    return $diff->days;
}

var_dump( days_between() );
var_dump( days_between('September 15, 1950') );
var_dump( days_between('Yesterday', 'Tomorrow') );


Practical Application #3

You want to know how many month, days, hours, minutes, and/or seconds will elapse before an upcoming event.  Given a DATETIME string this function can return a text string or array with the answers.  PHP uses years and months, which can be ambiguous, but the results of the computations and answers are usually satisfactory.

<?php 
/**
 * Elapsed time computations
 */
error_reporting(E_ALL);
echo '<pre>';


function elapsed($then, $return_array=FALSE)
{
    $string = NULL;

    // TIME DESCRIPTIVE TERMS
    $terms["Y"] = 'year';
    $terms["M"] = 'month';
    $terms["D"] = 'day';
    $terms["H"] = 'hour';
    $terms["I"] = 'minute';
    $terms["S"] = 'second';

    $other = new DateTime($then);
    $curnt = new DateTime('NOW');

    // COMPUTE THE DIFFERENCE
    $diff = $curnt->diff($other);

    // SINCE $then IN THE PAST, UNTIL $then IN THE FUTURE
    $since_until = 'until';
    if ($diff->invert) $since_until = 'since';

    // CREATE A PERIOD DESIGNATOR
    $pd
    = 'P'
    . $diff->y    . 'Y'
    . $diff->m    . 'M'
    . $diff->d    . 'D'
    . 'T'
    . $diff->h    . 'H'
    . $diff->i    . 'M'
    . $diff->s    . 'S'
    ;
    $intvl = new dateInterval($pd);

    $formt['Y'] = $intvl->format('%y');
    $formt['M'] = $intvl->format('%m');
    $formt['D'] = $intvl->format('%d');
    $formt['H'] = $intvl->format('%h');
    $formt['I'] = $intvl->format('%i');
    $formt['S'] = $intvl->format('%s');

    // IF RETURNING AN ARRAY
    if ($return_array) return $formt;

    // MAP VALUES TO TERMS
    foreach ($formt as $key => $num)
    {
        if ($num != 1) $terms[$key] = $terms[$key] . 's';
        if ($num == 0) continue;
        $string .= $num . ' ' . $terms[$key] . ', ';
    }
    $string = rtrim($string, ', ');
    $string .= " $since_until $then";
    return $string;
}


// USE CASES
$cases =
[ '+ 1 week'
, '- 1 week'
, '0 seconds'
, '+1304 hours +2917 seconds'
, 'May 12, 1993 2:05pm'
, '45 days, 6 hours, 277 seconds'
, '-375 days'
]
;
foreach ($cases as $case)
{
    echo PHP_EOL . elapsed($case);
}


Practical Application #3a

You want to know if a given date is yesterday, today or tomorrow.  This function can tell you.

<?php 
/**
 * Yesterday, Today, Tomorrow have special meanings in DateTime
 */
error_reporting(E_ALL);
echo '<pre>';

function ytt($string)
{
    // NORMALIZE THE DATES
    $then      = new DateTime($string);
    $yesterday = new DateTime('Yesterday');
    $today     = new DateTime('Today');
    $tomorrow  = new DateTime('Tomorrow');

    // CHOOSE A RETURN
    if ($then->format('Y-m-d') == $yesterday->format('Y-m-d')) return 'Yesterday';
    if ($then->format('Y-m-d') == $today->format('Y-m-d'))     return 'Today';
    if ($then->format('Y-m-d') == $tomorrow->format('Y-m-d'))  return 'Tomorrow';

    return NULL;
}

// USE CASE
$y = date('r');
echo PHP_EOL . ytt($y);
echo PHP_EOL . ytt("$y + 1 DAY");


Practical Application #4

You decide you want to have a party on the fifth Friday of every month that has five Fridays.  When will you be celebrating?

<?php 
/**
 * Finding months with five Fridays
 */
error_reporting(E_ALL);
echo '<pre>';

echo 'WE PARTY ON: ';
print_r(find_fifth_friday());

function find_fifth_friday($input='Today')
{
    // GET VALUES FOR FIRST AND LAST OF THE YEAR
    $date  = new DateTime($input);
    $alpha = new DateTime($date->format('Y-1-1'));
    $omega = new DateTime($date->format('Y-12-31'));

    while ($alpha < $omega)
    {
        $first = new DateTime($alpha->format('Y-m-d') . ' first Friday');
        $fifth = new DateTime($alpha->format('Y-m-d') . ' fifth Friday');
        $month = $first->format('m');
        $day_n = $fifth->format('Y-m-d D');
        if (substr($day_n,5,2) == $month) $my_fridays[] = $day_n;
        $alpha = $alpha->add(new DateInterval('P1M'));
    }
    return $my_fridays;
}


Practical Application #5

You wonder if there could ever be a fifth Friday in that short month February.  The answer is "yes" and here is proof.

<?php 
/**
 * Could there be five Fridays in February?
 */
error_reporting(E_ALL);
echo '<pre>';

function find_weekdays($date='Today', $day='Sunday')
{
    $weekdays    = array();

    $datething   = new DateTime($date . ' - 1 DAY');
    $dateformat  = $datething->format('Y-m-d');
    $weekdays[1] = new DateTime($dateformat . " first  $day");
    $weekdays[2] = new DateTime($dateformat . " second $day");
    $weekdays[3] = new DateTime($dateformat . " third  $day");
    $weekdays[4] = new DateTime($dateformat . " fourth $day");
    $weekdays[5] = new DateTime($dateformat . " fifth  $day");

    if ( $weekdays[5]->format('m') != $weekdays[1]->format('m') ) { unset($weekdays[5]); }
    return $weekdays;
}

function count_weekdays($date='Today', $day='Sunday')
{
    $weekdays = find_weekdays($date, $day);
    return count($weekdays);
}

// USE CASE
$month = 'February 2008';
$wkday = 'Friday';
$kount = count_weekdays($month, $wkday);
echo PHP_EOL . "$wkday OCCURS $kount TIMES IN $month";


Practical Application #6

It would be nice to be able to print out a little calendar table... And if you wanted to send calendar information between applications, you might be interested in the hCalendar microformat.


<?php 
/**
 * Printing a little calendar table
 */
error_reporting(E_ALL);

function little_calendar_table($date='Today')
{
    $dateobj           = new DateTime($date);
    $month             = new DateTime($dateobj->format('Y-m-01'));
    $caption           = $month->format("F Y");
    $first_day_number  = $month->format("w");
    $last_day_of_month = $month->format("t");
    $day_counter       = 0;

    // USE HEREDOC NOTATION TO START THE HTML DOCUMENT
    $html  = <<<EOT
<style type="text/css">
caption { text-align:left; }
th,td   { text-align:right; width:14%; padding-right:0.2em; }
th      { color:gray;    border:1px solid silver;    }
td      { color:dimgray; border:1px solid gainsboro; }
td.nul  {                border:1px solid white;     }
</style>

<table>
<caption>$caption</caption>
<tr class="cal">
<th abbr="Sunday">    S </th>
<th abbr="Monday">    M </th>
<th abbr="Tuesday">   T </th>
<th abbr="Wednesday"> W </th>
<th abbr="Thursday">  T </th>
<th abbr="Friday">    F </th>
<th abbr="Saturday">  S </th>
</tr>
EOT;

    // THE FIRST ROW MAY HAVE DAYS THAT ARE NOT PART OF THIS MONTH
    $html .= '<tr>';
    while ($day_counter < $first_day_number)
    {
        $html .= '<td class="nul">&nbsp;</td>';
        $day_counter++;
    }

    // THE DAYS OF THE MONTH
    $mday = 1;
    while ($mday <= $last_day_of_month)
    {
        // THE DAYS OF THE WEEK
        while ($day_counter < 7)
        {
            $html .= "<td> $mday </td>";
            $day_counter++;
            $mday++;
            if ($mday > $last_day_of_month) break 2;
        }

        $html .= '</tr>' . PHP_EOL;
        $html .= '<tr>';
        $day_counter = 0;
    }

    // THE LAST ROW MAY HAVE DAYS THAT ARE NOT PART OF THIS MONTH
    while ($day_counter < 7)
    {
        $html .= '<td class="nul">&nbsp;</td>';
        $day_counter++;
    }

    $html .= '</tr>'    . PHP_EOL;
    $html .= '</table>' . PHP_EOL;

    return $html;
}

echo little_calendar_table('February 2008');
echo little_calendar_table();


Practical Application #6a

The author of this question wanted a weekly calendar with "previous" and "next" links.  This function returns an array of nine elements giving previous, weekdays, and next.


<?php 
/**
 * A weekly calendar with Prev and Next links
 */
error_reporting(E_ALL);
echo '<pre>';

function weekly_calendar($input='TODAY')
{
    $dateobj = new DateTime($input);

    // WE WILL NEED TO ADD ONE DAY TO OUR OBJECTS
    $intvl_p1d = new DateInterval('P1D');

    // NORMALIZE TO THE ISO-8601 DATE WITHOUT TIME
    $ymd = $dateobj->format('Y-m-d');
    $day = $dateobj->format('D');

    // THE WEEKLY CALENDAR WILL START ON SUNDAY AND END ON SATURDAY
    if ($day == 'Sun')
    {
        $alphaobj = new DateTime($ymd);
        $alpha    = $ymd;
    }
    else
    {
        $alphaobj = new DateTime($dateobj->format('Y-m-d') . ' LAST SUNDAY');
        $alpha    = $alphaobj->format('Y-m-d');
    }

    $omegaobj = new DateTime($alphaobj->format('Y-m-d') . ' SATURDAY');
    $omega    = $omegaobj->format('Y-m-d');

    // THE PREV AND NEXT WEEKS WILL BE COMPUTED FROM THE CURRENT DAY
    $prevobj = new DateTime($ymd . ' - 1 WEEK');
    $prev    = $prevobj->format('Y-m-d');
    $nextobj = new DateTime($ymd . ' + 1 WEEK');
    $next    = $nextobj->format('Y-m-d');

    // MAKE THE URLS
    $prev_uri = $_SERVER['PHP_SELF'] . "?d=$prev";
    $next_uri = $_SERVER['PHP_SELF'] . "?d=$next";

    // MAKE THE LINK TEXTS
    $prev_lnk = '<a href="' . $prev_uri . '">Prev</a>';
    $next_lnk = '<a href="' . $next_uri . '">Next</a>';

    // MAKE THE REPRESENTATION OF THE WEEK
    $week['prev'] = $prev_lnk;
    while ($alphaobj <= $omegaobj)
    {
        // A TEXT VERSION OF THE WEEKDAY
        $day = $alphaobj->format('D');
        $txt = $alphaobj->format('D m/d');
        $ymd = $alphaobj->format('Y-m-d');

        // URL AND LINK TEXT
        $curr_uri = $_SERVER['PHP_SELF'] . "?d=$ymd";
        $curr_lnk = '<a href="' . $curr_uri . '">' . $txt . '</a>';

        $week[$day] = $curr_lnk;

        // ON TO THE NEXT DAY
        $alphaobj->add($intvl_p1d);
    }
    $week['next'] = $next_lnk;

    return $week;
}


// SHOW THE REPRESENTATION OF THE WEEK
$get = !empty($_GET['d']) ? $_GET['d'] : NULL;
$thing = weekly_calendar($get);
print_r($thing);


Practical Application #7

When you are working with human input to a computer program, it's good to know what works with the DateTime constructor method.  The script in this code snippet will tell you.

<?php 
/**
 * Catching Exception when the input is unusable
 */
error_reporting(E_ALL);

$zoneobj = new DateTimeZone('America/Chicago');

// IF WE HAVE INPUT FROM THE URL QUERY STRING
if (!empty($_GET["s"]))
{
    try
    {
        $dateobj = new DateTime($_GET['s'], $zoneobj);
    }
    catch(Exception $datex)
    {
        $mgsx = $datex->getMessage();
        echo "<strong>HONK!</strong><br /> <u>{$_GET["s"]}</u> NOT USEFUL: $mgsx <br/><br/><br/><br/>";
        goto FormCreation;
    }

    $textual = 'l dS \o\f F Y g:i:s A (T)';

    $unix_timestamp = $dateobj->format('U');
    $iso_datetime   = $dateobj->format(DateTime::ATOM);
    $txt_datetime   = $dateobj->format($textual);

    echo "<strong>BINGO</strong><br /> <strong><u>{$_GET["s"]}</u></strong> WORKS WITH PHP DateTime CLASS <br/>";
    echo "THE INTEGER TIMESTAMP VALUE IS ";
    echo number_format($unix_timestamp) . "<br />";
    echo "THE ISO8601 DATETIME STRING IS $iso_datetime<br />";
    echo "THE TEXTUAL DATE IS $txt_datetime<br />";
    echo PHP_EOL;
}

// END OF PROCESSING INPUT - PUT UP THE FORM
FormCreation:

?>
<html>
<head>
<title>TEST THE PHP DateTime CLASS</title>
</head>
<body onload="document.f.s.focus()">
<form name="f" method="get">
<br />TO TEST A STRING FOR A VALID DATE/TIME, TYPE IT HERE:<input name="s" />
<input type="submit" value="GO" />
</form>


Practical Application #8

What if you wanted to send an email message to your client at 7:00am every day?  But your clients may be all over the world!  You need a way to coordinate your server time with the time at the client's location.  This script shows how to work that out.

<?php 
/**
 * Compute the difference between the Client time and the Server time
 */
error_reporting(E_ALL);

// THE TIME WE WANT AT THE CLIENT LOCATION
$client_timestring ='TODAY 0700';

$server_timezone = new DateTimeZone('America/Denver');
$server_datetime = new DateTime($client_timestring, $server_timezone);
$server_offset_seconds = $server_timezone->getOffset($server_datetime);

// WHEN THE FORM IS SUBMITTED
if (!empty($_POST))
{
    // JAVASCRIPT TELLS US THE CLIENT TIME OFFSET FROM GMT / UTC
    $client_offset_minutes = $_POST["date_O"];
    $client_offset_seconds = $client_offset_minutes * 60;

    // ADD CLIENT OFFSET TO SERVER OFFSET
    $offset = $client_offset_seconds + $server_offset_seconds;

    // WORKAROUND FOR NO NEGATIVE OFFSET IN THE DateInterval CONSTRUCTOR
    $offset_abs = abs($offset);
    $offset_intvl = new DateInterval('PT' . $offset_abs . 'S');
    if ($offset < 0) $offset_intvl->invert = 1;

    $server_datetime->add($offset_intvl);
    $server_timestring = $server_datetime->format('l, F j, Y \a\t g:i a');

    echo "<br/>ACCORDING TO THE VALUE FROM PHP DateTimeZone::getOffset()";
    echo "<br/>SERVER IS LOCATED $server_offset_seconds SECONDS FROM UTC IN " . $server_timezone->getName();
    echo "<br/>";

    echo "<br/>ACCORDING TO THE VALUE FROM JS dateObject.getTimezoneOffset()";
    echo "<br/>CLIENT IS LOCATED $client_offset_minutes MINUTES FROM UTC";
    echo "<br/>";

    echo "<br/>WHEN IT IS '$client_timestring' AT THE CLIENT, IT IS '$server_timestring' AT THE SERVER IN " . $server_timezone->getName();
}

// END OF PHP - USE HTML AND JS TO CREATE THE FORM
echo PHP_EOL;

?>
<form method="post">
<input name="date_O" id="dateTime_O" type="hidden" />
<input type="submit" value="CHECK CLIENT DATETIME" />
</form>

<!-- NOTE THIS WILL GIVE YOU THE VALUES AT PAGE-LOAD TIME, NOT AT SUBMIT TIME -->
<!-- MAN PAGE REF: http://www.w3schools.com/jsref/jsref_obj_date.asp -->
<script type="text/javascript">
var dateObject = new Date();
document.getElementById("dateTime_O").value = dateObject.getTimezoneOffset();
</script>


Practical Application #9

We promise to ship our products within "n" business days.  This function can tell you the required shipment date.  Orders are deemed to have arrived at the close of business on the actual date of arrival.  Example: If you promise to ship within one business day and an order arrives at any time on Tuesday, you have all day Tuesday and until close of business on Wednesday before the shipment is required.  If the order arrives on Saturday or Sunday, it is deemed to have arrived on the following business day, probably Monday, thus shipment is not required until the close of business Tuesday.  This is probably good enough for commercial applications, but for Government work you might want to add a greater awareness of holidays into the function.  


A technical note: While we usually choose the ISO-8601 format for an internal representation of a date, we make an exception here and choose the RFC2822 format, created by date('r').  It has an advantage in this case because we want to eliminate the weekend days from the computation and these are the only days that start with S.  RFC2822 dates look something like this, with the abbreviation of the weekday at the left: Wed, 10 Aug 2011 00:00:00 -0400.  The PHP substr() function makes it easy to find the days that start with S.


<?php 
/**
 * Add "business days" to a given date, skipping weekends and holidays
 */
error_reporting(E_ALL);
echo '<pre>';


// A FUNCTION TO ADD BUSINESS DAYS TO A GIVEN DATE
function add_business_days($days=0, $date="TODAY", $format="c")
{
    // CREATE YOUR ARRAY OF HOLIDAYS
    $holidays = array();

    $today    = new DateTime($date);
    $january  = new DateTime($today->format('Y-01-1'));
    $november = new DateTime($today->format('Y-11-1'));

    $dateobj = new DateTime('Third Monday ' . $january->format('Y-01-0'));
    $holidays['Dr_M_L_King']  = $dateobj->format(DateTime::RSS);
    $dateobj = new DateTime($today->format('Y-07-04'));
    $holidays['Independence'] = $dateobj->format(DateTime::RSS);
    $dateobj = new DateTime('Fourth Thursday ' . $november->format('Y-11-0'));
    $holidays['Thanksgiving'] = $dateobj->format(DateTime::RSS);
    $dateobj = new DateTime($today->format('Y-12-25'));
    $holidays['Christmas']    = $dateobj->format(DateTime::RSS);
    $dateobj = new DateTime($today->format('Y-12-31') . '+ 1 DAY');
    $holidays['NewYear']      = $dateobj->format(DateTime::RSS);

    // ACTIVATE THIS TO SEE THE HOLIDAYS
    // print_r($holidays);

    // INTERPRET THE INPUTS INTO FUTURE DATES
    $current = $today;
    $weeks   = $days * 2 + 15;
    $future  = new DateTime("$date $weeks DAYS");

    // MAKE AN ARRAY OF FUTURE RSS DATES
    $interval_p1d = new DateInterval('P1D');
    while ($current < $future)
    {
        $dateobj = new DateTime($current->format('c'));
        $arr[]   = $dateobj->format(DateTime::RSS);
        $current->add($interval_p1d);
    }

    // REMOVE THE DAY FROM THE ARRAY IF IT IS A HOLIDAY OR WEEKEND DAY
    foreach ($arr as $key => $date_string)
    {
        if (in_array($date_string, $holidays)) $arr[$key] = 'S';
        if (substr($arr[$key],0,1) == 'S') unset($arr[$key]);
    }

    // RECAST THE ARRAY KEYS INTO OFFSETS FROM THE STARTING DATE
    $arr = array_values($arr);

    // RETURN THE FUTURE DATE ACCORDING TO THE REQUESTED FORMAT
    $business_day = new DateTime($arr[$days]);
    return $business_day->format($format);
}

var_dump( add_business_days() );
var_dump( add_business_days(1, 'December 23, 2015', 'r') );
var_dump( add_business_days(2, 'December 23, 2015', 'r') );


Practical Application #10

We need an array of 7 weekdays, beginning on a specified date, as expressed in this question:


<?php 
/**
 * Seven weekdays, beginning on an arbitrary weekday
 */
error_reporting(E_ALL);
echo '<pre>';

function weekdays($input)
{
    $dateobj = new DateTime($input);
    $day  = $dateobj->format('D');
    $days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat','Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
    return array_slice($days, array_search($day, $days), 7);
}

var_dump( weekdays('Tomorrow') );
var_dump( weekdays('Yesterday') );

After reviewing that question and answer, we might want a more generalized solution.  Perhaps we do not only want those abbreviations; we might want the 7 weekdays returned in a different format.  This function preserves the functionality of the original answer and generalizes the solution to allow for different return formats.  We use the DateTime::add() method with the DateInterval object to increment the days


<?php 
/**
 * Seven weekdays, in any date format
 */
error_reporting(E_ALL);
echo '<pre>';

function weekdays($input, $format='D')
{
    $interval_p1d = new DateInterval('P1D');
    $dateobj = new DateTime($input);
    $datemax = new DateTime($dateobj->format('c') . ' + 6 DAYS');
    while ($dateobj <= $datemax)
    {
        $dates[] = $dateobj->format($format);
        $dateobj->add($interval_p1d);
    }
    return $dates;
}

var_dump( weekdays('Tomorrow') );
var_dump( weekdays('Yesterday') );


Practical Application #11

Given the beginning date of a Fiscal Year, what are the beginning dates of the Fiscal Quarters?  Since each quarter is three months, we can easily get the dates for the quarters.  This application illustrates one of the strange behaviors of the DateTime class.  If you call the add() method, instead of receiving a new object as a return value, you mutate the object.  Therefore it is necessary to clone() the object before calling add(), if you want to keep a copy of the original object.


<?php 
/**
 * Fiscal Years
 *
 * Note the use of clone() because the add() method mutates the DateTime object
 */
error_reporting(E_ALL);
echo '<pre>';

// SOME TEST DATES FOR THE FISCAL YEARS
$fys = array
( 'CALENDAR' => 'January 1'
, 'ACADEMIC' => 'July 1'
, 'FEDERAL'  => 'October 1'
)
;

// COMPUTE THE QUARTERS
$interval_p3m = new DateInterval('P3M');
$interval_p6m = new DateInterval('P6M');
$interval_p9m = new DateInterval('P9M');
foreach ($fys as $key => $start)
{
    $dateobj = new DateTime($start);
    $res['q1'] = $dateobj->format(DateTime::ATOM);

    $datework = clone($dateobj); // MUST MAKE A CLONE BECAUSE add() MUTATES THE OBJECT
    $datework->add($interval_p3m);
    $res['q2'] = $datework->format(DateTime::ATOM);

    $datework = clone($dateobj);
    $datework->add($interval_p6m);
    $res['q3'] = $datework->format(DateTime::ATOM);

    $datework = clone($dateobj);
    $datework->add($interval_p9m);
    $res['q4'] = $datework->format(DateTime::ATOM);

    // SAVE THESE ELEMENTS
    $new[$key] = $res;
}

print_r($new);


Practical Application #12

What are the essential elements of a resource scheduling calendar?  Unlike an appointment calendar which assumes the general availability of all calendar dates and times, a resource calendar must enumerate the resources and keep track of when they are available and when they are scheduled, and therefore unavailable.  The important moving parts are the resource name and the period of unavailability as given by the beginning DATETIME and the ending DATETIME.  Armed with this information we can write SQL queries that will allow us to schedule the resources and detect potential conflicts.  A starting point for the resource scheduling calendar might be something like this.


<?php 
/**
 * Demonstrate the basics of a MySQL-based resource scheduling calendar
 */
error_reporting(E_ALL);
echo '<pre>';

// DATABASE CONNECTION AND SELECTION VARIABLES - GET THESE FROM YOUR HOSTING COMPANY
$db_host = "localhost"; // PROBABLY THIS IS OK
$db_name = "??";
$db_user = "??";
$db_word = "??";

// OPEN A CONNECTION TO THE DATA BASE SERVER AND SELECT THE DB
$mysqli = new mysqli($db_host, $db_user, $db_word, $db_name);

// DID THE CONNECT/SELECT WORK OR FAIL?
if ($mysqli->connect_errno)
{
    trigger_error("CONNECT FAIL: $mysqli->connect_errno $mysqli->connect_error", E_USER_ERROR);
}


// CREATING A TABLE FOR OUR TEST DATA
$sql
=
"
CREATE TEMPORARY TABLE my_calendar
( id    INT         NOT NULL AUTO_INCREMENT PRIMARY KEY
, rname VARCHAR(24) NOT NULL DEFAULT ''                    # RESOURCE NAME
, alpha DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00' # BEGINNING OF COMMITMENT
, omega DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00' # END OF COMMITMENT
)
"
;

// CREATE THE TABLE OR LOG AND SHOW THE ERROR
if (!$res = $mysqli->query($sql))
{
    $err
    = 'QUERY FAILURE:'
    . ' ERRNO: '
    . $mysqli->errno
    . ' ERROR: '
    . $mysqli->error
    . ' QUERY: '
    . $sql
    ;
    trigger_error($err, E_USER_ERROR);
}

// PRE-LOAD SOME SCHEDULED RESOURCES INTO THE CALENDAR
$mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( 'One', '2013-06-20T08:00:00', '2013-06-20T09:59:59' )");
$mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( 'Two', '2013-06-20T08:00:00', '2013-06-20T09:59:59' )");
$mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( 'Two', '2013-06-20T11:00:00', '2013-06-20T12:59:59' )");


// SHOW THE CALENDAR
$res = $mysqli->query("SELECT * FROM my_calendar");
while ($row = $res->fetch_object()) { print_r($row); }


// SCHEDULE A RESOURCE FROM 1:00pm TO 3:00pm
$rname = 'One';
$dateobj = new DateTime('June 20, 2013 1:00pm');
$alpha = $dateobj->format(DateTime::ATOM);
$dateobj = new DateTime('June 20, 2013 3:00pm - 1 SECOND');
$omega = $dateobj->format(DateTime::ATOM);

$res = $mysqli->query("LOCK TABLES my_calendar");
$res = $mysqli->query("SELECT id FROM my_calendar WHERE rname = '$rname' AND ( (alpha BETWEEN '$alpha' AND '$omega') OR (omega BETWEEN '$alpha' AND '$omega') )  LIMIT 1");
$num = $res->num_rows;
if ($num == 0)
{
    $res = $mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( '$rname', '$alpha', '$omega' )");
    echo "SCHEDULED $rname FROM $alpha TO $omega" . PHP_EOL;
}
else
{
    echo "CONFLICT! $rname FROM $alpha TO $omega" . PHP_EOL;
}
$res = $mysqli->query("UNLOCK TABLES my_calendar");


// TRY TO SCHEDULE THE SAME RESOURCE FROM 2:00pm TO 4:00pm
$rname = 'One';
$dateobj = new DateTime('June 20, 2013 2:00pm');
$alpha = $dateobj->format(DateTime::ATOM);
$dateobj = new DateTime('June 20, 2013 4:00pm - 1 SECOND');
$omega = $dateobj->format(DateTime::ATOM);

$res = $mysqli->query("LOCK TABLES my_calendar");
$res = $mysqli->query("SELECT id FROM my_calendar WHERE rname = '$rname' AND ( (alpha BETWEEN '$alpha' AND '$omega') OR (omega BETWEEN '$alpha' AND '$omega') )  LIMIT 1");
$num = $res->num_rows;
if ($num == 0)
{
    $mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( '$rname', '$alpha', '$omega' )");
    echo "SCHEDULED $rname FROM $alpha TO $omega" . PHP_EOL;
}
else
{
    echo "CONFLICT! $rname FROM $alpha TO $omega" . PHP_EOL;
}
$res = $mysqli->query("UNLOCK TABLES my_calendar");


// TRY TO SCHEDULE A DIFFERENT RESOURCE FROM 2:00pm TO 4:00pm
$rname = 'Two';
$dateobj = new DateTime('June 20, 2013 2:00pm');
$alpha = $dateobj->format(DateTime::ATOM);
$dateobj = new DateTime('June 20, 2013 4:00pm - 1 SECOND');
$omega = $dateobj->format(DateTime::ATOM);

$res = $mysqli->query("LOCK TABLES my_calendar");
$res = $mysqli->query("SELECT id FROM my_calendar WHERE rname = '$rname' AND ( (alpha BETWEEN '$alpha' AND '$omega') OR (omega BETWEEN '$alpha' AND '$omega') )  LIMIT 1");
$num = $res->num_rows;
if ($num == 0)
{
    $mysqli->query("INSERT INTO my_calendar (rname, alpha, omega) VALUES ( '$rname', '$alpha', '$omega' )");
    echo "SCHEDULED $rname FROM $alpha TO $omega" . PHP_EOL;
}
else
{
    echo "CONFLICT! $rname FROM $alpha TO $omega" . PHP_EOL;
    while ($row = $res->fetch_object()) { var_dump($row); }
}
$res = $mysqli->query("UNLOCK TABLES my_calendar");


// SHOW THE RESULTING CALENDAR
$res = $mysqli->query("SELECT * FROM my_calendar");
while ($row = $res->fetch_object()) { print_r($row); }


Practical Application #13

What time will the sun rise or set?  PHP can tell you this if you set your location and timezone correctly.  However your location in the default PHP installation is probably wrong unless you're in Israel near the PHP developers at Zend.  There are two initialization settings for your location: latitude and longitude, and conveniently they are settable in your PHP script.  They can also be set in the function calls to date_sunrise() and date_sunset().  Interestingly, they are independent of your timezone settings, and if you don't have all these settings in consonance, you may get incorrect output from the functions.  The code snippet shows how to determine the time of sunrise and sunset.  The user-contributed notes for date_sunrise() have some interesting and useful observations, too.


<?php 
/**
 * Getting the time of sunrise
 *
 * There is no date_default_longitude_set() or date_default_longitude_get()
 * These values are settable PHP_INI_ALL and gettable with ini_get()
 * If you're using the default values and you're not located in Israel...
 * your computations will probably come out wrong.
 *
 * Incorrect output if your location settings do not match your timezone.
 */
error_reporting(E_ALL);
echo '<pre>';

// SET LOCATION VALUES TO CHICAGO
ini_set('date.default_latitude',    '41.850687');
ini_set('date.default_longitude',  '-87.650046');
$zone = new DateTimeZone('America/Chicago');
$date = new DateTime(NULL, $zone);

// GET UNIX TIMESTAMP
$unix = date_sunrise
( $date->getTimestamp()
, SUNFUNCS_RET_TIMESTAMP
, ini_get('date.default_latitude')
, ini_get('date.default_longitude')
, ini_get("date.sunrise_zenith")
, $date->getOffset() / 3600 //--> NOTE THIS IS IN HOURS, NOT SECONDS
)
;
// SHOW ANSWER IN LOCAL TIME
$date = new DateTime("@$unix");
$date->setTimeZone($zone);
$sun = $date->format('g:ia (T)');
$day = $date->format('D, M jS Y');
$lat = ini_get('date.default_latitude');
$lon = ini_get('date.default_longitude');
$dtz = $zone->getName();
echo PHP_EOL . "LOCATION: $lat, $lon ($dtz) OBSERVES SUNRISE AT: $sun ON $day";


// CHANGE VALUES TO LOS ANGELES
ini_set('date.default_latitude',    '34.071103');
ini_set('date.default_longitude', '-118.440150');
$zone = new DateTimeZone('America/Los_Angeles');
$date = new DateTime(NULL, $zone);

// GET UNIX TIMESTAMP
$unix = date_sunrise
( $date->getTimestamp()
, SUNFUNCS_RET_TIMESTAMP
, ini_get('date.default_latitude')
, ini_get('date.default_longitude')
, ini_get("date.sunrise_zenith")
, $date->getOffset() / 3600 //--> NOTE THIS IS IN HOURS, NOT SECONDS
)
;
// SHOW ANSWER IN LOCAL TIME
$date = new DateTime("@$unix");
$date->setTimeZone($zone);
$sun = $date->format('g:ia (T)');
$day = $date->format('D, M jS Y');
$lat = ini_get('date.default_latitude');
$lon = ini_get('date.default_longitude');
$dtz = $zone->getName();
echo PHP_EOL . "LOCATION: $lat, $lon ($dtz) OBSERVES SUNRISE AT: $sun ON $day";


// CHANGE VALUES TO WRONG VALUES FOR WASHINGTON, DC
ini_set('date.default_latitude',    '38.898748');
ini_set('date.default_longitude',  '-77.037684');
$zone = new DateTimeZone('America/Los_Angeles'); //--> THIS IS INTENTIONALLY WRONG
$date = new DateTime(NULL, $zone);

// GET UNIX TIMESTAMP
$unix = date_sunrise
( $date->getTimestamp()
, SUNFUNCS_RET_TIMESTAMP
, ini_get('date.default_latitude')
, ini_get('date.default_longitude')
, ini_get("date.sunrise_zenith")
, $date->getOffset() / 3600 //--> NOTE THIS IS IN HOURS, NOT SECONDS
)
;
// SHOW WRONG ANSWER IN LOCAL TIME
$date = new DateTime("@$unix");
$date->setTimeZone($zone);
$sun = $date->format('g:ia (T)');
$day = $date->format('D, M jS Y');
$lat = ini_get('date.default_latitude');
$lon = ini_get('date.default_longitude');
$dtz = $zone->getName();
echo PHP_EOL . "LOCATION: $lat, $lon ($dtz) OBSERVES SUNRISE AT: $sun ON $day --> NOT!!";


// CHANGE VALUES TO CORRECT VALUES FOR WASHINGTON, DC
ini_set('date.default_latitude',    '38.898748');
ini_set('date.default_longitude',  '-77.037684');
$zone = new DateTimeZone('America/New_York'); //--> THIS HAS BEEN CORRECTED
$date = new DateTime(NULL, $zone);

// GET UNIX TIMESTAMP
$unix = date_sunrise
( $date->getTimestamp()
, SUNFUNCS_RET_TIMESTAMP
, ini_get('date.default_latitude')
, ini_get('date.default_longitude')
, ini_get("date.sunrise_zenith")
, $date->getOffset() / 3600 //--> NOTE THIS IS IN HOURS, NOT SECONDS
)
;
// SHOW WRONG ANSWER IN LOCAL TIME
$date = new DateTime("@$unix");
$date->setTimeZone($zone);
$sun = $date->format('g:ia (T)');
$day = $date->format('D, M jS Y');
$lat = ini_get('date.default_latitude');
$lon = ini_get('date.default_longitude');
$dtz = $zone->getName();
echo PHP_EOL . "LOCATION: $lat, $lon ($dtz) OBSERVES SUNRISE AT: $sun ON $day";


Practical Application #14

When it is some given time in one time zone, what time is it in another time zone?  This function can tell you.  It is much easier to use PHP's built-in time computations than trying to write your own because PHP will automatically account for leap years and daylight savings time. 


If you need a list of the timezone identifiers, PHP can give you that: print_r( timezone_identifiers_list() );


<?php 
/**
 * Compute the clock-setting difference between two timezones
 */
error_reporting(E_ALL);
echo '<pre>';

function timezone_difference($tz1, $tz2, $date='NOW')
{
    $now = new DateTime($date);
    $ond = $now->format(DateTime::ATOM);
    $sig = NULL;

    // THE RETURN ARRAY HAS THESE FIELDS
    $ret = array
    ( 'from' => $tz1
    , 'is'   => 'same as'
    , 'to'   => $tz2
    , 'secs' => '0'
    , 'hhmm' => '00:00'
    , 'asof' => $ond
    )
    ;
    // GET TIMEZONE SETTINGS
    $zone1 = new DateTimeZone($tz1);
    $zone2 = new DateTimeZone($tz2);
    $zz1 = $zone1->getOffset($now);
    $zz2 = $zone2->getOffset($now);

    // COMPUTE THE CLOCK DIFFERENCE
    $dif = $zz2 - $zz1;
    if ($dif < 0) $sig = '-';
    if ($dif > 0) $sig = '+';
    if ($sig == '-') $ret['is'] = 'ahead of';
    if ($sig == '+') $ret['is'] = 'behind';

    $dif = abs($dif);
    $dat = new DateTime("TODAY + $dif SECONDS");
    $fmt = $dat->format('H:i');
    $ret['secs'] = $sig . $dif;
    $ret['hhmm'] = $sig . $fmt;

    return $ret;
}

// OBSERVE THE FUNCTION IN ACTION
echo '<pre>';
$tz1 = 'America/Chicago';
$tz2 = 'America/St_Johns';
$tzd = timezone_difference($tz1, $tz2);
var_dump($tzd);


Practical Application #15

Given a country code, what are its time zones?  Given a time zone, what country is in play?  This application can answer that question.


<?php 
/**
 * Get the TimeZone Identifiers
 */
error_reporting(E_ALL);
echo '<pre>';

// CLASSES TO HOLD TIMEZONE-TO-ISO_3166 DATA
Class CountryTimeZone
{
    public function __construct($timezone, $iso_3166)
    {
        $this->timezone = $timezone;
        $this->country  = $iso_3166;
    }
}

Class CountryTimeZones
{
    public $timezones = [];
    public $countries = [];
    public function add($timezone, $iso_3166)
    {
        $this->timezones[$timezone] = new CountryTimeZone($timezone, $iso_3166);
        $this->countries[$iso_3166][] = $timezone;
    }
    public function getCountry($timezone)
    {
        return array_key_exists($timezone, $this->timezones)
        ? $this->timezones[$timezone]->country
        : FALSE
        ;
    }
    public function getTimeZones($iso_3166)
    {
        return array_key_exists($iso_3166, $this->countries)
        ? $this->countries[$iso_3166]
        : FALSE
        ;
    }
}


// COLLECT THE DATA SET
$ctz = new CountryTimeZones;
$arr = DateTimeZone::listIdentifiers(DateTimeZone::ALL);

foreach ($arr as $tz_name)
{
    $obj = new DateTimeZone($tz_name);
    $sub = $obj->getLocation();
    $iso = $sub['country_code'];
    $ctz->add($tz_name, $iso);
}


// DEMONSTRATE THE CLASS
echo '<pre>';
var_dump($ctz->getCountry('America/New_York'));
var_dump($ctz->getCountry('America/Panama'));
var_dump($ctz->getCountry('America/Los_Angeles'));
var_dump($ctz->getCountry('UTC'));
var_dump($ctz->getCountry('wtf'));

var_dump($ctz->getTimeZones('PA'));
var_dump($ctz->getTimeZones('CN'));
var_dump($ctz->getTimeZones('US'));
var_dump($ctz->getTimeZones('??'));


// SAVE THE DATA SET
var_export($ctz);


Practical Application #16

How can I create a range of dates (inspired by this question)


<?php 
/**
 * Get a range of dates
 */
error_reporting(E_ALL);
echo '<pre>';

// SET THE START AND END DATES
$alpha = 'July 15, 2015';
$omega = 'Aug  14, 2015';

// COMPUTE AND DISPLAY THE RANGE OF DATES
$range = date_range($alpha, $omega);
print_r($range);


function date_range($a, $z, $format='Y-m-d')
{
    $return = [];

    // AN INTERVAL SETTING
    $interval_p1d = new DateInterval('P1D');

    // NORMALIZE THE DATES
    $alpha   = new DateTime($a);
    $omega   = new DateTime($z);

    // CREATE THE RANGE
    while ($alpha <= $omega)
    {
        $return[] = $alpha->format($format);
        $alpha->add($interval_p1d);
    }
    return $return;
}


Practical Application #17

You might like the jQuery datepicker.  This is not a PHP issue, but it's a popular question, so I'll just leave the example here.


<?php
/**
 * Demonstrate the jQuery Date Picker
 */
error_reporting(E_ALL);
echo '<pre>';


// IF THE DATE WAS SUBMITTED, USE IT IN PHP
if (!empty($_POST['chosen_date']))
{
    $dateobj = FALSE;
    try
    {
        $dateobj = new DateTime($_POST['chosen_date']);
    }
    catch(Exception $e)
    {
        error_log($e);
    }
    if ($dateobj)
    {
        $chosen_date = $dateobj->format(DateTime::ATOM);
        $chosen_date = $dateobj->format('Y-m-d');
        echo PHP_EOL . "You chose date: <b>$chosen_date</b>";
    }
}

// CREATE THE HTML USING HEREDOC NOTATION ADAPTED FROM http://jqueryui.com/datepicker/
$htm = <<<EOD
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jQuery UI Datepicker - Default functionality</title>
<script>
$(function() {
    $("#datepicker").datepicker();
});
</script>
</head>
<body>
<form method="post">
<p>Date: <input name="chosen_date" type="text" id="datepicker" /></p>
<input type="submit" />
</form>
</body>
</html>
EOD;

echo $htm;



Practical Application #18

How long does it take your client to fill out a form?  A bit of jQuery can identify the load time and the submit time.  The difference between these two times gives us the answer.

<?php 
/**
 * Find out how long it takes your client to submit a form
 *
 * JavaScript date().toString() returns something like this:
 *    Sat Apr 09 2016 07:33:27 GMT-0400 (Eastern Daylight Time)
 * PHP needs something like this (33 characters):
 *    Sat Apr 09 2016 07:33:27 GMT-0400
 */
error_reporting(E_ALL);


if (!empty($_POST))
{
    $alpha = new DateTime( substr($_POST['alpha'],0,33) );
    $omega = new DateTime( substr($_POST['omega'],0,33) );
    $lapse = $alpha->diff($omega);
    $timex = $lapse->format('%I:%S');
    echo PHP_EOL . "It took $timex (minutes:seconds) to submit this form";

    // ACTIVATE THIS TO SHOW THE REQUEST
    // var_dump($_POST);
}


// CREATE OUR WEB PAGE IN HTML5 FORMAT
$htm = <<<HTML5
<!DOCTYPE html>
<html dir="ltr" lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<style type="text/css">
/* STYLE SHEET HERE */
</style>

<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script>
$(document).ready(function(){
    var tload = new Date().toString();
    var alpha = $("<input>").attr("type", "hidden").attr("name", "alpha").val(tload);
    $("#myForm").append($(alpha));

    $("#myForm").submit(function(e){
        var tsubm = new Date().toString();
        var omega = $("<input>").attr("type", "hidden").attr("name", "omega").val(tsubm);
        $("#myForm").append($(omega));
    });
});
</script>

<title>HTML5 Page With jQuery in UTF-8 Encoding</title>
</head>
<body>

<noscript>Your browsing experience will be much better with JavaScript enabled!</noscript>

<form id="myForm" method="post">
<input type="submit" />
</form>

</body>
</html>
HTML5;


// RENDER THE WEB PAGE
echo $htm;


Procedural DATETIME Notation

Here is a link to the original procedural article about date/time.

http://www.experts-exchange.com/articles/201/Handling-Date-and-Time-in-PHP-and-MySQL.html


Creativity and Routine

Like most things in computer programming, there are several ways to achieve equivalent results.  This article has shown some tried-and-true ways to use PHP's powerful date / time processing functions.  The methods shown here will help you get quick and accurate answers to many common questions, all with a minimum risk of introducing bugs into the code.


References and Further Reading 

http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html

http://php.net/manual/en/class.datetime.php

http://php.net/manual/en/class.datetime.php#120264

http://php.net/manual/en/class.datetime.php#datetime.constants.types

http://php.net/manual/en/class.datetimezone.php

http://php.net/manual/en/dateinterval.construct.php

http://php.net/manual/en/dateinterval.format.php

http://php.net/manual/en/datetime.add.php

http://php.net/manual/en/datetime.construct.php

http://php.net/manual/en/datetime.diff.php#113283

http://php.net/manual/en/datetime.diff.php#114881

http://php.net/manual/en/datetime.format.php

http://php.net/manual/en/datetime.formats.relative.php

http://php.net/manual/en/datetime.sub.php#114780

http://php.net/manual/en/datetimezone.construct.php

http://php.net/manual/en/datetimezone.gettransitions.php

http://php.net/manual/en/function.date.php

http://php.net/manual/en/function.date-default-timezone-set.php

http://php.net/manual/en/ref.calendar.php

http://php.net/manual/en/refs.calendar.php

http://php.net/manual/en/timezones.america.php#85815

http://php.net/manual/en/timezones.php

http://php.net/manual/en/function.date.php

http://www.timeanddate.com/time/change/usa/new-york?year=2014

http://www.w3schools.com/jsref/jsref_obj_date.asp

https://en.wikipedia.org/wiki/ISO_8601#Durations


Please give us your feedback!

If you found this article helpful, please click the "thumb's up" button below. Doing so lets the E-E community know what is valuable for E-E members and helps provide direction for future articles.  If you have questions or comments, please add them.  Thanks!


 

3
9,261 Views

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.