使用 Joda 处理时区偏移转换和夏令时
Handling time zone offset transition and daylight savings time with Joda
我正在尝试解析日期时间字符串并创建 Joda DateTime 对象。
我的数据来自旧数据库,该数据库存储日期时间字符串但未指定 timezone/offset。尽管未存储日期时间字符串的 timezone/offset,但遗留系统的业务规则是所有日期时间均以东部时间存储。不幸的是,我无权更新遗留数据库存储日期时间字符串的方式。
因此,我使用 JODA 的 "US/Eastern" 时区解析日期时间字符串。
当 dateTime 字符串落在 "disappears" 开启夏令时的小时内时,此方法会引发 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());
}
}
}
}
产生的输出:
*********** 1st attempt ***********
Cannot parse "2014-03-09 02:00:00.000": Illegal instant due to time zone offset transition (America/New_York)
2014-03-08T02:00:00.000-05:00
*********** 2nd attempt ***********
2014-03-09T02:00:00.000-05:00
2014-03-08T02:00:00.000-05:00
*********** 3rd attempt ***********
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 经验的人能看到任何东西 wrong/dangerous 在这种方法中。
或者:是否有更好的方法来处理 Joda 的时区偏移转换?
您的问题归结为
I have to cope with timestamp values that are not technically valid and interpret them consistently
你用它们做什么完全取决于你的要求。如果您正在为雇主编写软件,则项目所有者必须是做出该决定的人。如果 designer/architect 未指定,您作为开发人员尚无权决定如何处理无效输入。
我建议你回到项目 owner/manager,将问题告知他们(输入包含 date/time 个实际不存在的邮票)并让他们决定如何解决处理它们。
小心。方法 withZone(...) 的行为记录如下:
Returns a copy of this datetime with a different time zone, preserving
the millisecond instant.
记住这一点,你必须明白 EST 和 "America/New_York"(比过时的 id "US/Eastern" 更好)是不一样的。第一个 (EST) 具有固定偏移量(无 DST),但第二个具有 DST,包括可能的间隙。如果您确定
,您应该只应用 EST 作为 Eastern 的替代品
a) 您已经处于异常模式(应该接受在东区正常解析的日期时间而无需重复解析,否则应用 EST 会伪造解析的瞬间),
b) 你明白在第二次(和第三次)尝试中选择 EST 就是选择 DST 转换后的瞬间。
关于此 limitations/constraints,您的变通方法将起作用(但仅适用于特殊的 EST 对 America/New_York)。我个人觉得使用 exception-based 逻辑来解决 Joda-Time 的严重限制是很可怕的。作为反例,新的 JSR-310 在处理间隙时不使用异常策略,而是在间隙的大小推动间隙后选择稍后的瞬间的策略(就像旧的 java.util.Calendar
-stuff)。
我建议你先听从@Jim Garrison 的建议,看看是否可以在应用这样的解决方法之前纠正错误的数据(我赞成他的回答)。
阅读原始规范要求后更新(过时 - 见下文):
如果遗留系统的规范说所有时间都存储在 EST 中,那么您应该这样解析它并且根本不要使用 "America/New_York" 进行解析.相反,您可以在第二阶段将解析的 EST-instants 转换为 New-York-time(使用 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
评论和澄清OP后更新:
现在确认 遗留系统不在 EST 中存储时间戳(具有固定偏移量 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
根据 OP 的最后评论再次澄清:
生成DateTime
的方法toDateTime(DateTimeZone)记录如下:
In a daylight saving overlap, when the same local time occurs twice,
this method returns the first occurrence of the local time.
换句话说,如果重叠(秋天),它会选择较早的偏移量。所以不需要调用
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-behaviour。在解决差距(EDT 或 EST)的情况下,您更喜欢哪个偏移量取决于您,但在这里获得相同的瞬间至关重要(ldt.plusHours(1).toDateTime(EDT)
与 result = ldt.toDateTime(EST)
)。
我正在尝试解析日期时间字符串并创建 Joda DateTime 对象。
我的数据来自旧数据库,该数据库存储日期时间字符串但未指定 timezone/offset。尽管未存储日期时间字符串的 timezone/offset,但遗留系统的业务规则是所有日期时间均以东部时间存储。不幸的是,我无权更新遗留数据库存储日期时间字符串的方式。
因此,我使用 JODA 的 "US/Eastern" 时区解析日期时间字符串。
当 dateTime 字符串落在 "disappears" 开启夏令时的小时内时,此方法会引发 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());
}
}
}
}
产生的输出:
*********** 1st attempt *********** Cannot parse "2014-03-09 02:00:00.000": Illegal instant due to time zone offset transition (America/New_York) 2014-03-08T02:00:00.000-05:00 *********** 2nd attempt *********** 2014-03-09T02:00:00.000-05:00 2014-03-08T02:00:00.000-05:00 *********** 3rd attempt *********** 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 经验的人能看到任何东西 wrong/dangerous 在这种方法中。
或者:是否有更好的方法来处理 Joda 的时区偏移转换?
您的问题归结为
I have to cope with timestamp values that are not technically valid and interpret them consistently
你用它们做什么完全取决于你的要求。如果您正在为雇主编写软件,则项目所有者必须是做出该决定的人。如果 designer/architect 未指定,您作为开发人员尚无权决定如何处理无效输入。
我建议你回到项目 owner/manager,将问题告知他们(输入包含 date/time 个实际不存在的邮票)并让他们决定如何解决处理它们。
小心。方法 withZone(...) 的行为记录如下:
Returns a copy of this datetime with a different time zone, preserving the millisecond instant.
记住这一点,你必须明白 EST 和 "America/New_York"(比过时的 id "US/Eastern" 更好)是不一样的。第一个 (EST) 具有固定偏移量(无 DST),但第二个具有 DST,包括可能的间隙。如果您确定
,您应该只应用 EST 作为 Eastern 的替代品a) 您已经处于异常模式(应该接受在东区正常解析的日期时间而无需重复解析,否则应用 EST 会伪造解析的瞬间),
b) 你明白在第二次(和第三次)尝试中选择 EST 就是选择 DST 转换后的瞬间。
关于此 limitations/constraints,您的变通方法将起作用(但仅适用于特殊的 EST 对 America/New_York)。我个人觉得使用 exception-based 逻辑来解决 Joda-Time 的严重限制是很可怕的。作为反例,新的 JSR-310 在处理间隙时不使用异常策略,而是在间隙的大小推动间隙后选择稍后的瞬间的策略(就像旧的 java.util.Calendar
-stuff)。
我建议你先听从@Jim Garrison 的建议,看看是否可以在应用这样的解决方法之前纠正错误的数据(我赞成他的回答)。
阅读原始规范要求后更新(过时 - 见下文):
如果遗留系统的规范说所有时间都存储在 EST 中,那么您应该这样解析它并且根本不要使用 "America/New_York" 进行解析.相反,您可以在第二阶段将解析的 EST-instants 转换为 New-York-time(使用 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
评论和澄清OP后更新:
现在确认 遗留系统不在 EST 中存储时间戳(具有固定偏移量 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
根据 OP 的最后评论再次澄清:
生成DateTime
的方法toDateTime(DateTimeZone)记录如下:
In a daylight saving overlap, when the same local time occurs twice, this method returns the first occurrence of the local time.
换句话说,如果重叠(秋天),它会选择较早的偏移量。所以不需要调用
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-behaviour。在解决差距(EDT 或 EST)的情况下,您更喜欢哪个偏移量取决于您,但在这里获得相同的瞬间至关重要(ldt.plusHours(1).toDateTime(EDT)
与 result = ldt.toDateTime(EST)
)。