24

I want to create a DatePeriod object with a negative DateInterval.

This creates a DatePeriod with the year increasing from today to 2016.

$this->Set('dates', new DatePeriod(new DateTime(), new DateInterval('P1Y'), new DateTime('2016-06-06')));

I want to start at 2016, and using a negative DateInterval move towards todays year

Something like this might illustrate my desire

$this->Set('dates', new DatePeriod(new DateTime('2016-06-06'), new DateInterval('-P1Y'), new DateTime()));

I just can't find any extended info on either DatePeriod or DateInterval on how to do this. All i find is that DateInterval can be inverted.

KingCrunch
  • 128,817
  • 21
  • 151
  • 173
user798584
  • 243
  • 1
  • 2
  • 7

8 Answers8

46

According to comment by kevinpeno at 17-Mar-2011 07:47 on php.net's page about DateInterval::__construct(), you cannot directly create negative DateIntervals through the constructor:

new DateInterval('-P1Y'); // Exception "Unknown or bad format (-P1Y)"

Instead of this you are required to create a positive interval and explicitly set it's invert property to 1:

$di = new DateInterval('P1Y');
$di->invert = 1; // Proper negative date interval

Just checked the above code by myself, it's working exactly in this way.

hijarian
  • 2,159
  • 1
  • 28
  • 34
  • 4
    "invert = 1" returns no exception but still produces _positive_ result. The DateInterval::createFromDateString('-1 day'); solution _does_ work, however. (Why is this so poorly documented?) – mpemburn Oct 11 '13 at 01:10
  • @mpemburn What do you mean "produces _positive_ result"? Here's the one-liner to check yourself (on PHP 5.4 at least): `$ php -r '$di = new DateInterval("P1Y");$di->invert = 1; var_dump($di); echo $di->format("%r%y years");'` It prints "-1 years", as it should. How did you got "positive" results after `invert = 1`? – hijarian Oct 14 '13 at 06:46
  • And of course `createFromDateString` works, to. I suppose that this is cleaner solution than using a constructor and hacking through the `invert` inner property. – hijarian Oct 14 '13 at 06:49
  • 2
    This code actually creates a DateInterval with a negative period but the DatePeriod object still iterates from past to future with this $di. – ChristopheBrun Jul 10 '14 at 18:26
  • mpemburn -- THANK YOU! Also: "Why is this so poorly documented?" Because you didn't make it an Answer so we could all upvote it! – Stephen R Sep 20 '16 at 16:52
23

This took a little digging. The only way I was able to get a negative DateInterval was by doing this:

$interval = DateInterval::createFromDateString('-1 day');

However, there is a catch. DatePeriod seems to not work for negative intervals. If you set the start date to be before the end date then it doesn't contain any dates at all and if you flip so that the start date is after the end date then it looks backwards indefinitely.

You're probably going to have to restructure your code to loop through the dates using DateTime::sub with a positive DateInterval or DateTime::add with the negative one.

Andrew Curioso
  • 2,181
  • 13
  • 24
16

You can used sub http://php.net/manual/en/datetime.sub.php

Here is example

$startDate = new \DateTime('2018-01-08 13:54:06');
$startDate->sub(new \DateInterval('P1D'));
Ramadhan
  • 824
  • 8
  • 10
4

I tried it myself and it isn't possible with DatePeriod alone, but I think that makes sense: It just reflects the periods, that usually doesn't have any specific order and therefore cannot get reordered (it can be treated as a set).

The only way to retrieve the dates and sort it in reversed order, as far as I can see, is something like this

$result = array();
forech ($dateperiod as $date) {
  array_push ($result, $data);
}

Update

