使用Joda处理时区偏移转换和夏令时

6

我正在尝试解析日期时间字符串并创建Joda DateTime对象。

我的数据来自遗留数据库,存储日期时间字符串时没有指定时区/偏移量。虽然日期时间字符串的时区/偏移量未存储,但是遗留系统的业务规则要求所有日期时间都以东部时间存储。不幸的是,我没有权限更新遗留数据库存储日期时间字符串的方式。

因此,我使用JODA的“US / Eastern”时区解析日期时间字符串。

当夏令时开启时,如果dateTime字符串落在“消失”的那个小时内,这种方法会抛出illegalInstance异常。

我创建了以下示例代码来演示这种行为并展示我的提议的解决方法。

public class FooBar {
public static final DateTimeZone EST = DateTimeZone.forID("EST");
public static final DateTimeZone EASTERN = DateTimeZone.forID("US/Eastern");

public static final DateTimeFormatter EST_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EST);
public static final DateTimeFormatter EASTERN_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EASTERN);


public static void main(String[] args) {
    final String[] listOfDateTimeStrings = {"2014-03-09 02:00:00.000", "2014-03-08 02:00:00.000"}; 

    System.out.println(" *********** 1st attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            final DateTime dateTime = DateTime.parse(dateTimeString, EASTERN_FORMATTER);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }

    System.out.println(" *********** 2nd attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            final DateTime dateTime = DateTime.parse(dateTimeString, EST_FORMATTER);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }

    System.out.println(" *********** 3rd attempt  *********** ");
    for (String dateTimeString: listOfDateTimeStrings){
        try{
            DateTime dateTime = DateTime.parse(dateTimeString, EST_FORMATTER);
            dateTime = dateTime.withZone(EASTERN);
            System.out.println(dateTime);       
        }
        catch(Exception e){
            System.out.println(e.getMessage());
        }
    }       

}

产生的输出:

 *********** 第一次尝试 *********** 
无法解析“2014-03-09 02:00:00.000”:由于时区偏移转换(美国/纽约),因此时间不合法
2014-03-08T02:00:00.000-05:00
 *********** 第二次尝试 *********** 
2014-03-09T02:00:00.000-05:00
2014-03-08T02:00:00.000-05:00
 *********** 第三次尝试 *********** 
2014-03-09T03:00:00.000-04:00
2014-03-08T02:00:00.000-05:00

在“第三次尝试”中,我得到了预期的结果:第一个日期时间具有-04:00的偏移量,因为它落在2015年夏令时的第一小时内。第二个时间戳具有-05:00的偏移量,因为它落在夏令时之外。

这样做是否安全:

DateTime dateTime = DateTime.parse(dateTimeString, A_FORMATTER_WITH_TIME_ZONE_A);
dateTime = dateTime.withZone(TIME_ZONE_B);

我已经用几种不同的日期时间字符串和时区测试了这段代码(到目前为止,它对所有测试用例都有效),但我想知道有没有更多Joda经验的人能够看出这种方法是否存在任何错误/危险。

或者说:有没有更好的方法来处理使用Joda进行时区偏移转换?


1
+1 很好的问题,只有一个小小的例外:“代码是否安全可用?”你所说的“安全”是什么意思?如果您使用足够的边缘情况进行测试,它是否会产生预期和期望的行为?您需要更详细地描述您具体要求的内容。 - Jim Garrison
@JimGarrison,感谢您的指引;我已经更新了问题的结尾,希望现在更加具体明确。 - YB -Abeokuta
2
当你说“所有日期时间都存储在EST中”时,你是真的意味着它们都存储在UTC-5 - 东部标准时间,还是指它们存储在东部时间,这是一年中的一部分为UTC-5(EST),另一部分为UTC-4(EDT)?因为你也说,“当在EST时区切换夏令时”,我想你是指“东部时间”,而不是“EST”。(夏令时不适用于标准时间。) - Matt Johnson-Pint
@MattJohnson 您是正确的,我想说的是“所有日期时间都存储在东部时间”。我已经编辑了问题。对于造成的困惑,我深表歉意。 - YB -Abeokuta
2个回答

3
您的问题可以简化为:
我需要处理一些时间戳值,这些值在技术上并不合法,需要保持解释一致性。
您对它们的处理完全取决于您的要求。如果您正在为雇主编写软件,则项目所有者必须决定如何处理无效输入。如果设计师/架构师尚未指定如何处理无效输入,您作为开发人员还没有权力决定如何处理。
我建议您回到项目所有者/经理那里,告知他们存在问题(输入包含实际不存在的日期/时间戳),并让他们决定如何处理它们。

这是最正确的答案。尽管如此,我不能接受它。不幸的是,我需要找到一种解决方法,可以将无效值解释为我的PO没有采取修复根本问题的艰巨任务。 - YB -Abeokuta
我并没有说他们必须修复根本问题,只是要决定用于解释您正在编写的代码中无效时间戳的规则。在处理遗留系统时经常会发生这种情况。没有决策是完美的,但关键的任务是记录您所做出的决定。未来的开发人员将感谢您识别问题并建立如何处理它的方法。 - Jim Garrison

3

请注意,方法withZone(...)的行为如下所述:

返回一个带有不同时区的此日期时间的副本,保留毫秒级别的瞬时值。

记住,你必须理解EST和"America/New_York"(比过时的ID "US/Eastern" 更好)并不相同。第一个(EST)具有固定偏移量(无DST),但第二个包括可能的间隙DST。只有在确保以下两点的情况下,才应该将EST应用于Eastern的替换:

a)您已经处于异常模式(通常应接受Eastern区域中解析的日期时间而无需重复解析,否则应用EST将造成解析瞬时值的错误),

b)您了解在第二次(和第三次)尝试中选择EST是选择DST转换后的瞬时值。

