Tired of Date and Calendar?

Ulrik Sandberg

It's not always easy to decide whether to add yet another dependency to yet another framework. It's especially hard when it's about a very central part of the JDK, like Date and Calendar. They are not great, there's little doubt about that. However, the benefits of a new framework would have to be numerous in order for us to make the switch. It must also be simple to convert between the standard classes and the framework classes. The impact on our project must be minimal. Ideally, it would be a question of dropping in a single jar-file. This article not only finds the weak spots of Date and Calendar, but also presents a compelling alternative.

How bad are Date and Calendar?

Consider java.util.Date. It has been around since JDK 1.0, and it's actually not a date, but a date and a time. In order to get the date (and the time), you call the getTime() method. It uses two-digit years from 1900, day-of-month is one-based, while month and hours are zero-based, it should have been immutable, and most methods have been deprecated in JDK 1.1. Since it's not final, it's possible to hack it by overriding critical methods, like for example after.

The state of java.util.Calendar is not much better. Month is zero-based, it should have been immutable, and it allows only one hour offset for daylight savings time. Some years have more, you know. British timezone in 1943, 1944 and 1945, for example, had two hours. The worst part is that it uses two different representations internally: a value for each field, and milliseconds since 1970. The scary part is that these two representations are not continuously kept in synch. Instead they are resynched as a side effect of other method calls, like equals for example. Wouldn't that be something? You call equals on your Calendar and suddenly it changes to a different date.

Then we have java.util.SimpleDateFormat. It requires a Date object, it's not very fast, and it's not thread-safe. If concurrent threads call its format method on a shared instance, it can produce bizarre results.

They can't be that bad, can they?

Let's print out a date. We want July 2, 1945 and the time should be 12.30 PM. Here we go:

 
Date date = new Date(1945, 7, 2, 12, 30, 0);
System.out.println(date.toString());
 
// Sat Aug 02 12:30:00 CEST 3845
 

Oops. Forgot the two-digit year and the zero-based month. Let's try again:

 
int year = 1945 - 1900;
int month = 7 - 1;
Date date = new Date(year, month, 2, 12, 30, 0);
System.out.println(date.toString());
 
// Mon Jul 02 12:30:00 CEST 1945
 

Yes, I know that the constructor used above is deprecated. This is how it really should be done:

 
TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
Calendar cal = Calendar.getInstance(tz);
cal.set(1945, Calendar.JULY, 2, 12, 30, 0);
DateFormat f= new SimpleDateFormat("yyyy-MM-dd hh:mm");
System.out.println(f.format(cal));
 
// java.lang.IllegalArgumentException: Cannot format given Object as a Date
// at java.text.DateFormat.format(DateFormat.java:279)
// at java.text.Format.format(Format.java:133)
 

Oops. DateFormat cannot take a Calendar. Let's try with a Date:

 
TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
Calendar cal = Calendar.getInstance(tz);
cal.set(1945, Calendar.JULY, 2, 12, 30, 0);
DateFormat f= new SimpleDateFormat("yyyy-MM-dd hh:mm");
Date date = cal.getTime();
System.out.println(f.format(date));
 
// 1945-07-02 06:30
 

Well, almost right. I asked for the time to be 12.30, not 6.30. Ah, of course. It's not enough to set the timezone on the Calendar that we used to get the Date that we want to print. The DateFormat that prints the Date also needs the timezone. Silly me.

 
TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
Calendar cal = Calendar.getInstance(tz);
cal.set(1945, Calendar.JULY, 2, 12, 30, 0);
DateFormat f= new SimpleDateFormat("yyyy-MM-dd hh:mm");
f.setTimeZone(cal.getTimeZone());
Date date = cal.getTime();
System.out.println(f.format(date));
 
// 1945-07-02 12:30
 

Finally.

The Contender: Joda-Time

There is a candidate that has all the characteristics we mentioned earlier, and more: Joda-Time. It's a single jar of 518k with no other dependencies. It replaces Date, Calendar and TimeZone. It has interfaces for datetimes, intervals and durations. The key classes are immutable. It has a pluggable calendar system where the default is ISO8601. And one more thing: it's easy to use.

DateTime

The most useful class in Joda-Time is the DateTime, which is immutable and thread-safe. It is measured in milliseconds since 1970, and it has simple getters for all fields. It supports timezones and multiple calendar systems. Its main purpose is to fill the most common needs as simple as possible:

 
DateTime now = new DateTime();
 
int y = now.getYear();
int hour = now.getHourOfDay();
int sec = now.getSecondOfMinute();
 
boolean leap = now.year().isLeap();
int daysInMonth = now.dayOfMonth().getMaximumValue();
 

Since it's immutable, all mutating methods return new objects:

 
DateTime now = new DateTime();
DateTime yesterday = now.minusDays(1).plusHours(3);
 

Partial

Partially defined datetimes have no timezone; they use local time. They are called "partial" because they don't have all the fields of a DateTime. We have YearMonthDay, TimeOfDay (hour, minute, second, and millisecond), and Partial (can store any fields). They can easily be converted to DateTime:

 
YearMonthDay ymd = new YearMonthDay(2005, 9, 20); // September 20, 2005
TimeOfDay tod = new TimeOfDay(14, 15);
 
DateTimeZone zone = DateTimeZone.forID("Asia/Tokyo");
DateTime dt = ymd.toDateTimeAtCurrentTime(zone);
DateTime dt2 = ymd.toDateTime(tod);
 

Period

A Period represents a period of time, like "5 years, 3 months, 2 days and 12 hours":

 
Period period = new Period(5, 3, 0, 2, 12, 0, 0, 0);
int years = period.getYears();
int days = period.getDays();
 

Interval

