31

I discovered that a DateTime object in PHP can be compared to another as the ">" and "<" operators are overloaded.

Is it the same with DateInterval?

As I was trying to answer this question, I found something strange:

<?php 

$today = new DateTime();
$release  = new DateTime('14-02-2012');
$building_time = new DateInterval('P15D');
var_dump($today->diff($release));
var_dump($building_time);
var_dump($today->diff($release)>$building_time);
var_dump($today->diff($release)<$building_time);
if($today->diff($release) < $building_time){
    echo 'oK';
}else{
    echo 'Just a test';
}

It always echoes "Just a test". The var_dump outputs are:

object(DateInterval)#4 (8) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(0)
  ["d"]=>
  int(18)
  ["h"]=>
  int(16)
  ["i"]=>
  int(49)
  ["s"]=>
  int(19)
  ["invert"]=>
  int(1)
  ["days"]=>
  int(18)
}
object(DateInterval)#3 (8) {
  ["y"]=>
  int(0)
  ["m"]=>
  int(0)
  ["d"]=>
  int(15)
  ["h"]=>
  int(0)
  ["i"]=>
  int(0)
  ["s"]=>
  int(0)
  ["invert"]=>
  int(0)
  ["days"]=>
  bool(false)
}
bool(false)
bool(true)

When I try with a DateTime as "01-03-2012" everything works.

artragis
  • 3,677
  • 1
  • 18
  • 30

7 Answers7

19

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)
Will B.
  • 17,883
  • 4
  • 67
  • 69
  • 1
    As you are talking about , why don't you use ImmutableDateTime and object dereference to make it quicker : `if((new ImmutableDateTime)->add($firstInt) < (new ImmutableDateTime)->add($secInt))` – artragis Feb 27 '15 at 08:52
  • `DateTimeImmutable` isn't available until `php 5.5` which returns a new DateTime object for every method call instead of updating it (slower). DateTime will return itself or false on failure for every method call. Additionally the buildDate and releaseDate may be needed elsewhere in the code - such as being stored in a database, in other calculations, or as display values. If you are wanting it on a single line you could do: `if((($b = (new DateTime)->modify('+15 day')) && ($r = new DateTime('2012-02-14'))) && $b > $r)` But why sacrifice readability? – Will B. Feb 27 '15 at 18:14
  • You are talking about php 5.6 so DateTimeImmutable is available. It seems legit to me that for such calculation we do not modify the very instance but create a brand new as it is made for C#, Java and many other. About readability, if your point is "you can do worse", i don't care. AFAIK, creating a function called `compareIntervals` that will cal my line is possible and truly readable independently the line number. – artragis Feb 27 '15 at 18:35
  • I used your OP as far as the objects were concerned - I didn't assume you had a version of PHP after the post date 2012. `DateTimeImmutable` was introduced in `(PHP 5 >= 5.5.0)` in 2013. My point was on reusability and readability in general. It depends on your use case. For example if it were OOP it is more viable to modify the DateTime object. EG: `Order->buildDate->modify('+15 day');` for DateTime as opposed to `Order->buildDate = Order->buildDate->modify('+15 day');` for DateTimeImmutable. Then using `Order->isBuildDateValid();` for the comparison on Order->buildDate > Order->releaseDate. – Will B. Feb 27 '15 at 19:50
  • I Understand your point. It's just that you said "as of 5.6"... By the way everything you added in your last comment is true. – artragis Feb 27 '15 at 20:42
  • This should be the accepted answer. (edit: Suggestion: One might want to create one `new DateTime` and then `clone` it twice and add the `DateInterval` to the clones, as an alternative to using `DateTimeImmutable`.) – Lars Nyström Jan 24 '17 at 15:12
  • Another aspect to watch for is the `time` and `timezone` associated with the compared `DateTime` objects are taken into account. So it is best to normalize the desired time/timezone before comparing. This also may become compounded if `DateTime` ever implements `microtime` or `milliseconds` in later versions. However the example provides a viable alternative for `DateInterval` comparisons as of `php 5.6` and now `php 7.0/1`. https://3v4l.org/uWVLD (note the 7.1 result) – Will B. Jan 24 '17 at 15:40
  • -1; in the end, this doesn't actually present code to compare two `DateInterval`s - and the approach described is a dubious way of doing that, since it may give different results (when e.g. comparing 1 month with 29 days) depending upon the base `DateTime` object used for the comparison. – Mark Amery Jan 07 '18 at 16:56
  • @MarkAmery My answer was to provide the OP with the desired result to their question, to which you removed. Furthermore at the time of my answer someone had already provided an example of the `DateInterval` bug report/pull request to implement the behavior in PHP. Lastly your points on the dubious nature of the use of this answer are unsubstantiated. As when used as described, the base `DateTime` object is only used in the comparison in determining a difference between the distances between two points in time as modified by the `DateInterval`, as was described by the OP. – Will B. Jan 07 '18 at 18:10
  • @MarkAmery if the question is not asked to satisfy your desired answer, you should post another question. You should not edit someone else's question around what you perceive their question should have been. As it removes the context to why answers were given in the manner they were. – Will B. Jan 07 '18 at 18:23
  • *"their question, to which you removed"* - I did no such thing. Nothing I removed was pertinent to the question being asked, nor did the edit in any way invalidate this answer. *"You should not edit someone else's question around what you perceive their question should have been."* - I didn't. Or if you think I did, by all means explain how my edit invalidated any of the answers here. – Mark Amery Jan 07 '18 at 18:24
  • *"Lastly your points on the dubious nature of the use of this answer are unsubstantiated."* - then let me be more explicit: let `$a = new DateInterval('P30D'); $b = new DateInterval('P1M'); $c = new DateTime('2017-02-28'); $d = new DateTime('2017-03-31');`. Now `(clone $c)->add($a) < (clone $c)->add($b);` is false but `(clone $d)->add($a) < (clone $d)->add($b);` is true; i.e. the choice of base date alters the result of the interval comparison. – Mark Amery Jan 07 '18 at 18:30
  • @MarkAmery your removal of the demonstrated works, removed the context surrounding the OP's desired end result. `if($today->diff($release) < $building_time){` which is to compare the distance of time between two points. Thereby altering the question to simply "Are DateInterval objects comparable?" – Will B. Jan 07 '18 at 18:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162696/discussion-between-fyrye-and-mark-amery). – Will B. Jan 07 '18 at 18:31
