开发者

Negative DateInterval

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.


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.


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.


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'));


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);
}


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;
    }
}


This extract worked for me:

    $iDate = $endDate;
    while($iDate >= $startDate) {
        $dates[] = new DateTime($iDate->format('Y-m-d'));
        $iDate->sub(new DateInterval("P1D"));
    }


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
)


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,
   }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