PHP DateTime::修改加减月份
我一直在和很多人合作,最近在添加月份时遇到了我认为是个bug的问题。经过一点研究,它似乎不是一个bug,而是按照预期工作。根据发现的文件: 示例#2在添加或 减去月份 有人能证明为什么这不是一个bug吗 此外,是否有人有任何优雅的解决方案来纠正该问题,并使其在一个月后按预期而不是按预期工作?为什么它不是一个bug: 当前的行为是正确的。以下情况在内部发生:PHP DateTime::修改加减月份,php,datetime,date,Php,Datetime,Date,我一直在和很多人合作,最近在添加月份时遇到了我认为是个bug的问题。经过一点研究,它似乎不是一个bug,而是按照预期工作。根据发现的文件: 示例#2在添加或 减去月份 有人能证明为什么这不是一个bug吗 此外,是否有人有任何优雅的解决方案来纠正该问题,并使其在一个月后按预期而不是按预期工作?为什么它不是一个bug: 当前的行为是正确的。以下情况在内部发生: +1个月将月数(最初为1)增加1。这使得日期2010-02-31 第二个月(2月)在2010年只有28天,所以PHP通过继续计算2月1日起的
+1个月
将月数(最初为1)增加1。这使得日期2010-02-31
第一天的
。此节可与下个月
、第五个月
或+8个月
结合使用,以转到指定月份的第一天。您可以使用此代码获得下个月的第一天,而不是从您正在做的事情中获得+1个月的时间,如下所示:
<?php
$d = new DateTime( '2010-01-31' );
$d->modify( 'first day of next month' );
echo $d->format( 'F' ), "\n";
?>
此脚本将正确输出2月份的。当PHP处理下个月的第一天时,会发生以下事情:
next month
将月份数(最初为1)增加1。这使得日期为2010-02-31
的第一天
将天数设置为1
,从而产生日期2010-02-01
如果您只是想避免跳过一个月,您可以执行类似的操作来获取日期,并在下一个月运行循环,将日期减少一个,然后重新检查直到一个有效日期,其中$starting_COMPUTED是strotime的有效字符串(即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"));
$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);
这可能有用:
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
我同意OP的观点,即这是违反直觉和令人沮丧的,但在发生这种情况的场景中,确定+1个月
意味着什么也是如此。考虑这些例子:
您从2015-01-31开始,希望增加一个月6次,以获得发送电子邮件时事通讯的计划周期。考虑到OP最初的期望,这将返回:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
请立即注意,我们希望+1个月
是指月的最后一天
,或者,每次迭代增加1个月,但始终参考起点。我们可以将其理解为“下个月的第31天或该月内的最后一天”,而不是将其解释为“本月的最后一天”。这意味着我们从4月30日跳到5月31日,而不是5月30日。请注意,这不是因为它是“月的最后一天”,而是因为我们希望“最接近开始月份的日期”
因此,假设我们的一个用户订阅了另一份时事通讯,从2015-01-30开始。+1个月的直观日期是什么?一种解释是“下个月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个月
只需返回m/$d/Y
,或者在所有可能的月份返回有吸引力且简单的m/30/Y
。所以现在让我们考虑使用两个日期:<代码> + 1个月< /代码>的第三个解释:
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
以上有一些问题。二月被跳过,这可能是一个问题,无论是供应端(比如说,如果有一个月的带宽分配,二月被浪费,三月被加倍)还是需求端(用户觉得二月被骗了,认为额外的三月是为了纠正错误)。另一方面,请注意两个日期集:
- 永不重叠
- 当该月有日期时,总是在同一日期(因此1月30日的套装看起来很干净)
- 在“正确”日期的3天内(大多数情况下为1天)
- 他们的继任者和前任至少有28天(一个农历月),所以分布非常均匀
考虑到最后两组数据,如果其中一个数据超出了预期范围,那么简单地将其回滚并不困难
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;
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);
}
$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 = 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');
date("Y-m-d",strtotime("+1 month",time()));
<?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 = 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
/**
* 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;
}
$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');
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));
$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'));
$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