PHP DateTime::修改加减月份
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3602405/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
PHP DateTime::modify adding and subtracting months
提问by tplaner
I've been working a lot with the DateTime class
and recently ran into what I thought was a bug when adding months. After a bit of research, it appears that it wasn't a bug, but instead working as intended. According to the documentation found here:
我一直在与 一起工作DateTime class
,最近遇到了我认为添加月份时的错误。经过一番研究,它似乎不是一个错误,而是按预期工作。根据此处找到的文档:
Example #2 Beware when adding or subtracting months
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
The above example will output: 2001-01-31 2001-03-03
Can anyone justify why this isn't considered a bug?
谁能证明为什么这不被视为错误?
Furthermore does anyone have any elegant solutions to correct the issue and make it so +1 month will work as expected instead of as intended?
此外,有没有人有任何优雅的解决方案来纠正问题并使其 +1 个月按预期工作而不是按预期工作?
回答by shamittomar
Why it's not a bug:
为什么它不是错误:
The current behavior is correct. The following happens internally:
目前的行为是正确的。以下发生在内部:
+1 month
increases the month number (originally 1) by one. This makes the date2010-02-31
.The second month (February) only has 28 days in 2010, so PHP auto-corrects this by just continuing to count days from February 1st. You then end up at March 3rd.
+1 month
将月份数(原为 1)增加 1。这使得日期2010-02-31
。第二个月(二月)在 2010 年只有 28 天,因此 PHP 会通过继续从 2 月 1 日开始计算天数来自动更正这一点。然后你会在 3 月 3 日结束。
How to get what you want:
如何获得你想要的:
To get what you want is by: manually checking the next month. Then add the number of days next month has.
要得到你想要的东西是:手动检查下个月。然后加上下个月的天数了。
I hope you can yourself code this. I am just giving what-to-do.
我希望你可以自己编码。我只是给出了该做什么。
PHP 5.3 way:
PHP 5.3 方式:
To obtain the correct behavior, you can use one of the PHP 5.3's new functionality that introduces the relative time stanza first day of
. This stanza can be used in combination with next month
, fifth month
or +8 months
to go to the first day of the specified month. Instead of +1 month
from what you're doing, you can use this code to get the first day of next month like this:
要获得正确的行为,您可以使用 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";
?>
This script will correctly output February
. The following things happen when PHP processes this first day of next month
stanza:
此脚本将正确输出February
. PHP 处理本first day of next month
节时会发生以下情况:
next month
increases the month number (originally 1) by one. This makes the date 2010-02-31.first day of
sets the day number to1
, resulting in the date 2010-02-01.
next month
将月份数(原为 1)增加 1。这使得日期为 2010-02-31。first day of
将天数设置为1
,从而得到日期 2010-02-01。
回答by Rudiger W.
Here is another compact solution entirely using DateTime methods, modifying the object in-place without creating clones.
这是另一个完全使用 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;
It outputs:
它输出:
2012-01-31
2012-02-29
回答by nicolaas thiemen francken
This may be useful:
这可能很有用:
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
回答by bernland
My solution to the problem:
我对问题的解决方案:
$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' );
}
}
回答by Anthony
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:
我同意 OP 的观点,即这是违反直觉和令人沮丧的,但确定+1 month
在发生这种情况的情况下意味着什么也是如此。考虑以下示例:
You start with 2015-01-31 and want to add a month 6 times to get a scheduling cycle for sending an email newsletter. With the OP's initial expectations in mind, this would return:
您从 2015-01-31 开始并希望添加一个月 6 次以获得发送电子邮件简报的计划周期。考虑到 OP 的最初期望,这将返回:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
Right away, notice that we are expecting +1 month
to mean last day of month
or, alternatively, to add 1 month per iteration but always in reference to the start point. Instead of interpreting this as "last day of month" we could read it as "31st day of next month or last available within that month". This means that we jump from April 30th to May 31st instead of to May 30th. Note that this is not because it is "last day of month" but because we want "closest available to date of start month."
马上通知我们期待+1 month
的意思last day of month
,或者每一次迭代,但总是在参考起点1个月补充。我们可以将其解读为“下个月的第 31 天或该月内最后一天可用”,而不是将其解释为“一个月的最后一天”。这意味着我们从 4 月 30 日跳到 5 月 31 日而不是 5 月 30 日。请注意,这不是因为它是“一个月的最后一天”,而是因为我们想要“最接近开始月份的日期”。
So suppose one of our users subscribes to another newsletter to start on 2015-01-30. What is the intuitive date for +1 month
? One interpretation would be "30th day of next month or closest available" which would return:
因此,假设我们的一个用户订阅了另一份时事通讯,从 2015 年 1 月 30 日开始。什么是直观的日期+1 month
?一种解释是“下个月的第 30 天或最近可用”,它将返回:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
This would be fine except when our user gets both newsletters on the same day. Let's assume that this is a supply-side issue instead of demand-side We're not worried that the user will be annoyed with getting 2 newsletters in the same day but instead that our mail servers can't afford the bandwidth for sending twice as many newsletters. With that in mind, we return to the other interpretation of "+1 month" as "send on the second to last day of each month" which would return:
除非我们的用户在同一天收到两个时事通讯,否则这会很好。让我们假设这是一个供应方问题而不是需求方我们不担心用户会因为在同一天收到 2 封新闻通讯而烦恼,而是我们的邮件服务器无法负担发送两倍的带宽许多时事通讯。考虑到这一点,我们回到“+1 个月”的另一种解释,即“在每个月的倒数第二天发送”,它将返回:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
Now we've avoided any overlap with the first set, but we also end up with April and June 29th, which certainly does match our original intuitions that +1 month
simply should return m/$d/Y
or the attractive and simple m/30/Y
for all possible months. So now let's consider a third interpretation of +1 month
using both dates:
现在我们避免了与第一组的任何重叠,但我们也以 4 月和 6 月 29 日结束,这当然符合我们最初的直觉,即+1 month
应该返回m/$d/Y
或在m/30/Y
所有可能的月份中都有吸引力和简单。所以现在让我们考虑+1 month
使用这两个日期的第三种解释:
Jan. 31st
1 月 31 日
- 2015-01-31
- 2015-03-03
- 2015-03-31
- 2015-05-01
- 2015-05-31
- 2015-07-01
- 2015-01-31
- 2015-03-03
- 2015-03-31
- 2015-05-01
- 2015-05-31
- 2015-07-01
Jan. 30th
1 月 30 日
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
- 2015-01-30
- 2015-03-02
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
The above has some issues. February is skipped, which could be a problem both supply-end (say if there is a monthly bandwidth allocation and Feb goes to waste and March gets doubled up on) and demand-end (users feel cheated out of Feb and perceive the extra March as attempt to correct mistake). On the other hand, notice that the two date sets:
上面有一些问题。2 月被跳过,这可能是供应端(比如如果有每月带宽分配,2 月浪费,3 月加倍)和需求端(用户感觉被骗了 2 月并认为额外的 3 月)的问题作为试图纠正错误)。另一方面,请注意两个日期设置:
- never overlap
- are always on the same date when that month has the date (so the Jan. 30 set looks pretty clean)
- are all within 3 days (1 day in most cases) of what might be considered the "correct" date.
- are all at least 28 days (a lunar month) from their successor and predecessor, so very evenly distributed.
- 从不重叠
- 总是在那个月有日期的同一天(所以 1 月 30 日集看起来很干净)
- 都在可能被视为“正确”日期的 3 天内(在大多数情况下为 1 天)。
- 与他们的继任者和前任相距至少 28 天(农历月份),因此分布非常均匀。
Given the last two sets, it would not be difficult to simply roll back one of the dates if it falls outside of the actual following month (so roll back to Feb 28th and April 30th in the first set) and not lose any sleep over the occasional overlap and divergence from the "last day of month" vs "second to last day of month" pattern. But expecting the library to choose between "most pretty/natural", "mathematical interpretation of 02/31 and other month overflows", and "relative to first of month or last month" is always going to end with someone's expectations not being met and some schedule needing to adjust the "wrong" date to avoid the real-world problem that the "wrong" interpretation introduces.
鉴于最后两集,如果其中一个日期在下个月之外,那么简单地回滚一个日期并不困难(因此在第一组中回滚到 2 月 28 日和 4 月 30 日)并且不会在整个过程中失眠“每月的最后一天”与“每月的倒数第二天”模式的偶尔重叠和背离。但期望图书馆在“最漂亮/自然”、“02/31 和其他月份溢出的数学解释”和“相对于第一个月或上个月”之间进行选择总是会以没有满足某人的期望而告终,一些时间表需要调整“错误”日期以避免“错误”解释引入的现实问题。
So again, while I also would expect +1 month
to return a date that actually is in the following month, it is not as simple as intuition and given the choices, going with math over the expectations of web developers is probably the safe choice.
再说一次,虽然我也希望+1 month
返回一个实际在下个月的日期,但这并不像直觉那么简单,并且给出了选择,用数学计算超过 Web 开发人员的期望可能是安全的选择。
Here's an alternative solution that is still as clunky as any but I think has nice results:
这是一个替代解决方案,它仍然像任何解决方案一样笨拙,但我认为有很好的结果:
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);
}
It's not optimal but the underlying logic is : If adding 1 month results in a date other than the expected next month, scrap that date and add 4 weeks instead. Here are the results with the two test dates:
这不是最佳的,但潜在的逻辑是:如果添加 1 个月的结果不是预期的下个月,则取消该日期并添加 4 周。以下是两个测试日期的结果:
Jan. 31st
1 月 31 日
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
Jan. 30th
1 月 30 日
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(My code is a mess and wouldn't work in a multi-year scenario. I welcome anyone to rewrite the solution with more elegant code so long as the underlying premise is kept intact, i.e. if +1 month returns a funky date, use +4 weeks instead.)
(我的代码一团糟,不能在多年的情况下工作。只要底层前提保持完整,我欢迎任何人用更优雅的代码重写解决方案,即如果 +1 个月返回一个时髦的日期,请使用+4 周。)
回答by A-R
I made a function that returns a DateInterval to make sure that adding a month shows the next month, and removes the days into the after that.
我创建了一个返回 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');
}
}
回答by patrickzzz
In conjunction with shamittomar's answer, it could then be this for adding months "safely":
结合 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;
}
回答by Rommel Paras
I found a shorter way around it using the following code:
我使用以下代码找到了一种更短的方法:
$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');
回答by derekm
Here is an implementation of an improved version of Juhana's answerin 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;
}
}
This version takes $createdDate
under the presumption that you are dealing with a recurring monthly period, such as a subscription, that started on a specific date, such as the 31st. It always takes $createdDate
so late "recurs on" dates won't shift to lower values as they are pushed forward thru lesser-valued months (e.g., so all 29th, 30th or 31st recur dates won't eventually get stuck on the 28th after passing thru a non-leap-year February).
此版本$createdDate
假定您正在处理从特定日期(例如 31 日)开始的重复性月度周期(例如订阅)。总是需要$createdDate
这么晚的“重复”日期不会转移到较低的值,因为它们会被推到价值较低的月份(例如,因此所有第 29、30 或 31 日的重复日期最终不会在过去后停留在 28 日)通过非闰年二月)。
Here is some driver code to test the algorithm:
下面是一些用于测试算法的驱动程序代码:
$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;
}
Which outputs:
哪些输出:
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
回答by H?i Phong
This is an improved version of Kasihasi's answerin a related question. This will correctly add or subtract an arbitrary number of months to a date.
这是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');
}
For example:
例如:
addMonths(-25, '2017-03-31')
will output:
将输出:
'2015-02-28'