简体   繁体   English

在 DST 当天将 GregorianCalendar 转换为 Date 会丢失一个小时吗?

[英]Converting GregorianCalendar to Date on day of DST loses an hour?

I'm troubleshooting an issue with converting a GregorianCalendar that only represents the current date (ie// 2013-03-10 00:00:00) to a java.util.Date object.我正在解决将仅表示当前日期(即// 2013-03-10 00:00:00)的 GregorianCalendar 转换为 java.util.Date 对象的问题。 The idea behind this test is to take two dates - one with only the current date, and one with only the current time (ie// 1970-01-01 12:30:45), and combine them into one date representing the Date and Time (2013-03-10 12:30:45).这个测试背后的想法是取两个日期 - 一个只有当前日期,一个只有当前时间(即// 1970-01-01 12:30:45),并将它们组合成一个代表日期的日期和时间 (2013-03-10 12:30:45)。

On the day when the DST switch occured, the test failed - because converting the GregorianCalendar to a date object (Date date = dateCal.getTime(); in the code below) lost an hour and thus rolled back to (2013-03-09 23:00:00).在 DST 切换发生的那天,测试失败了 - 因为将 GregorianCalendar 转换为日期对象(Date date = dateCal.getTime(); 在下面的代码中)丢失了一个小时,因此回滚到 (2013-03-09 23:00:00)。 How can I make this not happen?我怎样才能让这不会发生?

public static Date addTimeToDate(Date date, Date time) {
    if (date == null) {
        throw new IllegalArgumentException("date cannot be null");
    } else if (time == null) {
        throw new IllegalArgumentException("time cannot be null");
    } else {
        Calendar timeCal = GregorianCalendar.getInstance();
        timeCal.setTime(time);

        long timeMs = timeCal.getTimeInMillis() + timeCal.get(Calendar.ZONE_OFFSET) + timeCal.get(Calendar.DST_OFFSET);
        return addMillisecondsToDate(date, timeMs);
    }
}


@Test
public void testAddTimeToDate() {
    Calendar expectedCal = Calendar.getInstance();
    Calendar dateCal = Calendar.getInstance();
    dateCal.clear();
    dateCal.set(expectedCal.get(Calendar.YEAR), expectedCal.get(Calendar.MONTH), expectedCal.get(Calendar.DAY_OF_MONTH));

    Calendar timeCal = Calendar.getInstance();
    timeCal.clear();
    timeCal.set(Calendar.HOUR_OF_DAY, expectedCal.get(Calendar.HOUR_OF_DAY));
    timeCal.set(Calendar.MINUTE, expectedCal.get(Calendar.MINUTE));
    timeCal.set(Calendar.SECOND, expectedCal.get(Calendar.SECOND));
    timeCal.set(Calendar.MILLISECOND, expectedCal.get(Calendar.MILLISECOND));

    Date expectedDate = expectedCal.getTime();
    Date date = dateCal.getTime();
    Date time = timeCal.getTime();

    Date actualDate = DateUtil.addTimeToDate(date, time);

    assertEquals(expectedDate, actualDate);
}

Why are you including the timezone offsets in your calculation?为什么要在计算中包含时区偏移量? when you are working with milliseconds in Java, they are always in UTC.当您在 Java 中使用毫秒时,它们始终使用 UTC。 you don't need to do any additional conversions.您不需要进行任何额外的转换。

Your biggest problem is probably trying to do these date/time calculations manually.您最大的问题可能是尝试手动进行这些日期/时间计算。 you should be using the Calendar class itself to handle the calculations.您应该使用 Calendar 类本身来处理计算。

I tried and did not get a difference.我试过了,没有区别。 Even varied locale, and substituted GregorianCalendar with Calendar..甚至不同的语言环境,并用 Calendar 替换 GregorianCalendar ..

Used:用过的:

private static Date addMillisecondsToDate(Date date, long timeMs) {
    return new Date(date.getTime() + timeMs);
}

The upcoming Java 8 has better date/time support.即将推出的 Java 8 具有更好的日期/时间支持。

