将句点添加到开始日期不会生成结束日期



我有两个LocalDate声明如下:

val startDate = LocalDate.of(2019, 10, 31)  // 2019-10-31
val endDate = LocalDate.of(2019, 9, 30)     // 2019-09-30

然后我使用Period.between函数计算它们之间的周期:

val period = Period.between(startDate, endDate) // P-1M-1D

在这里,该周期具有月和天的负数,这是预期的,因为endDate早于startDate

然而,当我将period添加回startDate时,我得到的结果不是endDate,而是提前一天的日期:

val endDate1 = startDate.plus(period)  // 2019-09-29

所以问题是,为什么不变

startDate.plus(Period.between(startDate, endDate)) == endDate

这两次约会还可以吗?

Period.between返回了错误的句点,还是LocalDate.plus添加了错误的句号?

如果您了解plus是如何为LocalDate实现的

@Override
public LocalDate plus(TemporalAmount amountToAdd) {
if (amountToAdd instanceof Period) {
Period periodToAdd = (Period) amountToAdd;
return plusMonths(periodToAdd.toTotalMonths()).plusDays(periodToAdd.getDays());
}
...
}

你会在那里看到plusMonths(...)plusDays(...)

plusMonths在一个月有31天,而另一个月则有30天的情况下处理案件。因此,以下代码将打印2019-09-30,而不是不存在的2019-09-31

println(startDate.plusMonths(period.months.toLong()))

之后,减去一天得到2019-09-29。这是正确的结果,因为2019-09-292019-10-31相隔1个月1天

Period.between的计算很奇怪,在这种情况下可以归结为

LocalDate end = LocalDate.from(endDateExclusive);
long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();
int days = end.day - this.day;
long years = totalMonths / 12;
int months = (int) (totalMonths % 12);  // safe
return Period.of(Math.toIntExact(years), months, days);

其中getProlepticMonth是从00-00-00开始的总月数。在这种情况下,是1个月零1天。

根据我的理解,这是Period.betweenLocalDate#plus中负周期交互的一个错误,因为下面的代码具有相同的含义

val startDate = LocalDate.of(2019, 10, 31)
val endDate = LocalDate.of(2019, 9, 30)
val period = Period.between(endDate, startDate)
println(endDate.plus(period))

但是它打印正确的CCD_ 25。

问题是LocalDate#plusMonths将日期标准化为始终"正确"。在下面的代码中,您可以看到从2019-10-31减去1个月后,结果是2019-09-31,然后将其归一化为2019-10-30

public LocalDate plusMonths(long monthsToAdd) {
...
return resolvePreviousValid(newYear, newMonth, day);
}
private static LocalDate resolvePreviousValid(int year, int month, int day) {
switch (month) {
...
case 9:
case 11:
day = Math.min(day, 30);
break;
}
return new LocalDate(year, month, day);
}

我相信你只是运气不好。你发明的不变量听起来很合理,但在java.time.中不成立

似乎between方法只是减去了月数和月的天数,由于结果具有相同的符号,因此对这个结果感到满意。我想我同意在这里可能会做出更好的决定,但正如@Meno Hochschild正确地指出的那样,涉及29、30或31个月的数学很难清晰明了,我不敢提出更好的规则。

我打赌他们现在不会改变。即使你提交了一份错误报告(你可以随时尝试),也不会。太多的代码已经依赖于它五年半以来的工作方式。

P-1M-1D重新添加到开始日期中是我所期望的方式。从10月31日减去1个月(实际加上-1个月)将产生9月30日的收益,减去9月29日的1天收益。再说一遍,这并不明确,你可以主张9月30日。

分析您的期望(在伪代码中)

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〕-P1M=〔2019-08-30〕

这个结果也是合理的,因为a)月加法的基本规则是尽可能多地保留月的日期,以及b)[2019-08-30]+P1M=[2019-09-30]。

一个持续时间(周期)到底是多少

java.time中,Period是由具有任意整数偏量的年、月和天组成的项目的组合。因此,CCD_ 35的添加可以解析为开始日期的部分量的添加。由于年总是可以转换为12个月的倍数,我们可以先将年和月组合起来,然后一步相加,以避免闰年出现奇怪的副作用。可以在最后一步中添加天数。在CCD_ 36中进行了合理的设计。

如何确定两个日期之间的正确Period

让我们首先讨论持续时间为正的情况,即开始日期在结束日期之前。然后,我们总是可以通过首先确定以月为单位,然后以天为单位的差异来定义持续时间。这个顺序对于实现一个月的组成部分很重要,因为否则两个日期之间的每个持续时间都只有几天。使用您的示例日期:

〔2019-09-30〕+P1M1D=〔2019-10-31〕

从技术上讲,开始日期首先提前计算开始和结束之间的月份差异。然后,作为移动的开始日期和结束日期之间的差值的日期增量被添加到移动的开始日。通过这种方式,我们可以将持续时间计算为示例中的P1M1D。到目前为止是合理的。

如何减去持续时间

在前面的添加示例中,最有趣的一点是,意外地没有月底的更正。然而CCD_ 38未能进行反向减法。它首先减去月份,然后减去天数:

〔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] :-(

更改顺序并不是坏事,因为它会产生更一致的结果。但是如何弥补剩余的不足?剩下的唯一方法就是改变持续时间的计算。与使用P3M1D不同,我们可以看到P2M31D的持续时间将双向工作:

[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); // 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

最新更新