我一直在使用DateTime class
进行了大量工作,最近遇到了我认为添加月份时的错误。经过一些研究后,似乎它不是一个bug,而是按预期工作。根据发现的文件here:
示例#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个月将按预期工作而不是按预期工作?
答案 0 :(得分:97)
目前的行为是正确的。内部发生以下情况:
+1 month
将月份数(原来为1)增加1。这使得日期为2010-02-31
。
第二个月(二月)在2010年只有28天,因此PHP会自动更正这一点,只需继续计算从2月1日起的天数。然后你在3月3日结束。
通过以下方式获得您想要的:手动检查下个月。然后添加下个月的天数。
我希望你能亲自编码。我只是在做什么。
要获得正确的行为,您可以使用PHP 5.3的新功能之一来引入相对时间节first day of
。此节可与next month
,fifth 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
节时会发生以下情况:
next month
将月份数(原来为1)增加1。这使得日期为2010-02-31。
first day of
将日期编号设置为1
,结果日期为2010-02-01。
答案 1 :(得分:11)
这可能很有用:
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 :(得分: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 :(得分:5)
这是另一个完全使用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
答案 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');
}
}
答案 5 :(得分:3)
我同意OP的观点,认为这是违反直觉和令人沮丧的,但确定+1 month
在出现这种情况的情况下的含义也是如此。请考虑以下示例:
您从2015-01-31开始,并希望添加一个月6次以获得发送电子邮件简报的计划周期。考虑到OP最初的期望,这将会回归:
立即注意,我们期望+1 month
表示last day of month
,或者,每次迭代添加1个月,但总是参考起点。而不是将其解释为&#34;月的最后一天&#34;我们可以将它读作#34;下个月的第31天或最后一个月内可用的#34;这意味着我们从4月30日跳到5月31日而不是5月30日。请注意,这不是因为它是&#34;月份的最后一天&#34;但是因为我们希望在开始月份之前最接近可用。&#34;
因此,假设我们的一位用户订阅了另一份时事通讯,将于2015-01-30开始。 +1 month
的直观日期是什么?一种解释将是下个月的第30天或最接近的可用&#34;将返回:
除非我们的用户在同一天收到两份简报,否则这样会很好。让我们假设这是供应方面的问题,而不是需求方面我们并不担心用户会在同一天收到2条新闻通讯而烦恼,而是我们的邮件服务器可以“为发送两倍的新闻通讯提供带宽。考虑到这一点,我们回到&#34; +1个月&#34; as&#34;发送每个月的第二天到第二天&#34;将返回:
现在我们已经避免与第一组有任何重叠,但我们也最终得到了4月和6月29日,这肯定符合+1 month
应该返回m/$d/Y
的原始直觉或所有可能的月份都很有吸引力且简单m/30/Y
。现在让我们考虑使用两个日期对+1 month
的第三种解释:
以上有一些问题。 2月被跳过,这可能是供应端的一个问题(比如说每月带宽分配,2月份浪费,3月份增加一倍)和需求结束(用户感到2月被骗,并认为额外的3月份试图纠正错误)。另一方面,请注意两个日期集:
鉴于最后两组,如果它超出实际的下个月(因此在第一组中回滚到2月28日和4月30日)并且不会丢失任何日期,则简单地回滚其中一个日期并不困难。睡在偶尔的重叠和分歧上,从第一天的最后一天开始#34; vs&#34;第二个月到最后一天&#34;图案。但希望图书馆能够选择最相当/最自然的&#34;,#34; 02/31的数学解释和其他月份溢出&#34;和#34;相对于月末或月末& #34;总是会因为某人的期望没有得到满足而有些时间表需要调整错误的&#34;日期,以避免现实世界的问题,&#34;错误&#34;解释介绍。
所以再次,虽然我也希望+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个月返回一个时髦的约会,改为使用+4周。)
答案 6 :(得分:3)
结合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;
}
答案 7 :(得分: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');
答案 8 :(得分:1)
已被接受的答案已经解释了为什么这不是一个问题,而其他一些答案则为first day of the +2 months
这样的php表达式提供了一个简洁的解决方案。这些表达式的问题在于它们没有自动完成。
该解决方案非常简单。首先,您应该找到反映问题空间的有用抽象。在这种情况下,它是ISO8601DateTime
。其次,应该有多种实现方式可以带来所需的文本表示形式。例如,Today
,Tomorrow
,The first day of this month
,Future
都代表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
与一个月的最后几天相同。有关此方法的更多示例,请read this。
答案 9 :(得分:1)
这是相关问题中Kasihasi's answer的改进版本。这将正确地添加或减去任意数月的日期。
let sqlDate = post.age
var hoursSincePost = sqlDate.hoursAfterDate(Date())
hoursSincePost = abs(hoursSincePost)
var daysSincePost = sqlDate.daysAfterDate(Date())
daysSincePost = abs(daysSincePost)
if hoursSincePost < 24 {
age.text = String(hoursSincePost) + " hours ago"
} else if daysSincePost < 7 {
age.text = String(daysSincePost) + " days ago"
} else {
age.text = sqlDate.toString(.custom("MMM dd yyyy"))
}
例如:
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')
答案 10 :(得分:1)
以下是相关问题中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
这么晚才会再次发生在&#34;日期不会转移到较低的价值,因为它们被推进到较低价值的月份(例如,因此所有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
答案 11 :(得分:0)
你也可以用date()和strtotime()来实现。例如,在今天的日期添加1个月:
日期(“Y-m-d”,strtotime(“+ 1个月”,时间()));
如果您想要使用日期时间类,那也很简单。 more details here
答案 12 :(得分: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'));
重要提示:
DateTime类的方法add()
修改对象值,因此在对DateTime对象调用add()
之后,它会返回新的日期对象,并自行修改该对象。
答案 13 :(得分:0)
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));
将输出2
(2月)。也将在其他月份工作。
答案 14 :(得分:0)
我需要得到“去年这个月”的日期,并且当这个月是闰年的2月时,它会很快变得不愉快。但是,我相信这是有效的......: - /诀窍似乎是在每个月的第一天根据你的变化。
NSManagedObject
答案 15 :(得分:0)
如果使用strtotime()
,请使用$date = strtotime('first day of +1 month');
答案 16 :(得分:0)
DateTime类的扩展,解决了添加或减去月数的问题
答案 17 :(得分: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"));
答案 18 :(得分:-1)
$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');
答案 19 :(得分:-2)
$date = date('Y-m-d', strtotime("+1 month"));
echo $date;