首页 > 解决方案 > java.time.Period.between() 在处理不同长度的月份时使用的算法?

问题描述

在不同长度的月份中使用时java.time.Period.between(),为什么下面的代码会根据操作的方向报告不同的结果?

import java.time.LocalDate;
import java.time.Period;
class Main {
  public static void main(String[] args) {
    LocalDate d1 = LocalDate.of(2019, 1, 30);
    LocalDate d2 = LocalDate.of(2019, 3, 29); 
    Period period = Period.between(d1, d2);
    System.out.println("diff: " + period.toString());
    // => P1M29D
    Period period2 = Period.between(d2, d1);
    System.out.println("diff: " + period2.toString());
    // => P-1M-30D
  }
}

实时复制:https ://repl.it/@JustinGrant/BigScornfulQueryplan#Main.java

这是我希望它工作的方式:

2019-01-30 => 2019-03-29

  1. 2019-01-30 加一个月 => 2019-02-30,限制为 2019-02-28
  2. 加 29 天到 2019-03-29

这与 Java 的结果相匹配:P1M29D

(反向)2019-03-29 => 2019-01-30

  1. 从 2019-03-29 减去一个月 => 2019-02-29,限制为 2019-02-28
  2. 减去 29 天得到 2019-01-30

但是 Java 又回到P-1M-30D了这里。我预计P-1M-29D

参考文档说:

周期的计算方法是删除完整的月份,然后计算剩余的天数,调整以确保两者具有相同的符号。然后根据 12 个月的年份将月数拆分为年和月。如果月份的结束日期大于或等于月份的开始日期,则考虑一个月。例如,从2010-01-152011-03-18是一年两个月零三天。

也许我没有仔细阅读这篇文章,但我认为这篇文章并不能完全解释我所看到的不同行为。

我对java.time.Period.between应该如何工作有什么误解?具体来说,当“删除完整月份”的中间结果是无效日期时,预计会发生什么?

该算法是否在其他地方更详细地记录?

标签: javajodatimejava-timejsr310

解决方案


TL;博士

我在源代码中看到的算法(复制如下)似乎并不假设Period两个日期之间的 a 预期具有相同两个日期之间的天数的准确性(我什至怀疑Period不打算用于计算关于连续时间变量)。

它计算月和天的差异,然后进行调整以确保两者具有相同的符号。由此产生的时期是建立在这两个值的基础上的。

主要的挑战是添加两个月LocalDate.of(2019, 1, 28)与添加(31 + 28) days或添加(28 + 31) days到该日期不同。LocalDate.of(2019, 1, 28)它只是将 2 个月添加到LocalDate.of(2019, 3, 28).

换句话说,在 的上下文中LocalDatePeriods 表示准确的月数(以及派生年数),但天数对计算的月数很敏感。


这是我看到的来源(java.time.LocalDate.until(ChronoLocalDate)最终完成了这项工作):

public Period until(ChronoLocalDate endDateExclusive) {
    LocalDate end = LocalDate.from(endDateExclusive);
    long totalMonths = end.getProlepticMonth() - this.getProlepticMonth();  // safe
    int days = end.day - this.day;
    if (totalMonths > 0 && days < 0) {
        totalMonths--;
        LocalDate calcDate = this.plusMonths(totalMonths);
        days = (int) (end.toEpochDay() - calcDate.toEpochDay());  // safe
    } else if (totalMonths < 0 && days > 0) {
        totalMonths++;
        days -= end.lengthOfMonth();
    }
    long years = totalMonths / 12;  // safe
    int months = (int) (totalMonths % 12);  // safe
    return Period.of(Math.toIntExact(years), months, days);
}

可以看出,当月差与日差的符号不同时,就会进行符号调整(是的,它们是单独计算的)。两者totalMonths > 0 && days < 0totalMonths < 0 && days > 0适用于您的示例(每个计算一个)。

碰巧的是,当月的周期差为正时,周期的天数是使用纪元天数计算的,因此会产生准确的结果。当有必要剪辑新的结束日期以适应月份长度时,它仍然可能会受到影响 - 例如:

jshell> LocalDate.of(2019, 1, 31).plusMonths(1)
$42 ==> 2019-02-28

但这在您的示例中不会发生,因为您根本无法为该方法提供无效的结束日期,如

// Period.between(LocalDate.of(2019, 1, 31), LocalDate.of(2019, 2, 30)

用于裁剪结果期间中的结果天数。

但是,当以月为单位的时差为负时,会发生:

//task: account for the 1-day difference
jshell> LocalDate.of(2019, 5, 30).plusMonths(-1)
$50 ==> 2019-04-30

jshell> LocalDate.of(2019, 5, 31).plusMonths(-1)
$51 ==> 2019-04-30

并且,使用句点和本地日期:

jshell> Period.between(LocalDate.of(2019, 3, 31), LocalDate.of(2019, 2, 28))
$39 ==> P-1M-3D //3 days? It didn't look at February's length (2<3 && 28<31)

jshell> Period.between(LocalDate.of(2019, 3, 31), LocalDate.of(2019, 1, 31))
$40 ==> P-2M

在您的情况下(第二次调用),-30是 的结果(30 - 29) - 31,其中31是一月份的天数。

我认为这里的短篇故事不Period用于时间价值计算。在time的上下文中,我认为month是一个名义单位。当一个月被定义为抽象时期时(例如在计算每月租金支付时),时期会很有效,但当涉及到连续时间时,它们通常会失败。


推荐阅读