分析你的期望(伪代码)
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] .
添加的持续时间(期间)到底是什么?
在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 很遗憾没有提供这样的解决方案。这不是错误,但可以视为设计限制。
替代方案?
我通过部署上述想法的可逆指标增强了我的时间库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); // P2M31D
Duration<CalendarUnit> invDur =
metric.between(d2, d1); // -P2M31D
assertThat(d1.plus(duration), is(d2)); // first invariance
assertThat(invDur, is(duration.inverse())); // second invariance
assertThat(d2.minus(duration), is(d1)); // third invariance