A Better Date and Time API: Joda Time

by
Lance Finney, Senior Software Engineer
Object Computing, Inc. (OCI)

Introduction

Calculating, storing, manipulating, and recording time is vital for many types of applications: banking applications use time to calculate interest; supply-chain forecasting applications use time to analyze the past and predict the future; etc. Unfortunately, it is difficult to meet these requirements. With internationalization issues, time zone handling, different calendar systems, and Daylight Saving Time to be considered, the subject matter is far from trivial and is best handled by a specialized API.

Unfortunately, the API included so far with JavaTM is insufficient:

Fortunately, a powerful replacement is on its way: Joda Time. Not only is this replacement available as an open-source library, but it is also the basis for JSR 310 which is under consideration to be added to Java. This article presents an overview of this important and useful API.

Learning Through Example

As mentioned, one of the problems with the JDK API is difficulty in calculating the number of days between two different dates. To learn a bit about how to use Joda Time, let's see how one can solve that problem a few different ways using Joda Time. The example finds the number of days from January 1, 2008 to July 1, 2008.

package com.ociweb.jnb.joda;

import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.LocalDate;
import org.joda.time.Months;
import org.joda.time.PeriodType;


public class DateDiff {
    public static int getDaysBetween(Interval interval) {
        return interval.toPeriod(PeriodType.days()).getDays();
    }

    public static void main(String[] args) {
        // Use simple constructor for DateTime - January is 1, not 0!
        DateTime start = new DateTime(2008, 1, 1, 0, 0, 0, 0);

        // Create a DateMidnight from a LocalDate
        DateMidnight end = new LocalDate(2008, 7, 1).toDateMidnight();

        // Create Day-specific Period from a DateTime and DateMidnight
        int days = Days.daysBetween(start, end).getDays();
        System.out.println("days = " + days);

        // Create an Interval from a DateTime and DateMidnight
        days = getDaysBetween(new Interval(start, end));
        System.out.println("days = " + days);

        // Create an Interval from the ISO representation
        String isoString =
                "2008-01-01T00:00:00.000/2008-07-01T00:00:00.000";
        days = getDaysBetween(new Interval(isoString));
        System.out.println("days = " + days);

        // Create an Interval from a DateTime and a Period
        days = getDaysBetween(new Interval(start, Months.SIX));
        System.out.println("days = " + days);
    }
}
>days = 182
>days = 182
>days = 182
>days = 182

Note that each gives the correct answer of 182 days. While this might seem a trivial problem, it actually is not simple using just the JDK. The typical approach used with the JDK is to compare the timestamps for the midnights of January 1 and July 1 and divide the result by the number of milliseconds in a day. While this works in some cases, my computer would give 181.958333 using that approach instead of 182 for the given example. Why? Because my computer is set to use Daylight Saving Time in the northern hemisphere, and one hour was skipped in the first half of the year.

Joda Time avoids these calculation errors by extracting a Day-specific Period of using direct millisecond math. However, the way we generate the Period and the type of Period used varies.

The first example shows the simplest approach to solve the problem; we directly use the type of Period designed for date ranges. It's that easy.

In the second example, we create Day-specific form of a generic Period from an Interval that itself is created from a DateTime and a DateMidnight (We could also use Days.daysIn(interval).getDays() instead as an alternative to the generic Period used here). DateTime is the main user class in Joda Time. In a way, it's a replacement for java.util.Date, encapsulating all information about date, time, and time zone. DateMidnight is a similar class that is restricted to midnight. The DateTime is created simply, but the DateMidnight is created from a LocalDate, a class that represents only the date information. LocalDate does not represent as much information as DateTime, lacking information about time of day, time zone, and calendar system. I discuss these core classes more below.

In the third example, we create the Interval by parsing an ISO 8601 String representation of a date interval.

In the last example, we create the Interval from a DateTime and a Month-specific Period. Interestingly, even though we use one Period to create the Interval and extract another Period from the Interval to calculate the number of days, we cannot directly use the initial Period. The reason for this is that Months.SIX does not represent a consistent number of days, depending on the months included.

Finally, notice that the key for January is 1, not 0. This fixes the source of many bugs using Date and Calendar. Alternately, DateTimeConstants.JANUARY is available.

Primary User Classes

Now that we've seen an example that introduced some of the API, let's look at the major classes that a user of Joda Time would use. Each of the concepts will be discussed in depth later. I am introducing a style convention here: from now on, a concept will be presented in italics, and a concrete class will be presented in a code block. This is necessary because, for example, there is both an Instant concept and an Instant concrete class.