$date = new DateTime('2016-06-06');
$i = new DateInterval('P1Y');
$now = new DateTime;
while ($date >= $now) {
  echo $date->format('c') . PHP_EOL;
  $date = $date->sub($i);
}
KingCrunch
  • 128,817
  • 21
  • 151
  • 173
  • how about the DateInterval object? it says in manual that DateInterval can have invert = 1. But how do I create it? Or have I misunderstood? – user798584 Jun 14 '11 at 22:33
  • Not completely misunderstood: An interval usually cannot get negative, because its just an interval. The `invert`-property gives a hint, on how the interval is created (from end to start, if `invert === 1`. However, updated my answer by adding another solution, that just iterates from "then" into "the past", until now. I use `invert` here, because of the lack of a `compare`-method ;) – KingCrunch Jun 14 '11 at 22:54
  • Wow, just found out, that one can use the comparison operators with `DateTime`-Objects. Cool ^^ – KingCrunch Jun 14 '11 at 22:58
  • ok thanks a bunch, I'm using a MVC structure on my code so I don't really like mixing in subtracting logic in the loop where i want to echo. That was the main reason i wanted the format from my first post. I ended up just doing a for-loop in the view decreasing this year until a set endyear. – user798584 Jun 14 '11 at 23:22
2

I had the same problem (and some other) and have created a class in order to be able to add and substact DateInterval. It supports also negative ISO8601 date interval ('P-2M1DT3H-56M21S' for example).

