PHP DateTime ::修改添加和减去月份

时间:2010-08-30 16:44:04

标签: php datetime date

我一直在使用DateTime class进行了大量工作,最近遇到了我认为添加月份时的错误。经过一些研究后,似乎它不是一个bug,而是按预期工作。根据发现的文件here

  

示例#2小心添加或   减去几个月

<?php
$date = new DateTime('2000-12-31');

$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";

$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";
?>
The above example will output:
2001-01-31
2001-03-03

有人可以证明为什么这不被视为错误吗?

此外,是否有人有任何优雅的解决方案来纠正问题并使其成为+1个月将按预期工作而不是按预期工作?

20 个答案:

答案 0 :(得分:97)

为什么它不是一个错误:

目前的行为是正确的。内部发生以下情况:

  1. +1 month将月份数(原来为1)增加1。这使得日期为2010-02-31

  2. 第二个月(二月)在2010年只有28天,因此PHP会自动更正这一点,只需继续计算从2月1日起的天数。然后你在3月3日结束。

  3. 如何得到你想要的东西:

    通过以下方式获得您想要的:手动检查下个月。然后添加下个月的天数。

    我希望你能亲自编码。我只是在做什么。

    PHP 5.3方式:

    要获得正确的行为,您可以使用PHP 5.3的新功能之一来引入相对时间节first day of。此节可与next monthfifth month+8 months结合使用,以转到指定月份的第一天。您可以使用此代码来代替下一个月的第一天,而不是+1 month

    <?php
    $d = new DateTime( '2010-01-31' );
    $d->modify( 'first day of next month' );
    echo $d->format( 'F' ), "\n";
    ?>
    

    此脚本将正确输出February。 PHP处理此first day of next month节时会发生以下情况:

    1. next month将月份数(原来为1)增加1。这使得日期为2010-02-31。

    2. first day of将日期编号设置为1,结果日期为2010-02-01。

答案 1 :(得分:11)

这可能很有用:

echo Date("Y-m-d", strtotime("2013-01-01 +1 Month -1 Day"));
  // 2013-01-31

echo Date("Y-m-d", strtotime("2013-02-01 +1 Month -1 Day"));
  // 2013-02-28

echo Date("Y-m-d", strtotime("2013-03-01 +1 Month -1 Day"));
  // 2013-03-31

echo Date("Y-m-d", strtotime("2013-04-01 +1 Month -1 Day"));
  // 2013-04-30

echo Date("Y-m-d", strtotime("2013-05-01 +1 Month -1 Day"));
  // 2013-05-31

echo Date("Y-m-d", strtotime("2013-06-01 +1 Month -1 Day"));
  // 2013-06-30

echo Date("Y-m-d", strtotime("2013-07-01 +1 Month -1 Day"));
  // 2013-07-31

echo Date("Y-m-d", strtotime("2013-08-01 +1 Month -1 Day"));
  // 2013-08-31

echo Date("Y-m-d", strtotime("2013-09-01 +1 Month -1 Day"));
  // 2013-09-30

echo Date("Y-m-d", strtotime("2013-10-01 +1 Month -1 Day"));
  // 2013-10-31

echo Date("Y-m-d", strtotime("2013-11-01 +1 Month -1 Day"));
  // 2013-11-30

echo Date("Y-m-d", strtotime("2013-12-01 +1 Month -1 Day"));
  // 2013-12-31

答案 2 :(得分:6)

我对问题的解决方案:

$startDate = new \DateTime( '2015-08-30' );
$endDate = clone $startDate;

$billing_count = '6';
$billing_unit = 'm';

$endDate->add( new \DateInterval( 'P' . $billing_count . strtoupper( $billing_unit ) ) );

if ( intval( $endDate->format( 'n' ) ) > ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) ) % 12 )
{
    if ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) != 12 )
    {
        $endDate->modify( 'last day of -1 month' );
    }
}

答案 3 :(得分:5)

这是另一个完全使用DateTime方法的紧凑解决方案,在不创建克隆的情况下就地修改对象。

$dt = new DateTime('2012-01-31');

echo $dt->format('Y-m-d'), PHP_EOL;

$day = $dt->format('j');
$dt->modify('first day of +1 month');
$dt->modify('+' . (min($day, $dt->format('t')) - 1) . ' days');

echo $dt->format('Y-m-d'), PHP_EOL;

输出:

2012-01-31
2012-02-29

答案 4 :(得分:4)

我创建了一个返回DateInterval的函数,以确保添加一个月显示下个月,并将日期删除到之后。

$time = new DateTime('2014-01-31');
echo $time->format('d-m-Y H:i') . '<br/>';

$time->add( add_months(1, $time));

echo $time->format('d-m-Y H:i') . '<br/>';



