Военные часовые пояса с использованием JSR 310 (DateTime API)

Я использую JSR 310 DateTime API * в своем приложении, и мне нужно анализировать и форматировать военные даты (известные как DTG или "дата-группа времени" ).

Формат, который я обрабатываю, выглядит так (используя DateTimeFormatter):

"ddHHmm'Z' MMM yy" // (ie. "312359Z DEC 14", for new years eve 2014)

Этот формат довольно легко разобрать, как описано выше. Проблема возникает, когда даты содержат другой часовой пояс, чем "Z" (часовой пояс Zulu, то же, что и 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() вызывается, как ожидалось. Еще не разобратся. Как побочная проблема (Edit: теперь это, по-видимому, главная проблема), API-интерфейсы идентификаторов часовых поясов одиночного символа (или "регионов" ), кроме "Z" , не допускаются.

Например:

ZoneId alpha = ZoneId.of("A"); // boom

Бросит DateTimeException: Invalid zone: A (даже не получив доступ к моему провайдеру правил, чтобы узнать, существует ли он).

Является ли это надзором в API? Или я делаю что-то неправильно?


*) На самом деле, я использую Java 7 и ThreeTen Backport, но я не думаю, что это важно для этого вопроса.

PS: Моим текущим обходным путем является использование 25 различных DateTimeFormatter с литеральным идентификатором зоны (т.е. "ddHHmm'A' MMM yy", "ddHHmm'B' MMM yy" и т.д.), используйте RegExp для извлечения идентификатора зоны и делегирования правильному форматирование на основе зоны. Идентификаторы зоны в провайдере называются "Альфа", "Браво" и т.д., Чтобы позволить ZoneId.of(...) находить зоны. Оно работает. Но это не очень элегантно, и я надеюсь, что там будет лучшее решение.

Ответ 1

В java.time значение ZoneId ограничено 2 символами или более. По иронии судьбы, это было резервное пространство, позволяющее добавлять военные идентификаторы в будущий выпуск JDK, если он оказался востребованным. Таким образом, к сожалению, ваш провайдер не будет работать, и нет никакого способа создать экземпляры 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));

Ответ 2

Поведение java.time-пакета (JSR-310) в отношении поддержки идентификаторов зоны указано - см. Javadoc. Явная ссылка на соответствующий раздел (другие идентификаторы рассматриваются только как смещенные идентификаторы в формате "Z", "+ hh: mm", "-hh: mm" или "UTC + hh: mm" и т.д.):

Идентификатор на основе региона должен состоять из двух или более символов

Требование наличия как минимум двух символов также реализовано в исходном коде класса 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 о необязательном зоны.