GregorianCalendar 的奇怪行为

发布于 2024-09-03 10:43:16 字数 855 浏览 5 评论 0原文

我刚刚遇到 GregorianCalendar 类的奇怪行为,我想知道我是否真的做了坏事。

仅当初始化日期的月份的实际最大值大于我要设置日历的月份时才会附加。

下面是示例代码:

    // today is 2010/05/31  
    GregorianCalendar cal = new GregorianCalendar();

    cal.set(Calendar.YEAR, 2010);
    cal.set(Calendar.MONTH, 1); // FEBRUARY

    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
    cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
    cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
    cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
    cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

    return cal.getTime(); // => 2010/03/03, wtf

我知道问题是由于日历初始化日期是 31 天的月份(五月)而引起的,这与设置为二月(28 天)的月份混淆了。修复很简单(只需在设置年份和月份之前将 day_of_month 设置为 1 ),但我想知道这是否真的是想要的行为。有什么想法吗?

I just encountered a strange behaviour with the GregorianCalendar class, and I was wondering if I really was doing something bad.

This only appends when the initialization date's month has an actualMaximum bigger than the month I'm going to set the calendar to.

Here is the example code :

    // today is 2010/05/31  
    GregorianCalendar cal = new GregorianCalendar();

    cal.set(Calendar.YEAR, 2010);
    cal.set(Calendar.MONTH, 1); // FEBRUARY

    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
    cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
    cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
    cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
    cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

    return cal.getTime(); // => 2010/03/03, wtf

I know the problem is caused by the fact that the calendar initialization date is a 31 day month ( may ), which mess with the month set to february (28 days). The fix is easy ( just set day_of_month to 1 before setting year and month ), but I was wondering is this really was the wanted behaviour. Any thoughts ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

讽刺将军 2024-09-10 10:43:16

它正在获取当前日期/时间的实际最大值。 5月有31天,比2月28日多了3天,因此将转移到3月3日。

您需要致电 Calendar#clear() 获取/创建它后:

GregorianCalendar cal = new GregorianCalendar();
cal.clear();
// ...

这会导致:(

Sun Feb 28 23:59:59 GMT-04:00 2010

根据我的时区,这是正确的)

正如答案之一所述, java.util.Calendar< /code> 和 Date 是史诗般的失败。在进行密集的日期/时间操作时,请考虑 JodaTime

It is getting the actual maximums of the current date/time. May has 31 days which is 3 more than 28 February and it will thus shift to 3 March.

You need to call Calendar#clear() after obtaining/creating it:

GregorianCalendar cal = new GregorianCalendar();
cal.clear();
// ...

This results in:

Sun Feb 28 23:59:59 GMT-04:00 2010

(which is correct as per my timezone)

As said in one of the answers, the java.util.Calendar and Date are epic failures. Consider JodaTime when doing intensive date/time operations.

暖风昔人 2024-09-10 10:43:16

是的,这就是它的工作原理。如果您从具有精确日期的 GregorianCalendar 开始,并通过使其不一致来修改它,那么您不应该相信您获得的结果。

根据有关 getActualMaximum(..) 的文档,它指出:

例如,如果此实例的日期为 2004 年 2 月 1 日,则 DAY_OF_MONTH 字段的实际最大值为 29,因为 2004 年是闰年,如果此实例的日期为 2005 年 2 月 1 日,则为 28

所以应该有效,但你必须为其提供一致的价值观。 2010 年 2 月 31 日 不正确,并且应用依赖于日期值的内容(例如 getActualMaximum)无法工作。应该如何自行修复呢?通过确定那个月份是错误的?或者说日子不对?

顺便说一句,正如每个人总是声明使用 JodaTime.. :)

Yes, this is how it is intended to work. If you start from a GregorianCalendar that has a precise date and you modify it by making it inconsistent then you shouldn't trust the results you obtain.

According to the documentation about getActualMaximum(..) it states:

For example, if the date of this instance is February 1, 2004, the actual maximum value of the DAY_OF_MONTH field is 29 because 2004 is a leap year, and if the date of this instance is February 1, 2005, it's 28.

So it is supposed to work but you have to feed it with consistent values. 31 February 2010 is not correct and applying things that relies on the date value (like getActualMaximum) can't work. How should it fix it by itself? By deciding that month is wrong? or that the day is wrong?

By the way, as everyone always states use JodaTime.. :)