function add_months( $months, \DateTime $object ) {
    $next = new DateTime($object->format('d-m-Y H:i:s'));
    $next->modify('last day of +'.$months.' month');

    if( $object->format('d') > $next->format('d') ) {
        return $object->diff($next);
    } else {
        return new DateInterval('P'.$months.'M');
    }
}

答案 5 :(得分:3)

我同意OP的观点,认为这是违反直觉和令人沮丧的,但确定+1 month在出现这种情况的情况下的含义也是如此。请考虑以下示例:

您从2015-01-31开始,并希望添加一个月6次以获得发送电子邮件简报的计划周期。考虑到OP最初的期望,这将会回归:

  • 2015年1月31日
  • 2015年2月28日
  • 2015年3月31日
  • 2015年4月30日
  • 2015年5月31日
  • 2015年6月30日

立即注意,我们期望+1 month表示last day of month,或者,每次迭代添加1个月,但总是参考起点。而不是将其解释为&#34;月的最后一天&#34;我们可以将它读作#34;下个月的第31天或最后一个月内可用的#34;这意味着我们从4月30日跳到5月31日而不是5月30日。请注意,这不是因为它是&#34;月份的最后一天&#34;但是因为我们希望在开始月份之前最接近可用。&#34;

因此,假设我们的一位用户订阅了另一份时事通讯,将于2015-01-30开始。 +1 month的直观日期是什么?一种解释将是下个月的第30天或最接近的可用&#34;将返回:

  • 二零一五年一月三十零日
  • 2015年2月28日
  • 2015年3月30日
  • 2015年4月30日
  • 2015年5月30日
  • 2015年6月30日

除非我们的用户在同一天收到两份简报,否则这样会很好。让我们假设这是供应方面的问题,而不是需求方面我们并不担心用户会在同一天收到2条新闻通讯而烦恼,而是我们的邮件服务器可以“为发送两倍的新闻通讯提供带宽。考虑到这一点,我们回到&#34; +1个月&#34; as&#34;发送每个月的第二天到第二天&#34;将返回:

  • 二零一五年一月三十零日
  • 2015年2月27日
  • 2015年3月30日
  • 2015年4月29日
  • 2015年5月30日
  • 2015-07-02

现在我们已经避免与第一组有任何重叠,但我们也最终得到了4月和6月29日,这肯定符合+1 month应该返回m/$d/Y的原始直觉或所有可能的月份都很有吸引力且简单m/30/Y。现在让我们考虑使用两个日期对+1 month的第三种解释:

一月31

  • 2015年1月31日
  • 2015年3月3日
  • 2015年3月31日
  • 2015年5月1日
  • 2015年5月31日
  • 2015年7月1日

一月第30

  • 二零一五年一月三十零日
  • 2015年3月2日
  • 2015年3月30日
  • 2015年4月30日
  • 2015年5月30日
  • 2015年6月30日

以上有一些问题。 2月被跳过,这可能是供应端的一个问题(比如说每月带宽分配,2月份浪费,3月份增加一倍)和需求结束(用户感到2月被骗,并认为额外的3月份试图纠正错误)。另一方面,请注意两个日期集:

  • 永不重叠
  • 总是在该月有该日期的同一天(因此1月30日看起来非常干净)
  • 都可以在3天内(大多数情况下为1天)可以被认为是&#34;正确的&#34;日期。
  • 从他们的继任者和前任开始至少28天(农历月),所以分布均匀。

鉴于最后两组,如果它超出实际的下个月(因此在第一组中回滚到2月28日和4月30日)并且不会丢失任何日期,则简单地回滚其中一个日期并不困难。睡在偶尔的重叠和分歧上,从第一天的最后一天开始#34; vs&#34;第二个月到最后一天&#34;图案。但希望图书馆能够选择最相当/最自然的&#34;,#34; 02/31的数学解释和其他月份溢出&#34;和#34;相对于月末或月末& #34;总是会因为某人的期望没有得到满足而有些时间表需要调整错误的&#34;日期,以避免现实世界的问题,&#34;错误&#34;解释介绍。

所以再次,虽然我也希望+1 month返回实际上在下个月的日期,但它并不像直觉那么简单并且给出了选择,与数学结果相比,Web开发人员的期望是可能是安全的选择。

这是一个替代解决方案,仍然像任何人一样笨重,但我认为有很好的结果:

foreach(range(0,5) as $count) {
    $new_date = clone $date;
    $new_date->modify("+$count month");
    $expected_month = $count + 1;
    $actual_month = $new_date->format("m");
    if($expected_month != $actual_month) {
        $new_date = clone $date;
        $new_date->modify("+". ($count - 1) . " month");
        $new_date->modify("+4 weeks");
    }

    echo "* " . nl2br($new_date->format("Y-m-d") . PHP_EOL);
}