Concept Sub-concept Immutable Mutable
Instant DateTime
DateMidnight
Instant
MutableDateTime
Partial LocalDate
LocalDateTime
LocalTime
Partial
Interval Interval MutableInterval
Duration Duration
Period Any Field Period MutablePeriod
Single Field Seconds
Minutes
Hours
Days
Weeks
Months
Years
Chronology BuddhistChronology
CopticChronology
EthiopicChronology
GJChronology
GregorianChronology
IslamicChronology
ISOChronology
JulianChronology

The main point to notice here is that Joda Time often provides both immutable and mutable implementations of its concepts. In general, it is preferred for performance and thread-safety to use the immutable versions.

If you need to modify an immutable object (for example, a DateTime), there are two options:

Instant

The Instant is the core time representation with Joda Time. An Instant is defined as "an instant in the datetime continuum specified as a number of milliseconds from 1970-01-01T00:00Z." In general, it is not important that the starting point is 1970, except that it simplifies interoperability with the JDK classes. The key point is that an Instant knows the time zone and calendar system being used (in contrast, we will later discuss the Partial, which has some time information, but not the time zone and calendar context).

Joda Time offers four implementations of Instant:

DateTime
The most common implementation — this immutable representation allows full definition of date, time, time zone, and calendar system.
MutableDateTime
This is a mutable, non-thread-safe version of DateTime.
DateMidnight
Similar to DateTime, except that the information is date-only (the time is forced to be midnight). This is also immutable.
Instant
This is a much simpler immutable implementation that contains date and time information, but is always UTC. This cannot be used in time zone or calendar sensitive situations, but is instead useful as a Daylight Saving Time-independent event timestamp.

Some of these classes are demonstrated in DateDiff example above.

Partial

Compared to an Instant, a Partial contains less information. A Partial does not know about time zone, and it may contain only part of the information contained in an Instant (hence the name).

Joda Time offers four implementations of Partial (all are immutable):

LocalDate
This contains date-only information. The difference between LocalDate and DateMidnight is that LocalDate represents the entire day, while DateMidnight represents the moment of midnight at the beginning of the day.
LocalTime
This contains time-only information. A particular LocalTime instance applies to the same part of any day, not to a unique moment in time.
LocalDateTime
This contains both date and time information. Unlike DateTime, however, this should be considered only a local instant that works in an assumed and unspecified time zone and calendar system.
Partial
A special implementation that can handle any desired combination of date and time information, created by specifying a single or multiple DateTimeFieldTypes in the constructor.
    DateTimeFieldType[] types = {
            DateTimeFieldType.year(),
            DateTimeFieldType.dayOfMonth(),
            DateTimeFieldType.minuteOfHour()
    };
    Partial partial = new Partial(types, new int[]{2008, 3, 15});
In this example, the Partial defines a moment in the unspecified time zone that is in the 15th minute, the 3rd day, and the 2008th year of the default calendar system. However, the month, hour, and all other fields are empty. They do not even default to 0 — they are completely empty. Because one can create nonsensical time concepts like this, it is not as easy to convert a Partial to an Instant as it is to convert a LocalDate.

Converting to and from an Instant can be simple using one of many provided methods for LocalDate, LocalTime, and LocalDateTime. However, note that the Partial instance does not contain any information about time zone and may be missing other time information, so defaults for those fields will be assumed unless they are specified. Additionally, since a LocalTime is equally valid for any day, additional information will be necessary to specify the date information for the Instant.

Interval, Duration, and Period

These three concepts all express information about a range of time, but there are significant differences between them:

Interval
A fully-defined range, specifying the starting Instant and the ending Instant
Duration
The simplest of the three, representing the scientific quantity of a number of milliseconds, typically calculated between two Instants
Period
Similar to Duration in that it represents the difference between times; however, the representation is stored in terms of months and/or weeks and/or days and/or hours, etc.

The difference between Duration and Period to demonstrated by the following example of code intended to add a month to an Instant:

package com.ociweb.jnb.joda;

import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;


