Joda Time? You might be overusing DateTime

We generally prefer to use Joda-Time for our date and time representation. Its immutable objects fit our house style, and the plusHours, plusDays etc methods usually produce much more readable and maintainable code than the giant bag of static methods based on Date and Calendar that we had before. Throw in easy construction from components, thread-safe parsers and formatters, a richer type catalogue for representing date-only, time-only, timezone-less values and integrated conversion to/from ISO-style strings, and working with date and time values becomes more comfortable and less fragile.

This snippet is fairly typical:

DateTime now = new DateTime(2015, 4, 13, 9, 15 /*default timezone*/);
DateTime laterToday = now.withTime(17, 30, 0, 0);
DateTime tomorrow = now.plusDays(1);
DateTime muchLater = now.plusMonths(6);

In principle, a DateTime contains a date, a time and a timezone. These three correspond to an instant- a number of milliseconds since the epoch, 1st Jan 1970 00:00 UTC.

Actually, that’s not quite correct- there are some date/time/timezone combinations that don’t correspond to an instant, and some that ambiguously reference multiple instants. This occurs when the timezone includes rules for daylight savings time, producing gaps where the clocks go forward and overlaps when the clocks go back. (Consider how you would interpret “2015-03-29T01:30:00” as London time, or “2015-10-25T01:30:00”).

Here’s a nasty thing that can occur when you use DateTime, though. If you wrote this:

assertThat(new DateTime(2008, 6, 1, 14, 0, 0) /* Europe/London */,
    equalTo(DateTime.parse("2008-06-01T14:00:00+01:00")));

Then (if your system timezone is “Europe/London”, or you explicitly passed it in as the parameter) you would get this highly unhelpful failure:

Expected: <2008-06-01T14:00:00.000+01:00>
     but: was <2008-06-01T14:00:00.000+01:00>

What does this mean? The issue here is that DateTime embeds a timezone rule set. In this instance, “Europe/London” is a ruleset that specifies an offset of 0 during the winter half the year, and +1 hours during the summer half. By constrast, simply parsing a string with “+01:00” on the end yields a DateTime with a timezone that is +1 hours, with no transition rules. Sadly this difference is not reflected in the toString() output.

However, if our intent was to assert that the point-in-time (instant) referred to was as expected, then clearly this assertion isn’t doing what we want. And the reason for that is that a DateTime is not an instant, but kind of an instant and timezone.

In our application, it’s often the case that we don’t have any useful timezone information. A timestamp stored in the database is just an instant. A timestamp in a JSON event structure is just an instant (typically even if it was transmitted with a zone offset, we would collapse it to a timestamp in UTC). So if all we want is an instant, and we want equality of values to be based on that, maybe we need a class that represents exactly that instead?

In fact, Joda-Time does have such a class: org.joda.time.Instant. This is an immutable value class wrapping around just a “long epochMillis” value. It adds the usual Joda-Time sugar: toString and parse methods for going to/from ISO-style representation, plus and minus methods for producing updated values, conversions to DateTime and Date. The toString output will always be in UTC, making different instants visually distinct.

As a wrapper around epoch time, this type is much more suited to represent timestamps from databases, in messages. However, an Instant lacks those convenient transformation methods “plusDays”, “plusMonths” and their friends. Isn’t that dreadfully inconvenient? Well, that may be so, but this is a push towards correctness. Without a timezone to provide context, “add 6 months” is a dubious operation. Compare and contrast performing “plusMonths(6)” against our two DateTimes up above. They started out representing the same instant, but the addition will produce two DateTimes at different instants:

new DateTime(2008, 6, 1, 14, 0, 0).plusMonths(6)
-> 2008-12-01T14:00Z
DateTime.parse("2008-06-01T14:00:00").plusMonths(6)
-> 2008-12-01T14:00+01:00 one hour later!

(And if you add another 6 months, they will point at the same instant again!)

Performing date-based manipulation on a “pure instant” isn’t really well-defined. Performing time-based manipulation can be, so in a way it’s a shame that plusHours etc are missing.

JSR-310 and java.time

This realisation about using Instant came due to looking at how the JDK8 java.time library differed from Joda-Time. The JSR-310 process to include it was led by Stephen Colebourne, who originally wrote and still maintains Joda-Time. In an article entitled Why JSR-310 isn’t Joda-Time, he listed several design flaws in Joda-Time that he felt needed to be fixed: first on the list was that both Instant and DateTime “are implementations of ReadableInstant. This is wrong”.

In the JSR-310 library, DateTime is split into ZonedDateTime (essentially equivalent) and OffsetDateTime (the timezone can only be a fixed offset). These can only be converted to a “long millis” value via the Instant class, bringing that class to the fore. Additionally, ZonedDateTime’s toString output will now include the timezone rule set name, if the rule set is anything other than a fixed offset, avoiding the unhelpful assertion failure above.

Leave a Reply

Your email address will not be published. Required fields are marked *