Are PHP DateInterval comparable like DateTime?
Looks like there was a related bug/feature request, not sure if that ever made it in the trunk. It's not documented (that I Can find) either way - so probably not safe to use.
That said, after some testing it seems that they can be compared, but only after they've been 'evaluated' in some way (doing a var dump changes the outcome). Here's my test/result:
<?php
$int15 = new DateInterval('P15D');
$int20 = new DateInterval('P20D');
var_dump($int15 > $int20); //should be false;
var_dump($int20 > $int15); //should be true;
var_dump($int15 < $int20); //should be true;
var_dump($int20 < $int15); //should be false;
var_dump($int15);
var_dump($int20);
var_dump($int15 > $int20); //should be false;
var_dump($int20 > $int15); //should be true;
var_dump($int15 < $int20); //should be true;
var_dump($int20 < $int15); //should be false;
$date = new DateTime();
$diff = $date->diff(new DateTime("+10 days"));
var_dump($int15 < $diff); //should be false;
var_dump($diff < $int15); //should be true;
var_dump($int15 > $diff); //should be true;
var_dump($diff > $int15); //should be false;
var_dump($diff);
var_dump($int15 < $diff); //should be false;
var_dump($diff < $int15); //should be true;
var_dump($int15 > $diff); //should be true;
var_dump($diff > $int15); //should be false;
Result (I've omitted the full dumps of the interval objects):
bool(false) bool(false) bool(false) bool(false) object(DateInterval)#1 (8) {...} object(DateInterval)#2 (8) {...} bool(false) bool(true) bool(true) bool(false) bool(false) bool(true) bool(true) bool(false) object(DateInterval)#5 (8) {...} bool(false) bool(true) bool(true) bool(false)
In short, comparing of DateInterval
objects is not currently supported by default (as of php 5.6).
As you already know, the DateTime
Objects are comparable.
A way to achieve the desired result, is to subtract or add the DateInterval
from a DateTime
object and compare the two to determine the difference.
Example: https://3v4l.org/kjJPg
$buildDate = new DateTime('2012-02-15');
$releaseDate = clone $buildDate;
$releaseDate->setDate(2012, 2, 14);
$buildDate->add(new DateInterval('P15D'));
var_dump($releaseDate < $buildDate); //bool(true)
Edit
As of the release of PHP 7.1 the results are different than with PHP 5.x, due to the added support for microseconds.
Example: https://3v4l.org/rCigC
$a = new \DateTime;
$b = new \DateTime;
var_dump($a < $b);
Results (7.1+):
bool(true)
Results (5.x - 7.0.x, 7.1.3):
bool(false)
To circumvent this behavior, it is recommended that you use clone
to compare the DateTime
objects instead.
Example: https://3v4l.org/CSpV8
$a = new \DateTime;
$b = clone $a;
var_dump($a < $b);
Results (5.x - 7.x):
bool(false)
EDIT:
class ComparableDateInterval extends DateInterval
{
/**
* Leap-year safe comparison of DateInterval objects.
*/
public function compare(DateInterval $oDateInterval)
{
$fakeStartDate1 = date_create();
$fakeStartDate2 = clone $fakeStartDate1;
$fakeEndDate1 = $fakeStartDate1->add($this);
$fakeEndDate2 = $fakeStartDate2->add($oDateInterval);
if($fakeEndDate1 < $fakeEndDate2) {
return -1;
} elseif($fakeEndDate1 == $fakeEndDate2) {
return 0;
}
return 1;
}
}
$int15 = new ComparableDateInterval('P15D');
$int20 = new ComparableDateInterval('P20D');
var_dump($int15->compare($int20) == -1); // should be true;
See @fyrye's answer for the rationale (and upvote it!). My original answer did not deal with leap years safely.
Original Answer
While I upvoted this question, I downvoted the accepted answer. That's because it didn't work for me on any of my PHP installations and because fundamentally it's hinging on something broken internally.
What I did instead is migrate the aforementioned patch which never made it into trunk. FWIW I checked a recent release, PHP 5.6.5, and the patch still isn't there. The code was trivial to port. The only thing is a warning in how it makes the comparison
If $this->days has been calculated, we know it's accurate, so we'll use that. If not, we need to make an assumption about month and year length, which isn't necessarily a good idea. I've defined months as 30 days and years as 365 days completely out of thin air, since I don't have the ISO 8601 spec available to check if there's a standard assumption, but we may in fact want to error out if we don't have $this->days available.
Here's an example. Note, if you need to compare a DateInterval
that was returned from some other call, you'll have to create
a ComparableDateInterval
from it first, if you want to use it as the source of the comparison.
$int15 = new ComparableDateInterval('P15D');
$int20 = new ComparableDateInterval('P20D');
var_dump($int15->compare($int20) == -1); // should be true;
Here's the code
/**
* The stock DateInterval never got the patch to compare.
* Let's reimplement the patch in userspace.
* See the original patch at http://www.adamharvey.name/patches/DateInterval-comparators.patch
*/
class ComparableDateInterval extends DateInterval
{
static public function create(DateInterval $oDateInterval)
{
$oDi = new ComparableDateInterval('P1D');
$oDi->s = $oDateInterval->s;
$oDi->i = $oDateInterval->i;
$oDi->h = $oDateInterval->h;
$oDi->days = $oDateInterval->days;
$oDi->d = $oDateInterval->d;
$oDi->m = $oDateInterval->m;
$oDi->y = $oDateInterval->y;
$oDi->invert = $oDateInterval->invert;
return $oDi;
}
public function compare(DateInterval $oDateInterval)
{
$oMyTotalSeconds = $this->getTotalSeconds();
$oYourTotalSeconds = $oDateInterval->getTotalSeconds();
if($oMyTotalSeconds < $oYourTotalSeconds)
return -1;
elseif($oMyTotalSeconds == $oYourTotalSeconds)
return 0;
return 1;
}
/**
* If $this->days has been calculated, we know it's accurate, so we'll use
* that. If not, we need to make an assumption about month and year length,
* which isn't necessarily a good idea. I've defined months as 30 days and
* years as 365 days completely out of thin air, since I don't have the ISO
* 8601 spec available to check if there's a standard assumption, but we
* may in fact want to error out if we don't have $this->days available.
*/
public function getTotalSeconds()
{
$iSeconds = $this->s + ($this->i * 60) + ($this->h * 3600);
if($this->days > 0)
$iSeconds += ($this->days * 86400);
// @note Maybe you prefer to throw an Exception here per the note above
else
$iSeconds += ($this->d * 86400) + ($this->m * 2592000) + ($this->y * 31536000);
if($this->invert)
$iSeconds *= -1;
return $iSeconds;
}
}
No, this is not possible right now and it never will be. There is a fundamental problem with comparing two DateInterval
's.
A DateInterval
is relative, while a DateTime
is absolute: P1D
means 1 day, so you would think that means (24*60*60) 86.400 seconds. But due to the Leap Second it isn't always the case.
That looks like a rare situation, don't forget comparing months with days is even harder:
P1M and P30D - which one is the greater one? is it P1M even though I'm currently in february? Or is it P30D even though I'm currently in August? What about PT24H30M and P1D? https://bugs.php.net/bug.php?id=49914#1490336933