PHP DateTime ::修改加减月份


101

我已经与进行了很多合作DateTime class,最近在添加几个月时遇到了我认为是错误的错误。经过一些研究,看来它不是错误,而是按预期工作。根据发现的文件此处

Example#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个月能按预期而不是预期的方式工作?


您希望“ 2001-01-31”加上1个月会是什么?...“ 2001-02-28”?“ 2001-03-01”?
Artefacto

57
我个人希望它是2001-02-28。
tplaner


2
是的,这是一个很烦人的怪癖。您已经阅读了细则,以确认P1M为31天。不太了解为什么人们会一直以“正确”的行为辩护。
Indivision Dev

似乎流行的观点是,逻辑应该四舍五入(至2/28),而PHP则四舍五入(至3/1)...虽然我更喜欢PHP的方式,但是Microsoft的Excel四舍五入,使Web开发人员无法与电子表格用户竞争...
Dave Heq

Answers:


106

为什么不是错误:

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

  1. +1 month将月份数(最初为1)增加1。这就是约会2010-02-31

  2. 第二个月(二月)在2010年只有28天,因此PHP会通过自动计算从2月1日开始的天数来自动更正此问题。然后,您将在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
那么,您要说的是,它实际上会增加1个月,而完全忽略了日子吗?因此,我假设如果在a年添加+1年,可能会遇到类似的问题?
tplaner 2010年

@evolve,是的,它的文学作品增加了1个月。
shamittomar

13
我认为,如果您在添加后减去1个月,则最终会得到完全不同的日期。这似乎很不直观。
丹·布雷恩

2
关于在PHP 5.3中使用新节的出色示例,您可以在其中使用第一天,最后一天,本月,下个月和上个月。
Kim Stacks 2014年

6
恕我直言,这是一个错误。一个严重的错误。如果我想增加31天,那么我要增加31天。II要添加一个月,应添加一个月,而不是31天。
low_rents '16

12

这是另一种完全使用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

1
谢谢。到目前为止,这里提供了最好的解决方案。您也可以将代码缩短为$dt->modify()->modify()。效果也一样。
Alph.Dev

10

这可能有用:

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
这不是一般的解决方案,因为这仅适用于某些输入,例如每月的1号。例如,在1月30日这样做会导致痛苦。
詹斯·罗兰

或者你可以做$dateTime->modify('first day of next month')->modify('-1day')
安东尼

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
“克隆”命令是我的变量分配问题的解决方案。这次真是万分感谢。
Steph Rose

4

我同意OP的观点,即这是违反直觉和令人沮丧的,但是确定什么 +1 month发生这种情况的情况下。考虑以下示例:

您从2015年1月31日开始,想要添加一个月6次以获取发送电子邮件新闻通讯的计划周期。考虑到OP的最初期望,这将返回:

  • 2015-01-31
  • 2015-02-28
  • 2015-03-31
  • 2015-04-30
  • 2015-05-31
  • 2015-06-30

马上,请注意,我们期望+1 month平均last day of month每次迭代要增加1个月,或者总是要参考起点。与其将其解释为“月份的最后一天”,不如将其理解为“下个月的31日或该月的最后一个可用日期”。这意味着我们从4月30日跳到5月31日,而不是5月30日。请注意,这不是因为它是“月份的最后一天”,而是因为我们想要“最接近开始月份的日期”。

因此,假设我们的一个用户订阅了另一封新闻通讯,该通讯将从2015年1月30日开始。直观的日期是+1 month什么?一种解释是“下个月的30日或最接近的时间”,该结果将返回:

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

除非我们的用户在同一天收到两个新闻通讯,否则就可以了。让我们假设这是一个供应方问题,而不是需求方问题。我们不担心用户会在同一天收到2个新闻通讯而感到烦恼,但是我们的邮件服务器负担不起两次发送的带宽。许多新闻通讯。考虑到这一点,我们返回“ +1个月”的另一种解释为“在每个月的第二天到最后一天发送”,它将返回:

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

现在我们避免了与第一组的任何重叠,但我们也将在4月和6月29日结束,这肯定与我们的原始直觉相符,这些直觉应该+1 month简单地返回,m/$d/Y或者在m/30/Y所有可能的月份中都具有吸引力和简单性。现在,让我们考虑+1 month使用两个日期的第三种解释:

1月31日

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

1月30日

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

上面有一些问题。跳过2月,这可能会成为供应端(例如,如果每月分配带宽,而2月浪费,3月成倍增长)和需求端(用户感到2月被骗并感觉到额外的3月)问题作为纠正错误的尝试)。另一方面,请注意两个日期集:

  • 永不重叠
  • 当那个月有日期时,它们总是在同一日期(因此1月30日的设置看起来很干净)
  • 都在可能被视为“正确”日期的3天内(大多数情况下为1天)。
  • 距离其继承人和前任至少28天(农历月份),因此分布非常均匀。

给定最后两套,如果它不在下个月的实际月份内,那么简单地回滚一个日期就不会很困难(因此回滚到第一套的2月28日和4月30日)并且不会在从“每月的最后一天”与“每月的第二天到最后一天”的模式偶尔出现重叠和背离。但是,期望图书馆在“最漂亮/最自然”,“ 02/31和其他月份溢出的数学解释”和“相对于本月初或上个月的数学解释”之间进行选择,总会因未达到某人的期望而结束,并且某些时间表需要调整“错误的”日期,以避免“错误的”解释引入的实际问题。

再说一次,虽然我也希望+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周。以下是两个测试日期的结果:

1月31日

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

1月30日

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

(我的代码很乱,无法在多年使用的情况下使用。我欢迎任何人使用更简洁的代码重写该解决方案,只要保持基本前提不变即可,即如果+1个月返回一个时髦的日期,请使用+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');
    }
}

4

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

非常感谢!!
壁虎

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

1

这是在相关问题中Juhana答案的改进版本的实现:

<?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很晚所以“递归发生”日期不会转移到较低的值,因为它们会被推迟到价值较低的月份(例如,因此所有第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

1

这是Kasihasi在相关问题中的答案的改进版本。这将为日期正确添加或减去任意月份。

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'

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


0

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


0

我需要为“去年的这个月”指定一个日期,当这个月是a年的2月时,它很快变得不愉快。但是,我相信这是可行的...:-/诀窍似​​乎是在每月的第一天进行更改。

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


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

将输出2(2月)。也会工作其他几个月。


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

重要:

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


0

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

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

如果您也想使用datetime类,那也很容易。这里有更多细节


0

可接受的答案已经解释了为什么这不是一个问题,而其他一些答案则用php表达式(如)构成了一个简洁的解决方案first day of the +2 months。这些表达式的问题在于它们不会自动完成。

解决方案虽然很简单。首先,您应该找到反映问题空间的有用抽象。在这种情况下,它是一个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

一个月的最后几天也是一样。有关此方法的更多示例,请阅读this


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.