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

我们在应用程序中使用固定时间段。 当用户添加新的时段时,默认情况下应该是第二天的上午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. 

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

问题是:我们怎样才能独立于机器的时区设计我们的unit testing?

目前,小时跨度的计算是使用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服务器上失败的unit testing:

  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,所以测试甚至不在我们这边。

先谢谢你。

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

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

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

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

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

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

有夏令时抵消。

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

指定时区

我们怎样才能独立于机器的时区设计我们的unit testing?

始终指定时区

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

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

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

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

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

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

在Joda-Time ,那将是……

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

思考/使用UTC

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

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

24小时与1天

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

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

示例代码

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

 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 

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