public class PeriodDuration {
    public static void main(String[] args) {
        final DateTimeFormatter pattern = DateTimeFormat.forStyle("MS");

        for (int i = 1; i <= 31; i += 10) {
            DateTime initial =
                    new DateMidnight(2008, 3, i).toDateTime();

            // will always add exactly a month
            DateTime periodTime = initial.plusMonths(1);
            System.out.println("period: " +
                    initial.toString(pattern) +
                    " -> " + periodTime.toString(pattern));

            // will always add exactly 31 days
            Duration duration =
                    new Duration(31L * 24L * 60L * 60L * 1000L);
            final DateTime durTime = initial.plus(duration);
            System.out.println("duration: " +
                    initial.toString(pattern) +
                    " -> " + durTime.toString(pattern));
            System.out.println();
        }
    }
}
>period: Mar 1, 2008 12:00 AM -> Apr 1, 2008 12:00 AM
>duration: Mar 1, 2008 12:00 AM -> Apr 1, 2008 1:00 AM
>
>period: Mar 11, 2008 12:00 AM -> Apr 11, 2008 12:00 AM
>duration: Mar 11, 2008 12:00 AM -> Apr 11, 2008 12:00 AM
>
>period: Mar 21, 2008 12:00 AM -> Apr 21, 2008 12:00 AM
>duration: Mar 21, 2008 12:00 AM -> Apr 21, 2008 12:00 AM
>
>period: Mar 31, 2008 12:00 AM -> Apr 30, 2008 12:00 AM
>duration: Mar 31, 2008 12:00 AM -> May 1, 2008 12:00 AM

In this example, we try to add a month to a start time two different ways. Using a Period, we explicitly add a month, and we always get the right answer (assuming that adding a month to the last day in March should give the last day in April). Using a Duration, we add the number of milliseconds equivalent to 31 days, and we run into two problems:

Notice also the special date and time formatting provided by DateTimeFormat.forStyle("MS"). This is just one of many ways in which Joda Time provides significant tools for String parsing and formatting. See the references for more detail on this feature.

Interval

An Interval is a fully-defined range, specifying the starting Instant (inclusive) and the ending Instant (exclusive). The Interval is defined in terms of a specific time zone and calendar system.

Joda Time offers two implementations of Interval:

Interval
The most common implementation — this immutable representation allows full definition of the range of date and time, given a time zone and calendar system.
MutableInterval
This is a mutable, non-thread-safe version of Interval.

The DateDiff example above shows some simple Interval processing.

Duration

Duration represents the scientific quantity of a number of milliseconds, typically calculated between two Instants. It is the simplest concept in Joda Time, with only a single immutable implementation. A Duration can be derived from two Instants or from two millisecond representations of time.

Period

Similar to Duration in that it represents the difference between times, a Period is more complex in that it is defined in terms of one or more particular time units, or fields. There are two distinct sub-concepts of Period, those that are defined in terms of any field, and those specific to a single field.

Single Field Period

These implementations are very simple. If you wish to figure out the number of seconds between two Instants, you can use Seconds. To find out the number of minutes between them, use Minutes, and so on. This example shows each of the Single Field implementations used to examine the difference between 7:40:20.500 AM on February 7, 2000 and 3:30:45.100 PM on July 4, 2008:

package com.ociweb.jnb.joda;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Hours;
import org.joda.time.Minutes;
import org.joda.time.Months;
import org.joda.time.Seconds;
import org.joda.time.Weeks;
import org.joda.time.Years;


public class SingleFields {
    public static void main(String[] args) {
        // 7:40:20.500 AM on February 7, 2000
        DateTime start = new DateTime(2000, 2, 7, 7, 40, 20, 500);

        // 3:30:45.100 PM on July 4, 2008
        DateTime end = new DateTime(2008, 7, 4, 15, 30, 45, 100);

        // Years
        Years years = Years.yearsBetween(start, end);
        System.out.println("years = " + years.getYears());

        // Months
        Months months = Months.monthsBetween(start, end);
        System.out.println("months = " + months.getMonths());

        // Weeks
        Weeks weeks = Weeks.weeksBetween(start, end);
        System.out.println("weeks = " + weeks.getWeeks());

        // Days
        Days days = Days.daysBetween(start, end);
        System.out.println("days = " + days.getDays());

        // Hours
        Hours hours = Hours.hoursBetween(start, end);
        System.out.println("hours = " + hours.getHours());

        // Minutes
        Minutes minutes = Minutes.minutesBetween(start, end);
        System.out.println("minutes = " + minutes.getMinutes());

        // Seconds
        Seconds seconds = Seconds.secondsBetween(start, end);
        System.out.println("seconds = " + seconds.getSeconds());
    }
}
>years = 8
>months = 100
>weeks = 438
>days = 3070
>hours = 73686
>minutes = 4421210
>seconds = 265272624