这不是最优的,但基本逻辑是:如果添加1个月导致的日期不是预期的下个月,请废弃该日期并添加4周。以下是两个测试日期的结果:

一月31

  • 2015年1月31日
  • 2015年2月28日
  • 2015年3月31日
  • 2015年4月28日
  • 2015年5月31日
  • 2015年6月28日

一月第30

  • 二零一五年一月三十零日
  • 2015年2月27日
  • 2015年3月30日
  • 2015年4月30日
  • 2015年5月30日
  • 2015年6月30日

(我的代码很乱,并且不会在多年的情况下工作。我欢迎任何人用更优雅的代码重写解决方案,只要基础前提保持不变,即+1个月返回一个时髦的约会,改为使用+4周。)

答案 6 :(得分:3)

结合shamittomar的回答,可能就是“安全地”添加几个月:

/**
 * Adds months without jumping over last days of months
 *
 * @param \DateTime $date
 * @param int $monthsToAdd
 * @return \DateTime
 */

public function addMonths($date, $monthsToAdd) {
    $tmpDate = clone $date;
    $tmpDate->modify('first day of +'.(int) $monthsToAdd.' month');

    if($date->format('j') > $tmpDate->format('t')) {
        $daysToAdd = $tmpDate->format('t') - 1;
    }else{
        $daysToAdd = $date->format('j') - 1;
    }

    $tmpDate->modify('+ '. $daysToAdd .' days');


    return $tmpDate;
}

答案 7 :(得分:2)

我使用以下代码找到了更短的方法:

                   $datetime = new DateTime("2014-01-31");
                    $month = $datetime->format('n'); //without zeroes
                    $day = $datetime->format('j'); //without zeroes

                    if($day == 31){
                        $datetime->modify('last day of next month');
                    }else if($day == 29 || $day == 30){
                        if($month == 1){
                            $datetime->modify('last day of next month');                                
                        }else{
                            $datetime->modify('+1 month');                                
                        }
                    }else{
                        $datetime->modify('+1 month');
                    }
echo $datetime->format('Y-m-d H:i:s');

答案 8 :(得分:1)

已被接受的答案已经解释了为什么这不是一个问题,而其他一些答案则为first day of the +2 months这样的php表达式提供了一个简洁的解决方案。这些表达式的问题在于它们没有自动完成。

该解决方案非常简单。首先,您应该找到反映问题空间的有用抽象。在这种情况下,它是ISO8601DateTime。其次,应该有多种实现方式可以带来所需的文本表示形式。例如,TodayTomorrowThe first day of this monthFuture都代表ISO8601DateTime概念的特定实现。

因此,对于您而言,您需要的实现是TheFirstDayOfNMonthsLater。只需在任何IDE中查看子类llist即可轻松找到。这是代码:

$start = new DateTimeParsedFromISO8601String('2000-12-31');
$firstDayOfOneMonthLater = new TheFirstDayOfNMonthsLater($start, 1);
$firstDayOfTwoMonthsLater = new TheFirstDayOfNMonthsLater($start, 2);
var_dump($start->value()); // 2000-12-31T00:00:00+00:00
var_dump($firstDayOfOneMonthLater->value()); // 2001-01-01T00:00:00+00:00
var_dump($firstDayOfTwoMonthsLater->value()); // 2001-02-01T00:00:00+00:00

与一个月的最后几天相同。有关此方法的更多示例,请read this

答案 9 :(得分:1)

这是相关问题中Kasihasi's answer的改进版本。这将正确地添加或减去任意数月的日期。

let sqlDate = post.age
        var hoursSincePost = sqlDate.hoursAfterDate(Date())
        hoursSincePost = abs(hoursSincePost)

       var daysSincePost = sqlDate.daysAfterDate(Date())
        daysSincePost = abs(daysSincePost)
        if hoursSincePost < 24 {
            age.text = String(hoursSincePost) + " hours ago"
        } else if daysSincePost < 7 {
            age.text = String(daysSincePost) + " days ago"
        } else {
            age.text =  sqlDate.toString(.custom("MMM dd yyyy"))
        }

例如:

public static function addMonths($monthToAdd, $date) {
    $d1 = new DateTime($date);

    $year = $d1->format('Y');
    $month = $d1->format('n');
    $day = $d1->format('d');

    if ($monthToAdd > 0) {
        $year += floor($monthToAdd/12);
    } else {
        $year += ceil($monthToAdd/12);
    }
    $monthToAdd = $monthToAdd%12;
    $month += $monthToAdd;
    if($month > 12) {
        $year ++;
        $month -= 12;
    } elseif ($month < 1 ) {
        $year --;
        $month += 12;
    }

    if(!checkdate($month, $day, $year)) {
        $d2 = DateTime::createFromFormat('Y-n-j', $year.'-'.$month.'-1');
        $d2->modify('last day of');
    }else {
        $d2 = DateTime::createFromFormat('Y-n-d', $year.'-'.$month.'-'.$day);
    }
    return $d2->format('Y-m-d');
}