葬シ愛 2024-09-10 10:43:16

我确信这不是想要的行为。我同样确信没有人在上课时真正考虑过这个用例。事实上,Calendar 在内部状态以及它如何管理所有 set 方法中的所有潜在转换方面存在一个非常大的问题。

如果您无法在项目中使用 JodaTime 或 JSR-310,请在使用 Calendar 类时进行大量单元测试。正如您在本例中所看到的,日历代码的行为有所不同,具体取决于您运行代码的月份的哪一天(或一天中的什么时间)。

I'm sure it is not wanted behavior. I'm just equally sure no one really thought that use case through when they made the class. The fact of the matter is that Calendar has a very big problem with internal state and how it manages all of the potential transitions in all the set methods.

If you can't use JodaTime or JSR-310 in your project, unit test heavily when using the Calendar class. As you can see in this case Calendar code behaves differently depending on what day of the month (or what time of the day) you run the code.

拔了角的鹿 2024-09-10 10:43:16

也许setLenient(boolean lenient)会为你解决这个问题。当我运行下面的代码时出现异常。

如果没有,乔达是一个更好的答案。

import java.util.Calendar;

public class CalTest
{
    public static void main(String[] args)
    {
        // today is 2010/05/31
        Calendar cal = Calendar.getInstance();
        cal.setLenient(false);

        cal.set(Calendar.YEAR, 2010);
        cal.set(Calendar.MONTH, 1); // FEBRUARY

        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        System.out.println(cal.getTime());
    }
}

Maybe setLenient(boolean lenient) will sort it out for you. I get an exception when I run the code below.

If not, Joda is a better answer.

import java.util.Calendar;

public class CalTest
{
    public static void main(String[] args)
    {
        // today is 2010/05/31
        Calendar cal = Calendar.getInstance();
        cal.setLenient(false);

        cal.set(Calendar.YEAR, 2010);
        cal.set(Calendar.MONTH, 1); // FEBRUARY

        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        System.out.println(cal.getTime());
    }
}
初心未许 2024-09-10 10:43:16

我想贡献现代答案。

    ZonedDateTime endOfFebruary2010 = LocalDate.of(2010, Month.MARCH, 1)
            .atStartOfDay(ZoneId.systemDefault())
            .minusNanos(1);
    System.out.println(endOfFebruary2010);

在我的时区运行,打印:

2010-02-28T23:59:59.999999999+01:00[欧洲/哥本哈根]

无论您在一年中的什么时间运行,打印输出都是相同的。对时区的依赖可能会令人遗憾,但可以通过指定所需的时区来修复,例如 ZoneId.of("Asia/Oral")。我正在使用并推荐 java.time,现代 Java 日期和时间 API。

如果您不可避免地需要一个老式的 java.util.Date 对象(并且仅在这种情况下),请转换:

    Date oldFashionedDate = Date.from(endOfFebruary2010.toInstant());
    System.out.println(oldFashionedDate);

2010 年欧洲中部时间 2 月 28 日星期日 23:59:59

如果您只需要计算某个月份的天数(这在 重复的问题):

    YearMonth ym = YearMonth.of(2011, Month.FEBRUARY);
    int numDays = ym.lengthOfMonth();
    System.out.println(numDays);

28

据我了解,您真正的问题是:

…我想知道这是否真的是想要的行为。有任何想法

我坚信无参数 GregorianCalendar 构造函数返回当前日期和当前时间是想要的行为。并且 Calender.set() 仅设置您显式设置的字段,并尝试保持其他字段不变。 2010 年 2 月 31 日溢出到 3 月,没有任何错误迹象,因为该月只有 28 天。这些设计决策的结合使我得出了不可避免的结论:您观察到的行为是设计使然的。

如果您认为这是一个糟糕的设计,我们很多人都同意您的观点。这也是为什么四年前用 java.time 来替代 CalendarGregorianCalendar 的原因。您将永远不需要再次使用日历

保留:我们的工具仍然依赖于 Java 1.7

java.time 在 Java 7 上运行良好。它只需要至少 Java 6

  • 在 Java 8 及更高版本以及较新的 Android 设备(据我所知,从 API 级别 26 开始)中,现代 API 是内置的。
  • 在 Java 6 和 7 中,获取 ThreeTen Backport,即新类的向后移植(ThreeTen for JSR 310;请参阅底部的链接)。
  • 在(较旧的)Android 上使用 ThreeTen Backport 的 Android 版本。它被称为 ThreeTenABP。并确保使用子包从 org. Threeten.bp 导入日期和时间类。

