代码之家  ›  专栏  ›  技术社区  ›  tplaner

PHP DateTime::修改加减月份

  •  88
  • tplaner  · 技术社区  · 15 年前

    我一直在和 DateTime class here :

    减去月份

    <?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
    

    有人能证明为什么这不是一个bug吗?

    19 回复  |  直到 15 年前
        1
  •  112
  •   rink.attendant.6    12 年前

    为什么不是虫子:

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

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

    2. 第二个月(2月)在2010年只有28天,所以PHP自动纠正了这一点,从2月1日开始继续计算天数,然后在3月3日结束。

    要得到你想要的是:手动检查下个月。然后加上下个月已经过的天数。

    我希望你能自己编写代码。我只是想知道该怎么办。

    PHP 5.3方式:

    first day of next month , fifth month +8 months 转到指定月份的第一天。而不是 根据您所做的,您可以使用以下代码获得下个月的第一天,如下所示:

    <?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. 下个月

    2. 开学第一天 1 ,最终日期为2010-02-01。

        2
  •  14
  •   Rudiger W.    10 年前

    这是另一个完全使用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
    
        3
  •  10
  •   DACrosby    12 年前

    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
    
        4
  •  6
  •   bernland    12 年前

    我的解决方案是:

    $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' );
        }
    }
    
        5
  •  4
  •   Community Mohan Dere    5 年前

    我同意OP的观点,即这是违反直觉和令人沮丧的,但决定什么也是如此 +1 month

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

    • 2015-01-31
    • 2015-03-31
    • 2015-05-31
    • 2015-06-30

    请注意,我们正在期待 +1个月 意思 last day of month 或者,每个迭代增加1个月,但始终以起始点为参考。与其将其解释为“本月的最后一天”,我们可以将其理解为“下个月的第31天或该月内的最后一天”。这意味着我们从4月30日跳到5月31日,而不是5月30日。请注意,这并不是因为它是“本月的最后一天”,而是因为我们希望“最接近开始月份的日期”

    所以假设我们的一个用户订阅了另一个新闻稿,从2015年1月30日开始。什么是直观的日期 +1个月 ? 一种解释是“下个月30日或最接近可用日期”,返回:

    • 2015-01-30
    • 2015-02-28
    • 2015-03-30
    • 2015-04-30

    这将是好的,除非我们的用户在同一天得到两个时事通讯。让我们假设这是一个供应方而不是需求方的问题,我们不担心用户会因为在同一天收到两份时事通讯而烦恼,而是我们的邮件服务器无法负担发送两倍多时事通讯的带宽。考虑到这一点,我们将“+1个月”的另一种解释返回为“在每月的第二天到最后一天发送”,返回:

    • 2015-01-30
    • 2015-02-27
    • 2015-03-30

    现在我们已经避免了与第一盘的任何重叠,但我们也以4月和6月29日结束,这当然符合我们最初的直觉 +1个月 只是应该回来 m/$d/Y m/30/Y 所有可能的月份。现在让我们来考虑第三种解释

    • 2015-01-31
    • 2015-05-01
    • 2015-05-31
    • 2015-07-01

    1月30日

    • 2015-03-02
    • 2015-03-30
    • 2015-06-30

    • 总是在同一个日期,当那个月有日期(所以1月30日设置看起来相当干净)
    • 都是在3天内(在大多数情况下是1天)什么可能被认为是“正确的”日期。
    • 都是从他们的继承人和前任至少28天(农历月),所以分布非常均匀。

    考虑到最后两组,如果其中一个日期不在下一个月的实际日期范围内(因此,在第一组中回滚到2月28日和4月30日),那么简单地回滚其中一个日期并不困难,并且不会因为“月的最后一天”与“月的第二天到最后一天”模式的偶尔重叠和偏离而失眠。但希望图书馆在“最美/最自然”、“02/31和其他月份的数学解释”之间做出选择,而“相对月初或上月”总是会以某人的期望得不到满足而结束,一些日程安排需要调整“错误”的日期,以避免“错误”解释带来的现实问题。

    所以再说一遍,虽然我也希望 +1个月 要返回一个实际是在下个月的日期,这并不像直觉那么简单,在给定的选择下,用数学来超越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周。以下是两个测试日期的结果:

    1月31日

    • 2015-02-28
    • 2015-03-31
    • 2015-04-28
    • 2015-05-31

    1月30日

    • 2015-01-30
    • 2015-02-27
    • 2015-03-30
    • 2015-04-30
    • 2015-05-30

    (我的代码乱七八糟,在多年的情况下无法工作。我欢迎任何人用更优雅的代码重写解决方案,只要底层前提保持不变,即如果+1个月返回一个时髦的日期,则使用+4周代替。)

        6
  •  4
  •   A-R    12 年前

    我创建了一个函数,返回一个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');
        }
    }
    
        7
  •  4
  •   patrickzzz Olivier de Jonge    10 年前

    结合沙米托马尔的回答,这可能是为了“安全地”增加几个月:

    /**
     * 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;
    }
    
        8
  •  2
  •   Pedro Lobito    11 年前

    我使用以下代码找到了一个较短的方法:

                       $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');
    
        9
  •  1
  •   Community Mohan Dere    9 年前

    下面是 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创建日期

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

    $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
    
        10
  •  1
  •   Hải Phong    9 年前

    这是的改进版本 Kasihasi's answer 在一个相关的问题上。这将正确地增加或减少一个日期的任意月数。

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

    将输出:

    '2015-02-28'
    
        11
  •  1
  •   Vadim Samokhin    6 年前

    公认的答案已经解释了为什么这不是一个but,而其他一些答案提供了一个使用php表达式的简洁解决方案,如 first day of the +2 months

    不过,解决办法很简单。首先,您应该找到反映您的问题空间的有用抽象。在这种情况下,这是一个 ISO8601DateTime . 其次,应该有多个实现可以带来所需的文本表示。例如, Today Tomorrow , The first day of this month , Future ISO8601日期 概念。

    因此,在您的情况下,您需要的实现是 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 .

        12
  •  0
  •   user1590391    13 年前

    如果您只是想避免跳过一个月,您可以执行类似这样的操作来获取日期,并在下个月运行一个循环,将日期减少1,然后重新检查直到一个有效的日期,其中$start\u calculated是strotime的有效字符串(即mysql datetime或“now”)。这会发现一个月的最后一分钟到午夜,而不是跳过一个月。

        $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"));
    
        13
  •  0
  •   66Ton99    12 年前

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

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

        14
  •  0
  •   Primoz Rome    11 年前

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

        15
  •  0
  •   Just Plain High    10 年前

    我需要为“去年的这个月”定个日期,当这个月是闰年的二月时,它很快就变得不愉快了。不过,我相信这是可行的。。。:-/诀窍似乎是把你的零钱放在每月的第一天。

    $this_month_last_year_end = new \DateTime();
    $this_month_last_year_end->modify('first day of this month');
    $this_month_last_year_end->modify('-1 year');
    $this_month_last_year_end->modify('last day of this month');
    $this_month_last_year_end->setTime(23, 59, 59);
    
        16
  •  0
  •   Kevin Kopf    9 年前
    $ds = new DateTime();
    $ds->modify('+1 month');
    $ds->modify('first day of this month');
    
        17
  •  0
  •   galki    8 年前
    $month = 1; $year = 2017;
    echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));
    

    将输出 2

        18
  •  0
  •   MiharbKH    7 年前
    $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'));
    

    重要提示:

    add() 在调用 添加()

        19
  •  0
  •   Skatox    6 年前

    实际上,也可以使用date()和strotime()来完成。例如,要将1个月添加到今天的日期:

    date("Y-m-d",strtotime("+1 month",time()));
    

    如果你想使用datetime类,那也很好,但是这也很简单。 more details here

        20
  •  -2
  •   Mohammed F. Ghazo    13 年前
         $date = date('Y-m-d', strtotime("+1 month"));
         echo $date;