Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 DateTimeFormatter rejects correct ISO 8601 date/time with offset

I use format string for DateTimeFormatter: uuuu-MM-dd'T'HH:mm:ssX

which must support all possible formats of timezone offset, including: Z, 00, 00:00, 0000

According to official DateTimeFormatter documentation, 'X' qualifier must match to offset in these formats:

X zone-offset 'Z' for zero offset-X Z; -08; -0830; -08:30; -083015; -08:30:15;

but in fact, it doesn't

input string: "2014-01-01T00:30:00+00:00"

result: java.time.format.DateTimeParseException: Text '2014-01-01T00:30:00+00:00' could not be parsed, unparsed text found at index 22

input string: "2014-01-01T00:30:00Z"

result: correct

the code:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssX");
OffsetDateTime parsed = OffsetDateTime.parse(dateTimeAsString, formatter);

JDK 1.8.0_192 (Oracle, not OpenJDK)

like image 980
Victor Mikhailov Avatar asked Feb 05 '19 14:02

Victor Mikhailov


2 Answers

This is a little bit complicated. As jvdmr says, the count of Xs matters. XXXXX will recognize -08:30:15, but not -083015. XXXX will recognize the latter, but not the former.

To take all possible example formats into account, we need to specify different possibilities. This can be done within the format pattern string using square brackets. These enclose optional parts. A little experimentation showed that the following pattern covers all examples:

uuuu-MM-dd'T'HH:mm:ss[XXXXX][XXXX][X]

Let’s try it out:

    DateTimeFormatter formatter
            = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss[XXXXX][XXXX][X]");
    for (String dts : new String[] {
            "2014-01-01T00:30:00-08:30:15", "2014-01-01T00:30:00-083015",
            "2014-01-01T00:30:00-08:30", "2014-01-01T00:30:00-0830",
            "2014-01-01T00:30:00-08", "2014-01-01T00:30:00Z",
    }) {
        System.out.println(OffsetDateTime.parse(dts, formatter));
    }

Output from this snippet is:

2014-01-01T00:30-08:30:15
2014-01-01T00:30-08:30:15
2014-01-01T00:30-08:30
2014-01-01T00:30-08:30
2014-01-01T00:30-08:00
2014-01-01T00:30Z

Edit

VelNaga suggests not to hardcode ISO date-time formats. Since writing format pattern strings is error-prone, this can be a good idea. For example:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
            .appendPattern("[XXXXX][XXXX][X]")
            .toFormatter();

Output using this formatter is the same as with using the one above. It’s wordier, but we could easily end up thinking that it’s worth it since it is less error-prone and may be clearer and easier to read.

like image 66
Ole V.V. Avatar answered Nov 02 '22 23:11

Ole V.V.


From the docs (emphasis mine):

Offset X and x: This formats the offset based on the number of pattern letters. One letter outputs just the hour, such as '+01', unless the minute is non-zero in which case the minute is also output, such as '+0130'. Two letters outputs the hour and minute, without a colon, such as '+0130'. Three letters outputs the hour and minute, with a colon, such as '+01:30'. Four letters outputs the hour and minute and optional second, without a colon, such as '+013015'. Five letters outputs the hour and minute and optional second, with a colon, such as '+01:30:15'. Six or more letters throws IllegalArgumentException. Pattern letter 'X' (upper case) will output 'Z' when the offset to be output would be zero, whereas pattern letter 'x' (lower case) will output '+00', '+0000', or '+00:00'.

This also works in reverse for parsing dates. You want to parse both with and without colons, which means you'll have to use optional sections as no single pattern supports this. Try this pattern: "uuuu-MM-dd'T'HH:mm:ss[XXX][XXXX]"

like image 39
jvdmr Avatar answered Nov 03 '22 01:11

jvdmr