链接

I should like to contribute the modern answer.

    ZonedDateTime endOfFebruary2010 = LocalDate.of(2010, Month.MARCH, 1)
            .atStartOfDay(ZoneId.systemDefault())
            .minusNanos(1);
    System.out.println(endOfFebruary2010);

Running in my time zone this prints:

2010-02-28T23:59:59.999999999+01:00[Europe/Copenhagen]

The printout is the same no matter the time of year and month you run it. The dependency on time zone may be unfortunate, but can be mended by specifying which time zone you want, for example ZoneId.of("Asia/Oral"). I am using and recommending java.time, the modern Java date and time API.

If you indispensably need an old-fashioned java.util.Date object (and only in this case), convert:

    Date oldFashionedDate = Date.from(endOfFebruary2010.toInstant());
    System.out.println(oldFashionedDate);

Sun Feb 28 23:59:59 CET 2010

If you only needed a count of days in some month (this was asked in a duplicate question):

    YearMonth ym = YearMonth.of(2011, Month.FEBRUARY);
    int numDays = ym.lengthOfMonth();
    System.out.println(numDays);

28

As I understand, your real question was:

…I was wondering is this really was the wanted behaviour. Any thoughts
?

I strongly believe that it is wanted behaviour that the no-arg GregorianCalendar constructor returns the current day and the current time of day. And that Calender.set() only sets the fields that you explicitly set and tries to keep other fields unchanged. And that February 31, 2010 overflows into March without any sign of error because there were only 28 days in the month. The combination of these design decisions leads me to the inevitable conclusion: the behaviour you observed is by design.

If you think this is a poor design, we are many that agree with you. This was also why the replacement for Calendar and GregorianCalendar came out with java.time four years ago now. You will never need to use Calendar again.

Reservation: Our tool is still dependent on Java 1.7

java.time works nicely on Java 7. It just requires at least Java 6.

  • In Java 8 and later and on newer Android devices (from API level 26, I’m told) the modern API comes built-in.
  • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom).
  • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

Links

童话 2024-09-10 10:43:16

原因应该是,MONTH 具有类似枚举的逻辑结构。您可以轻松填充和读取数组/集合/列表。由于国际化,它必须是可枚举的(间接访问)。 DAY 只是一个可直接访问的整数。这就是区别。

The reason should be, that MONTH has an enumeration-like logical structure. You can easyly fill and read Arrays/Collections/Lists. Due to internationalization it has to be enumeratable (indirect access). DAY is just an direct accessible Integer. That's the difference.

稍尽春風 2024-09-10 10:43:16

日历从当天开始 - 在您的示例中为 2010 年 5 月 31 日。当您将月份设置为二月时,日期将更改为 2010 年 2 月 31 日,标准化为 2010 年 3 月 3 日,因此 cal.getActualMaximum(Calendar.DAY_OF_MONTH) 为 3 月返回 31。

Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, 2010);
c.set(Calendar.MONTH, Calendar.MAY);
c.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(c.getTime());
c.set(Calendar.MONTH, Calendar.FEBRUARY);
System.out.println(c.getTime());

输出:

Mon May 31 20:20:25 GMT+03:00 2010
Wed Mar 03 20:20:25 GMT+03:00 2010

要修复代码,您可以添加 cal.clear();或在设置月份之前设置日期 1..28

Calendar starts with current day - 31 may 2010 in your example. When you set month to February, date changes to 31 February 2010 which normalized to 3 March 2010, so cal.getActualMaximum(Calendar.DAY_OF_MONTH) returns 31 for March.

Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, 2010);
c.set(Calendar.MONTH, Calendar.MAY);
c.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(c.getTime());
c.set(Calendar.MONTH, Calendar.FEBRUARY);
System.out.println(c.getTime());

output:

Mon May 31 20:20:25 GMT+03:00 2010
Wed Mar 03 20:20:25 GMT+03:00 2010

To fix you code, you can add cal.clear(); or set day 1..28 before setting month

内心旳酸楚 2024-09-10 10:43:16

问题是 DAY_OF_MONTH 是从 1 开始的,而 0 则少一天!

The problem is that DAY_OF_MONTH is 1-based, day 0 is one day less!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文