Joda DateTime with TimeZone Information

I have the following:

val startWithTZ = new DateTime("2016-10-01T00:00:00", DateTimeZone.UTC)
val endWithTZ = new DateTime("2016-10-01T11:00:00", DateTimeZone.UTC)
val intervalWithTZ = new Interval(startWithTZ, endWithTZ)

val startWithoutTZ = new DateTime("2016-10-01T00:00:00")
val endWithoutTZ = new DateTime("2016-10-01T11:00:00")
val intervalWithoutTZ = new Interval(startWithoutTZ, endWithoutTZ)

With this, I now do the following:

val utcInterval = new Interval(intervalWithoutTZ.getStart.toDateTime(DateTimeZone.UTC),
intervalWithoutTZ.getEnd.toDateTime(DateTimeZone.UTC))

What I see with the utcInterval is not what I was expecting:

2016-09-30T22:00:00.000Z/2016-10-01T09:00:00.000Z

Why is this? I was expecting the utcInterval to be:

2016-10-01T00:00:00.000Z/2016-10-01T11:00:00.000Z

Here is what I get when I use the Scala REPL:

scala> import org.joda.time._
import org.joda.time._

scala> val startWithoutTZ = new DateTime("2016-10-01T00:00:00")
startWithoutTZ: org.joda.time.DateTime = 2016-10-01T00:00:00.000+02:00

scala> val endWithoutTZ = new DateTime("2016-10-01T11:00:00")
endWithoutTZ: org.joda.time.DateTime = 2016-10-01T11:00:00.000+02:00

scala> val intervalWithoutTZ = new Interval(startWithoutTZ, endWithoutTZ)
intervalWithoutTZ: org.joda.time.Interval = 2016-10-01T00:00:00.000+02:00/2016-10-01T11:00:00.000+02:00

scala> val utcInterval = new Interval(intervalWithoutTZ.getStart.toDateTime(DateTimeZone.UTC),
     | intervalWithoutTZ.getEnd.toDateTime(DateTimeZone.UTC))
utcInterval: org.joda.time.Interval = 2016-09-30T22:00:00.000Z/2016-10-01T09:00:00.000Z

scala> 

Why is it not?

Jon Skeet
people
quotationmark

Logically, I've always felt that an Interval should be between two Instant values rather than two DateTime values. However, the Joda Time API is what it is.

When you create an Interval from two DateTime values, the end time is converted into the time zone of the start time. In your case, this doesn't actually need to do anything because for intervalWithTZ they're both in UTC, and for intervalWithoutTZ they're both in your system default time zone. Your naming is inaccurate here, because all the values are in a time zone, even though you didn't specify one. A DateTime always has a time zone; for a value without a time zone you want LocalDateTime instead.

So, we can get rid of your intervalWithoutTZ and simply write your code as:

DateTime startInDefaultZone = new DateTime("2016-10-01T00:00:00")
DateTime endInDefaultZone = new DateTime("2016-10-01T11:00:00")

val utcInterval = new Interval(startInDefaultZone.toDateTime(DateTimeZone.UTC),
                               endInDefaultZone.toDateTime(DateTimeZone.UTC));

That won't change the result at all... but the basic point is that you're converting a DateTime of "2016-10-01T00:00:00 in your system default time zone" into UTC... and presumably your default time zone is 2 hours behind UTC, so 11am in UTC is 9am in your local time, etc.

In short, we can actually get rid of intervals entirely here - what you're seeing can be shown more simply as:

DateTime startInDefaultZone = new DateTime("2016-10-01T00:00:00")
DateTime startInUtc = startInDefaultZone.toDateTime(DateTimeZone.UTC);

You appear to expect that to be 2016-10-01T00:00:00Z, but it's actually "the instant in time represented by startInDefaultZone, but expressed in UTC" which is 2016-10-01T02:00:00Z on your machine.

If you really want to "change time zone without changing local values" you could use withZoneRetainFields, e.g.

DateTime startInDefaultZone = new DateTime("2016-10-01T00:00:00")
DateTime startInUtc = startInDefaultZone.withZoneRetainFields(DateTimeZone.UTC);

However, that usually means you've used the wrong type to start with, and should have been using LocalDateTime. It's rarely a useful operation in a well-designed system.

people

See more on this question at Stackoverflow