Cannot parse String in ISO 8601 format, lacking colon in offset, to Java 8 Date
Asked Answered
T

3

25

I'm a little bit frustrated of java 8 date format/parse functionality. I was trying to find Jackson configuration and DateTimeFormatter to parse "2018-02-13T10:20:12.120+0000" string to any Java 8 date, and didn't find it.
This is java.util.Date example which works fine:

Date date = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSSZZZ")
                      .parse("2018-02-13T10:20:12.120+0000");

The same format doesn't work with new date time api

ZonedDateTime dateTime = ZonedDateTime.parse("2018-02-13T10:20:12.120+0000",
                   DateTimeFormatter.ofPattern("yyyy-MM-dd'T'hh:mm:ss.SSSZZZ"));

We should be able to format/parse date in any format suitable for FE UI application. Maybe I misunderstand or mistake something, but I think java.util.Date gives more format flexibility and easier to use.

Terrify answered 12/4, 2017 at 5:18 Comment(2)
I think it doesn't like that fact that there's no am/pm marker, so 10 can be either, trying using HH instead hh ... or add a am/pm markerBolen
There's a comprehensive answer on how to handle multiple ISO-8601 representations in this answer: #46487903Marshallmarshallese
H
39

tl;dr

Until bug is fixed:

OffsetDateTime.parse( 
    "2018-02-13T10:20:12.120+0000" , 
    DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSX" )
)

When bug is fixed:

OffsetDateTime.parse( "2018-02-13T10:20:12.120+0000" )

Details

You are using the wrong classes.

Avoid the troublesome old legacy classes such as Date, Calendar, and SimpleDateFormat. Now supplanted by the java.time classes.

The ZonedDateTime class you used is good, it is part of java.time. But it is intended for a full time zone. Your input string has merely an offset-from-UTC. A full time zone, in contrast, is a collection of offsets in effect for a region at different points in time, past, present, and future. For example, with Daylight Saving Time (DST) in most of North America, the offsets change twice a year growing smaller in the Spring as we shift clocks forward an hour, and restoring to a longer value in the Autumn when we shift clocks back an hour.

OffsetDateTime

For only an offset rather than a time zone, use the OffsetDateTime class.

Your input string complies with the ISO 8601 standard. The java.time classes use the standard formats by default when parsing/generating strings. So no need to specify a formatting pattern.

OffsetDateTime odt = OffsetDateTime.parse( "2018-02-13T10:20:12.120+0000" );

Well, that should have worked. Unfortunately, there is a bug in Java 8 (at least up through Java 8 Update 121) where that class fails to parse an offset omitting the colon between hours and minutes. So the bug bites on +0000 but not +00:00. So until a fix arrives, you have a choice of two workarounds: (a) a hack, manipulating the input string, or (b) define an explicit formatting pattern.

The hack: Manipulate the input string to insert the colon.

String input = "2018-02-13T10:20:12.120+0000".replace( "+0000" , "+00:00" );
OffsetDateTime odt = OffsetDateTime.parse( input );

DateTimeFormatter

The more robust workaround is to define and pass a formatting pattern in a DateTimeFormatter object.

String input = "2018-02-13T10:20:12.120+0000" ;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSX" );
OffsetDateTime odt = OffsetDateTime.parse( input , f );

odt.toString(): 2018-02-13T10:20:12.120Z

By the way, here is a tip: I have found that with many protocols and libraries, your life is easier if your offsets always have the colon, always have both hours and minutes (even if minutes are zero), and always use a padding zero (-05:00 rather than -5).

DateTimeFormatterBuilder

For a more flexible formatter, created via DateTimeFormatterBuilder, see this excellent Answer on a duplicate Question.

Instant

If you want to work with values that are always in UTC (and you should), extract an Instant object.

Instant instant = odt.toInstant();

ZonedDateTime

If you want to view that moment through the lens of some region’s wall-clock time, apply a time zone.

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = odt.atZoneSameInstant( z );

See this code run live at IdeOne.com.