Note that only complete time periods are returned, not partial years, etc.

Any Field Period

While the Single Field Periods are nice in many cases, what if we wanted to see a combination of fields? For example, what if we wanted to see the number of years, months, days, and minutes between the two Instants without weeks or hours? For that, we use the Any Field variants, with Period being the mutable implementation and MutablePeriod being the immutable twin.

The following example shows how one could create such a Period as desired in the previous paragraph. For this type of Period, we need first to create an Interval from the Instants and extract the Period from it.

package com.ociweb.jnb.joda;

import org.joda.time.DateTime;
import org.joda.time.DurationFieldType;
import org.joda.time.Interval;
import org.joda.time.Period;
import org.joda.time.PeriodType;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;


public class AnyFields {
    public static void main(String[] args) {
        // 7:40:20.500 AM on February 7, 2000
        DateTime start = new DateTime(2000, 2, 7, 7, 40, 20, 500);

        // 3:30:45.100 PM on July 4, 2008
        DateTime end = new DateTime(2008, 7, 4, 15, 30, 45, 100);

        // Generate desired Period
        DurationFieldType[] types =
            {
                DurationFieldType.years(), DurationFieldType.months(),
                DurationFieldType.days(), DurationFieldType.minutes()
            };
        PeriodType periodType = PeriodType.forFields(types);
        Period period = new Interval(start, end).toPeriod(periodType);

        // Print default representation
        System.out.println("period = " + period);

        // Print fields
        System.out.println("years = " + period.getYears());
        System.out.println("months = " + period.getMonths());
        System.out.println("days = " + period.getDays());
        System.out.println("hours = " + period.getHours());
        System.out.println("minutes = " + period.getMinutes());

        // Print pretty version
        PeriodFormatter formatter = new PeriodFormatterBuilder().
                appendYears().appendSeparator(" years ").
                appendMonths().appendSeparator(" months ").
                appendDays().appendSeparator(" days ").
                appendMinutes().
                appendSeparatorIfFieldsBefore(" minutes").
                toFormatter();
        System.out.println("period = " + period.toString(formatter));
    }
}
>period = P8Y4M27DT470M
>years = 8
>months = 4
>hours = 0
>days = 27
>minutes = 470
>period = 8 years 4 months 27 days 470 minutes

Notice that the default toString() implementation of Period includes all the necessary information, but in a format that isn't particularly readable. One option is to extract each of the fields explicitly, as shown (notice that extracting the hours results in a value of 0 because the Period isn't configured to return hours). The other way is to generate a PeriodFormatter from a builder to format the Period exactly how we want it.

Because a Duration knows only the number of milliseconds that elapsed between the two Instants, we would not be able to get anywhere near this level of detail with a Duration.

Chronology

This article has mentioned Joda Time's calendar systems several times, particularly in the difference between Instant and Partial and between Period and Duration. While these calendar systems are key to the library, for most scenarios the can be ignored. But what are they?

The Joda Time term for a calendar system is Chronology. The eight different concrete Chronology implementations are listed in the table above. Of those implementations, GJChronology matches GregorianCalendar in that both include the cutover from the Julian calendar system in 1582. However, Joda Time's default Chronology is ISOChronology, a system based on the Gregorian calendar but formalized for use throughout the business and computing world.

For most applications, this default Chronology will be sufficient, and the entire concept of Chronology can be ignored safely. However, the other Chronology implementations are available for calculating dates before October 15, 1582 (when the Julian calendar was abandoned in the Western world), for years for countries that adopted the Gregorian calendar later (like Russia, which changed in 1918), or for parts of the world that use the Coptic, Islamic, or other calendars.

Time Zone

The idea of time zone in Joda Time is very similar to its implementation in the JDK. However, it is reimplemented in Joda Time to provide more flexibility in keeping up with recent time zone changes. Joda Time updates the included time zones with each release, but if you need to update due to a time zone change between releases, refer to the Joda Time web page.

Converter

The authors of Joda Time made a very interesting choice in developing the constructors for the key classes; in most cases, there is an overloaded version of the constructor that takes an Object. For example, the constructor for Interval that took a String in the DateDiff example above really took an Object. Why did they do this, and how does it work?