Here the code (suggestions are welcome, I'm a very beginner in PHP):

class MyDateInterval extends DateInterval
{
    public function __construct($interval_spec)
    {
        $interval_spec = str_replace('+', '', $interval_spec);
        $pos = strpos($interval_spec, '-');
        if ($pos !== false) {
            // if at least 1 negative part
            $pattern = '/P(?P<ymd>(?P<years>-?\d+Y)?(?P<months>-?\d+M)?(?P<days>-?\d+D)?)?(?P<hms>T(?P<hours>-?\d+H)?(?P<minutes>-?\d+M)?(?P<seconds>-?\d+S)?)?/';
            $match = preg_match($pattern, $interval_spec, $matches);
            $group_names = array('years', 'months', 'days', 'hours', 'minutes', 'seconds');
            $negative_parts = array();
            $positive_parts = array();
            $all_negative = true;
            foreach ($matches as $k => $v) {
                if (in_array($k, $group_names, true)) {
                    if (substr($v, 0, 1) == '-' and $v != '')
                        $negative_parts[$k] = $v;
                    if (substr($v, 0, 1) != '-' and $v != '')
                        $positive_parts[$k] = $v;
                }
            }
            if (count($positive_parts) == 0) {
                // only negative parts
                $interval_spec = str_replace('-', '', $interval_spec);
                parent::__construct($interval_spec);
                $this->invert = 1;
            } else {
                // the negative and positive parts are to be sliced
                $negative_interval_spec = 'P';
                $positive_interval_spec = 'P';
                if ($matches['ymd'] != '') {
                    foreach ($matches as $k => $v) {
                        if (in_array($k, array_slice($group_names, 0, 3))) {
                            $negative_interval_spec .= $negative_parts[$k];
                            $positive_interval_spec .= $positive_parts[$k];
                        }
                    }
                }
                if ($matches['hms'] != '') {
                    $negative_ok = false;
                    $positive_ok = false;
                    foreach ($matches as $k => $v) {
                        if (in_array($k, array_slice($group_names, 3, 3))) {
                            if ($negative_parts[$k] != '' and ! $negative_ok) {
                                $negative_interval_spec .= 'T';
                                $negative_ok = true;
                            }
                            $negative_interval_spec .= $negative_parts[$k];
                            if ($positive_parts[$k] != '' and ! $positive_ok) {
                                $positive_interval_spec .= 'T';
                                $positive_ok = true;
                            }
                            $positive_interval_spec .= $positive_parts[$k];
                        }
                    }
                }
                $negative_interval_spec = str_replace('-', '', $negative_interval_spec);
                $from = new DateTime('2013-01-01');
                $to = new DateTime('2013-01-01');
                $to = $to->add(new DateInterval($positive_interval_spec));
                $to = $to->sub(new DateInterval($negative_interval_spec));
                $diff = $from->diff($to);
                parent::__construct($diff->format('P%yY%mM%dDT%hH%iM%sS'));
                $this->invert = $diff->invert;
            }
        } else {
            // only positive parts
            parent::__construct($interval_spec);
        }
    }

    public static function fromDateInterval(DateInterval $from)
    {
        return new MyDateInterval($from->format('P%yY%mM%dDT%hH%iM%sS'));
    }

    public static function fromSeconds($from)
    {
        $invert = false;
        if ($from < 0)
            $invert = true;
        $from = abs($from);

        $years = floor($from / (365 * 30 * 24 * 60 * 60));
        $from = $from % (365 * 30 * 24 * 60 * 60);

        $months = floor($from / (30 * 24 * 60 * 60));
        $from = $from % (30 * 24 * 60 * 60);

        $days = floor($from / (24 * 60 * 60));
        $from = $from % (24 * 60 * 60);

        $hours = floor($from / (60 * 60));
        $from = $from % (60 * 60);

        $minutes = floor($from / 60);
        $seconds = floor($from % 60);

        if ($invert)
            return new MyDateInterval(sprintf("P-%dY-%dM-%dDT-%dH-%dM-%dS", $years, $months, $days, $hours, $minutes, $seconds));
        return new MyDateInterval(sprintf("P%dY%dM%dDT%dH%dM%dS", $years, $months, $days, $hours, $minutes, $seconds));
    }

    public function to_seconds()
    {
        $seconds = ($this->y * 365 * 24 * 60 * 60)
                    + ($this->m * 30 * 24 * 60 * 60)
                    + ($this->d * 24 * 60 * 60)
                    + ($this->h * 60 * 60)
                    + ($this->i * 60)
                    + $this->s;
        if ($this->invert == 1)
            return $seconds * -1;
        return $seconds;
    }

    public function to_hours()
    {
        $hours = round($this->to_seconds() / (60 * 60), 2);
        return $hours;
    }

    public function add($interval)
    {
        $sum = $this->to_seconds() + $interval->to_seconds();
        $new = MyDateInterval::fromSeconds($sum);
        foreach ($new as $k => $v) $this->$k = $v;
        return $this;
    }

    public function sub($interval)
    {

        $diff = $this->to_seconds() - $interval->to_seconds();
        $new = MyDateInterval::fromSeconds($diff);
        foreach ($new as $k => $v) $this->$k = $v;
        return $this;
    }

    public function recalculate()
    {
        $seconds = $this->to_seconds();
        $new = MyDateInterval::fromSeconds($seconds);
        foreach ($new as $k => $v) $this->$k = $v;
        return $this;
    }
}
Michel
  • 571
  • 1
  • 4
  • 19
1

This extract worked for me:

    $iDate = $endDate;
    while($iDate >= $startDate) {
        $dates[] = new DateTime($iDate->format('Y-m-d'));
        $iDate->sub(new DateInterval("P1D"));
    }
khany
  • 1,167
  • 15
  • 33
0

EDIT: note that this was inspired by khany's code above.

Here's a fully working script for my use-case, which is to display year+month strings from the current month going back N number of months. It should work with days or years as intervals and is tested with PHP version 5.3.3.

<?php

date_default_timezone_set('America/Los_Angeles');

$monthsBack=16;

$monthDateList = array();
$previousMonthDate = new DateTime();
for($monthInterval = 0; $monthInterval < $monthsBack; $monthInterval++) {
    array_push($monthDateList, $previousMonthDate->format('Ym'));
    $previousMonthDate->sub(new DateInterval("P1M"));
}

print_r($monthDateList) . "\n";

?>

The output is:

Array
(
    [0] => 201705
    [1] => 201704
    [2] => 201703
    [3] => 201702
    [4] => 201701
    [5] => 201612
    [6] => 201611
    [7] => 201610
    [8] => 201609
    [9] => 201608
    [10] => 201607
    [11] => 201606
    [12] => 201605
    [13] => 201604
    [14] => 201603
    [15] => 201602
)
Antony Nguyen
  • 179
  • 1
  • 5
-1

You can create negative DateInterval directly through constructor by passing strings in ISO 8601 format - ie:

>>> new \DateInterval("2009-03-01T00:00:00Z/2008-05-11T00:00:00Z")
=> DateInterval {#3947
     interval: - 9m 21d,
   }
eithed
  • 3,933
  • 6
  • 40
  • 60