38

I'm trying to parse the date returned as a value from the HTML5 datetime input field. Try it in Opera to see an example. The date returned looks like this: 2011-05-03T11:58:01Z.

I'd like to parse that into a Java Date or Calendar Object.

Ideally a solution should have the following things:

  • No external libraries (jars)
  • Handles all acceptable RFC 3339 formats
  • A String should be able to be easily validated to see if it is a valid RFC 3339 date
Adam
  • 43,763
  • 16
  • 104
  • 144
  • 1
    Why the requirement for no external libraries? Joda Time just *does* this. – Ian McLaird May 18 '11 at 03:03
  • I do like Joda Time. But this is for part of a framework and I don't want to add Joda Time as a dependency of the framework. – Adam May 18 '11 at 15:54
  • 1
    FYI, the [*Joda-Time*](http://www.joda.org/joda-time/) project is now in [maintenance mode](https://en.wikipedia.org/wiki/Maintenance_mode), with the team advising migration to the [*java.time*](http://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes. See [Tutorial by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque Apr 03 '18 at 21:45
  • 1
    FYI, the troublesome old date-time classes such as [`java.util.Date`](https://docs.oracle.com/javase/10/docs/api/java/util/Date.html), [`java.util.Calendar`](https://docs.oracle.com/javase/10/docs/api/java/util/Calendar.html), and `java.text.SimpleDateFormat` are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes built into Java 8 and later. See [*Tutorial* by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque May 02 '18 at 21:43

9 Answers9

39

tl;dr

Instant.parse( "2011-05-03T11:58:01Z" )

ISO 8601

Actually, RFC 3339 is but a mere self-proclaimed “profile” of the actual standard, ISO 8601.

The RFC is different in that it purposely violates ISO 8601 to allow a negative offset of zero hours (-00:00) and gives that a semantic meaning of “offset unknown“. That semantic seems like a very bad idea to me. I advise sticking with the more sensible ISO 8601 rules. In ISO 8601, having no offset at all means the offset is unknown – an obvious meaning, whereas the RFC rule is abstruse.

The modern java.time classes use the ISO 8601 formats by default when parsing/generating strings.

Your input string represents a moment in UTC. The Z on the end is short for Zulu and means UTC.

Instant (not Date)

The modern class Instant represents a moment in UTC. This class replaces java.util.Date, and uses a finer resolution of nanoseconds rather than milliseconds.

Instant instant = Instant.parse( "2011-05-03T11:58:01Z" ) ;

ZonedDateTime (not Calendar)

To see that same moment through the wall-clock time used by the people of a certain region (a time zone), apply a ZoneId to get a ZonedDateTime. This class ZonedDateTime replaces the java.util.Calendar class.

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;  // Same moment, same point on the timeline, different wall-clock time.

Converting

I strongly recommend avoiding the legacy date-time classes when possible. But if you must inter-operate with old code not yet updated to java.time, you may convert back-and-forth. Call new methods added to the old classes.

Instant replaces java.util.Date.

java.util.Date myJUDate = java.util.Date.from( instant ) ;  // From modern to legacy.
Instant instant = myJUDate.toInstant() ;                    // From legacy to modern.

ZonedDateTime replaces GregorianCalendar.

java.util.GregorianCalendar myGregCal = java.util.GregorianCalendar.from( zdt ) ;  // From modern to legacy.
ZonedDateTime zdt = myGregCal.toZonedDateTime() ;           // From legacy to modern.

If you have a java.util.Calendar that is actually a GregorianCalendar, cast.

java.util.GregorianCalendar myGregCal = ( java.util.GregorianCalendar ) myCal ;  // Cast to the concrete class.
ZonedDateTime zdt = myGregCal.toZonedDateTime() ;           // From legacy to modern.

Bulleted concerns

Regarding your Question’s specific issues…

  • No external libraries (jars)

The java.time classes are built into Java 8, 9, 10, and later. An implementation is also included in later Android. For earlier Java and earlier Android, see the next section of this Answer.

  • Handles all acceptable RFC 3339 formats

The various java.time classes handle every ISO 8601 format I know of. They even handle some formats that mysteriously disappeared from later editions of the standard.

For other formats, see the parse and toString methods of the various classes such as LocalDate, OffsetDateTime, and so on. Also, search Stack Overflow as there are many examples and discussions on this topic.

  • A String should be able to be easily validated to see if it is a valid RFC 3339 date

To validate input strings, trap for DateTimeParseException.

try {
    Instant instant = Instant.parse( "2011-05-03T11:58:01Z" ) ;
} catch ( DateTimeParseException e ) {
    … handle invalid input
}

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.

Community
  • 1
  • 1
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • 5
    Unfortunately, `Instant` keeps failing on dates with timezone, `2019-11-30T10:30:00+02:00` or `2019-10-12T07:20:50.52+00:00`, both with and without milliseconds. It fails with errors like `java.time.format.DateTimeParseException: Text '2019-11-30T10:30:00+02:00' could not be parsed at index 19`, or same on `index 23`. Therefore I can't use it in production, it does not actually support RFC-3339. – Dmitriy Popov Jun 03 '20 at 17:46
  • 2
    @DmitriyPopov `OffsetDateTime.parse( "2019-11-30T10:30:00+02:00" )` and `OffsetDateTime.parse( "2019-10-12T07:20:50.52+00:00" )`. All of the *java.time* classes use ISO 8601 formats by default for parsing/generating text. You just need to choose the appropriate *java.time* class for the semantics of your text. – Basil Bourque Jul 03 '22 at 01:50
  • 1
    Saying one needs to choose the appropriate `java.time` class for the semantics of the text is a bit disingenuous. The semantics is all valid RFC3339 and you seem to be saying I need to first parse it to figure out what class to use to parse it? – Daniel C. Sobral Jan 05 '23 at 18:45
  • @DanielC.Sobral Use the class appropriate to each kind of value. For a value in UTC, use `Instant`. For a value with an offset, use `OffsetDateTime`. For a value with a time zone, use `ZonedDateTime`. For a value lacking any offset or time zone, use `LocalDateTime`. It’s really not that complicated. How is that “disingenuous”? – Basil Bourque Jan 09 '23 at 08:29
  • It is disingenuous because finding out whether a value is UTC or offset is part of the parsing. If I have a protocol with a field saying "RFC3339 timestamp", then all valid RFC3339 values must be accepted when decoding it. Alas, named time zones are not valid RFC3339. – Daniel C. Sobral Jan 10 '23 at 16:51
16

So, in principle this would be done using different SimpleDateFormat patterns.

Here a list of patterns for the individual declarations in RFC 3339:

  • date-fullyear: yyyy
  • date-month: MM
  • date-mday: dd
  • time-hour: HH
  • time -minute: mm
  • time-second: ss
  • time-secfrac: .SSS (S means millisecond, though - it is not clear what would happen if there are more or less than 3 digits of these.)
  • time-numoffset: (like +02:00 seems to be not supported - instead it supports the formats +0200, GMT+02:00 and some named time zones using z and Z.)
  • time-offset: 'Z' (not supporting other time zones) - you should use format.setTimezone(TimeZone.getTimeZone("UTC")) before using this.)
  • partial-time: HH:mm:ss or HH:mm:ss.SSS.
  • full-time: HH:mm:ss'Z' or HH:mm:ss.SSS'Z'.
  • full-date: yyyy-MM-dd
  • date-time: yyyy-MM-dd'T'HH:mm:ss'Z' or yyyy-MM-dd'T'HH:mm:ss.SSS'Z'

As we can see, this seems not to be able to parse everything. Maybe it would be a better idea to implement an RFC3339DateFormat from scratch (using regular expressions, for simplicity, or parsing by hand, for efficiency).

Community
  • 1
  • 1
Paŭlo Ebermann
  • 73,284
  • 20
  • 146
  • 210
  • Shouldn't `date-fullyear` be `yyyy`? – Buhake Sindi Aug 10 '13 at 13:49
  • Looks like you are right here – EEEE is for "day of week". I'm not sure where I got this from, maybe from another library which made something different for `y`? – Paŭlo Ebermann Aug 12 '13 at 07:31
  • With Java 7+ SimpleDateFormat adds the [X timezone format](http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html) a superset of Z format accepting +/- two-digit or four-digit with optional colon (but minutes are ignored) – karmakaze Apr 11 '15 at 04:14
  • 3
    SimpleDateFormat chokes on fractions beyond millis. – mmindenhall Jan 05 '16 at 20:07
  • @Thomas While I'm using Java 8, I've never had the need of parsing dates with it (it was always already done in the libraries I've been using for HTTP and/or Json mapping). Feel free to add your own answer, I would upvote it. (It looks like [`DateTimeFormatter.ISO_OFFSET_DATE_TIME`](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#ISO_OFFSET_DATE_TIME) includes this functionality.) – Paŭlo Ebermann Sep 08 '17 at 19:32
  • @Thomas Yes, I posted [an Answer](https://stackoverflow.com/a/49639543/642706) showing the modern approach using *java.time* classes built into Java 8, 9, 10, and later. The legacy date-time classes really are a wretched mess of poor design (however well-intentioned) that should be avoided. – Basil Bourque Apr 03 '18 at 21:50
13

Just found that google implemented Rfc3339 parser in Google HTTP Client Library

https://github.com/google/google-http-java-client/blob/dev/google-http-client/src/main/java/com/google/api/client/util/DateTime.java

Tested. It works well to parse varies sub seconds time fragment.

import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;

import com.google.api.client.util.DateTime;

DateTimeFormatter formatter = DateTimeFormatter
            .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
            .withZone(ZoneId.of("UTC"));

@Test
public void test1e9Parse() {
    String timeStr = "2018-04-03T11:32:26.553955473Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.553Z");
}

@Test
public void test1e3Parse() {
    String timeStr = "2018-04-03T11:32:26.553Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.553Z");
}

@Test
public void testEpochSecondsParse() {

    String timeStr = "2018-04-03T11:32:26Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.000Z");
}
vicknite
  • 192
  • 1
  • 7
  • 3
    You seem to be using *java.time* classes (good). But there is no `DateTime` class in *java.time*. I am confused. – Basil Bourque Apr 03 '18 at 21:14
  • 1
    Im Sorry, forgot to write the import for DateTime class. It is belong to com.google.api.client.util package. – vicknite Apr 04 '18 at 13:48