The authors decided to sacrifice type safety for increased extensibility. When that constructor is called, it internally checks a manager, ConverterManager to find a converter for the type and generates the Interval instance based on the result returned by the converter (the lists of default converters for each of the classes are given in the API documentation for ConverterManager, not in the individual classes). While there is definitely a cost here (the constructor will throw an IllegalArgumentException if it receives an inappropriate object), there is also the opportunity to provide a constructor that satisfies your project's needs.

For example, the constructor for DateTime will not accept a LocalDate. This is not surprising, as a LocalDate does not have the time zone and Chronology information that DateTime needs, and because LocalDate provides a toDateTime() method to convert the LocalDate using the default time zone and Chronology. However, what if we decided that we wanted to be able to have the same functionality available through the constructor? The following example shows how we might do this:

package com.ociweb.jnb.joda;

import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.chrono.ISOChronology;
import org.joda.time.convert.ConverterManager;
import org.joda.time.convert.InstantConverter;


public class LocalDateConversion {
    public static void main(String[] args) {
        final DateTimeFormatter pattern = DateTimeFormat.forStyle("M-");
        final LocalDate date = new LocalDate(2008, 1, 1);

        try {
            new DateTime(date);
        } catch (IllegalArgumentException e) {
            // should be thrown
            System.out.println(e.getMessage());
        }

        LocalDateConverter converter = new LocalDateConverter();
        ConverterManager.getInstance().addInstantConverter(converter);

        final DateTime dateTime = new DateTime(date);
        System.out.println("dateTime = " + dateTime.toString(pattern));

    }

    private static class LocalDateConverter
            implements InstantConverter {
        public Chronology getChronology(
                Object object, DateTimeZone zone) {
            return ISOChronology.getInstance();
        }

        public Chronology getChronology(
                Object object, Chronology chrono) {
            return ISOChronology.getInstance();
        }

        public long getInstantMillis(
                Object object, Chronology chrono) {
            final LocalDate localDate = (LocalDate) object;
            return localDate.toDateMidnight().getMillis();
        }

        public Class getSupportedType() {
            return LocalDate.class;
        }
    }
}
>No instant converter found for type: org.joda.time.LocalDate
>dateTime = Jan 1, 2008

This converter extracts the date information for getInstantMillis() and assumes the default Chronology for the getChronology() methods. When trying to generate a DateTime from a LocalDate before the converter is registered, we get the expected exception. When we try again after registering the converter, we get the expected DateTime.

Properties

In addition to simple getters, several of the key classes in Joda Time supply an alternate means of accessing state information — properties. For example, both of these are ways to find out the month from a DateTime:

    DateTime dateTime =
            new LocalDate(2008, 7, 1).toDateTimeAtStartOfDay();
    System.out.println("month = " + dateTime.getMonthOfYear());
    System.out.println("month = " + dateTime.monthOfYear().get());

However, there's a lot more that we can do with the property:

    DateTime.Property month = dateTime.monthOfYear();
    System.out.println("short = " + month.getAsShortText());
    System.out.println("short = " +
        month.getAsShortText(Locale.ITALIAN));
    System.out.println("string = " + month.getAsString());
    System.out.println("text = " + month.getAsText());
    System.out.println("text = " + month.getAsText(Locale.GERMAN));
    System.out.println("max = " + month.getMaximumValue());

    dateTime = month.withMaximumValue();
    final DateTimeFormatter pattern = DateTimeFormat.forStyle("M-");
    System.out.println("changedDate = " + dateTime.toString(pattern));
>short = Jul
>short = lug
>string = 7
>text = July
>text = Juli
>max = 12
>changedDate = Dec 1, 2008

Through the property, we have name and localization access to the field, and we also gain many specialized methods to gain modified copies of the original DateTime.

Summary

For date and time calculations, the Joda Time API is superior to java.util.Date (Sun's initial attempt) and java.util.Calendar (the improvement). By providing separate systems that allow both adhering to and ignoring time zones, Daylight Saving Time, etc., Joda Time supports much more comprehensive and robust date and time calculation than is available by default.

Given the importance and difficulty of date and time calculations, and Joda Time's excellence, it is not a surprise that JSR 310 (for Joda Time) passed with flying colors.

References

Lance Finney thanks Stephen Colebourne, Tom Wheeler, and Michael Easter for reviewing this article and providing useful suggestions.


Valid XHTML 1.0 Strict [Valid RSS]
RSS
Top