修改加减月份

我一直在与 DateTime class工作了很多,最近遇到了什么,我认为是一个错误时,增加了几个月。经过一些研究,它似乎不是一个错误,而是工作的目的。根据发现的文件 给你:

示例 # 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个月将工作的预期,而不是预期?

125486 次浏览

为什么它不是一个错误:

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

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

  2. 第二个月(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";
?>

This script will correctly output February. The following things happen when PHP processes this first day of next month stanza:

  1. next month把月份数字(原来是1)增加了一个,这使得日期变成了2010-02-31。

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

如果你只是想避免跳过一个月,你可以执行类似这样的操作来获取日期,然后在下个月运行一个循环,把日期减少一个,并重新检查直到一个有效的日期,其中 $start _ Calcul是 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"));

这可能有用:

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
     $date = date('Y-m-d', strtotime("+1 month"));
echo $date;

I agree with the sentiment of the OP that this is counter-intuitive and frustrating, but so is determining what +1 month means in the scenarios where this occurs. Consider these examples:

你从2015-01-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-01-30开始。+1 month的直观日期是多少?一种解读是“下个月第30天或最近的可用日期”,它将返回:

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

这将是罚款,除非当我们的用户得到两个时事通讯在同一天。让我们假设这是一个供应方面的问题,而不是需求方面的问题。我们并不担心用户会因为在同一天收到两份新闻通讯而烦恼,而是担心我们的邮件服务器无法承担发送两倍多的新闻通讯的带宽。考虑到这一点,我们回到对“ + 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周代替。)

我创建了一个返回 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');
}
}

我对这个问题的解决方案是:

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

解决月份加减问题的 DateTime 类的扩展

Https://gist.github.com/66ton99/60571ee49bf1906aaa1c

我用下面的代码找到了一个更简短的方法:

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

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

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

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

如果你想使用的日期时间类也很好,但这是一样容易

Here is an implementation of an improved version of Juhana 的回答 in a related question:

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

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

下面是另一个完全使用 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

我需要为“去年的这个月”定一个日期,这个月是闰年的二月,它很快就变得令人不快。然而,我相信这个方法是有效的... :-/诀窍似乎是把你的变化建立在每个月的第一天。

$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);
$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');

This is an improved version of Kasihasi's answer in a related question. This will correctly add or subtract an arbitrary number of months to a date.

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'
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));

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

$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()之后返回新的日期对象并自己修改对象。

接受的答案已经解释了为什么这不是一个但是,其他一些答案提出了一个简洁的解决方案与 PHP 表达式,如 first day of the +2 months。这些表达式的问题在于它们不是自动完成的。

The solution is quite simple though. First, you should find useful abstractions that reflect your problem space. In this case, it's an ISO8601DateTime. Second, there should be multiple implementations that can bring a desired textual representation. For example, Today, Tomorrow, The first day of this month, Future -- all represent a specific implementation of ISO8601DateTime concept.

因此,在您的例子中,您需要的实现是 TheFirstDayOfNMonthsLater。只要查看任何 IDE 中的子类列表就很容易找到。密码是这样的:

$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

一个月的最后几天也是如此。