This is how I ended up refactoring my method to compensate for the lost / gained hour due to DST:这就是我最终重构我的方法以补偿由于夏令时而损失/增加的小时数的方式:

public static Date addTimeToDate(Date date, Date time) {
    if (date == null) {
        throw new IllegalArgumentException("date cannot be null");
    } else if (time == null) {
        throw new IllegalArgumentException("time cannot be null");
    } else {
        Calendar dateCal = GregorianCalendar.getInstance();
        dateCal.setTime(date);

        Calendar timeCal = GregorianCalendar.getInstance();
        timeCal.setTime(time);
        int zoneOffset = timeCal.get(Calendar.ZONE_OFFSET);

        if (dateCal.get(Calendar.MONTH) == Calendar.MARCH) {
            if (Calendar.SUNDAY == dateCal.get(Calendar.DAY_OF_WEEK) && dateCal.get(Calendar.DAY_OF_MONTH) >= 7
                    && dateCal.get(Calendar.DAY_OF_MONTH) <= 14 && timeCal.get(Calendar.HOUR_OF_DAY) >= 3) {
                zoneOffset -= TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
            }
        } else if (dateCal.get(Calendar.MONTH) == Calendar.NOVEMBER) {
            if (Calendar.SUNDAY == dateCal.get(Calendar.DAY_OF_WEEK) && dateCal.get(Calendar.DAY_OF_MONTH) <= 7
                    && timeCal.get(Calendar.HOUR_OF_DAY) >= 3) {
                zoneOffset += TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
            }
        }
        long timeMs = timeCal.getTimeInMillis() + zoneOffset + timeCal.get(Calendar.DST_OFFSET);
        return addMillisecondsToDate(date, timeMs);
    }
}

I'm not fond of this method because if the rules for DST ever change then this method will need to be updated.我不喜欢这种方法,因为如果 DST 的规则发生变化,那么这种方法将需要更新。 Is there a library that would perform a similar function ?是否有可以执行类似功能的库?

tl;dr tl;博士

ZonedDateTime.of(
    LocalDate.parse( "2013-03-10" ) ,
    LocalTime.parse( "12:30:45" ) ,
    ZoneId.of( "Africa/Tunis" )
)                                    // Instantiate a `ZonedDateTime` object.
.toString()                          // Moment seen through wall-clock time of people in Tunisia time zone.

2013-03-10T12:30:45+01:00[Africa/Tunis] 2013-03-10T12:30:45+01:00[非洲/突尼斯]

ZonedDateTime.of(
    LocalDate.parse( "2013-03-10" ) ,
    LocalTime.parse( "12:30:45" ) ,
    ZoneId.of( "Africa/Tunis" )
)
.toInstant()                         // Convert to `Instant` from `ZonedDateTime`, for UTC value.
.toString()                          // Same moment, adjusted into wall-clock time of UTC. The Tunisian wall-clock is an hour ahead of UTC, but both represent the same simultaneous moment, same point on the timeline.

2013-03-10T11:30:45Z 2013-03-10T11:30:45Z

UTC versus Zoned UTC 与分区

converting the GregorianCalendar to a date object … lost an hour and thus rolled back将 GregorianCalendar 转换为日期对象……丢失了一个小时,因此回滚了

A GregorianCalendar includes a time zone. GregorianCalendar包括时区。 If you do not specify a time zone, the JVM's current default time zone is implicitly assigned.如果不指定时区,则隐式分配 JVM 的当前默认时区。 A java.util.Date , in contrast, is always in UTC.相比之下, java.util.Date始终采用 UTC。 Confusingly, the Date::toString method dynamically assigns the JVM's current default time zone while generating the string, creating the illusion of an assigned time zone when in fact the internal value is UTC.令人困惑的是, Date::toString方法在生成字符串时动态分配 JVM 的当前默认时区,造成分配时区的错觉,而实际上内部值为 UTC。 An awful confounding mess.一个可怕的混乱混乱。

