tpl*_*ner 96 php datetime date
我一直在努力工作,DateTime class并且最近遇到了我认为添加月份时的错误.经过一些研究后,似乎它不是一个bug,而是按预期工作.根据此处的文档:
示例#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";
?>
Run Code Online (Sandbox Code Playgroud)
Run Code Online (Sandbox Code Playgroud)The above example will output: 2001-01-31 2001-03-03
任何人都可以证明为什么这不被视为错误?
此外,是否有人有任何优雅的解决方案来纠正问题并使其成为+1个月将按预期工作而不是按预期工作?
sha*_*mar 100
目前的行为是正确的.内部发生以下情况:
+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";
?>
Run Code Online (Sandbox Code Playgroud)
此脚本将正确输出February.当PHP处理此first day of next month节时,会发生以下情况:
next month将月份数(原来为1)增加1.这使得日期为2010-02-31.
first day of将日期编号设置为1,导致日期为2010-02-01.
小智 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
Run Code Online (Sandbox Code Playgroud)
这是另一种完全使用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;
Run Code Online (Sandbox Code Playgroud)
它输出:
2012-01-31
2012-02-29
Run Code Online (Sandbox Code Playgroud)
我对问题的解决方案:
$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' );
}
}
Run Code Online (Sandbox Code Playgroud)
我同意OP的观点,即这是违反直觉且令人沮丧的,但确定+1 month在发生这种情况的情况下意味着什么也是如此。考虑这些例子:
您从 2015 年 1 月 31 日开始,想要添加一个月 6 次以获得发送电子邮件通讯的调度周期。考虑到OP的最初期望,这将返回:
请立即注意,我们期望+1 month每次last day of month迭代增加 1 个月,但始终参考起点。我们可以将其解读为“下个月的 31 日或该月内最后一天”,而不是将其解释为“本月的最后一天”。这意味着我们从 4 月 30 日跳到 5 月 31 日,而不是 5 月 30 日。请注意,这并不是因为它是“一个月的最后一天”,而是因为我们想要“最接近开始月份的日期”。
因此,假设我们的一位用户订阅了另一份于 2015 年 1 月 30 日开始的时事通讯。直观的日期是什么+1 month?一种解释是“下个月的 30 日或最接近的可用日期”,它将返回:
除非我们的用户在同一天收到两份新闻通讯,否则这很好。让我们假设这是一个供应方问题而不是需求方问题。我们并不担心用户会因为在同一天收到两份新闻通讯而感到恼火,而是担心我们的邮件服务器无法承担发送两倍新闻通讯的带宽。许多时事通讯。考虑到这一点,我们返回“+1 个月”的另一种解释,即“在每月倒数第二天发送”,这将返回:
现在我们已经避免了与第一组的任何重叠,但我们也以 4 月和 6 月 29 日结束,这当然符合我们最初的直觉,即+1 month应该回归或所有可能的月份都m/$d/Y具有吸引力和简单。m/30/Y现在让我们考虑+1 month使用这两个日期的第三种解释:
以上有一些问题。跳过 2 月,这可能会成为供应端(例如,如果每月分配带宽,而 2 月被浪费,3 月加倍)和需求端(用户感觉 2 月被骗了,并认为 3 月多了)的问题。作为尝试纠正错误)。另一方面,请注意两个日期集:
考虑到最后两组,如果其中一个日期超出了下个月的实际日期,那么简单地回滚并不困难(因此在第一组中回滚到 2 月 28 日和 4 月 30 日),并且不会在这段时间失眠。 “每月最后一天”与“每月倒数第二天”模式偶尔有重叠和背离。但是期望图书馆在“最漂亮/自然”、“02/31 和其他月份溢出的数学解释”和“相对于第一个月或上个月”之间进行选择总是会以某人的期望没有得到满足而告终某些时间表需要调整“错误”日期,以避免“错误”解释引入的现实问题。
所以,虽然我也希望+1 month返回一个实际在下个月的日期,但这并不像直觉那么简单,并且考虑到选择,使用数学超过网络开发人员的期望可能是安全的选择。
这是一个替代解决方案,它仍然像其他解决方案一样笨重,但我认为效果很好:
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);
}
Run Code Online (Sandbox Code Playgroud)
这不是最优的,但基本逻辑是:如果添加 1 个月导致的日期不是预期的下个月,则废弃该日期并添加 4 周。以下是两个测试日期的结果:
(我的代码一团糟,在多年的情况下无法工作。我欢迎任何人用更优雅的代码重写解决方案,只要基本前提保持不变,即如果 +1 个月返回一个时髦的日期,请使用+4 周。)
小智 5
我创建了一个返回 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');
}
}
Run Code Online (Sandbox Code Playgroud)
小智 5
结合 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;
}
Run Code Online (Sandbox Code Playgroud)