12

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)
Tim Lytle
  • 17,549
  • 10
  • 60
  • 91
  • 4
    Yep. Neither would I. But now I understand what's happening and that's th point. I will use sub/add and DateTime comparisons. They are more reliable. – artragis Mar 03 '12 at 17:38
  • See my new answer, it's reliable and easy enough to use. – quickshiftin Feb 09 '15 at 20:44
  • @quickshiftin As artragis' comment shows, the question was about the actual behaviour, and why it was odd. This answers that. – Tim Lytle Oct 09 '16 at 00:38
10

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

Stephan Vierkant
  • 9,674
  • 8
  • 61
  • 97
  • 1
    Understood from the other answers it wasn't possible, understood from this answer WHY it was'nt possible, which is even better ! – Axi Jan 28 '22 at 08:55
8

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;
    }
}
quickshiftin
  • 66,362
  • 10
  • 68
  • 89
  • in what case isn't date calculated? – Matthew Jan 31 '16 at 10:32
  • If `$this->days` is `null` and the current year doesn't have 365 days. – quickshiftin Feb 01 '16 at 00:43
  • Even with the ample comments concerning years which are not 365 days, I think this answer encourages the usage of a function which will not work for leap years, leap seconds, etc. This can introduce subtle but serious bugs. Please use fyrye's solution instead. – Lars Nyström Jan 24 '17 at 15:17
  • @LarsNyström It's an interesting solution and safer to be sure. My only gripe is that it doesn't answer from the context of 2 `DateInterval` instances. – quickshiftin Jan 24 '17 at 16:01
  • I should point out I've updated my answer to use the technique introduced by @fyrye, and that my original answer works fine for intervals less than a year. – quickshiftin Jan 24 '17 at 17:09
  • @LarsNyström what does it even mean for the answer to "work for leap years"? The correct behaviour isn't well defined. At least quickshiftin's original solution is consistent in what intervals it considers larger than other intervals, even if the choice is somewhat arbitrary; his new solution, like fyryre's, changes its mind about which value is bigger depending upon which day of the year you run it on! – Mark Amery Jan 07 '18 at 17:07
  • -1 for two reasons. First, the edit makes it hard to follow what you're actually recommending or why; it would be clearer to just, uh, *edit* your answer rather than effectively posting a completely new one split off from the first by a `
    `. Secondly, using a subclass for this is misguided, because it makes it difficult to use on normal `DateInterval` objects. Your original answer says that the caller must *"`create` a `ComparableDateInterval` from"* their `DateInterval` first, but gives no hint on how to go about doing this.
    – Mark Amery Jan 07 '18 at 17:11
  • @MarkAmery, regarding my original answer, creating a `ComparableDateInterval` from a `DateInterval` is done by `$oComparableDateInterval = ComparableDateInterval::create($yourExistingDateInterval);`. I assumed it was self-explanatory. I need to read through your comments on mine and @LarsNyström's answer. When I get round to updating, I'll consider splitting my original and second answers out into separate answers proper, or potentially leaving just one sans the `
    `.
    – quickshiftin Jan 13 '18 at 22:34
  • @quickshiftin Aha - I didn't notice the `::create()` method on the second `ComparableDateInterval` implementation. Oops. Sorry; yes, that *was* self-explanatory. The version at the top of the answer doesn't have that method, though. – Mark Amery Jan 13 '18 at 22:35
  • 1
    Your "original answer" is the right approach when you only deal with 3 weeks at best. – Martin Braun Feb 28 '23 at 16:31
-1

If you're working with time intervals that are not longer than a month, it's easy to convert 2 intervals to seconds and compare. $dateInterval->format("%s") only returns the seconds component so I ended up doing this:

function intervalToSeconds($dateInterval) {
        $s = (
            ($dateInterval->format("%d")*24*60*60) + 
            ($dateInterval->format("%h")*60*60) + 
            ($dateInterval->format("%i")*60) + 
            $dateInterval->format("%s")
        );
        return $s;
    }
111
  • 1,788
  • 1
  • 23
  • 38
-1

Where does $aujourdhui come from? Sure it's the same as $today linguistically, but PHP doesn't know that! Changing your code to use $today will print "oK"!

If not defined, $aujourdhui->diff($release) will evaluate to 0 if your PHP interpreter does not abort with an error (mine does).

jclehner
  • 1,440
  • 10
  • 18
  • sorry, it is just a failed translation. As it was only personnal tests, I did not write it in English. I edit my post, the problem is still here. – artragis Mar 03 '12 at 17:12
  • That's strange... What version of PHP are you using? I tried it on my machine with version 5.3.6 and it echoes `oK`! – jclehner Mar 03 '12 at 17:19
-3

I used the following workaround comparing DateIntervals:

version_compare(join('.', (array) $dateIntervalA), join('.', (array) $dateIntervalB));
  • 1
    does not work: $int20 = new DateInterval('P20D'); $int20_2 = new DateInterval('P20D'); var_dump(join('.', (array) $int20), join('.', (array) $int20_2)); gives two "équivalent versions" but version_compare send (-1) so the equality is not handled. Moreover I remember that when you use the substraction method a new field is created inside the DateInterval making it uncomparable to a hand-created DateInterval. – artragis Aug 09 '12 at 16:19
  • @artragis I can't reproduce `version_compare` giving -1 for identical strings; that said, using it on strings that aren't valid version strings *does* seem clearly ill-advised. Perhaps the behaviour in PHP 7 is different to whatever version you tested on 5 years ago. – Mark Amery Dec 03 '17 at 15:51