问题 军事时区使用JSR 310(DateTime API)


我在我的应用程序中使用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种不同的方法 DateTimeFormatters具有文字区域ID(即。 "ddHHmm'A' MMM yy""ddHHmm'B' MMM yy"等),使用一个 RegExp 用于提取区域ID,并根据区域委派给正确的格式化程序。提供程序中的区域ID被命名为“Alpha”,“Bravo”等 ZoneId.of(...) 找到区域。有用。但它不是很优雅,我希望有更好的解决方案。


4837
2018-01-21 10:36


起源

你有没有尝试过 V 对于格式字符串中的zone-id(即 "ddHHmmV MMM yy")?据我所知 'Z' 意思就是 Z 字符。 docs.oracle.com/javase/8/docs/api/java/time/format/... - Pavel Horal
是的,我理解 'Z' 是一个文字,解析器不解释。 Z (未引用的)虽然是一个符号(但不起作用)。是的,我试过了 V。虽然它给出了不同的错误...... IllegalArgumentException: Pattern letter count must be 2: V 在尝试创建格式化程序时。运用 VV,我在解析时回到“...无法解析索引6”异常。不管怎么说,还是要谢谢你! :-) - haraldK
@PavelHoral实际上,似乎“正确”的符号是 z(小写)...这似乎正确地解析了区域,但当然不会因为单个字母区域被允许而爆炸.. :-P“...could not be parsed: Invalid zone: A“我会更新问题...... - haraldK
关于 ZoneRulesProvider  - 你能分享一下吗?你是否正确注册(docs.oracle.com/javase/tutorial/sound/SPI-intro.html  - 对不起,如果这是noobish问题:))? - Pavel Horal
@PavelHoral是的,我已经验证了调用了预期的SPI方法并应用了我的区域规则(我可以执行'ZoneId alpha = ZoneId.of(“Alpha”);`并获得所需的结果)。 - haraldK


答案:


java.timeZoneId 限制为2个字符或更多。具有讽刺意味的是,这是为了保留空间,以便在未来的JDK版本中添加军事ID,如果它被证明需求量很大的话。因此,遗憾的是,您的提供商将无法工作,并且无法创建 ZoneId 你想要这些名字的实例。

一旦您考虑使用,解析问题就可以解决了 ZoneOffset 代替 ZoneId (并且考虑到军事区域是固定的抵消,这是查看问题的好方法)。

关键是方法 DateTimeFormatterBuilder.appendText(TemporalField, Map) 它允许使用您选择的文本格式化数字字段并将其解析为文本。和 ZoneOffset 是一个数字字段(值是偏移中的总秒数)。

我这个例子,我已经设置了映射 ZA 和 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));

8
2018-01-22 11:39



谢谢!绝对是一个更清洁,更优雅的解决方案!经过测试,并且与我的格式配合得很好。 - haraldK
PS:我目前正在将所有日期/时间内部转换为Zulu并使用 ZonedDateTime 而不是 OffsetDateTime。我已经习惯了Joda,但这是我使用JSR 310的第一个项目。是否有充分的理由使用ODT而不是ZDT(我意识到你想在某些时候合并这些类...... :-)? - haraldK
ODT更简单,没有DST问题。它也是标准网络ISO08601格式。否则这些类非常相似。使用最适合你的方法。 - JodaStephen


有关区域ID支持的java.time-package(JSR-310)的行为是指定的  - 看 的javadoc。相关部分的明确引用(其他ID仅被视为格式为“Z”,“+ hh:mm”,“ - hh:mm”或“UTC + hh:mm”等的offset-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 是更好的答案 关于解析问题,也得到了我的upvote(忽略了这个功能)。它并不是更好,因为它给用户带来了负担来定义从军事区域字母到偏移的映射 - 就像我的字符串预处理建议一样。 但它更好,因为它可以使用构建器方法 optionalStart() 和 optionalEnd() 所以可以处理可选的时区字母A,B,.... 另请参阅OP关于可选区域ID的注释。


3
2018-01-21 17:04



感谢您的确认,我想我会将此标记为正确的答案,因为我想要做的事情似乎是不可能的,因为当前的规范/实现(我确实提出了threetenbp项目的问题,等待反馈)。 - haraldK
回覆。字符串预处理和区分大小写,是的,我知道这一点。我用于解析的实际格式比我所显示的格式更复杂。区域id是可选的(如果省略,则假定为Z),并且空格是可选的(意思是 charAt(6) 可能是4月的A ...)。我会坚持我的基于正则表达式的解决方案。我会考虑使用正则表达式替换,它可能会工作!而且 ZoneRulesProvider 我做的可能是多余的,我可以使用 ZoneOffset直接无论如何,谢谢你的回答! :-) - haraldK