Joda-Time,夏令时计算,时区独立测试

时间:2012-10-26 05:45:14

标签: java jodatime dst period

我们在应用程序中使用固定时间段。当用户添加新的时段时,默认情况下应该是第二天的上午6:00到上午6:00。

通常情况下,这是24小时,但有一个问题:执行夏令时更改时,该时间段的长度会发生变化。例如:

10月27日上午6:00至10月28日上午6:00。在此期间执行从CEST到CET时区的转换。因此,这段时间包含25小时:

From 27 October 6:00 AM to 28 October 3:00 AM - there are 21 hours
at 3:00 am the time is shifted back by 1 hour, so there are 4 hours until 28 October 6:00 AM.

我们遇到了这个问题并尝试编写单元测试以防止它再次出现。测试在我们的计算机上成功通过,但在CI服务器上失败(它在另一个时区)。

问题是:我们怎样才能独立于机器的时区设计我们的单元测试?

目前,小时跨度的计算是使用Joda-Time

计算的
    if ((aStartDate == null) || (aEndDate == null)) {
        return 0;
    }
    final DateTime startDate = new DateTime(aStartDate);
    final DateTime endDate = new DateTime(aEndDate);
    return Hours.hoursBetween(startDate, endDate).getHours();

单位测试在我们这边传递但在CI服务器上失败:

    Calendar calendar = Calendar.getInstance();
    calendar.set(2012, Calendar.OCTOBER, 27, 6, 0);
    startDate= calendar.getTime();
    calendar.set(2012, Calendar.OCTOBER, 28, 6, 0);
    endDate= calendar.getTime();

我们尝试将时区用于日历:

    TimeZone.setDefault(TimeZone.getTimeZone(TimeZone.getTimeZone("GMT").getID()));
    Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(TimeZone.getTimeZone("GMT").getID()));
    calendar.set(2012, Calendar.OCTOBER, 27, 6, 0, 0);
    startDate= calendar.getTime();
    calendar.set(2012, Calendar.OCTOBER, 28, 6, 0, 0);
    endDate= calendar.getTime();

但在这种情况下,结果startDate是10月27日9:00 CEST,endDate是10月28日8:00 CET,所以测试甚至不在我们这边。

提前谢谢。

3 个答案:

答案 0 :(得分:7)

指定时区

  

我们怎样才能独立于机器的时区设计我们的单元测试?

始终指定时区

几乎总是,您的代码应指定时区。仅当您真正需要用户/服务器的默认时区时才省略时区;即使这样,您也应该明确访问默认时区以使您的代码自我记录。

如果省略时区,将使用JVM的默认时区。

Joda-Time有多个地方可以传递DateTimeZone个对象,也可以调用withZone方法。

避免3-4个字母时区代码

避免使用CESTCET等时区代码。它们既不标准也不独特。当混淆/忽略夏令时(DST)时,它们经常被错误地使用。

相反,请使用proper time zone names。大多数名称是时区区域的大陆和主要城市的组合,用纯ASCII字符(没有变音符号)编写。例如,Europe/Chisinau

Joda-Time中,那将是......

DateTimeZone timeZone = DateTimeZone.forID( "Europe/Chisinau" );

思考/使用UTC

您真正的问题是考虑本地日期时间值,或尝试使用本地时间执行业务逻辑。这样做非常困难,因为一般的夏令时及其频繁的变化以及其他异常情况。

相反,请考虑宇宙的时间线而不是壁钟时间。尽可能将日期时间值存储和处理为UTC(GMT)值。转换为本地分区时间以呈现给用户(就像您将本地化字符串一样)以及用于商业目的所需的地方,例如确定" day"由特定时区定义。

24小时与1天

所以,如果你真的想要24小时之后,那就加上24小时,让挂钟时间落到可能的地方。如果您想要"第二天",意味着用户希望在时钟上看到相同的时间,那么添加1天(可能会在25小时之后)。 Joda-Time支持这两种逻辑。

注意下面的示例代码是Joda-Time支持计算一天24小时或通过调整时间来匹配相同的wall-clock time,因为夏令时或其他异常。

示例代码

以下是使用Joda-Time 2.3的一些示例代码。 Java 8中的新java.time包应具有与Joda-Time相似的功能。

DateTimeZone timeZone = DateTimeZone.forID( "Europe/Chisinau" );
DateTime start = new DateTime( 2012, 10, 27, 6, 0, 0, timeZone );
DateTime stop = start.plusHours( 24 ); // Time will be an hour off because of Daylight Saving Time (DST) anomaly.

DateTime nextDay = start.plusDays( 1 ); // A different behavior (25 hours).

Interval interval = new Interval( start, stop );
Period period = new Period( start, stop );
int hoursBetween = Hours.hoursBetween( start, stop ).getHours();

DateTime startUtc = start.withZone( DateTimeZone.UTC );
DateTime stopUtc = stop.withZone( DateTimeZone.UTC );

转储到控制台...

System.out.println( "start: " + start );
System.out.println( "stop: " + stop );
System.out.println( "nextDay: " + nextDay );
System.out.println( "interval: " + interval );
System.out.println( "period: " + period );
System.out.println( "hoursBetween: " + hoursBetween );
System.out.println( "startUtc: " + startUtc );
System.out.println( "stopUtc: " + stopUtc );

跑步时......

start: 2012-10-27T06:00:00.000+03:00
stop: 2012-10-28T05:00:00.000+02:00
nextDay: 2012-10-28T06:00:00.000+02:00
interval: 2012-10-27T06:00:00.000+03:00/2012-10-28T05:00:00.000+02:00
period: PT24H
hoursBetween: 24
startUtc: 2012-10-27T03:00:00.000Z
stopUtc: 2012-10-28T03:00:00.000Z

答案 1 :(得分:6)

根据我的理解,您希望在服务器上设置时区,以便在白天时间切换期间测试正确的小时数,而不是使用标准服务器时区。为此,时区需要能够使用夏令时偏移。

您在示例中在服务器上使用的时区

TimeZone zone = TimeZone.getTimeZone("GMT");

不使用日光时间:zone.useDaylightTime()返回false

请尝试使用特定的时区,例如

TimeZone zone = TimeZone.getTimeZone("Europe/Chisinau");

有夏令时偏移。

您还可以在Joda Time的DateTime类中使用时区,并避免使用Java的Calendar和Date类。

答案 2 :(得分:-2)

为什么不使用普通的java.util.Calendar?

    final Calendar startTime = new GregorianCalendar( 2012, Calendar.OCTOBER, 27, 2, 12, 0 );
    startTime.setTimeZone( TimeZone.getTimeZone( strTimeZone ) );
    System.out.println( startTime.getTimeInMillis() );