2个日历实例之间的差异给出不一致的答案

时间:2018-09-27 23:53:06

标签: java calendar java-7 days

我正在使用jdk 1.7并使用Calendar类计算两个日期之间的差。我正在使用下面的代码,但结果不一致。意思是有时是正确的,但有时是一天或一天​​的时间,并且没有任何规律。

public class Test {

    public static void main(String ar[]) {

        System.out.println(calculateDays());
    }

    private static long calculateDays() {
        long days_past_due;
           Calendar cal1 = Calendar.getInstance();
           Calendar cal2 = Calendar.getInstance();
           cal1.set(2013, 12, 1);
           cal2.set(2013, 12, 30);
           days_past_due = getDifference(cal1, cal2, TimeUnit.DAYS);
        return days_past_due;
    }

    public static long getDifference(Calendar b, Calendar a, TimeUnit units) 
    {

        return units.convert(b.getTimeInMillis() - a.getTimeInMillis(), TimeUnit.MILLISECONDS);
    }

示例:

Example 1: cal1.set(2013, 12, 1);
           cal2.set(2013, 12, 1);
           Answer returned: 0 (Correct)
Example 2: cal1.set(2013, 12, 1);
           al2.set(2013, 12, 2);
           Answer returned: -1 (Correct)
Example 3: cal1.set(2013, 11, 30);
           cal2.set(2013, 12, 1);
           Answer returned: -2 (Incorrect)
Example 4: cal1.set(2013, 8, 31);
           cal2.set(2013, 8, 31);
           Answer returned: 0 (Correct)
Example 5: cal1.set(2013, 8, 31);
           cal2.set(2013, 9, 1);
           Answer returned: 0 (Incorrect)
Example 6: cal1.set(2013, 6, 30);
           cal2.set(2013, 6, 30);
           Answer returned: 0 (Correct)
Example 7: cal1.set(2013, 6, 30);
           cal2.set(2013, 7, 1);
           Answer returned: -2 (Incorrect)

我在这里做错了什么?

3 个答案:

答案 0 :(得分:2)

tl; dr

您应该使用LocalDate进行合理的计数,而不是麻烦的Calendar类。

ChronoUnit.DAYS.between(
    LocalDate.of( 2013 , 11 , 30 ) ,
    LocalDate.of( 2013 , 12 , 1 )
)

请参阅此code run live at IdeOne.com

  

1

疯狂计数

您似乎没有意识到可怕的Calendar类中使用的疯狂计数:从1月到12月的0-11个月。

所以cal1.set(2013, 11, 30)cal2.set(2013, 12, 1)的意思是10月30日至11月1日,实际上是两天,占10月31日。您显然误认为这是11月30日至12月1日,但没有。 / p>

这是index-counting, zero-based。不幸的是,索引计数经常在某些程序员中经常出现,他们不恰当地使用原始数组或系统编程或老式C风格编程中的内存跳转的意义。在使用现代语言的常见商业应用程序中,序数计数通常更有意义。例如,将1月视为第一个月(第1个月),将12月视为第十二个月(第12个月)。

java.time

幸运的是,我们现在有了 java.time 类。没有理由让您使用可怕的混乱,它们是诸如DateCalendarSimpleDateFormat之类的传统日期时间类。

要在Java 7中使用,请参见下面底部的项目符号中链接的 ThreeTen-Backport 项目。

LocalDate

LocalDate类表示没有日期和时区的仅日期值。

时区对于确定日期至关重要。在任何给定时刻,日期都会在全球范围内变化。例如,Paris France午夜之后的几分钟是新的一天,而Montréal Québec仍然是“昨天”。

如果未指定时区,则JVM隐式应用其当前的默认时区。该默认值可能在运行时(!)期间change at any moment,因此您的结果可能会有所不同。最好将desired/expected time zone明确指定为参数。

continent/region的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用3-4个字母的缩写,例如ESTIST,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "America/Montreal" ) ;  
LocalDate today = LocalDate.now( z ) ;

