负数DateInterval

24

我想创建一个带有负的DateInterval的DatePeriod对象。

这将创建一个从今天开始增加到2016年的年份的DatePeriod。

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

我想从2016年开始,并使用负的DateInterval向今天的年份移动

类似这样的东西可能会说明我的愿望

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

我只是找不到任何关于如何进行此操作的DatePeriod或DateInterval的扩展信息。所有我找到的都是DateInterval可以被反转。

8个回答

46
根据php.net关于DateInterval::__construct()页面中kevinpeno在2011年3月17日07:47的评论,你不能通过构造函数直接创建负数的DateIntervals:
new DateInterval('-P1Y'); // Exception "Unknown or bad format (-P1Y)"

你需要创建一个正的区间,并明确设置它的 invert 属性为 1,而不是这样子做:

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

我刚刚亲自检查了上述代码,它确实是按照这种方式工作的。


4
"invert = 1"没有返回异常,但仍会产生一个_正数_结果。然而,使用DateInterval :: createFromDateString('-1天');解决方案确实有效。(为什么这方面的文档记录如此不好?) - mpemburn
@mpemburn 你说的“positive result”是什么意思?这是代码一行式子可以让你自己检查(至少在 PHP 5.4 环境下):$ php -r '$di = new DateInterval("P1Y");$di->invert = 1; var_dump($di); echo $di->format("%r%y years");'它会输出“-1 years”,这是正确的输出结果。你是如何得到 invert=1 后的“positive”结果的呢? - hijarian
当然,createFromDateString 也可以工作。我认为这比使用构造函数并通过 invert 内部属性进行操作更加简洁。 - hijarian
2
这段代码实际上创建了一个带有负周期的DateInterval,但是DatePeriod对象仍然使用这个$di从过去到未来进行迭代。 - ChristopheBrun
mpemburn -- 谢谢你!另外:“为什么这个文档如此不完整?”因为你没有将它作为答案发布,以便我们所有人可以给它点赞! - Stephen R

24
这需要花费一些时间去挖掘。我之前唯一能够获得负数 DateInterval 的方法是这样的:
$interval = DateInterval::createFromDateString('-1 day');

然而,有个问题。 DatePeriod 似乎不能处理负时间间隔。如果你将开始日期设置在结束日期之前,则不包含任何日期;如果你反转开始日期和结束日期,则会无限期地向后看。

你可能需要重新构造代码,使用正的 DateIntervalDateTime::sub 或负的 DateIntervalDateTime::add 循环遍历日期。


16

4

我自己尝试过,仅使用DatePeriod是不可能的。但我认为这是有道理的:它只反映了通常没有任何特定顺序并且无法重新排序(可以视为集合)的时间段。

据我所知,唯一检索日期并以相反顺序排序的方法类似于此:

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

更新

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

DateInterval对象怎么样?手册中说DateInterval可以有invert = 1。但是我该如何创建它?或者我误解了吗? - user798584
不完全误解:一个区间通常不可能为负,因为它只是一个区间。invert 属性提供了一个提示,关于如何创建该区间(如果 invert === 1,则从结束到开始)。然而,我通过添加另一种解决方案来更新我的答案,即从“那时”迭代到“过去”,直到现在。我在这里使用 invert,因为缺少 compare 方法 ;) - KingCrunch
哇,刚发现可以在DateTime对象上使用比较运算符。真酷^^ - KingCrunch
好的,非常感谢。我的代码使用MVC结构,所以我不太喜欢在循环中混合减法逻辑来输出内容。这也是我想要第一篇帖子中提到的格式的主要原因。最终,我在视图中使用了一个for循环,将今年的年份递减直到设定的结束年份。 - user798584

2

我遇到了同样的问题(还有其他一些问题),因此创建了一个类,以便能够添加和减去DateInterval。它还支持负的ISO8601日期间隔(例如“P-2M1DT3H-56M21S”)。

以下是代码(欢迎提出建议,我是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
1
检查Andrew Curioso的上面答案。无需重新发明轮子,只需使用Datetime和DateInterval类。 - Edson Medina

1
这个片段对我有用:
    $iDate = $endDate;
    while($iDate >= $startDate) {
        $dates[] = new DateTime($iDate->format('Y-m-d'));
        $iDate->sub(new DateInterval("P1D"));
    }

0

编辑:请注意,这是受khany上面的代码启发而编写的。

这是一个完全可用的脚本,适用于我的用例,即显示从当前月份开始往回N个月的年份+月份字符串。它应该可以使用天数或年份间隔,并已在PHP版本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";

?>

输出结果为:

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
)

-1

你可以直接通过构造函数使用 ISO 8601 格式的字符串创建负的 DateInterval —— 例如:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接