关于这些限制/约束,你的解决方法可以解决问题(但仅适用于特殊的EST与America/New_York组合)。就个人而言,我发现使用基于异常的逻辑来解决Joda-Time的严重限制令人震惊。作为反例,新的JSR-310在处理间隙时不使用异常策略,而是选择间隙后推动间隔大小的较晚瞬时值的策略(类似于旧的java.util.Calendar)。

我建议您首先遵循@Jim Garrison的建议,查看是否可以更正损坏的数据,然后再应用此解决方法(对他的答案点赞)。

阅读您原始规范要求后的更新(已过时-请参见下文):

如果传统系统的规范说所有时间都保存在EST中,那么你应该按照这种方式解析它,并且根本不使用"America/New_York"来进行解析。相反,您可以在第二阶段将解析的EST瞬时值转换为New-York时间(使用withZone(EASTERN))。这样,您就没有任何异常逻辑,因为(解析的)瞬时值始终可以以无歧义的方式转换为本地时间表示形式(类型为DateTime的解析瞬时值,转换结果包含本地时间)。代码示例:

public static final DateTimeZone EST = DateTimeZone.forID("EST");
public static final DateTimeZone EASTERN = DateTimeZone.forID("US/Eastern");
public static final DateTimeFormatter EST_FORMATTER = 
  DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(EST);

// in your parsing method...
String input = "2014-03-09 02:00:00.000";
DateTime dt = EST_FORMATTER.parseDateTime(input);
System.out.println(dt); // 2014-03-09T02:00:00.000-05:00
System.out.println(dt.withZone(EASTERN)); // 2014-03-09T03:00:00.000-04:00

根据原作者的评论和澄清更新:

现在已经确认遗留系统不会以固定偏移UTC-05存储时间戳,而是以EASTERN区域 ("America/New_York",具有EST或EDT的可变偏移量) 存储。首要行动应该是联系无效时间戳的供应商,以查看他们是否可以更正数据。否则,您可以使用以下解决方法。

关于您的输入中包含没有任何偏移或区域信息的时间戳的事实,我建议首先将其解析为LocalDateTime

=> 静态初始化部分

// Joda-Time cannot parse "EDT" so we use hard-coded offsets
public static final DateTimeZone EST = DateTimeZone.forOffsetHours(-5);
public static final DateTimeZone EDT = DateTimeZone.forOffsetHours(-4);

public static final DateTimeZone EASTERN = DateTimeZone.forID("America/New_York");
public static final org.joda.time.format.DateTimeFormatter FORMATTER = 
    org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");