如果要使用JVM的当前默认时区,请提出要求并作为参数传递。如果省略,则会隐式应用JVM的当前默认值。最好明确一点,因为默认值可能会在运行时的任何时候被JVM中任何应用程序的任何线程中的任何代码更改。

ZoneId z = ZoneId.systemDefault() ;  // Get JVM’s current default time zone.

或指定日期。您可以用数字设置月份,一月至十二月的理智编号为1-12。

LocalDate ld = LocalDate.of( 1986 , 2 , 23 ) ;  // Years use sane direct numbering (1986 means year 1986). Months use sane numbering, 1-12 for January-December.

或者更好的是,使用预定义的Month枚举对象,每年的每个月使用一个。提示:在整个代码库中使用这些Month对象,而不是仅使用整数,可以使您的代码更具自文档性,确保有效值并提供type-safety

LocalDate ld = LocalDate.of( 1986 , Month.FEBRUARY , 23 ) ;

ChronoUnit.DAYS

要计算经过的天数,请使用ChronoUnit枚举。

long days = ChronoUnit.DAYS.between( start , stop ) ;

关于 java.time

java.time框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

目前位于Joda-Timemaintenance mode项目建议迁移到java.time类。

要了解更多信息,请参见Oracle Tutorial。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

您可以直接与数据库交换 java.time 对象。使用符合JDBC driver或更高版本的JDBC 4.2。不需要字符串,不需要java.sql.*类。

在哪里获取java.time类?

ThreeTen-Extra项目使用其他类扩展了java.time。该项目为将来可能在java.time中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

答案 1 :(得分:1)

Calendar#set(int,int,int)的Javadoc说(我的重点)

  

参数:

     

year-用于设置YEAR日历字段的值。

     

month-用于设置MONTH日历字段的值。 月份值从0开始。例如,一月为0。

     

date-用于设置DAY_OF_MONTH日历字段的值。

如果您牢记这一点重新检查示例,并记住日期被“规范化”(即,如果您指定第13个月成为下一年的第一个月),则会发现所有计算出的差异实际上是正确的。

答案 2 :(得分:0)

  

那么什么是最简单的方法呢?使用相同的Calendar类

使用Calendar没有更简单的方法。有一种更麻烦的广告容易出错的方式。

首先,您需要正确初始化两个Calendar对象:

    Calendar cal1 = Calendar.getInstance();
    Calendar cal2 = Calendar.getInstance();
    cal1.clear();
    cal2.clear();
    cal1.set(2013, Calendar.NOVEMBER, 30);
    cal2.set(2013, Calendar.DECEMBER, 1);

Calendar对象,尽管其名称代表日期,一天中的时间,时区和日历系统。当我们只想将其用于某个时区的公历中的某个日期时,我们需要首先清除字段以摆脱一天中的时间,否则它将干扰后续的计算。接下来,将Calendar类的命名常量用于您希望设置的日期月份,以减少混乱。

第二,让Calendar考虑到夏令时(DST)和其他异常之间的过渡,除了增加天数并查看我们何时到达别无其他方法,这是相反的做法:

    if (cal1.before(cal2)) {
        int daysPastDue = 0;
        while (! cal1.after(cal2)) {
            daysPastDue++;
            cal1.add(Calendar.DATE, 1);
        }
        // Now cal1 is after cal2, so we’ve counted 1 day too many. Compensate:
        daysPastDue--;
        System.out.println("Answer returned: " + daysPastDue);
    }

此打印:

  

答案返回:1

我当然不鼓励按照我所展示的方式进行。这是相对多的代码行,而且很容易忘记一个细节并得到错误的结果或犯错。如果可以,请升级到Java 8或9或10或11。如果不能,Basil Bourque’s answer也可以与ThreeTen Backport库一起很好地工作,请参阅他关于Java SE 6和Java SE 7的项目符号。

PS:尽管TimeUnit枚举非常适合微秒到小时之间的转换,但请不要使用它几天,至少在这种情况下不要使用。 TimeUnit.DAYS可以算24小时,而日历中的天数可能是23、24或25小时,有时还有其他时长。