Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't OffsetDateTime parse '2016-08-24T18:38:05.507+0000' in Java 8

The expression

OffsetDateTime.parse("2016-08-24T18:38:05.507+0000")

results in the following error:

java.time.format.DateTimeParseException: Text '2016-08-24T18:38:05.507+0000' could not be parsed at index 23

On the other hand,

OffsetDateTime.parse("2016-08-24T18:38:05.507+00:00")

works as expected.

DateTimeFormatter's doc page mentions zone offsets without colons as examples. What am I doing wrong? I'd rather not mangle my date string to appease Java.

like image 980
Ryan Russell Avatar asked Aug 24 '16 22:08

Ryan Russell


4 Answers

You are calling the following method.

public static OffsetDateTime parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

It uses uses DateTimeFormatter.ISO_OFFSET_DATE_TIME as DateTimeFormatter which, as stated in the javadoc, does the following:

The ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30+01:00'.

If you want to parse a date with a different format as in 2016-08-24T18:38:05.507+0000 you should use OffsetDateTime#parse(CharSequence, DateTimeFormatter). The following code should solve your problem:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
OffsetDateTime.parse("2016-08-24T18:38:05.507+0000", formatter);
like image 135
acm Avatar answered Nov 12 '22 16:11

acm


Update

Thanks to Ole V.V. for suggesting this simpler pattern:

DateTimeFormatter dtf = new DateTimeFormatterBuilder()
                        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                        .appendPattern("[XXX][XX][X]")
                        .toFormatter(Locale.ENGLISH);

The original answer is still useful if the units (e.g. month, day, hour etc.) can be in single-digit or double-digit. This alternative pattern will fail in case units are in single-digit.

Original answer

The solution is to use a DateTimeFormatter with optional patterns. The DateTimeFormatter allows us to specify optional patterns in the square bracket.

Demo:

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
                "u-M-d'T'H:m:s[.[SSSSSSSSS][SSSSSSSS][SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]][XXX][XX][X]",
                Locale.ENGLISH);
        
        // Test
        Stream.of(
                "2021-07-22T20:10:15+0000",
                "2021-07-22T20:10:15+00:00",
                "2021-07-22T20:10:15+00",
                "2021-07-22T20:10:15.123456789+0000",
                "2021-07-22T20:10:15.12345678+0000",
                "2021-07-22T20:10:15.123+0000",
                "2021-07-22T20:10:15.1+0000"                
        ).forEach(s -> System.out.println(OffsetDateTime.parse(s, dtf)));
    }
}

Output:

2021-07-22T20:10:15Z
2021-07-22T20:10:15Z
2021-07-22T20:10:15Z
2021-07-22T20:10:15.123456789Z
2021-07-22T20:10:15.123456780Z
2021-07-22T20:10:15.123Z
2021-07-22T20:10:15.100Z

The Z in the output is the timezone designator for zero-timezone offset. It stands for Zulu and specifies the Etc/UTC timezone (which has the timezone offset of +00:00 hours).

Learn more about the modern Date-Time API from Trail: Date Time.


Check the documentation page of DateTimeFormatter for the complete list of pattern letters.

like image 3
Arvind Kumar Avinash Avatar answered Nov 12 '22 17:11

Arvind Kumar Avinash


Although DateTimeFormatter's pattern language does not provide a code for zone offsets that fail to accommodate your no-colon form, that does not imply that the pre-defined instances that handle zone offsets accept the no-colon form. The one-arg version of OffsetDateTime.parse() specifies that it uses DateTimeFormatter.ISO_OFFSET_DATE_TIME as its formatter, and that formatter's docs specify that it supports three formats, as described in the docs of ZoneOffset.getId(). None of those formats (which are drawn from ISO-8601) is consistent with your no-colon form.

But not to worry: just use the two-arg from of OffsetDateTime.parse(), providing an appropriate formatter. That's a bit less convenient, but quite doable.

like image 2
John Bollinger Avatar answered Nov 12 '22 18:11

John Bollinger


Paypal incorrectly sends the offsets as +0000 which is actually contradicting their spec https://developer.paypal.com/docs/api/transaction-search/v1/ that says it has to be in an Internet date/time format where the offset is written as

 time-numoffset  = ("+" / "-") time-hour ":" time-minute

The : is required.

To work around this, a custom date time formatter should be created, but avoid using the simple pattern like yyyy-MM-dd'T'HH:mm:ssZ because it will fail if milliseconds suddenly appear in the output.

public static final DateTimeFormatter PAYPAL_DATE_TIME_FORMAT = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    .parseLenient()
    .appendPattern("Z")
    .parseStrict()
    .toFormatter();

This is one way of doing it, but it has a flaw in that when Paypal corrects their output it won't be able to parse the : offset correctly.

Also Paypal does not support nanos so you should also do .truncatedTo(SECONDS) before sending it to their APIs

like image 2
Archimedes Trajano Avatar answered Nov 12 '22 16:11

Archimedes Trajano