Notes Information Apocalypse

Date-Time Handling in PHP 5.2+

After running into a weird bug with historical dates recently, I was forced to re-discover that the standard distribution date and time functionality in PHP is only capable of supporting a limited range of dates. Unfortunately I'd never really thought about it too carefully before, but I should have. Usually it's easy to abstract this stuff away behind the language type system, but where historical dates are concerned, the implementation matters a great deal.

Want evidence? Take something like a date representing this milestone in the history of communications, and see what happens:

echo date('Y-m-d', strtotime('1844-05-24'));

On Unix and Win32 systems, the result will be 1970-01-01. How useful. This is because the traditional PHP datetime functions rely on operating system support for a 32 bit integer timestamp which can only represent date values in the range of 1902 to 2038. The actual result will depend on the underlying operating system, but it seems that an out of range date defaults to the beginning of the Unix epoch.

If you're building any kind of historically aware application in PHP, it will be worth running off version 5.2+ which has support for a new set of date handling functions, superseding the old PHP 4 compatible library.

Forget Everything You Ever Knew about Date-Time Handling in PHP... It's time to lose mktime(), and strtotime(). Sure, they seemed powerful in the younger days of PHP but they're little more than legacy cruft to a 2007 era programmer. Take a breath, exhale. They're gone now.

The new DateTime functions have broken away from the reliance on native operating system functionality and ramped up to use a signed 64 bit representation which finally brings the Victorian Era into the realm of timestamps! The wraparound date for these 64 bit integers is approximately 290 billion years...

In PHP 5.2+, A DateTime object can be constructed in the following way:

$dt = new DateTime("1844-05-24");

Or using the procedural syntax:

$ts = date_create("1844-05-24");

If no input is provided, the timestamp defaults to the value of now.

Input Formats

Various forms of input are supported in the same GNU Date format as accepted by strtotime(), including the complete W3 recommended ISO 8601 profile:

Basic ISO 8601 Date Formats
Year: YYYY 1844
Year and month: YYYY-MM 1844-05
Complete date: YYYY-MM-DD 1844-05-24
Basic ISO 8601 Time Formats
Complete date plus hours and minutes: YYYY-MM-DDThh:mmTZD 1997-07-16T19:20+01:00
Complete date plus hours, minutes and seconds: YYYY-MM-DDThh:mm:ssTZD 1997-07-16T19:20:30+01:00
Complete date plus hours, minutes, seconds and a decimal fraction of a second: YYYY-MM-DDThh:mm:ss.sTZD 1997-07-16T19:20:30.45+01:00

It's not always convenient to use ISO 8601 compatible date mappings - of course, it is also possible to construct a date from basic textual input:

Relative Formats
Current time: now
The day after current timestamp: tomorrow
Last weekday before current timestamp: last saturday, last wednesday
Next weekday after current timestamp: next saturday, next wednesday
Last day before current timestamp: 4 days ago, 2 weeks ago, 6 months ago, 3 years ago
Next day after current timestamp: 4 days, 2 weeks, 6 months after, 3 years later
Plus or Minus units beyond current timestamp: +2 weeks, -3 years 2 months, +40 days 6 hours

Output Formats

The format() method accepts a date format constant or any other date format parameter, and returns the formatted string:

$dt = new DateTime();
echo $dt->format(DateTime::ISO8601);
echo $dt->format(DateTime::ATOM);
echo $dt->format(DateTime::RSS);
echo $dt->format(DateTime::W3C);
echo $dt->format("Y-m-d");

Date-Time Modifiers

The modify() method changes the referenced DateTime to a new time, based on the specified modifier:

$dt = new DateTime();

$dt->modify("2 weeks ago");
echo $dt->format("Y-m-d");

$dt->modify("+2 days");
echo $dt->format("Y-m-d");

As yet, there is no direct method to compare two dates — the most assured way is to convert to epoch seconds, and compare or run arithmetic on the resulting integers. Unfortunately, PHP represents this epoch timestamp as a signed int, so the range is subject to the same limitations as discussed earlier (most likely the 138 years, from 1901 to 2038).

$dt1 = new DateTime("today");
$dt2 = new DateTime("tomorrow");

if ($dt1->format('U') format('U')) echo "d2 is after d1";
echo "Difference in seconds: " . $d2->format('U') - $d1->format('U');

Beware of casting to seconds, then multiplying by 60, 24, etc, to convert to minutes, hours, days — this is not guaranteed to give you an exact representation of the time difference, as it fails to take into account the variability of leap seconds, daylight savings, and the like. Not all minutes and days are of exactly equal length.

The setDate() method modifies the date with integer input representing a date in years, months, and days. All parameters are required.

$dt = new DateTime();
$dt->setDate(1844, 5, 24);

The setTime() method works in the same way as setDate(), accepting integers for hours, minutes, and seconds (with seconds being optional). Values beyond the 24 hour range act as modifiers, pushing the timestamp forward or backward of the given date:

$dt = new DateTime();
$dt->setTime(15, 30); // today, 3.30pm
$dt->setTime(26, 15); // tomorrow, 2.15am
$dt->setTime(-2, 0); // yesterday, 10pm

An alternative modifier also exists, setISODate() which accepts a year, week number, and optional day date. It follows the same pattern for out of range values as the other methods:

$dt = new DateTime();
$dt->setISODate(2007, 5); // the 5th week of 2007
$dt->setISODate(2007, 10, 3); // the 3rd day of the 10th week
$dt->setISODate(2007, 10, 8); // the 1st day of the 11th week

Localizing Context

PHP now ships with a timezone database, and the DateTime object is paired with a DateTimeZone object which provides access to changeable locale information. A DateTimeZone can be passed directly to a DateTime object in the constructor, or else the timezone information can be parsed out from the input format:

$dtz = new DateTimeZone('Europe/London');
$dt = new DateTime('now', $dtz);
$dt = new DateTime('2007-04-03 Europe/London');

Further Information

Derick Rethans gave a presentation in 2006 which contains a good level of detail about the structure and rationale of the new DateTime functionality.