All of this has been covered many times in many Answers for many Questions. Please search Stack Overflow thoroughly before posting. You would have discovered many dozens, if not hundreds, of examples.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Hannie answered 12/4, 2017 at 5:58 Comment(10)
Your example throws Exception. What i'm doing wrong. java.time.format.DateTimeParseException: Text '2018-02-13T10:20:12.120+0000' could not be parsed at index 23Terrify
Yep, just updated my Answer. I forgot about a formatting parsing bug over the optional colon in offset, fixed in Java 9. Try my revised code at IdeOne.com.Hannie
This answer should link to the JDK bug. A quick search resulted in bugs.java.com/view_bug.do?bug_id=JDK-8032051, but I'm not sure it's the same issue.Banner
Yes, I think this is the mentioned issue. And this got fixed. It's now possible to parse a date of the form 2018-08-26T15:00:00+01, but 2018-08-26T15:00:00+0100 still raises an exception. Tested with OpenJDK 11.Phillane
@BasilBourque Any advice on how to pass the DateTimeFormatter into a RestTemplate, so it will parse both?Fullscale
@BasilBourque #54676882Fullscale
@Fullscale Good question; I upvoted. I don’t know the answer. Tip: I have seen other libraries choke on abbreviated offset-from-UTC strings. Best to change the source of your data to always use full length, +08:00 rather than +0800 or +8.Hannie
@BasilBourque Ended up switching to using Joda time, because I can't change the source.Fullscale
It doesn't sound like this is a bug: JDK-8176547 explains that +0000 is in "basic" format but +00:00 is in "extended" format. java.time is documented to use extended format. It sounds like the DateTimeFormatter (or DateTimeFormatterBuilder) solution above is the way to go.Lamoreaux
There is no bug here, so this answer is currently wrong. The problem with the OPs code is using ZonedDateTime instead of OffsetDateTime and hh instead of HH. Thus the correct pattern is "uuuu-MM-dd'T'HH:mm:ss.SSSZZZ" which uses "+0000" not "Z" for the offset. The bug you are looking at is about allowing offsets that only have hours, which is a separate issue. See the full range of pattern letters: docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/…Mehitable
M
4

Short: Not a bug, just your pattern is wrong.

Please use the type OffsetDateTime which is especially designed for time zone offsets and use a pattern this way:

OffsetDateTime odt =
    OffsetDateTime.parse( 
        "2018-02-13T10:20:12.120+0000" , 
        DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSZZZ" )
    )

Problems in detail:

a) 12-hour-clock versus 24-hour-clock

"h" indicates the hour of AM/PM on a 12-hour-clock but you obviously need "H" for the 24-hour-clock as required by ISO-8601.

b) The form of zero offset

If you want to parse zero offset like "+0000" instead of "Z" (as described in ISO-paper) you should not use the pattern symbol "X" but "ZZZ". Citing the pattern syntax:

Offset Z: This formats the offset based on the number of pattern letters. One, two or three letters outputs the hour and minute, without a colon, such as '+0130'. The output will be '+0000' when the offset is zero.

c) Your input is NOT ISO-8601-compatible therefore no bug in Java

Your assumption that "2018-02-13T10:20:12.120+0000" shall be valid ISO is wrong because you are mixing basic format (in the offset part) and extended format which is explicitly prohibited in ISO-paper (see sections 4.3.2 (example part) and 4.3.3d). Citing ISO-8601:

[...]the expression shall either be completely in basic format, in which case the minimum number of separators necessary for the required expression is used, or completely in extended format[...]

The statement of B. Bourque that java.time has a bug is based on the same wrong expectation about ISO-compatibility. And the documentation of let's say ISO_OFFSET_DATE_TIME describes the support of the extended ISO-format only. See also the related JDK issue. Not all ISO-8601-variants are directly supported hence a pattern-based construction of the parser in the right way is okay.

Mori answered 31/1, 2020 at 17:31 Comment(1)
The link to the JDK issue is extremely helpful to understand the issue. The nuance of the "extended" vs. "basic" format is not going to be immediately obvious to most programmers. The part of the specification where it must be completely in extended format or not at all is most definitely cumbersome and causes workarounds for incoming dates which might not strictly adhere. Maybe this isn't strictly a JDK bug, but it would have been nice if the JDK could be a little more forgiving here and just do the right thing. Good references though, thank you.Fluor
A
0

if offset +0000 try this

DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuuu-MM-dd'T'HH:mm:ss.SSSX" )
LocalDate from =LocalDate.parse("2018-02-13T10:20:12.120+0000",f);
Averroes answered 8/9, 2020 at 5:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.