将输出:

addMonths(-25, '2017-03-31')

答案 10 :(得分:1)

以下是相关问题中Juhana's answer的改进版本的实现:

<?php
function sameDateNextMonth(DateTime $createdDate, DateTime $currentDate) {
    $addMon = clone $currentDate;
    $addMon->add(new DateInterval("P1M"));

    $nextMon = clone $currentDate;
    $nextMon->modify("last day of next month");

    if ($addMon->format("n") == $nextMon->format("n")) {
        $recurDay = $createdDate->format("j");
        $daysInMon = $addMon->format("t");
        $currentDay = $currentDate->format("j");
        if ($recurDay > $currentDay && $recurDay <= $daysInMon) {
            $addMon->setDate($addMon->format("Y"), $addMon->format("n"), $recurDay);
        }
        return $addMon;
    } else {
        return $nextMon;
    }
}

此版本需要$createdDate,假设您正在处理在特定日期(例如31日)开始的定期月度期间(例如订阅)。它总是需要$createdDate这么晚才会再次发生在&#34;日期不会转移到较低的价值,因为它们被推进到较低价值的月份(例如,因此所有29日,30日或31日的复发日期最终都会在通过非飞跃后第28次被卡住年2月)。

以下是测试算法的一些驱动程序代码:

$createdDate = new DateTime("2015-03-31");
echo "created date = " . $createdDate->format("Y-m-d") . PHP_EOL;

$next = sameDateNextMonth($createdDate, $createdDate);
echo "   next date = " . $next->format("Y-m-d") . PHP_EOL;

foreach(range(1, 12) as $i) {
    $next = sameDateNextMonth($createdDate, $next);
    echo "   next date = " . $next->format("Y-m-d") . PHP_EOL;
}

哪个输出:

created date = 2015-03-31
   next date = 2015-04-30
   next date = 2015-05-31
   next date = 2015-06-30
   next date = 2015-07-31
   next date = 2015-08-31
   next date = 2015-09-30
   next date = 2015-10-31
   next date = 2015-11-30
   next date = 2015-12-31
   next date = 2016-01-31
   next date = 2016-02-29
   next date = 2016-03-31
   next date = 2016-04-30

答案 11 :(得分:0)

你也可以用date()和strtotime()来实现。例如,在今天的日期添加1个月:

日期(“Y-m-d”,strtotime(“+ 1个月”,时间()));

如果您想要使用日期时间类,那也很简单。 more details here

答案 12 :(得分:0)

$current_date = new DateTime('now');
$after_3_months = $current_date->add(\DateInterval::createFromDateString('+3 months'));

几天:

$after_3_days = $current_date->add(\DateInterval::createFromDateString('+3 days'));

重要提示:

DateTime类的方法add()修改对象值,因此在对DateTime对象调用add()之后,它会返回新的日期对象,并自行修改该对象。

答案 13 :(得分:0)

$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));

将输出2(2月)。也将在其他月份工作。

答案 14 :(得分:0)

我需要得到“去年这个月”的日期,并且当这个月是闰年的2月时,它会很快变得不愉快。但是,我相信这是有效的......: - /诀窍似乎是在每个月的第一天根据你的变化。

NSManagedObject

答案 15 :(得分:0)

如果使用strtotime(),请使用$date = strtotime('first day of +1 month');

答案 16 :(得分:0)

DateTime类的扩展,解决了添加或减去月数的问题

https://gist.github.com/66Ton99/60571ee49bf1906aaa1c

答案 17 :(得分:0)

如果您只是想避免跳过一个月,您可以执行类似的操作以获取日期并在下个月运行循环,将日期减一,并重新检查直到$ starting_calculated为有效字符串的有效日期strtotime(即mysql datetime或“now”)。这可以在1分钟到午夜之间找到月末,而不是跳过月份。

    $start_dt = $starting_calculated;

    $next_month = date("m",strtotime("+1 month",strtotime($start_dt)));
    $next_month_year = date("Y",strtotime("+1 month",strtotime($start_dt)));

    $date_of_month = date("d",$starting_calculated);

    if($date_of_month>28){
        $check_date = false;
        while(!$check_date){
            $check_date = checkdate($next_month,$date_of_month,$next_month_year);
            $date_of_month--;
        }
        $date_of_month++;
        $next_d = $date_of_month;
    }else{
        $next_d = "d";
    }
    $end_dt = date("Y-m-$next_d 23:59:59",strtotime("+1 month"));

答案 18 :(得分:-1)

$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');

答案 19 :(得分:-2)

     $date = date('Y-m-d', strtotime("+1 month"));
     echo $date;