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

Posted

技术标签:

【中文标题】Joda-Time,夏令时计算,时区独立测试【英文标题】:Joda-Time, daylight savings calculation, timezone independent tests 【发布时间】:2012-10-16 09:26:44 【问题描述】:

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

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

10 月 27 日上午 6 点至 10 月 28 日上午 6 点。在此期间执行从 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 日 CEST 9:00,endDate 是 10 月 28 日 8:00 CET,所以即使在我们这边测试也失败了。

提前谢谢你。

【问题讨论】:

aStartDateaEndDate 是什么?目前尚不清楚您要尝试获得什么结果,或者您想要使用哪个时区(假设您曾在一个地方讨论过 CST/CEST,并且GMT 在另一个)。您希望它有什么用途:TimeZone.getTimeZone(TimeZone.getTimeZone("GMT").getID()) @JonSkeet 关于示例代码 - 我只是想得到任何结果,所以现在对我来说没有多大意义。关于预期结果 - 我们希望确保在 DST 轮班时 - 1“天”的长度为 23 或 25 小时,具体取决于用户的时区。 【参考方案1】:

指定时区

我们如何能够独立于机器的时区设计我们的单元测试?

始终指定时区

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

如果省略时区,将使用 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) 值。转换为本地分区时间以向用户展示(就像您将字符串本地化一样)以及在需要用于业务目的的地方,例如确定特定时区定义的“一天”的开始。

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

【讨论】:

【参考方案2】:

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

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

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

不使用夏令时:zone.useDaylightTime() 返回false

尝试使用特定时区,例如

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

有夏令时偏移。

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

【讨论】:

非常感谢!我们解决了这个问题,通过测试当中欧有时移时,澳大利亚没有时移,反之亦然 - 使用特定时区:)【参考方案3】:

为什么不使用普通的 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() );

【讨论】:

因为它的 API 比 Joda Time 差很多 很多?从长远来看,解决问题但坚持使用 Joda Time 会更好。

以上是关于Joda-Time,夏令时计算,时区独立测试的主要内容,如果未能解决你的问题,请参考以下文章

使用 Joda-Time 获取给定日期和时区的 UTC 偏移量

夏令时计算

程序里的国际时区和夏令时

使用 Joda-Time 计算持续时间

Calendar日期类详解SimpleDateFormat时区Date夏令时常用方法,日期差获取当前时间

如何在android中处理时区和夏令时?