Java 8 – Convert Joda-Time to New Date and Time API

datetimejava-8java-timejodatime

How can I achieve the following in Java 8 java.time:

DateTime dt = new DateTime(year, month, date, hours, min, sec, milli);

But I have found this option:

OffsetDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond, offset)

But it has only method without milliseconds.

How can I achieve the same as Joda-Time?

Best Answer

To adjust the milliseconds to nanoseconds, just multiply the milliseconds value by 1000000 (1 million).


To get the correct offset, though, it's a little bit tricky.

Joda's DateTime uses the system's default timezone when you don't specify one. So, in java.time, it's natural to think that it's just a matter of using ZoneId.systemDefault() (the system's default timezone) to get the valid offset for that date - and this will work for most times, but there are some corner cases that you must be aware of.

Timezones may have different offsets during history - nowadays, most of these changes occur due to Daylight Saving Time (DST), but it can also happen because some government/administration decided to change it.

Example: in 1985, the brazilian state of Acre had standard offset UTC-05:00 (and UTC-04:00 during DST), then in 1988 it was on UTC-05:00 without DST, then in 2008 the standard changed to UTC-04:00 (and no DST), and since 2013 it's back to UTC-05:00 and no DST. This may sound like an extreme case, but changes like that can occur at anytime/anywhere.

All of this just to make it clear that an offset depends on both the timezone and the date. And you'll have to make some decisions when facing corner cases.

For this example, I'll use my default timezone, which is America/Sao_Paulo (here we have DST every year). Currently, our offset is -03:00, but in October 15th 2017, at midnight, the clocks will shift 1 hour forward (from midnight to 1 AM) and the offset will be changed to -02:00.

In this case, the period between midnight and 1 AM is "skipped" and any local time between them is invalid (this is called a gap). Actually, if you try to create such date and time in Joda-Time, it'll throw an exception:

// reminding that my system's default timezone is America/Sao_Paulo
DateTime d = new DateTime(2017, 10, 15, 0, 10, 0, 0);

This throws:

org.joda.time.IllegalInstantException: Illegal instant due to time zone offset transition (daylight savings time 'gap'): 2017-10-15T00:10:00.000 (America/Sao_Paulo)

But in java.time, the ZonedDateTime class adjusts it to the next valid offset:

// reminding that my system's default timezone is America/Sao_Paulo
ZonedDateTime z = ZonedDateTime.of(2017, 10, 15, 0, 10, 0, 0, ZoneId.systemDefault());

The value of z is:

2017-10-15T01:10-02:00[America/Sao_Paulo]

Note that it adjusted 00:10 to the next valid offset (-02:00) and the time was also adjusted accordingly to 01:10. You can then do:

OffsetDateTime odt = z.toOffsetDateTime()

to get your OffsetDateTime (in this case, the value will be 2017-10-15T01:10-02:00).


When DST ends it's also complicated. In São Paulo, DST will end in February 18th, 2018. At midnight, the clocks will shift back 1 hour (to February 17th at 11 PM) and the offset will be back to -03:00.

So, the period between February 17th at 23 PM and February 18th at midnight will exist twice (first in -02:00 offset, then in -03:00 offset - that's called an overlap). For the local times between 11 PM and midnight, there will be 2 valid offsets, so you'll have to decide which one to use.

Fortunately, ZonedDateTime provides the 2 options: you can use withEarlierOffsetAtOverlap() to get the offset before the DST change (in my case, it'll be -02:00) and withLaterOffsetAtOverlap() to get the offset after the DST change (in my case, it'll be -03:00):

// reminding that my system's default timezone is America/Sao_Paulo
z = ZonedDateTime.of(2018, 2, 17, 23, 30, 0, 0, ZoneId.systemDefault());
System.out.println(z.withEarlierOffsetAtOverlap()); // 2018-02-17T23:30-02:00[America/Sao_Paulo]
System.out.println(z.withLaterOffsetAtOverlap()); // 2018-02-17T23:30-03:00[America/Sao_Paulo]

The output will be:

2018-02-17T23:30-02:00[America/Sao_Paulo]
2018-02-17T23:30-03:00[America/Sao_Paulo]

Note how the methods with...OffsetAtOverlap get the offsets before and after the DST change - also note that the local time 23:30 exists twice, in 2 different offsets (-02:00 and -03:00). Once you choose which one to use, you can call toOffsetDateTime() to get your OffsetDateTime instance:

OffsetDateTime beforeDSTChange = z.withEarlierOffsetAtOverlap().toOffsetDateTime();
OffsetDateTime afterDSTChange = z.withLaterOffsetAtOverlap().toOffsetDateTime();

beforeDSTChange will be 2018-02-17T23:30-02:00 and afterDSTChange will be 2018-02-17T23:30-03:00.

By default, ZonedDateTime creates the date/time with the earlier offset, but I believe it's better to make it explicit which one you're using, by calling the corresponding method. And in cases where only one offset is valid, both methods return the same thing.

By the way, I've tested this same date in Joda-Time 2.9.9 and it gets the earlier offset -02:00:

// reminding that my system's default timezone is America/Sao_Paulo
System.out.println(new DateTime(2018, 2, 17, 23, 30, 0, 0));

2018-02-17T23:30:00.000-02:00

And the DateTime class also has the methods withEarlierOffsetAtOverlap() and withLaterOffsetAtOverlap() that work in similar way to ZonedDateTime.


Of course for most times you can just use the ZonedDateTime without worries - DST and any other offset changes don't occur all the time. But it's important to be aware of these corner cases and adjust it accordingly.

One additional warning: instead of using ZoneId.systemDefault(), you can make it explicit which timezone you want, by using ZoneId.of("zonename"). The API uses IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Europe/Berlin). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds(). It's better to make it explicit what timezone you're using, because the system's default can be changed without notice, even at runtime.


If you have both Joda's DateTime and java.time available, you can also convert from one to another.

You can get the epoch millis value from the Joda's DateTime to create a java.time.Instant. Then you create a ZoneId with the same ID of the DateTime's zone, and get the respective offset for the Instant and join the pieces to create the OffsetDateTime:

DateTime d = new DateTime(2018, 2, 17, 23, 30, 0, 0);

// convert to java.time.Instant, using the same epoch millis value
Instant instant = Instant.ofEpochMilli(d.getMillis());

// get the offset for that instant, in the same zone of the Joda's DateTime instance
ZoneOffset offset = ZoneId.of(d.getZone().getID()).getRules().getOffset(instant);

// get the OffsetDateTime in the offset above
OffsetDateTime odt = instant.atOffset(offset); // 2018-02-17T23:30-02:00

The result will be 2018-02-17T23:30-02:00. Note that the DateTime object uses the earlier offset (as explained above), but you can optionally use d.withLaterOffsetAtOverlap().getMillis() if you want to use the later offset.

Just reminding that in cases of a DST gap, Joda-Time throws an exception, as previously explained.

Related Question