@shmosel's comment 注意到,javadoc says RFC_1123_DATE_TIME “不处理北美或军事区域名称,仅处理 'GMT' 和偏移量”.
要让它识别像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(),因为它可以有偏移/GMT或短区域ID)。
您还会注意到在source code 中它使用:
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
但是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")));
输出是:
2017-08-21T19:00:00Z
2017-08-20T00:30:00Z
PS: 像 EST 这样的短名称是 ambiguous and not standard。理想的情况是始终使用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[美国/纽约]
也许这不适用于您的情况,因为您将所有内容解析为 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[美国/印第安纳波利斯]
还要注意上面的ZonedDateTime 的偏移量是-04:00。那是因为在 8 月这些区域处于夏令时 (DST),所以实际上各自的简称是 EDT。如果您使用上述相同的格式化程序格式化日期:
System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York")).format(fmt));
输出将是:
2017 年 8 月 23 日星期三 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)。