Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Military time zones using JSR 310 (DateTime API)

I'm using the JSR 310 DateTime API* in my application, and I need to parse and format military date times (known as DTG or "date time group").

The format I'm parsing looks like this (using DateTimeFormatter):

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

This format is fairly easy to parse as described above. The problem arises when the dates contains a different time zone than 'Z' (Zulu time zone, same as UTC/GMT), for example 'A' (Alpha, UTC+1:00) or 'B' (Bravo, UTC+2:00). See Military time zones for the full list.

How can I parse these time zones? Or in other words, what can I put in the format above other than the literal 'Z' to have it parse all zones correctly? I have tried using "ddHHmmX MMM yy", "ddHHmmZ MMM yy" and "ddHHmmVV MMM yy", but none of them work (all will throw DateTimeParseException: Text '312359A DEC 14' could not be parsed at index 6 for the example above, when parsing). Using a single V in the format is not allowed (IllegalArgumentException when trying to instantiate DateTimeFormatter).

Edit: It seems that the symbol z could have worked, if it wasn't for the issue below.

I should also mention that I have created a ZoneRulesProvider with the all the named zones and correct offset. I have verified that these are registered correctly using the SPI mechanism, and my provideZoneIds() method is invoked as expected. Still won't parse. As a side issue (Edit: this now seems to be the main issue), single character time zone ids (or "regions") other than 'Z' are not allowed by the API.

For example:

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

Will throw DateTimeException: Invalid zone: A (without even accessing my rules provider to see if it exists).

Is this an oversight in the API? Or am I doing something wrong?


*) Actually, I'm using Java 7 and ThreeTen Backport, but I don't think that matters for this question.

PS: My current workaround is to use 25 different DateTimeFormatters with literal zone id (ie. "ddHHmm'A' MMM yy", "ddHHmm'B' MMM yy", etc), use a RegExp for extracting the zone id, and delegating to the correct formatter based on the zone. Zone ids in the provider are named "Alpha", "Bravo", etc to allow ZoneId.of(...) to find the zones. It works. But it's not very elegant, and I'm hoping there's a better solution.

like image 813
Harald K Avatar asked Jan 21 '15 10:01

Harald K


2 Answers

In java.time, the ZoneId is limited to be 2 characters or more. Ironically, this was to reserve space to allow the military IDs to be added in a future JDK release if it proved to be heavily in demand. As such, sadly your provider will not work, and there is no way to create the ZoneId instances you desire with those names.

The parsing problem is soluble once you consider working with ZoneOffset instead of ZoneId (and given that military zones are fixed offsets, that is a good way to look at the problem).

The key is the method DateTimeFormatterBuilder.appendText(TemporalField, Map) which allows a numeric field to be formatted and parsed as text, using text of your choice. And ZoneOffset is a numeric field (the value being the total number of seconds in the offset).

I this example, I've setup the mapping for Z, A and B, but you'd need to add them all. Otherwise, the code is pretty simple, setting up a formatter that can print and parse the military time (use OffsetDateTime for date and time).

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));
like image 156
JodaStephen Avatar answered Oct 18 '22 16:10

JodaStephen


The behaviour of java.time-package (JSR-310) with respect to support of zone ids is as specified - see javadoc. Explicit citation of the relevant section (other IDs are just considered as offset-ids in format "Z", "+hh:mm", "-hh:mm" or "UTC+hh:mm" etc.):

A region-based ID must be of two or more characters

The requirement of having at least two characters is also implemented in the source code of class ZoneRegion before any loading of timezone data starts:

/**
 * 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);
    }
}

This explains why it is not possible with JSR-310/Threeten to write an expression like ZoneId.of("A"). The letter Z works because it is specified as well in ISO-8601 as in JSR-310 to represent the zero offset.

The workaround you have found is fine within the scope of JSR-310 which does NOT support military timezones. Consequently you will not find any format support for it (just study the class DateTimeFormatterBuilder - every processing of format pattern symbols is routed to the builder). The only vague idea I have got was to implement a specialized TemporalField representing a military timezone offset. But the implementation is (if possible at all) with certainty more complex than your workaround.

Another more suitable workaround is just string preprocessing. Since you work with a fixed format expecting the military letter always at the same position in input, you can simply do this:

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

Notes:

  • I have used "Dec" instead of "DEC", otherwise the parser will complain. If your input really has capital letters then you might use the builder method parseCaseInsensitive().

  • The other answer using the field OffsetSeconds is the better answer with regards to the parsing problem and has also got my upvote (have overlooked this feature). It is not better because it puts the burden on the user to define a mapping from military zone letters to offsets - like my suggestion with string preprocessing. But it is better because it enables using the builder-methods optionalStart() and optionalEnd() so optional time zone letters A, B, ... can be handled. See also the comment of OP about optional zone ids.

like image 3
Meno Hochschild Avatar answered Oct 18 '22 15:10

Meno Hochschild