我同意OP的观点,即这是违反直觉和令人沮丧的,但是确定什么 +1 month
发生这种情况的情况下。考虑以下示例:
您从2015年1月31日开始,想要添加一个月6次以获取发送电子邮件新闻通讯的计划周期。考虑到OP的最初期望,这将返回:
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-30
- 2015-05-31
- 2015-06-30
马上,请注意,我们期望+1 month
平均last day of month
每次迭代要增加1个月,或者总是要参考起点。与其将其解释为“月份的最后一天”,不如将其理解为“下个月的31日或该月的最后一个可用日期”。这意味着我们从4月30日跳到5月31日,而不是5月30日。请注意,这不是因为它是“月份的最后一天”,而是因为我们想要“最接近开始月份的日期”。
因此,假设我们的一个用户订阅了另一封新闻通讯,该通讯将从2015年1月30日开始。直观的日期是+1 month
什么?一种解释是“下个月的30日或最接近的时间”,该结果将返回:
- 2015-01-30
- 2015-02-28
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
除非我们的用户在同一天收到两个新闻通讯,否则就可以了。让我们假设这是一个供应方问题,而不是需求方问题。我们不担心用户会在同一天收到2个新闻通讯而感到烦恼,但是我们的邮件服务器负担不起两次发送的带宽。许多新闻通讯。考虑到这一点,我们返回“ +1个月”的另一种解释为“在每个月的第二天到最后一天发送”,它将返回:
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-29
- 2015-05-30
- 2015-06-29
现在我们避免了与第一组的任何重叠,但我们也将在4月和6月29日结束,这肯定与我们的原始直觉相符,这些直觉应该+1 month
简单地返回,m/$d/Y
或者在m/30/Y
所有可能的月份中都具有吸引力和简单性。现在,让我们考虑+1 month
使用两个日期的第三种解释:
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
上面有一些问题。跳过2月,这可能会成为供应端(例如,如果每月分配带宽,而2月浪费,3月成倍增长)和需求端(用户感到2月被骗并感觉到额外的3月)问题作为纠正错误的尝试)。另一方面,请注意两个日期集:
- 永不重叠
- 当那个月有日期时,它们总是在同一日期(因此1月30日的设置看起来很干净)
- 都在可能被视为“正确”日期的3天内(大多数情况下为1天)。
- 距离其继承人和前任至少28天(农历月份),因此分布非常均匀。
给定最后两套,如果它不在下个月的实际月份内,那么简单地回滚一个日期就不会很困难(因此回滚到第一套的2月28日和4月30日)并且不会在从“每月的最后一天”与“每月的第二天到最后一天”的模式偶尔出现重叠和背离。但是,期望图书馆在“最漂亮/最自然”,“ 02/31和其他月份溢出的数学解释”和“相对于本月初或上个月的数学解释”之间进行选择,总会因未达到某人的期望而结束,并且某些时间表需要调整“错误的”日期,以避免“错误的”解释引入的实际问题。
再说一次,虽然我也希望+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月31日
- 2015-01-31
- 2015-02-28
- 2015-03-31
- 2015-04-28
- 2015-05-31
- 2015-06-28
1月30日
- 2015-01-30
- 2015-02-27
- 2015-03-30
- 2015-04-30
- 2015-05-30
- 2015-06-30
(我的代码很乱,无法在多年使用的情况下使用。我欢迎任何人使用更简洁的代码重写该解决方案,只要保持基本前提不变即可,即如果+1个月返回一个时髦的日期,请使用+4周。)