我在我的应用程序中使用了JSR 310 DateTime API*,我需要解析和格式化军事日期时间(称为DTG或"日期时间组")。
我正在解析的格式如下所示(使用 DateTimeFormatter
):
"ddHHmm'Z' MMM yy" // (ie. "312359Z DEC 14", for new years eve 2014)
如上所述,这种格式相当容易解析。当日期包含与"Z"(祖鲁时区,与 UTC/GMT 相同)不同的时区时,例如"A"(Alpha,UTC+1:00)或"B"(Bravo,UTC+2:00)时,就会出现问题。有关完整列表,请参阅军事时区。
如何解析这些时区?或者换句话说,除了文字"Z"之外,我可以在上面的格式中放入什么来让它正确解析所有区域?我尝试使用"ddHHmmX MMM yy"
,"ddHHmmZ MMM yy"
和"ddHHmmVV MMM yy"
,但它们都不起作用(解析时,所有这些都会为上面的示例抛出DateTimeParseException: Text '312359A DEC 14' could not be parsed at index 6
)。不允许使用格式的单个V
(IllegalArgumentException
尝试实例化DateTimeFormatter
时)。
编辑:如果不是因为下面的问题,符号z
似乎可以工作。
我还应该提到,我已经创建了一个包含所有命名区域和正确偏移的ZoneRulesProvider
。我已经验证了这些是使用 SPI 机制正确注册的,并且我的 provideZoneIds()
方法按预期调用。仍然不会解析。作为一个附带问题(编辑:现在这似乎是主要问题),API 不允许使用"Z"以外的单字符时区 ID(或"区域")。
例如:
ZoneId alpha = ZoneId.of("A"); // boom
会抛出DateTimeException: Invalid zone: A
(甚至不访问我的规则提供程序以查看它是否存在)。
这是 API 中的疏忽吗?还是我做错了什么?
*)实际上,我使用的是Java 7和ThreeTen Backport,但我认为这对这个问题并不重要。
PS:我目前的解决方法是使用25个不同的DateTimeFormatter
和文字区域id(即。 "ddHHmm'A' MMM yy"
、"ddHHmm'B' MMM yy"
等),使用RegExp
提取区域 ID,并根据区域委派给正确的格式化程序。提供程序中的区域 ID 被命名为"Alpha"、"Bravo"等,以允许ZoneId.of(...)
查找区域。它有效。但它不是很优雅,我希望有更好的解决方案。
在java.time
中,ZoneId
被限制为2个字符或更多。具有讽刺意味的是,这是为了保留空间,以便在将来的JDK版本中添加军事ID,如果它被证明需求量很大。因此,遗憾的是,您的提供程序将无法工作,并且无法使用这些名称创建所需的ZoneId
实例。
一旦您考虑使用 ZoneOffset
而不是ZoneId
,解析问题就可以解决(并且考虑到军事区域是固定偏移量,这是查看问题的好方法)。
关键是方法DateTimeFormatterBuilder.appendText(TemporalField, Map)
,它允许使用您选择的文本格式化数值字段并将其解析为文本。ZoneOffset
是一个数值字段(该值是偏移量中的总秒数)。
在这个例子中,我已经为Z
、A
和B
设置了映射,但你需要将它们全部添加。否则,代码非常简单,设置一个可以打印和解析军事时间的格式化程序(使用 OffsetDateTime
作为日期和时间)。
Map<Long, String> map = ImmutableMap.of(0L, "Z", 3600L, "A", 7200L, "B");
DateTimeFormatter f = new DateTimeFormatterBuilder()
.appendPattern("HH:mm")
.appendText(ChronoField.OFFSET_SECONDS, map)
.toFormatter();
System.out.println(OffsetTime.now().format(f));
System.out.println(OffsetTime.parse("11:30A", f));
java.time-package (JSR-310) 在支持区域 ID 方面的行为是指定的 - 参见 javadoc。明确引用相关部分(其他 ID 仅被视为格式为"Z"、"+hh:mm"、"-hh:mm"或"UTC+hh:mm"等格式的偏移 ID):
在基于区域的 ID 必须包含两个或更多字符
开始加载时区数据之前,在类 ZoneRegion 的源代码中也实现了至少包含两个字符的要求:
/**
* Checks that the given string is a legal ZondId name.
*
* @param zoneId the time-zone ID, not null
* @throws DateTimeException if the ID format is invalid
*/
private static void checkName(String zoneId) {
int n = zoneId.length();
if (n < 2) {
throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId);
}
for (int i = 0; i < n; i++) {
char c = zoneId.charAt(i);
if (c >= 'a' && c <= 'z') continue;
if (c >= 'A' && c <= 'Z') continue;
if (c == '/' && i != 0) continue;
if (c >= '0' && c <= '9' && i != 0) continue;
if (c == '~' && i != 0) continue;
if (c == '.' && i != 0) continue;
if (c == '_' && i != 0) continue;
if (c == '+' && i != 0) continue;
if (c == '-' && i != 0) continue;
throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId);
}
}
这就解释了为什么 JSR-310/Threeten 不可能编写像 ZoneId.of("A")
这样的表达式。字母 Z 之所以有效,是因为它在 ISO-8601 中和在 JSR-310 中一样被指定用于表示零偏移量。
您找到的解决方法在不支持军事时区的 JSR-310 范围内很好。因此,您将找不到对它的任何格式支持(只需研究类DateTimeFormatterBuilder
- 格式模式符号的每次处理都路由到构建器)。我得到的唯一模糊的想法是实现一个表示军事时区偏移量的专用TemporalField
。但是实现(如果可能的话)肯定比您的解决方法更复杂。
另一个更合适的解决方法是字符串预处理。由于您使用固定格式,期望军事信件始终位于输入中的相同位置,因此您可以简单地执行以下操作:
String input = "312359A Dec 14";
String offset = "";
switch (input.charAt(6)) {
case 'A':
offset = "+01:00";
break;
case 'B':
offset = "+02:00";
break;
//...
case 'Z':
offset = "Z";
break;
default:
throw new ParseException("Wrong military timezone: " + input, 6);
}
input = input.substring(0, 6) + offset + input.substring(7);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ddHHmmVV MMM yy", Locale.ENGLISH);
ZonedDateTime odt = ZonedDateTime.parse(input, formatter);
System.out.println(odt);
// output: 2014-12-31T23:59+01:00
笔记:
我使用了"Dec"而不是"DEC",否则解析器会抱怨。如果你的输入确实有大写字母,那么你可以使用构建器方法parseCaseInsensitive()。
使用字段
OffsetSeconds
的另一个答案是关于解析问题的更好答案,并且也得到了我的赞成(忽略了此功能)。它不是更好,因为它给用户带来了定义从军事区域字母到偏移量的映射的负担 - 就像我对字符串预处理的建议一样。但它更好,因为它允许使用构建器方法optionalStart()
和optionalEnd()
所以可选的时区字母 A、B、...可以处理。另请参阅 OP 关于可选区域 ID 的评论。