We cannot further diagnose your specifics because you did not provide information about the time zones involved on your machine.我们无法进一步诊断您的具体情况,因为您没有提供有关您机器上所涉及时区的信息。

But this is all moot, as you should be using the java.time classes instead.但这一切都没有实际意义,因为您应该改用java.time类。

Avoid legacy date-time classes避免遗留的日期时间类

You are using troublesome old date-time classes that are now legacy, supplanted by the modern java.time classes.您正在使用麻烦的旧日期时间类,这些类现在是遗留的,被现代java.time类所取代。

java.time时间

The idea behind this test is to take two dates - one with only the current date, and one with only the current time (ie// 1970-01-01 12:30:45), and combine them into one date representing the Date and Time (2013-03-10 12:30:45).这个测试背后的想法是取两个日期 - 一个只有当前日期,一个只有当前时间(即// 1970-01-01 12:30:45),并将它们组合成一个代表日期的日期和时间 (2013-03-10 12:30:45)。

For a time-of-day, use LocalTime .对于一天中的某个时间,请使用LocalTime For a date-only, use LocalDate .对于仅限日期,请使用LocalDate

LocalDate ld = LocalDate.parse( "2013-03-10" ) ;
LocalTime lt = LocalTime.parse( "12:30:45" ) ;

Neither of those have a time zone, nor offset-from-UTC.这些都没有时区,也没有从 UTC 偏移。 So they have no meaning until assigned a zone or offset.因此,在分配区域或偏移量之前,它们没有任何意义。

Specify a proper time zone name in the format of continent/region , such as America/Montreal , Africa/Casablanca , or Pacific/Auckland .continent/region的格式指定正确的时区名称,例如America/MontrealAfrica/CasablancaPacific/Auckland Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).永远不要使用ESTIST等 3-4 个字母的缩写,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;

Assign the zone to the date and time to get a ZonedDateTime .将区域分配给日期和时间以获得ZonedDateTime

ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

Now we have an actual moment, a point on the timeline.现在我们有一个实际的时刻,时间轴上的一个点。 The ZonedDateTime class adjusts your time-of-day if your passed LocalTime is not valid on that particular date in that zone.如果您传递的ZonedDateTime在该区域的特定日期无效, LocalTime ZonedDateTime类会调整您的时间。 Such an adjustment is needed in case of an anomaly such as Daylight Saving Time (DST) .在诸如夏令时 (DST)之类的异常情况下需要进行此类调整。 Be sure to read the doc to understand the algorithm of that adjustment, to see if you agree its approach.请务必阅读文档以了解该调整的算法,看看您是否同意其方法。

To see that same moment in UTC, extract an Instant .要在 UTC 中查看同一时刻,请提取Instant Same point on the timeline, different wall-clock time.时间线上的相同点,不同的挂钟时间。

Instant instant = zdt.toInstant() ;

About java.time关于java.time

The java.time framework is built into Java 8 and later. java.time框架内置于 Java 8 及更高版本中。 These classes supplant the troublesome old legacy date-time classes such as java.util.Date , Calendar , & SimpleDateFormat .这些类取代麻烦的老传统日期时间类,如java.util.DateCalendar ,和SimpleDateFormat

The Joda-Time project, now in maintenance mode , advises migration to the java.time classes.现在处于维护模式Joda-Time项目建议迁移到java.time类。

To learn more, see the Oracle Tutorial .要了解更多信息,请参阅Oracle 教程 And search Stack Overflow for many examples and explanations.并在 Stack Overflow 上搜索许多示例和解释。 Specification is JSR 310 .规范是JSR 310

Where to obtain the java.time classes?从哪里获得 java.time 类?

The ThreeTen-Extra project extends java.time with additional classes. ThreeTen-Extra项目用额外的类扩展了 java.time。 This project is a proving ground for possible future additions to java.time.该项目是未来可能添加到 java.time 的试验场。 You may find some useful classes here such as Interval , YearWeek , YearQuarter , and more .您可以在这里找到一些有用的类,比如IntervalYearWeekYearQuarter ,和更多

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM