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

Difference between 2 calendar instances giving inconsistent answers

我正在使用 jdk 1.7 并使用日历 class 来计算两个日期之间的差异。我正在使用下面的代码,但它给出了不一致的结果。这意味着有时它是正确的,但有时它会偏离一天或某天,而且没有规律可循。

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)

我哪里做错了?

tl;博士

您应该使用 LocalDate 和理智的计数,而不是麻烦的 Calendar class。

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

看到这个 code run live at IdeOne.com

1

疯狂数数

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

所以cal1.set(2013, 11, 30)cal2.set(2013, 12, 1)就是10月30日到11月1日,确实是两天,占10月31日,你显然误以为是11月30日到12月1日,但是没有.

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

java.time

幸运的是,我们现在有 java.time classes。您没有理由使用遗留日期时间 class 的可怕混乱,例如 DateCalendarSimpleDateFormat.

要与 Java 7 一起使用,请参阅下方项目符号中链接的 ThreeTen-Backport 项目。

LocalDate

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

时区对于确定日期至关重要。对于任何给定时刻,日期在全球范围内因地区而异。例如,Paris France is a new day while still “yesterday” in Montréal Québec.

午夜后几分钟

如果未指定时区,JVM 将隐式应用其当前默认时区。该默认值可以 change at any moment during runtime(!), so your results may vary. Better to specify your desired/expected time zone 明确作为参数。

指定 proper time zone name in the format of continent/region, such as America/Montreal, Africa/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 月的编号为 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 enum objects pre-defined, one for each month of the year. Tip: Use these Month objects throughout your codebase rather than a mere integer number to make your code more self-documenting, ensure valid values, and provide type-safety

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

ChronoUnit.DAYS

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

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

关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

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

从哪里获得java.time classes?

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

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

Parameters:

year - the value used to set the YEAR calendar field.

month - the value used to set the MONTH calendar field. Month value is 0-based. e.g., 0 for January.

date - the value used to set the DAY_OF_MONTH calendar field.

如果您考虑到这一点重新检查您的示例,并记住日期得到 "normalized"(即,如果您指定第 13 个月,它将成为下一年的第一个月),您会发现所有计算出的差异实际上都是正确的。

what would be the easier way to do it then? Using the same Calendar class

使用 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 class 的命名常量作为您希望设置的日期的月份,以减少混淆。

其次,要让 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);
    }

这会打印:

Answer returned: 1

我当然不鼓励按照我展示的方式来做。它的代码行数相对较多,而且很容易忘记一个细节并得到错误的结果或出现差一错误。如果可以,升级到 Java 8 或 9 或 10 或 11。如果你不能, 也可以很好地与 ThreeTen Backport 库一起工作,请参阅他关于 Java SE 6 和 Java SE 7 的项目符号。

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