A Better Date and Time API: Joda Time
By Lance Finney, OCI Senior Software Engineer
July 2008
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:
- The combination of
java.util.Date
andjava.util.Calendar
is notoriously counterintuitive to use. - They do not support some common date tasks, like calculating the number of days between two different dates.
- Supporting a calendar system other than one of the default calendar systems is very difficult to set up.
- January unexpectedly is stored as 0 instead of as 1.
- Strangely,
Date.getDate()
returns just the day of month and not the whole date.
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:
- Convert the immutable object to a mutable object and then convert back. Within most of the concepts, the classes are easily convertible to each other using conversion constructors or conversion factories:
- DateTime immutable = new DateTime();
- MutableDateTime mutableDateTime = immutable.toMutableDateTime();
- mutableDateTime.setDayOfMonth(3);
- immutable = mutableDateTime.toDateTime();
Note that this doesn't actually modify the original immutable object, but instead replaces the reference with a new object.
- A simpler alternative is to use a "with" function on the immutable object to replace the reference with a new immutable object based on the first:
DateTime immutable = new DateTime();
immutable = immutable.withDayOfMonth(3);
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:
- Because my machine uses a North American time zone that uses Daylight Savings Time, adding 31 days worth of milliseconds to midnight on March 1 results in 1:00 AM on April 1, an hour later than desired. Duration knows nothing about time zones and Daylight Saving Time, so this would be a frequently-occurring bug.
- Adding 31 days worth of milliseconds to the last day of March results in May 1, not the last day in April. While this could be correct in some applications, it is usually not the desired result.
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 endingInstant (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 twoInstants, 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 ofChronology 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 defaultChronology for the getChronology()
methods. When trying to generate a DateTime
from aLocalDate
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
- [1] Joda Time (version 1.5.2 used in this article)
http://joda-time.sourceforge.net/ - [2] JSR 310
http://jcp.org/en/jsr/detail?id=310 - [3] ISO 8601
http://en.wikipedia.org/wiki/ISO_8601
Lance Finney thanks Stephen Colebourne, Tom Wheeler, and Michael Easter for reviewing this article and providing useful suggestions.