java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME无法解析时区名称

Jul*_*les 6 java parsing date datetime-parsing java-time

我正在尝试从定义为使用RFC1123兼容日期时间规范的数据源解析时间戳.我的代码是:

value = Instant.from (DateTimeFormatter.RFC_1123_DATE_TIME.parse (textValue));
Run Code Online (Sandbox Code Playgroud)

这适用于某些数据,但我得到包含区域名称的字符串的例外,甚至是RFC2822中定义的字符串(由于它废弃RFC822而间接引用RFC1123).例子:

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
Run Code Online (Sandbox Code Playgroud)

我如何说服DateTimeFormatter接受这种类型的约会?

小智 7

由于注意到了@ shmosel的评论中,javadoc中说的是RFC_1123_DATE_TIME "不处理北美或军事区的名称,只有'GMT’和偏移量 ".

为了使识别短时区的名称,如UTEST,唯一的办法是建立一个自定义的格式,用类似于结构RFC_1123_DATE_TIME有,但最终加入短区ID.

此格式使用英文名称作为月份和星期几,因此一种替代方法是使用英语区域设置,但源代码使用具有固定值的自定义映射,如果更改则不依赖于区域设置(注释表示区域设置数据可以由应用程序代码更改).所以我们首先重新创建这些地图:

// 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");
Run Code Online (Sandbox Code Playgroud)

然后我重新创建相同的结构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);
Run Code Online (Sandbox Code Playgroud)

这里的区别是.appendZoneText(TextStyle.SHORT)(optionalStart()因为它可以有偏移/ GMT 短区ID).

您还会注意到它使用的源代码:

.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
Run Code Online (Sandbox Code Playgroud)

但这个重载版本toFormatter并不公开.所以我不得不使用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")));
Run Code Online (Sandbox Code Playgroud)

输出是:

2017-08-21T19:00:00Z
2017-08-20T00:30:00Z


PS:简短的名字EST模糊的而不是标准的.理想情况是始终使用IANA时区名称(始终采用格式Region/City,如America/New_YorkEurope/London).

EST是不明确的,因为有多个时区使用它.一些短名称未被识别,但由于复古兼容性原因,其中一些短名称被设置为任意违约.EST例如,映射到America/New_York,如果我将其解析为ZonedDateTime:

System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
Run Code Online (Sandbox Code Playgroud)

输出是:

2017-08-21T15:00-04:00 [美国/纽约]

也许这不适用于您的情况,因为您正在解析所有内容Instant,但如果您需要ZonedDateTime,可以通过定义一组首选区域来更改这些默认值:

// set of preferred zones
Set<ZoneId> preferredZones = new HashSet<>();
// add my arbitrary choices
preferredZones.add(ZoneId.of("America/Indianapolis"));
Run Code Online (Sandbox Code Playgroud)

America/Indianapolis是另一个EST用作短名称的时区,因此我可以将其设置为首选而不是默认值America/New_York.我只需要在格式化程序中设置它.而不是这个:

.appendZoneText(TextStyle.SHORT)
Run Code Online (Sandbox Code Playgroud)

我这叫:

.appendZoneText(TextStyle.SHORT, preferredZones)
Run Code Online (Sandbox Code Playgroud)

现在我将使用我喜欢的任意区域.同样的代码:

System.out.println(ZonedDateTime.from(fmt.parse("Mon, 21 Aug 2017 15:00:00 EST")));
Run Code Online (Sandbox Code Playgroud)

现在打印:

2017-08-21T15:00-04:00 [美国/印第安纳波利斯]

另请注意,ZonedDateTime上面的偏移量为-04:00.那是因为在8月这些区域处于夏令时(DST),所以实际上相应的短名称是EDT.如果使用上面相同的格式化程序格式化日期:

System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York")).format(fmt));
Run Code Online (Sandbox Code Playgroud)

输出将是:

2017年8月23日星期三08:43:52 EDT-0400

请注意,格式化程序使用所有可选部分来打印日期(因此它会打印区域ID EDT和偏移量-0400).如果你只想打印其中一个,你将不得不创建另一个格式化程序(或只是使用RFC_1123_DATE_TIME).


而不是appendZoneTextappendOffset,你也可以使用:

.appendPattern("[z][x]")
Run Code Online (Sandbox Code Playgroud)

请注意可选部分(由分隔符[]).这将解析区域Id(z) offset(x).查看文档以获取有关模式的更多详细信息.

唯一的区别是使用此模式您无法使用首选区域集.

并且要格式化,这也会打印两个字段(因此输出就像EDT-0400).