分析您的期望(伪代码)
startDate.plus(Period.between(startDate, endDate)) == endDate
我们需要讨论几个话题:
- 如何处理像月份或天数这样的单独单位?
- 如何定义持续时间(或“期间”)的添加?
- 如何确定两个日期之间的时间距离(持续时间)?
- 如何定义持续时间(或“期间”)的减法?
让我们先看看时间单位。 天没有问题,因为它们是可能日历单位中最小的单位,每个日历日期都以完整的天数相差。 因此我们在伪代码中始终保持相等,无论是正还是负:
startDate.plus(ChronoUnit.Days.between(startDate, endDate)) == endDate
由于公历定义的日历月份长度不同,因此月份可能比较棘手。因此,将任何整数个月添加到日期中可能会导致无效日期:
[2019-08-31] + P1M = [2019-09-31]
java.time
将结束日期减少到有效日期 - 这里是 [2019-09-30] - 是合理的,并且符合大多数用户的期望,因为最终日期仍然保留了计算出的月份。然而,包括月底校正的这种加法不可逆转,请参见被反转的操作叫做减法:
[2019-09-30] - P1M = [2019-08-30]
结果也是合理的,因为a)月份加法的基本规则是尽可能保持日期和 b)[2019-08-30] + P1M = [2019-09-30]。
什么是持续时间(period)的加法?
在java.time
中,Period
由年、月和天的项组成,具有任何整数部分量。因此,可以将Period
的加法解析为将部分量添加到起始日期。由于年总是可转换为12个月的倍数,因此我们可以先组合年和月,然后在一步中添加总和,以避免闰年中的奇怪副作用。最后一步可以添加天数。这是java.time
中一个合理的设计。
如何确定两个日期之间正确的Period
?
首先讨论持续时间为正的情况,意味着起始日期在结束日期之前。然后,我们始终可以通过首先确定月份差异,然后再确定日数差异来定义持续时间。这个顺序很重要,以获得一个月份组件,否则每个日期之间的持续时间只会包含天数。使用您的示例日期:
[2019-09-30] + P1M1D = [2019-10-31]
从技术上讲,首先将起始日期向前移动开始和结束之间计算出的月份差异。然后,将移动后的开始日期和结束日期之间的日差作为添加到移动后的开始日期。这样,我们可以在示例中计算出P1M1D的持续时间。目前为止还算合理。
如何减去持续时间?
前面加法示例中最有趣的一点是,偶然没有月底校正。尽管如此,java.time
却无法完成反向减法。它首先减去月份,然后再减去天数:
[2019-10-31] - P1M1D = [2019-09-29]
如果
java.time
尝试在加法中颠倒步骤,那么
自然的选择将是首先减去天数,然后再减去月份。按照这种更改顺序,我们将得到[2019-09-30]。只要相应的加法步骤没有月底修正,减法顺序的更改就会有所帮助。如果任何起始或结束日期的日数不大于28(最小可能的月份长度),则特别如此。不幸的是,
java.time
已经为
Period
的减法定义了另一个设计,这导致结果不够一致。
持续时间的加法是否可逆地进行减法运算?
首先,我们必须了解,在给定日历日期的情况下,建议更改减法的顺序并不能保证加法的可逆性。下面是一个有月底修正的反例:
[2011-03-31] + P3M1D = [2011-06-30] + P1D = [2011-07-01] (ok)
[2011-07-01] - P3M1D = [2011-06-30] - P3M = [2011-03-30] :-(
改变顺序并不是坏事,因为它会产生更一致的结果。但是如何治愈剩余的缺陷呢?唯一剩下的方法就是改变持续时间的计算方式。我们可以看到P2M31D的持续时间在两个方向上都能起作用,而不是使用P3M1D。
[2011-03-31] + P2M31D = [2011-05-31] + P31D = [2011-07-01] (ok)
[2011-07-01] - P2M31D = [2011-05-31] - P2M = [2011-03-31] (ok)
因此,想法是改变计算持续时间的标准化。这可以通过查看计算出的月份增量的加法是否可以在减法步骤中可逆来完成,即避免需要月底校正的需要。不幸的是,java.time
并没有提供这样的解决方案。这不是一个bug,但可以被视为设计上的限制。
替代方案?
我通过可逆指标增强了我的时间库 Time4J,其中部署了以上给出的思路。请参见以下示例:
PlainDate d1 = PlainDate.of(2011, 3, 31);
PlainDate d2 = PlainDate.of(2011, 7, 1);
TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
Duration.inYearsMonthsDays().reversible();
Duration<CalendarUnit> duration =
metric.between(d1, d2);
Duration<CalendarUnit> invDur =
metric.between(d2, d1);
assertThat(d1.plus(duration), is(d2));
assertThat(invDur, is(duration.inverse()));
assertThat(d2.minus(duration), is(d1));
date.plus(period).minus(period)
),结果并不总是相同的日期。 这个问题更多地涉及到Period.between
函数的不变性。 - Ilyajava.time
日历算术的工作原理。基本上,添加和删除不能相互转换,特别是如果一个或两个日期的月份大于28天。有关更多数学背景,请参阅我的时间库Time4J中的AbstractDuration类文档... - Meno Hochschild