Sunday 24 February 2008

Java date & time API vs. JODA

Java has simple API for working with date and time. Many people find the API deficient and I’m one of them. Although I was using SimpleDateFormat and GregorianCalendar classes in Java API for long time, sometimes I found it very cumbersome and unsuitable for my task. Sometimes there were problems with parsing input text and the problems couldn’t be directly solved using Java API. Java community is working on new Java date and time API (see https://jsr-310.dev.java.net/), but I think it will take a while to be finished. A few weeks ago I came across JODA API and without delay tried using it. I was really very pleasantly surprised how useful and simple the JODA is.

Java API has in fact 2 (in words two) classes for working with date and time – java.text.SimpleDateFormat for parsing and converting date and time and java.util.GregorianCalendar for manipulating a date and time. Either class extends its own abstract ascendant but it’s not important in light of functionality. Creating an instance of SimpleDateFormat is simple; you pass format string and a Locale instance and start using the instance. The most confusing thing for a beginner can be leniency of the new instance. Default behavior of a new instance of SimpleDateFormat is not to be lenient. That means you can pass any string to its parse(…) method and you get a result without any exception. If you forget to set leniency by setLenient(true) method you can later unreasonably get unknown behavior of your application.

JODA API (see http://joda-time.sourceforge.net/) has in contrast to Java API much more (tens) classes to work with data and time. JODA architecture contains Instants (a moment in the datetime continuum), Intervals (an interval of time from one instant to another instant), Durations (a duration between two Intervals in milliseconds, doesn’t have start and end), Periods (a duration in e.g. years, months, days and hours), Chronology (a calculation engine that supports the complex rules for a calendar system), TimeZones (don’t need commentary I hope) and then many tools to manipulate, parse and format date and time. First thing we need to do is create new Instant:
DateTime dateTime = new DateTime();
The dateTime instance contains date and time according to the date and time it was instantiated (similarly to Java API and its java.util.Date class). You should notice that a dateTime instance is immutable and can be shared among threads without need of access synchronization. Need to get last possible day in actual month? No problem:
int lastDay = dateTime.dayOfMonth().getMaximumValue();
Creating of a Duration instance is simple and intuitive:
DateTime before = new DateTime();
Thread.sleep(30);
Interval interval = new Interval(before, new DateTime());
int timeInterval = interval.toDuration().getMillis();
The snippet above should create interval of 30 miliseconds but the interval is longer because of time spent during threads scheduling. JODA uses formatter classes to format and parse date and time. Creating of a custom formatter looks similarly to Java API:
DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
Then simply use the instance of the formatter to format a DateTime instance:
String formattedTime = dateTime.toString(dtf);
Need to create DateTime with different time zone (e.g. New York)? No problem in JODA:
DateTime newYork2 = new DateTime(DateTimeZone.forID("America/New_York"));
All supported time zone ID strings are located in package org.joda.time.tz.data. Of course you can “change” time zone of existing DateTime instance (in fact you can’t change existing DateTime instance, you have to create new):
DateTime newYorkTZ = dateTime.withZone(DateTimeZone.forID("America/New_York"));
Need to add 3 days to an existing DateTime instance? There’s nothing simpler:
DateTime add3Days = dateTime.dayOfMonth().addToCopy(3);
You can really do many and many things using JODA API and much more simply then using Java API.

One of my targets when I was preparing this post was to do a performace test and measure performance of parsing and formatting input in either API. I decided to do it in the following way: call parse and format method of each API specific number of times (e.g. 100 000x) and measure time spent using System.currentTimeMillis(). Following snippet shows how I measured formatting performance of Java API:
DateFormat javaFormatter = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
Date date = new Date();
String result = null;
beforeTest = System.currentTimeMillis();
for (int i = 0; i < testCount; i++) {
result = javaFormatter.print(dateTime);
}
aftertest = System.currentTimeMillis();
I got the following results:



You can see that while Java and JODA formats an input string at the same speed, parsing takes much less time in JODA.

2 comments:

kreeble said...

Thanks man, this is cool. You've convinced me to try out JODA!

a.testing.time said...

I think your test is flawed. The stats do not pan out if you vary the input data. If you do that, Joda comes out slower.

Even so its not as slow as synchronising on a method...or tracking down a multi thread bug...

My Test:

@Test
public void testRawFormat() throws ParseException {
final String parsePattern = "dd-MM-yyyy HH:mm:ss";
DateFormat javaFormatter = new SimpleDateFormat(parsePattern);

final StopWatch stopWatch = new StopWatch();
stopWatch.start();


for (int i = 0; i < PRINT_ITERATIONS; i++) {
javaFormatter.format(new Date());
}
stopWatch.stop();

log.info( "SimpleDateFormat parse method: " + (stopWatch.getTime()) );

DateTimeFormatter yodaFormatter = DateTimeFormat.forPattern(parsePattern);

stopWatch.reset();
stopWatch.start();


for (int i = 0; i < PRINT_ITERATIONS; i++) {
DateTime inYodaFormat = new DateTime(new Date());
inYodaFormat.toString(yodaFormatter);
}
stopWatch.stop();

log.info( "DateTimeFormatter parse method: " + (stopWatch.getTime()) );

}




@Test
public void testRawParse() throws ParseException {
final String parsePattern = "dd-MM-yyyy HH:mm:ss";
DateFormat javaFormatter = new SimpleDateFormat(parsePattern);

List dates = getManyYears();


final StopWatch stopWatch = new StopWatch();
stopWatch.start();

for (String dateToParse : dates) {
javaFormatter.parse(dateToParse);
}
stopWatch.stop();

log.info( "SimpleDateFormat parse method: " + (stopWatch.getTime()) );

DateTimeFormatter yodaFormatter = DateTimeFormat.forPattern(parsePattern);

stopWatch.reset();
stopWatch.start();


for (String dateToParse : dates) {
yodaFormatter.parseDateTime(dateToParse);
}
stopWatch.stop();

log.info( "DateTimeFormatter parse method: " + (stopWatch.getTime()) );

}


private List getManyYears() {
final int myBirthYear = 1968;
final int iterationsPlusAge = PRINT_ITERATIONS + myBirthYear;

List results = new ArrayList();

for (int i = myBirthYear; i < iterationsPlusAge; i++) {
final String dateString = "10-12-" + i + " 10:35:22";
results.add(dateString);
}

return results;
}