java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME 无法解析时区名称
java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME fails to parse time zone names
我正在尝试从定义为使用 RFC1123 兼容日期时间规范的数据源解析时间戳。我的代码是:
value = Instant.from (DateTimeFormatter.RFC_1123_DATE_TIME.parse (textValue));
这对某些数据工作正常,但我得到了包含区域名称的字符串的异常,即使是在 RFC2822 中定义的字符串(它从 RFC1123 间接引用,因为它废弃了 RFC822)。示例:
java.time.format.DateTimeParseException: Text 'Sun, 20 Aug 2017 00:30:00 UT' could not be parsed at index 26
java.time.format.DateTimeParseException: Text 'Mon, 21 Aug 2017 15:00:00 EST' could not be parsed at index 26
如何说服DateTimeFormatter
接受这种类型的约会?
正如 , the javadoc says 注意到的 RFC_1123_DATE_TIME
"does not handle North American or military zone names, only 'GMT' and offset amounts".
要让它识别像 UT
和 EST
这样的短时区名称,唯一的方法是构建一个自定义格式化程序,其结构类似于 RFC_1123_DATE_TIME
的结构,但添加最后的短区ID。
此格式使用英语名称表示月份和星期几,因此一种替代方法是使用英语语言环境,但 source code 使用具有固定值的自定义地图,如果更改(评论说 区域设置数据可以通过应用程序代码 更改)。所以我们首先重新创建这些地图:
// custom map for days of week
Map<Long, String> dow = new HashMap<>();
dow.put(1L, "Mon");
dow.put(2L, "Tue");
dow.put(3L, "Wed");
dow.put(4L, "Thu");
dow.put(5L, "Fri");
dow.put(6L, "Sat");
dow.put(7L, "Sun");
// custom map for months
Map<Long, String> moy = new HashMap<>();
moy.put(1L, "Jan");
moy.put(2L, "Feb");
moy.put(3L, "Mar");
moy.put(4L, "Apr");
moy.put(5L, "May");
moy.put(6L, "Jun");
moy.put(7L, "Jul");
moy.put(8L, "Aug");
moy.put(9L, "Sep");
moy.put(10L, "Oct");
moy.put(11L, "Nov");
moy.put(12L, "Dec");
然后我重新创建与 RFC_1123_DATE_TIME
相同的结构,但在最后添加区域 ID:
// create with same format as RFC_1123_DATE_TIME
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseLenient()
.optionalStart()
.appendText(DAY_OF_WEEK, dow)
.appendLiteral(", ")
.optionalEnd()
.appendValue(DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
.appendLiteral(' ')
.appendText(MONTH_OF_YEAR, moy)
.appendLiteral(' ')
.appendValue(YEAR, 4) // 2 digit year not handled
.appendLiteral(' ')
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.optionalEnd()
.appendLiteral(' ')
// difference from RFC_1123_DATE_TIME: optional offset OR zone ID
.optionalStart()
.appendZoneText(TextStyle.SHORT)
.optionalEnd()
.optionalStart()
.appendOffset("+HHMM", "GMT")
// use the same resolver style and chronology
.toFormatter().withResolverStyle(ResolverStyle.SMART).withChronology(IsoChronology.INSTANCE);
这里的区别是 .appendZoneText(TextStyle.SHORT)
(optionalStart()
因为它可以有 offset/GMT 或 短区域 ID)。
您还会注意到,在 source code 中它使用:
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
但是 toFormatter
的重载版本不是 public。所以我不得不使用 with
方法来相应地调整值。
有了这个格式化程序,我可以解析输入:
System.out.println(Instant.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
System.out.println(Instant.from(fmt.parse("Sun, 20 Aug 2017 00:30:00 UT")));
输出为:
2017-08-21T19:00:00Z
2017-08-20T00:30:00Z
PS: 像 EST
这样的短名称是 ambiguous and not standard. The ideal is to always use IANA timezones names(格式总是 Region/City
,像 America/New_York
或 Europe/London
).
EST
是不明确的,因为有 more than one timezone that uses it。一些短名称无法识别,但由于追溯兼容性原因,其中一些短名称被设置为任意默认值。例如,EST
映射到 America/New_York
,如果我将其解析为 ZonedDateTime
:
System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
输出为:
2017-08-21T15:00-04:00[America/New_York]
也许这不适用于你的情况,因为你正在将所有内容解析为 Instant
,但如果你想要 ZonedDateTime
,可以通过定义一组首选来更改这些默认值区域:
// set of preferred zones
Set<ZoneId> preferredZones = new HashSet<>();
// add my arbitrary choices
preferredZones.add(ZoneId.of("America/Indianapolis"));
America/Indianapolis
是另一个使用 EST
作为简称的时区,因此我可以将其设置为首选而不是默认的 America/New_York
。我只需要在格式化程序中设置它。而不是这个:
.appendZoneText(TextStyle.SHORT)
我称之为:
.appendZoneText(TextStyle.SHORT, preferredZones)
现在将使用我的首选任意区域。同样的代码:
System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
现在打印:
2017-08-21T15:00-04:00[America/Indianapolis]
另请注意,上面的 ZonedDateTime
的偏移量为 -04:00
。那是因为在 8 月,这些区域处于夏令时 (DST),所以实际上各自的简称是 EDT
。如果您使用上面相同的格式化程序格式化日期:
System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York")).format(fmt));
输出将是:
Wed, 23 Aug 2017 08:43:52 EDT-0400
请注意,格式化程序使用所有可选部分来打印日期(因此它同时打印区域 ID EDT
和偏移量 -0400
)。如果只想打印其中一个,则必须创建另一个格式化程序(或者只使用 RFC_1123_DATE_TIME
)。
除了 appendZoneText
和 appendOffset
,您还可以使用:
.appendPattern("[z][x]")
注意可选部分(由 []
分隔)。这将解析区域 ID (z
) 或 偏移量 (x
)。查看有关模式的docs for more details。
唯一的区别是使用此模式时您不能使用首选区域集。
为了格式化,这也将打印两个字段(因此输出将类似于 EDT-0400
)。
我正在尝试从定义为使用 RFC1123 兼容日期时间规范的数据源解析时间戳。我的代码是:
value = Instant.from (DateTimeFormatter.RFC_1123_DATE_TIME.parse (textValue));
这对某些数据工作正常,但我得到了包含区域名称的字符串的异常,即使是在 RFC2822 中定义的字符串(它从 RFC1123 间接引用,因为它废弃了 RFC822)。示例:
java.time.format.DateTimeParseException: Text 'Sun, 20 Aug 2017 00:30:00 UT' could not be parsed at index 26
java.time.format.DateTimeParseException: Text 'Mon, 21 Aug 2017 15:00:00 EST' could not be parsed at index 26
如何说服DateTimeFormatter
接受这种类型的约会?
正如 RFC_1123_DATE_TIME
"does not handle North American or military zone names, only 'GMT' and offset amounts".
要让它识别像 UT
和 EST
这样的短时区名称,唯一的方法是构建一个自定义格式化程序,其结构类似于 RFC_1123_DATE_TIME
的结构,但添加最后的短区ID。
此格式使用英语名称表示月份和星期几,因此一种替代方法是使用英语语言环境,但 source code 使用具有固定值的自定义地图,如果更改(评论说 区域设置数据可以通过应用程序代码 更改)。所以我们首先重新创建这些地图:
// custom map for days of week
Map<Long, String> dow = new HashMap<>();
dow.put(1L, "Mon");
dow.put(2L, "Tue");
dow.put(3L, "Wed");
dow.put(4L, "Thu");
dow.put(5L, "Fri");
dow.put(6L, "Sat");
dow.put(7L, "Sun");
// custom map for months
Map<Long, String> moy = new HashMap<>();
moy.put(1L, "Jan");
moy.put(2L, "Feb");
moy.put(3L, "Mar");
moy.put(4L, "Apr");
moy.put(5L, "May");
moy.put(6L, "Jun");
moy.put(7L, "Jul");
moy.put(8L, "Aug");
moy.put(9L, "Sep");
moy.put(10L, "Oct");
moy.put(11L, "Nov");
moy.put(12L, "Dec");
然后我重新创建与 RFC_1123_DATE_TIME
相同的结构,但在最后添加区域 ID:
// create with same format as RFC_1123_DATE_TIME
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseLenient()
.optionalStart()
.appendText(DAY_OF_WEEK, dow)
.appendLiteral(", ")
.optionalEnd()
.appendValue(DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
.appendLiteral(' ')
.appendText(MONTH_OF_YEAR, moy)
.appendLiteral(' ')
.appendValue(YEAR, 4) // 2 digit year not handled
.appendLiteral(' ')
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.optionalEnd()
.appendLiteral(' ')
// difference from RFC_1123_DATE_TIME: optional offset OR zone ID
.optionalStart()
.appendZoneText(TextStyle.SHORT)
.optionalEnd()
.optionalStart()
.appendOffset("+HHMM", "GMT")
// use the same resolver style and chronology
.toFormatter().withResolverStyle(ResolverStyle.SMART).withChronology(IsoChronology.INSTANCE);
这里的区别是 .appendZoneText(TextStyle.SHORT)
(optionalStart()
因为它可以有 offset/GMT 或 短区域 ID)。
您还会注意到,在 source code 中它使用:
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
但是 toFormatter
的重载版本不是 public。所以我不得不使用 with
方法来相应地调整值。
有了这个格式化程序,我可以解析输入:
System.out.println(Instant.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
System.out.println(Instant.from(fmt.parse("Sun, 20 Aug 2017 00:30:00 UT")));
输出为:
2017-08-21T19:00:00Z
2017-08-20T00:30:00Z
PS: 像 EST
这样的短名称是 ambiguous and not standard. The ideal is to always use IANA timezones names(格式总是 Region/City
,像 America/New_York
或 Europe/London
).
EST
是不明确的,因为有 more than one timezone that uses it。一些短名称无法识别,但由于追溯兼容性原因,其中一些短名称被设置为任意默认值。例如,EST
映射到 America/New_York
,如果我将其解析为 ZonedDateTime
:
System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
输出为:
2017-08-21T15:00-04:00[America/New_York]
也许这不适用于你的情况,因为你正在将所有内容解析为 Instant
,但如果你想要 ZonedDateTime
,可以通过定义一组首选来更改这些默认值区域:
// set of preferred zones
Set<ZoneId> preferredZones = new HashSet<>();
// add my arbitrary choices
preferredZones.add(ZoneId.of("America/Indianapolis"));
America/Indianapolis
是另一个使用 EST
作为简称的时区,因此我可以将其设置为首选而不是默认的 America/New_York
。我只需要在格式化程序中设置它。而不是这个:
.appendZoneText(TextStyle.SHORT)
我称之为:
.appendZoneText(TextStyle.SHORT, preferredZones)
现在将使用我的首选任意区域。同样的代码:
System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
现在打印:
2017-08-21T15:00-04:00[America/Indianapolis]
另请注意,上面的 ZonedDateTime
的偏移量为 -04:00
。那是因为在 8 月,这些区域处于夏令时 (DST),所以实际上各自的简称是 EDT
。如果您使用上面相同的格式化程序格式化日期:
System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York")).format(fmt));
输出将是:
Wed, 23 Aug 2017 08:43:52 EDT-0400
请注意,格式化程序使用所有可选部分来打印日期(因此它同时打印区域 ID EDT
和偏移量 -0400
)。如果只想打印其中一个,则必须创建另一个格式化程序(或者只使用 RFC_1123_DATE_TIME
)。
除了 appendZoneText
和 appendOffset
,您还可以使用:
.appendPattern("[z][x]")
注意可选部分(由 []
分隔)。这将解析区域 ID (z
) 或 偏移量 (x
)。查看有关模式的docs for more details。
唯一的区别是使用此模式时您不能使用首选区域集。
为了格式化,这也将打印两个字段(因此输出将类似于 EDT-0400
)。