【问题标题】:Converting GregorianCalendar to Date on day of DST loses an hour?在 DST 当天将 GregorianCalendar 转换为 Date 会损失一个小时?
【发布时间】:2013-03-02 22:35:21
【问题描述】:

我正在解决将仅代表当前日期(即// 2013-03-10 00:00:00)的 GregorianCalendar 转换为 java.util.Date 对象的问题。这个测试背后的想法是取两个日期——一个只有当前日期,一个只有当前时间(即// 1970-01-01 12:30:45),并将它们组合成一个代表日期的日期和时间 (2013-03-10 12:30:45)。

在 DST 切换发生的那一天,测试失败 - 因为将 GregorianCalendar 转换为日期对象(Date date = dateCal.getTime(); 在下面的代码中)损失了一个小时,因此回滚到 (2013- 03-09 23:00:00)。我怎样才能避免这种情况发生?

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);
}

【问题讨论】:

  • Calendar.getInstance(Locale.FRANCE).getTime() 不使用公历不可行?
  • 为什么不清除dateCal 的时间字段(就像您已经做过的那样),然后将时间字段设置为timeCal 而不是DateUtil.addTimeToDate 的时间字段?即:使用Calendar 而不是Date
  • 顺便说一句:我猜assertEquals 有时或经常会失败,因为您调用Calendar.getInstance() 之间的时间间隔。
  • @Michael:Calendar 对象用于创建我们想要用于此测试的特定日期对象,目的是确保 DateUtil.addTimeToDate() 的功能。忽略 DateUtil 超出了此测试的范围。
  • @JoopEggen:您的建议只是不可行,因为我不知道您的意思。可以举个例子吗?

标签: java date


【解决方案1】:

为什么在计算中包含时区偏移量?当您在 Java 中使用毫秒时,它们在 UTC 中始终。您无需进行任何额外的转换。

您最大的问题可能是尝试手动进行这些日期/时间计算。您应该使用 Calendar 类本身来处理计算。

【讨论】:

  • 你认为Calendar类可以完全替代DateUtil方法吗?我想知道怎么做,但如果这太过分了,那么简单的真/假会让我更仔细地研究 Calendar 类。
  • @tamuren - 是的,这是整个类的 point,例如见docs.oracle.com/javase/6/docs/api/java/util/…
  • 我尝试为此使用日历,但简单地将两个日历相加并不能补偿 zone_offset 的变化,因此结果仍然相差 1 小时
【解决方案2】:

我试过了,但没有任何区别。甚至不同的语言环境,并用日历代替了 GregorianCalendar..

使用:

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

即将推出的 Java 8 具有更好的日期/时间支持。

【讨论】:

  • 如果您使用表示 3 月 10 日午夜的日期(表示从纪元凌晨 2 点开始的任何时间的日期),则结果比预期的多 1 小时。由于两个日期的偏移量均为 -8(如果您的 PST),那么当它们加在一起时,偏移量为 -7(因为您现在处于 PDT 中)并且时间比期望的时间长一个小时
【解决方案3】:

这就是我最终重构我的方法以补偿由于夏令时而损失/获得的时间的方式:

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);
    }
}

我不喜欢这种方法,因为如果 DST 的规则发生变化,那么这种方法就需要更新。有没有可以执行类似功能的库?

【讨论】:

    【解决方案4】:

    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[非洲/突尼斯]

    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

    UTC 与分区

    将 GregorianCalendar 转换为日期对象……损失了一个小时并因此回滚

    GregorianCalendar 包含一个时区。如果不指定时区,则隐式分配 JVM 当前的默认时区。相比之下,java.util.Date 始终采用 UTC。令人困惑的是,Date::toString 方法会在生成字符串时动态分配 JVM 的当前默认时区,从而产生分配时区的错觉,而实际上内部值是 UTC。一个可怕的混乱混乱。

    我们无法进一步诊断您的具体情况,因为您没有提供有关您机器上所涉及的时区的信息。

    但这一切都没有实际意义,因为您应该改用 java.time 类。

    避免使用旧的日期时间类

    您正在使用麻烦的旧日期时间类,这些类现在是遗留的,被现代 java.time 类所取代。

    java.time

    这个测试背后的想法是取两个日期——一个只有当前日期,一个只有当前时间(即// 1970-01-01 12:30:45),并将它们组合成一个日期表示日期和时间 (2013-03-10 12:30:45)。

    对于一天中的某个时间,请使用LocalTime。对于仅限日期,请使用 LocalDate

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

    它们都没有时区,也没有从 UTC 偏移。因此,在分配区域或偏移之前,它们没有任何意义。

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

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

    将区域分配给日期和时间以获取ZonedDateTime

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

    现在我们有了一个实际的时刻,时间线上的一个点。如果您通过的LocalTime 在该区域的特定日期无效,ZonedDateTime 课程会调整您的时间。如果出现Daylight Saving Time (DST) 等异常情况,则需要进行此类调整。请务必阅读文档以了解该调整的算法,看看您是否同意其方法。

    要在 UTC 中查看同一时刻,请提取 Instant。时间线上的同一点,不同的挂钟时间。

    Instant instant = zdt.toInstant() ;
    

    关于java.time

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

    Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

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

    从哪里获得 java.time 类?

    ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-08-24
      • 2023-03-06
      • 2018-08-14
      • 2014-09-11
      • 2019-06-04
      • 1970-01-01
      • 2020-12-21
      • 1970-01-01
      相关资源
      最近更新 更多