The class Interval represents an interval of time, represented by a start DateTime (included) and an end DateTime (excluded). Let's say we want to know how many complete weeks an interval is. To do that, we convert the Interval to a Period of the specific type we want. Using PeriodType, we can deduce that the interval below is 9 complete weeks (P9W is ISO8601-standard for a 9 week period):

 
DateMidnight start = new DateMidnight(2005, 7, 12); // July 12, 2005
DateMidnight end = new DateMidnight(2005, 9, 15); // September 15, 2005
Interval interval = new Interval(start, end);
Period period = interval.toPeriod(PeriodType.weeks());
System.out.println(period);
 
// P9W
 

Chronology

Joda-Time features a pluggable calendar system based on the Chronology class. There are for example ISO9601, Gregorian, Julian, GJ, Buddhist, Ethiopic, and Coptic. The default is ISOChronology. A Chronology can be passed in as extra parameter whenever needed, as in the following example where we print the year 2006 as the Buddhists see it:

 
Chronology buddhist = BuddhistChronology.getInstance();
DateTime now = new DateTime(buddhist);
int year = now.getYear(); // 2006
System.out.println(year);
 
// 2549
 

Time Zone

The DateTimeZone class encapsulates a time zone that contains not only the timezones in the current JDK implementations, but is also up to date with the most current time zone data available at the time zone database.

 
DateTimeZone.UTC;
DateTimeZone.forID("Asia/Tokyo");
DateTimeZone.forOffsetHours(6);
DateTimeZone.forTimeZone(TimeZone.getTimeZone("Europe/Stockholm"));
 

One problem with the JDK TimeZone is that it returns timezone UTC (also known as GMT) if the full name cannot be understood:

 
TimeZone validTz = TimeZone.getTimeZone("Asia/Tokyo");
System.out.println(validTz);
TimeZone invalidTz = TimeZone.getTimeZone("Asia/SomeUnknownCity");
System.out.println(invalidTz);
 
// sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,...
// sun.util.calendar.ZoneInfo[id="GMT",offset=0,dstSavings=0,useDaylight=false,...
 

By contrast, Joda-Time's DateTimeZone throws an IllegalArgumentException if it cannot understand the time zone name:

 
DateTimeZone validTz = DateTimeZone.forID("Asia/Tokyo");
System.out.println(validTz);
DateTimeZone invalidTz = DateTimeZone.forID("Asia/SomeUnknownCity");
System.out.println(invalidTz);
 
// Asia/Tokyo
// java.lang.IllegalArgumentException: The datetime zone id is not recognised: Asia/SomeUnknownCity
// at org.joda.time.DateTimeZone.forID(DateTimeZone.java:198)
 

Interoperability

One of our requirements for a new framework was that it should be easy to convert to and from the standard classes. Constructors in Joda-Time take Date, Calendar, String, and long. From DateTime we can easily get JDK Date and Calendar. The DateTimeZone class can be constructed from a TimeZone and also return a TimeZone. In the example below, we easily convert from a standard Date to Joda-Time's DateTime to a GregorianCalendar, and from the DateTime back to a Date:

 
Date date = new Date(106, 8, 19); // Sep 19, 2006
DateTime dt = new DateTime(date);
GregorianCalendar c = dt.toGregorianCalendar();
Date d = dt.toDate();
 

Formatting

What about formatting then? Compared to SimpleDateFormat, the DateTimeFormatter is fast, flexible, thread-safe, and immutable. Formatting can be done in several ways: using a pattern, a style, or a format builder. You can either pass a formatter to the toString method for DateTime, or you can ask the formatter to print the DateTime:

 
DateTimeFormatter f = DateTimeFormat.longDateTime();
DateTime dateTime = new DateTime();
System.out.println(dateTime.toString(f));
System.out.println(f.print(dateTime));
 
// May 25, 2006 9:45:09 PM CEST
// May 25, 2006 9:45:09 PM CEST
 

DateTimeUtils

If you need to stop time, or go back or forward in time, you can use the DateTimeUtils class. This code shows that it's possible to stop time:

 
DateTime now = new DateTime();
DateTimeUtils.setCurrentMillisFixed(now.getMillis()); // time has stopped!
System.out.println(new DateTime());
Thread.sleep(2000);
System.out.println(new DateTime());
 
// 2006-05-25T23:21:37.562+02:00
// 2006-05-25T23:21:37.562+02:00
 

Turning back the clock is also possible:

 
DateTime now = new DateTime();
Period period = Period.months(-1);
Duration dur = period.toDurationFrom(now);
DateTimeUtils.setCurrentMillisOffset(dur.getMillis()); // we've gone back in time!
System.out.println(new DateTime());
 
// 2006-04-25T23:24:03.796+02:00
 

Don't forget to reset to system time:

 
DateTimeUtils.setCurrentMillisSystem(); // we're back to normal
System.out.println(new DateTime());
 
// 2006-05-25T23:29:41.281+02:00
 

Where were we?

Oh, yes. Printing dates. Let's print out the same date using Joda-Time. It was 12.30 PM on July 2, 1945:

 
DateTime date = new DateTime(1945, 7, 2, 12, 30, 0, 0);
DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
System.out.println(date.toString(formatter));
 
// 1945-07-02 12:30
 

Couldn't be simpler.

Conclusion

The existing classes for date and time have drawbacks, some of them serious. The contender is Joda-Time, which is a complete rewrite and represents a mature API with a strong focus on usability, interoperability, stability and performance. What are you waiting for? Now is the time.

Update: It seems Java7 will be using JSR310, which basically is Joda-Time.

Ulrik Sandberg
Consultant at Jayway

Tags: , , , , , ,

0 comments ↓

There are no comments yet...Kick things off by filling out the form below.

Leave a Comment