2

Maybe not the most elegant way, but certainly working one I recently made:

Calendar cal = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
cal.setTime(sdf.parse(dateInString.replace("Z", "").replace("T", "-")));
S.I.
  • 3,250
  • 12
  • 48
  • 77
PecaWolf
  • 77
  • 1
  • 11
2

With the format you have e.g. 2011-05-03T11:58:01Z, below code will do. However, I recently tryout html5 datetime in Chrome and Opera, it give me 2011-05-03T11:58Z --> do not have the ss part which cannot be handled by code below.

new Timestamp(javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(date).toGregorianCalendar().getTimeInMillis());
Coolliam
  • 21
  • 1
1

I'm using this:

DateTimeFormatter RFC_3339_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .append(ISO_LOCAL_DATE_TIME)
            .optionalStart()
            .appendOffset("+HH:MM", "Z")
            .optionalEnd()
            .toFormatter();

Example:

String dateTimeString = "2007-05-01T15:43:26.3452+07:00";
ZonedDateTime zonedDateTime = ZonedDateTime.from(RFC_3339_DATE_TIME_FORMATTER.parse(dateTimeString));
Johan
  • 37,479
  • 32
  • 149
  • 237
  • Thanks for sharing! I have a tool which still must support Java 8 and I just discovered that the offset pattern "+HH:MM" was not standard in JDK8. In earlier JDK versions, however, (I tried on Java 17) just calling `Instant.parse(string)` will be enough to support both offset options out-of-the-box. – Oswaldo Junior May 12 '23 at 04:35
