java - 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
- 2019-01-30 加一个月 => 2019-02-30,限制为 2019-02-28
- 加 29 天到 2019-03-29
这与 Java 的结果相匹配:P1M29D
(反向)2019-03-29 => 2019-01-30
- 从 2019-03-29 减去一个月 => 2019-02-29,限制为 2019-02-28
- 减去 29 天得到 2019-01-30
但是 Java 又回到P-1M-30D
了这里。我预计P-1M-29D
。
参考文档说:
周期的计算方法是删除完整的月份,然后计算剩余的天数,调整以确保两者具有相同的符号。然后根据 12 个月的年份将月数拆分为年和月。如果月份的结束日期大于或等于月份的开始日期,则考虑一个月。例如,从
2010-01-15
到2011-03-18
是一年两个月零三天。
也许我没有仔细阅读这篇文章,但我认为这篇文章并不能完全解释我所看到的不同行为。
我对java.time.Period.between
应该如何工作有什么误解?具体来说,当“删除完整月份”的中间结果是无效日期时,预计会发生什么?
该算法是否在其他地方更详细地记录?
解决方案
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)
.
换句话说,在 的上下文中LocalDate
,Period
s 表示准确的月数(以及派生年数),但天数对计算的月数很敏感。
这是我看到的来源(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 < 0
都totalMonths < 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是一个名义单位。当一个月被定义为抽象时期时(例如在计算每月租金支付时),时期会很有效,但当涉及到连续时间时,它们通常会失败。
推荐阅读
- angular - Angular2数据表过滤列
- node.js - multer 中的 req.file 总是返回 undefined
- excel - VBA遍历文件夹中的文件并将变量范围复制/粘贴到主文件
- c# - 在 Blazor 中跨多个 cshtml 页面共享单个属性
- javascript - 将外部脚本事件绑定到组件中的 html
- shell - Bash 读取行并用上一行替换值
- angularjs - 如何为 ng-model 设置默认值?
- c# - 运行时引用的 System.Data.SqlClient 版本错误
- r - 'If condition' 出错 - 条件长度 > 1 且仅使用第一个元素
- python - 是否可以将 Jedi 嵌入到未安装 Python 的系统上的应用程序中?