在夏令时日将GregorianCalendar转换为日期会损失一小时?

时间:2013-03-14 14:10:24

标签: java date

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

在DST切换发生的那天,测试失败 - 因为将GregorianCalendar转换为日期对象(日期日期= 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);
}

4 个答案:

答案 0 :(得分:0)

为什么在计算中包含时区偏移量?当您在Java中使用毫秒时,它们总是在UTC中。您无需进行任何其他转换。

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

答案 1 :(得分:0)

我试过并没有发挥作用。即使是不同的语言环境,也可以用Calendar ...替换GregorianCalendar。

用于:

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

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

答案 2 :(得分:0)

这就是为什么我最终重构我的方法以弥补由于夏令时造成的损失/增加的小时:

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的规则发生变化,则需要更新此方法。是否有一个可以执行类似功能的库?

答案 3 :(得分:0)

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与Zoned

  

将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。切勿使用诸如ESTIST之类的3-4字母缩写,因为它们不是真正的时区,不是标准化的,甚至不是唯一的(!)。

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

将区域分配到日期和时间以获得ZonedDateTime

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

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

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

Instant instant = zdt.toInstant() ;

关于 java.time

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

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

要了解详情,请参阅Oracle Tutorial。并搜索Stack Overflow以获取许多示例和解释。规范是JSR 310

从哪里获取java.time类?

ThreeTen-Extra项目使用其他类扩展java.time。该项目是未来可能添加到java.time的试验场。您可以在此处找到一些有用的课程,例如IntervalYearWeekYearQuartermore