joda.time之如何获取到两个时间的差值(正确的使用Period类)

5,176 阅读5分钟

前言

此前Java处理时间日期所使用的 Date 和 Calendar 被诟病不已,Calendar 的主要问题对象可变,而像时间和日期这样的类应该是不可变的,另外其概念模型也有不明确的地方,月份计算从0开始等等。

JodaTime开源时间/日期库是很好的替代,另外Java8中也推出了新的java.time库,设计理念与JodaTime相似。

Joda-Time 令时间和日期值变得易于管理、操作和理解。易于使用是 Joda 的主要设计目标。Joda-Time主类 DateTime 和JDK旧有类 Date 和 Calendar之间可以互相转换。从而保证了与JDK框架的兼容。

本文主要介绍了下Period类及如何正确的两个时间的差值 通过分析构造方法的方式进行展开,详细见下文

常用操作

org.joda.time大部分用法计算机程序的思维逻辑使用Joda-Time优雅的处理日期时间这里面都有很好的介绍不再赘述,下面主要看下处理时间段(两个日期的差值)的Period类

Period简介

由一组持续时间字段值指定的不可变时间段

一段时间分为多个字段如年月日来表示,这些字段由PeriodType类定义,PeriodType默认值为standard类型,支持年,月,周,日,小时,分钟,秒和毫秒

构造函数

第一种:直接声明
public Period() {
	super(0L, (PeriodType)null, (Chronology)null);
}
第二种:传入年、月、周、日、时、分、秒、毫秒等值 此处一定要注意中间的周这个值的定义
public Period(int var1, int var2, int var3, int var4) {
	super(0, 0, 0, 0, var1, var2, var3, var4, PeriodType.standard());
}
第三种:获取两个ReadableInstant实现类(如DateTime)之间的时间段
public Period(ReadableInstant var1, ReadableInstant var2, PeriodType var3) {
	super(var1, var2, var3);
}

正确的获取两个时间之间的差值

网上有很多的案例说获取两个时间之间的差距年月日等信息的解决方案:

计算两个时间的差值

DateTime start = new DateTime(2016,8,18,10,58);
DateTime end = new DateTime(2016,9,19,12,3);
Period period = new Period(start,end);        
System.out.println(period.getMonths()+"月"+period.getDays()+"天"
       +period.getHours()+"小时"+period.getMinutes()+"分");

输出为:

1月1天1小时5分
复制代码

只要给定起止时间,Period就可以自动计算出来,两个时间之间有多少月、多少天、多少小时等。

这里有一个问题没有说明 拿一个案例来说

DateTime d1 = new DateTime(2018,1,27,0,30);
DateTime d2 = new DateTime(2018,2,26,2,30);
Period p0 = new Period(d1, d2);
System.out.println(p0.getYears() + "年" + p0.getMonths() + "月"  + p0.getWeeks() + "周" + p0.getDays() + "天");

输出为:

0年0月4周2天

是不是跟预期想要得到的30天不一致,下面看下原因

使用new Period(ReadableInstant var1, ReadableInstant var2)进行Period的实例化
这个构造方法默认使用的PeriodType
public Period(ReadableInstant var1, ReadableInstant var2) { 
   super(var1, var2, (PeriodType)null);
}
这时候指定的PeriodType为null,再继续向下看Period的上级抽象父类BasePeriod的构造方法
protected BasePeriod(ReadableInstant var1, ReadableInstant var2, PeriodType var3) {
   //1. 检查一下var3对应的值
   var3 = this.checkPeriodType(var3);
   if (var1 == null && var2 == null) {
   	//6. 这里将iType赋值为var3指定的Period
   	this.iType = var3;
       this.iValues = new int[this.size()];
    } else {
    	long var4 = DateTimeUtils.getInstantMillis(var1);
       long var6 = DateTimeUtils.getInstantMillis(var2);
       Chronology var8 = DateTimeUtils.getIntervalChronology(var1, var2);
       this.iType = var3;
       this.iValues = var8.get(this, var4, var6);
    }
}
protected PeriodType checkPeriodType(PeriodType var1) {
   //2. 调用DateTimeUtils工具类的getPeriodType()传入var1 也就是上一代码块的var3(也就是null)
   return DateTimeUtils.getPeriodType(var1);
}
public static final PeriodType getPeriodType(PeriodType var0) {
   //3. 在这里就可以看到 传入的null最终转化为了PeriodType.standard()
   return var0 == null ? PeriodType.standard() : var0;
}
//4. 在PeriodType看到静态方法 实例standard并返回对应的Period
public static PeriodType standard() {
   PeriodType var0 = cStandard;
   if (var0 == null) {
   	//5. 这里可以看到在Standard中指定了年月周天时分秒(有兴趣的可以看下PeriodType的实例的设计)
   	//这里的猫腻就在于Standard中指定了周!!!
  		var0 = new PeriodType("Standard", new DurationFieldType[]{DurationFieldType.years(), DurationFieldType.months(), DurationFieldType.weeks(), DurationFieldType.days(), DurationFieldType.hours(), DurationFieldType.minutes(), DurationFieldType.seconds(), DurationFieldType.millis()}, new int[]{0, 1, 2, 3, 4, 5, 6, 7});
   	cStandard = var0;
   }

   return var0;
}

所以获取两个日期的差值如年月日的正确姿势应该如下

DateTime d1 = new DateTime(2018,1,27,0,30);
DateTime d2 = new DateTime(2018,2,26,2,30);
//指定PeriodType为yearMonthDayTime
Period p2 = new Period(d1, d2, PeriodType.yearMonthDayTime());
System.out.println(p2.getYears() + "年" + p2.getMonths() + "月"+ p2.getWeeks() + "周" + p2.getDays() + "天");

输入结果:

0年0月0周30天

查询官方文档

public Period(ReadableInstant startInstant,
              ReadableInstant endInstant)

Most calculations performed by this method have obvious results. The special case is where the calculation is from a "long" month to a "short" month. Here, the result favours increasing the months field rather than the days. For example, 2013-01-31 to 2013-02-28 is treated as one whole month. By contrast, 2013-01-31 to 2013-03-30 is treated as one month and 30 days (exposed as 4 weeks and 2 days). The results are explained by considering that the start date plus the calculated period result in the end date.

大致的意思是说有的两点:

  1. 2013-01-31 to 2013-03-30 is treated as one month and 30 days (exposed as 4 weeks and 2 days)

从2013-01-31到2013-03-30被视为有一个月和30天(其中30天暴露为4周和2天)

上面已经分析过原因及实现,以及如果获取到想要的值

  1. 从大月到小月的计算需要注意,比如2013-01-31到2013-02-28是一个月

    2013-01-29到2013-02-28也是一个月

    2013-01-28到2013-02-28还是一个月

    2013-01-27到2013-02-28是一个月零一天

    这个也是需要注意的点

总结

笔者也是在项目中使用joda.time来处理两个时间的差值的时候使用Period这个处理时间段的类,但是在使用的过程中出现了跟预期值不同的问题,随后翻阅资料更正了用法总结了一下,希望能够帮助到大家。需要使用到更多的用法可以参考官方文档:User Guide

参考资料

joda.time官方API

计算机程序的思维逻辑