PHPdate时间::修改加减的月份
我一直在使用DateTime class
,最近遇到了我认为是一个错误添加几个月。 经过一番研究,似乎这不是一个错误,而是按预期工作。 根据这里find的文件:
Example#2当加或减月份的时候要小心
<?php $date = new DateTime('2000-12-31'); $date->modify('+1 month'); echo $date->format('Ym-d') . "\n"; $date->modify('+1 month'); echo $date->format('Ym-d') . "\n"; ?>
The above example will output: 2001-01-31 2001-03-03
任何人都可以certificate为什么这不被视为一个错误?
此外,是否有任何人有任何优雅的解决scheme来纠正这个问题,并使之成为一个月,如预期的那样工作,
为什么这不是一个错误:
目前的行为是正确的。 以下情况发生在内部:
-
+1 month
将月份数(原为1)加1。 这使得date2010-02-31
。 -
第二个月(二月)在2010年只有28天,所以PHP自动纠正这个只是继续从2月1日算起。 然后你3月3日结束。
如何得到你想要的:
要得到你想要的是:手动检查下个月。 然后添加下个月的天数。
我希望你能自己编码。 我只是在做什么。
PHP 5.3的方式:
为了获得正确的行为,你可以使用其中一个PHP 5.3的新function,介绍相对时间节的first day of
。 本节可与next month
, fifth month
或+8 months
结合使用+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
会发生以下情况:
-
next month
将月份数(原为1)加1。 这使得date2010-02-31。 -
first day of
将日数设置为1
,导致date为2010-02-01。
这可能是有用的:
echo Date("Ymd", strtotime("2013-01-01 +1 Month -1 Day")); // 2013-01-31 echo Date("Ymd", strtotime("2013-02-01 +1 Month -1 Day")); // 2013-02-28 echo Date("Ymd", strtotime("2013-03-01 +1 Month -1 Day")); // 2013-03-31 echo Date("Ymd", strtotime("2013-04-01 +1 Month -1 Day")); // 2013-04-30 echo Date("Ymd", strtotime("2013-05-01 +1 Month -1 Day")); // 2013-05-31 echo Date("Ymd", strtotime("2013-06-01 +1 Month -1 Day")); // 2013-06-30 echo Date("Ymd", strtotime("2013-07-01 +1 Month -1 Day")); // 2013-07-31 echo Date("Ymd", strtotime("2013-08-01 +1 Month -1 Day")); // 2013-08-31 echo Date("Ymd", strtotime("2013-09-01 +1 Month -1 Day")); // 2013-09-30 echo Date("Ymd", strtotime("2013-10-01 +1 Month -1 Day")); // 2013-10-31 echo Date("Ymd", strtotime("2013-11-01 +1 Month -1 Day")); // 2013-11-30 echo Date("Ymd", strtotime("2013-12-01 +1 Month -1 Day")); // 2013-12-31
我的问题的解决scheme:
$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' ); } }
我做了一个函数,返回一个DateInterval来确保添加一个月显示下个月,并删除后的日子。
$time = new DateTime('2014-01-31'); echo $time->format('dmY H:i') . '<br/>'; $time->add( add_months(1, $time)); echo $time->format('dmY H:i') . '<br/>'; function add_months( $months, \DateTime $object ) { $next = new DateTime($object->format('dmY 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'); } }
下面是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
,假定您正在处理从特定date(例如,第31次)开始的定期月度期(例如订阅)。 它总是需要$createdDate
这么晚“重复date”将不会转移到较低的价值,因为它们被推动通过较小的月份(例如,所以所有29日,30日或31日的复发date将不会终于卡住了28日经过非闰年2月)。
这里是一些驱动程序代码来testingalgorithm:
$createdDate = new DateTime("2015-03-31"); echo "created date = " . $createdDate->format("Ymd") . PHP_EOL; $next = sameDateNextMonth($createdDate, $createdDate); echo " next date = " . $next->format("Ymd") . PHP_EOL; foreach(range(1, 12) as $i) { $next = sameDateNextMonth($createdDate, $next); echo " next date = " . $next->format("Ymd") . 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
我用下面的代码find了一个更短的方法:
$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('Ymd H:i:s');
结合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; }
如果你只是想避免跳过一个月,你可以执行这样的事情来获取date,并在下个月运行一个循环,减less一个date,并重新检查,直到有效的date,其中$ starting_calculated是一个有效的strtotimestring(即MySQLdate时间或“现在”)。 这发现在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("Ym-$next_d 23:59:59",strtotime("+1 month"));
我同意OP的观点,认为这是违反直觉的和令人沮丧的,但是确定在这种情况发生的场景中+1 month
含义也是如此。 考虑这些例子:
您从2015-01-31开始,并希望添加一个月6次以获得发送电子邮件简报的计划周期。 考虑到OP最初的期望,这将返回:
- 2015年1月31日
- 2015年2月28日
- 2015年3月31日
- 二零一五年四月三十零日
- 2015年5月31日
- 2015年6月30日
马上,请注意,我们期望+1 month
是指+1 month
last day of month
或者每个迭代添加1个月,但总是参考起点。 我们可以把它理解为“下个月的第31天或在那个月内的最后一天”,而不是把它解释为“月的最后一天”。 这意味着我们从4月30日跳到5月31日,而不是5月30日。 请注意,这不是因为它是“月份的最后一天”,而是因为我们希望“距离开始月份的date最近”。
因此,假设我们的一个用户订阅另一个通讯,从2015-01-30开始。 什么是+1 month
的直观date? 一种解释是“下个月的第30天或最近的可用的”,它将返回:
- 二零一五年一月三十零日
- 2015年2月28日
- 手绘POP练习
- 二零一五年四月三十零日
- 2015年5月30日
- 2015年6月30日
除非我们的用户在同一天同时收到两个通讯,否则这将是正常的。 让我们假设这是一个供应方面的问题,而不是需求方我们并不担心用户会在同一天得到两个通讯,而是我们的邮件服务器无法承受带宽两次发送的烦恼许多通讯。 考虑到这一点,我们回到“+1月”的另一种解释是“每个月的第二天发送”,这将返回:
- 二零一五年一月三十零日
- 2015年2月27日
- 手绘POP练习
- 2015年4月29日
- 2015年5月30日
- 2015-07-02
现在我们已经避免了与第一组的重叠,但是我们最终也会在4月和6月29日结束,这肯定与我们原来的直觉相符,即+1 month
应该返回m/$d/Y
或者有吸引力和简单的m/30/Y
在所有可能的月份。 那么现在让我们考虑使用这两个date的+1 month
的第三种解释:
1月31日
- 2015年1月31日
- 2015年3月3日
- 2015年3月31日
- 2015年5月1日
- 2015年5月31日
- 2015年7月1日
1月30日
- 二零一五年一月三十零日
- 2015年3月2日
- 手绘POP练习
- 二零一五年四月三十零日
- 2015年5月30日
- 2015年6月30日
以上有一些问题。 2月份被跳过了,这可能是一个供应端问题(比如说如果每月带宽分配,2月份浪费,3月份增加一倍)和需求端(用户觉得2月份被欺骗,并感觉到额外的3月份作为尝试纠正错误)。 另一方面,请注意,这两个date集:
- 从不重叠
- 总是在同一个date当月有date(所以1月30日看起来很干净)
- 都是在3天内(大多数情况下是1天),可能被认为是“正确”的date。
- 他们的继任者和前任都至less有28天(阴历月),分布非常均匀。
考虑到最后两组,如果它不在下一个月的实际月份中回滚(如此回滚到第一组中的2月28日和4月30日),并且不会失去任何睡眠偶尔会出现“月份的最后一天”与“每月的最后一天”之间的重叠和分歧。 但是期待图书馆在“最漂亮/最自然”,“02/31和其他月份的math解释溢出”和“相对于上个月或上个月的相关”之间进行select,总是会以某个人的期望得不到满足有些时间表需要调整“错误的”date,以避免“错误的”解释引入的现实世界的问题。
再次,虽然我也希望+1 month
能够返回一个实际上是在+1 month
的date,但是它并不像直觉那么简单,并且给出了select,math与Web开发人员的期望相比可能是安全的select。
这是一个可选的解决scheme,但仍然像任何笨重,但我认为有很好的结果:
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("Ymd") . PHP_EOL); }
这不是最优的,但其基本逻辑是:如果在除了预期的下个月之外的date添加1个月的结果,则取消该date并添加4周。 以下是两个testingdate的结果:
1月31日
- 2015年1月31日
- 2015年2月28日
- 2015年3月31日
- 2015年4月28日
- 2015年5月31日
- 2015年6月28日
1月30日
- 二零一五年一月三十零日
- 2015年2月27日
- 手绘POP练习
- 二零一五年四月三十零日
- 2015年5月30日
- 2015年6月30日
(我的代码是一团糟,不会在多年的情况下工作,我欢迎任何人用更优雅的代码重写解决scheme,只要基本的前提保持不变,即如果+1个月返回一个时髦的date,使用+4周。)
DateTime类的扩展,解决了几个月的增加或减less问题
如果使用strtotime()
只是使用$date = strtotime('first day of +1 month');
你可以用date()和strtotime()来实现。 例如添加1个月到今天的date:
date(“Ymd”,strtotime(“+ 1 month”,time()));
如果你想使用date时间类,那也可以,但这同样简单。 更多细节在这里
这里是另一个完全使用DateTime方法的紧凑解决scheme,在原地修改对象而不创build克隆。
$dt = new DateTime('2012-01-31'); echo $dt->format('Ym-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('Ym-d'), PHP_EOL;
它输出:
2012-01-31 2012-02-29
我需要得到一个“去年这个月”的date,并且在这个月是闰年的二月份的时候会很快变得不愉快。 不过,我相信这是有效的。: – /这个技巧似乎是在一个月的第一天做出改变。
$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');
这是一个相关问题中Kasihasi答案的改进版本。 这将正确地添加或减去任意数量的月份到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('Yn-j', $year.'-'.$month.'-1'); $d2->modify('last day of'); }else { $d2 = DateTime::createFromFormat('Yn-d', $year.'-'.$month.'-'.$day); } return $d2->format('Ym-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
(二月)。 也会在其他几个月工作。
$date = date('Ym-d', strtotime("+1 month")); echo $date;