=> 在你的解析方法中

String input = "2014-03-09 02:00:00.000";
LocalDateTime ldt = FORMATTER.parseLocalDateTime(input); // always working
System.out.println(ldt); // 2014-03-09T02:00:00.000
DateTime result;

try {
    result = ldt.toDateTime(EASTERN);
} catch (IllegalInstantException ex) {
    result = ldt.plusHours(1).toDateTime(EDT); // simulates a PUSH-FORWARD-strategy at gap
    // result = ldt.toDateTime(EST); // the same instant but finally display with EST offset
}
System.out.println(result); // 2014-03-09T03:00:00.000-04:00
// if you had chosen <<<ldt.toDateTime(EST)>>> then: 2014-03-09T02:00:00.000-05:00

针对上一位用户的评论,需要澄清一下:

方法toDateTime(DateTimeZone)生成一个DateTime,其文档如下:

在夏令时重叠期间,当同一本地时间出现两次时, 此方法返回本地时间的第一次出现。

换句话说,在重叠期间(秋季),它会选择较早的偏移量。因此,在这种情况下无需调用

result = ldt.toDateTime(EASTERN).withEarlierOffsetAtOverlap();

然而,在这里它并不会造成任何伤害,考虑到文档的需要,你可能更喜欢使用它。另一方面,对于间隙而言,调用异常处理是没有任何意义的。

result = ldt.toDateTime(EDT).withEarlierOffsetAtOverlap();

因为EDT(以及EST)是一个固定的偏移量,永远不会发生重叠。因此,在这里方法withEarlierOffsetAtOverlap()不起作用。此外:在EDT的情况下省略ldt.plusHours(1)的更正是不可行的,并且将产生另一个瞬间。在编写此额外说明之前已由我进行了测试,但是当然,您可以使用替代方案ldt.toDateTime(EST)来实现您想要的效果(EDT!= EST,但通过plusHours(1)的更正,您可以获得相同的瞬间)。我只是注意到EDT示例是为了演示您如何精确地模拟标准JDK行为。在解决间隙时,您可以选择哪个偏移量(EDT或EST),但在此处获得相同的瞬间非常重要(ldt.plusHours(1).toDateTime(EDT)result = ldt.toDateTime(EST))。

2
虽然 EST 技术上意味着 UTC-5,但并不清楚 OP 是否理解这一点。他可能确实是指数据存储在“东部时间”,包括 EST 和 EDT。最好能够获得澄清。 - Matt Johnson-Pint
换句话说,虽然将 2014-03-09T02:00:00.000 转换为 2014-03-09T03:00:00.000-04:00 是有意义的,但其他有效的时间戳不太可能想要进行转换。如果 2014-07-01T00:00:00.000 被转化为 2014-07-01T01:00:00.000-04:00,这会很奇怪。 - Matt Johnson-Pint
FYI,我认为正确的解决方案应该涉及将字符串解析为LocalDateTime(使用我认为是格式化程序上的parseLocalDateTime),然后分别将其转换为在America/New_York(或US/Eastern如果你喜欢)时区的DateTime。但是,我不记得如何告诉Joda不要在转换间隙中抛出异常。(我知道它对于秋季有withEarlierOffsetAtOverlap,但我不确定它在春季能做什么。)(随意从中获取任何内容以进行更新)干杯! - Matt Johnson-Pint
@MattJohnson 很好的观点,我也不确定OP是否理解EST和America/New_York之间的区别。让我们看看。关于LocalDateTime,它对应于输入内容,但最终在使用夏令时区域(这里是类型为DateTime的实例)转换为即时时可能无法避免异常。Joda-Time没有提供平滑的过渡策略。其他库,如Noda-Time,在这方面提供了更多的选择(ZoneLocalMapping),或者我的库Time4J也可以。关于夏季的其他有效时间戳,我们不知道旧系统是否使用EST而不是UTC(奇怪但有可能)。 - Meno Hochschild
@YB-Abeokuta 感谢您的澄清。我已经更新了我的答案。抱歉使用基于异常的逻辑,如果您准备选择其他库(例如JDK中内置的时间库),则可以避免这种情况。 - Meno Hochschild
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接