1

Though, The question is very old, but it may help one who wants it Kotlin version of this answer. By using this file, anyone can convert a Rfc3339 date to any date-format. Here I take a empty file name DateUtil and create a function called getDateString() which has 3 arguments.

1st argument : Your input date
2nd argument : Your input date pattern
3rd argument : Your wanted date pattern

DateUtil.kt

object DatePattern {
    const val DAY_MONTH_YEAR = "dd-MM-yyyy"
    const val RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'"
}

fun getDateString(date: String, inputDatePattern: String, outputDatePattern: String): String {
    return try {
        val inputFormat = SimpleDateFormat(inputDatePattern, getDefault())
        val outputFormat = SimpleDateFormat(outputDatePattern, getDefault())

        outputFormat.format(inputFormat.parse(date))
    } catch (e: Exception) {
        ""
    }
}

And now use this method in your activity/fuction/dataSourse Mapper to get Date in String format like this

getDate("2022-01-18T14:41:52Z", RFC3339, DAY_MONTH_YEAR)

and output will be like this

18-01-2022
Aminul Haque Aome
  • 2,261
  • 21
  • 34
1

For future reference, as an alternative, you could use ITU[1] which is hand-written to deal with exactly RFC-3339 parsing and also lets you easily deal with leap seconds. The library is dependency-free and only weighs in at 18 kB.

Full disclosure: I'm the author

try 
{
    final OffsetDateTime dateTime = ITU.parseDateTime(dateTimeStr);
}
catch (LeapSecondException exc) 
{
  // The following helper methods are available let you decide how to progress
  //int exc.getSecondsInMinute()
  //OffsetDateTime exc.getNearestDateTime()
  //boolean exc.isVerifiedValidLeapYearMonth()
}

[1] - https://github.com/ethlo/itu

Morten Haraldsen
  • 1,013
  • 12
  • 24
-1
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(datetimeInFRC3339format)
Artsv
  • 1