So how often have you had to work with date/time functions in Java? I mean really WORK with them? I’m not talking about simply setting the current time by instantiating a java.util.Date object or getting a Calendar instance, maybe getting a java.sql.Timestamp from a ResultSet. No, I’m talking about manipulating dates, working in different time zones, and working with dates and date ranges with respect to Daylight Savings Time. If you have had to do these things with native Java classes, you’ve probably just groaned.
Think about creating a date representing the Apollo 11 moon landing, which occurred on July 20, 1969 at 4:17 pm US/Eastern time. How would you do that with a Calendar object? You might expect one of these Calendars to work:
Calendar calendar = Calendar.getInstance(); calendar.clear(); TimeZone tz = TimeZone.getTimeZone("US/Eastern"); Calendar calendar2 = Calendar.getInstance(tz); calendar2.clear(); System.out.println("TimeZone is " + tz.getDisplayName()); calendar.setTimeZone(tz); calendar.set(1969, 7, 20, 16, 17); calendar2.set(1969, 7, 20, 16, 17); SimpleDateFormat sdf= new SimpleDateFormat("M/dd/yyyy h:mm a zzz"); System.out.println("Calendar moon landing eastern is " + sdf.format(calendar.getTime())); System.out.println("Calendar2 moon landing eastern is " + sdf.format(calendar2.getTime())); //now change the time zone and see what the time looks like: calendar.setTimeZone(TimeZone.getTimeZone("US/Central")); System.out.println("Calendar moon landing central is " + sdf.format(calendar.getTime()));
Interestingly, here is the output:
TimeZone is Eastern Standard Time Calendar moon landing eastern is 8/20/1969 3:17 PM CDT Calendar2 moon landing eastern is 8/20/1969 3:17 PM CDT Calendar moon landing central is 8/20/1969 3:17 PM CDT
So…we got the current date-time to US/Eastern, then set the year to 1969, the month to the 7th, the date to the 20th, and the time to 16:17 (4:17pm) So then why did we get 8/20/1969 US/Central? First, the most obvious, is that Calendar months are zero-based, which is why July, while it is the 7th month of the year, is month #6…and while month #7 in Java is the 8th month of the year, August. This has been a problem since, well, the beginning. Definitely not intuitive, but it can be worked around. Sure, we could have “known” this or coded for this and set the month to 6 to represent the 7th month. But more interestingly, the time zone was Central time! Even though the time was adjusted correctly, wouldn’t you have expected that, since we changed the time zone to US/Eastern, that our Calendar object was set to US/Eastern? Furthermore, after adjusting the time zone back to US/Central, the time never changed? Consider the Javadoc for setTimeZone:
/* Recompute the fields from the time using the new zone. This also * works if isTimeSet is false (after a call to set()). In that case * the time will be computed from the fields using the new zone, then * the fields will get recomputed from that. Consider the sequence of * calls: cal.setTimeZone(EST); cal.set(HOUR, 1); cal.setTimeZone(PST). * Is cal set to 1 o'clock EST or 1 o'clock PST? Answer: PST. More * generally, a call to setTimeZone() affects calls to set() BEFORE AND * AFTER it up to the next call to complete(). */
Huh? Wait…okay, so setTimeZone affects calls to set(), but doesn’t change the existing Calendar time? So if we wanted to change the time zone we are working with, we have to adjust all the time fields too…but a SimpleDateFormatter still would show it as the current time zone. You see just how complex this is starting to get. And the more complex, the more unit tests are necessary to make sure your code is working properly.
Enter Joda-Time; a Java replacement date/time framework that aims to make time/date handling in Java much more intuitive and, by association, less error prone. Let’s take our previous example and do the exact same thing in JodaTime. The code looks like this:
DateTimeFormatter fmtDateTime = DateTimeFormat.forPattern("M/dd/yyyy h:mm a"); fmtDateTime = fmtDateTime.withZone(DateTimeZone.forID("US/Eastern")); DateTime dateTimeWithZone = new DateTime(fmtDateTime.parseDateTime("7/20/1969 4:17 pm")); System.out.println("DateTimeString = " + dateTimeString + " --> JodaDateTime FORMATTED W TZ = " + dateTimeWithZone.toString("MMMM dd, yyyy h:m z")); DateTime dateTimeCentral = new DateTime(dateTimeWithZone, DateTimeZone.forID("US/Central")); System.out.println("DateTimeString = " + dateTimeString + " --> JodaDateTime FORMATTED W Central TZ = " + dateTimeCentral.toString("MMMM dd, yyyy h:m z));
Now you can see the output:
DateTimeString = 7/20/1969 4:17 pm --> JodaDateTime FORMATTED W TZ = July 20, 1969 4:17 EDT DateTimeString = 7/20/1969 4:17 pm --> JodaDateTime FORMATTED W Central TZ = July 20, 1969 3:17 CDT
Fantastic! Less code, and it worked more intuitively. We were able to create a date/time object in a different time zone, and then change it to another time zone, and all the other fields adjusted to match.
Another important aspect of Joda-Time is its ability to work more effectively with Daylight Savings Time. Take for example March 13, 2011, which is the “leap forward” date of Daylight Savings time. At 2:00 am in the United States, the time advances to 3:00 am. Therefore, there is no 2:00-2:59 am timeframe on that day. So let’s look what happens when we create an invalid time in JDK 6:
Calendar calInvalid = cal.getInstance(); calInvalid.clear(); calInvalid.set(2011, 02, 13, 2, 5); System.out.println("Calendar invalid --> " + df2.format(calInvalid.getTime()));
We see this output:
Calendar invalid --> 2011-03-13T03:05:00.000
Notice that JDK6 adjusted the time for us to 3:05 am, because 2:05 actually was 3:05 on that day. Sounds reasonable, right? Well, what if you didn’t want the JDK to adjust for you and you wanted to know when the date specified was invalid? Enter Joda-Time:
DateTime dateSpringForward = null; try { fmtDateTime = fmtDateTime.withZone(DateTimeZone.forID("US/Central")); dateSpringForward = fmtDateTime.parseDateTime("03/13/2011 2:05 AM"); System.out.println("Spring forward date = " + dateSpringForward); } catch (IllegalArgumentException e) { System.err.println("Hey! Time doesn't exist! --> " + e.getMessage()); }
And our output is as expected:
Hey! Time doesn't exist! --> Cannot parse "03/13/2011 2:05 AM": Illegal instant due to time zone offset transition (America/Chicago)
At the risk of sounding like an infomercial, “wait…that’s not all!” While Joda-Time improves the predictability and intuitiveness of date/time programming, it helps make calculating dates a whole lot easier. Let’s say we want to know the days between two dates. Joda gives you a few nice features. First, it provides a DateMidnight class. No need to adjust a time to set the time to midnight, potentially accidentally changing some other field.
DateTimeFormatter fmtDate = DateTimeFormat.forPattern("M/dd/yyyy"); DateMidnight startMidnight = new DateMidnight(fmtDate.parseDateTime("02/13/2011")); DateMidnight endMidnight = new DateMidnight(fmtDate.parseDateTime("02/15/2011"));
Joda then provides an Interval class, which is very convenient:
//An interval is inclusive for the start date, and up to but non-inclusive of the end date Interval interval = new Interval(startMidnight, endMidnight); System.out.println("Here's the interval: " + interval); //traditionally, we could calculate the number of days this way: System.out.println("Number of days: " + interval.toDurationMillis()/(1000 * 60 * 60 * 24)); //1000ms/sec*60 secs/min * 60 mins/hr * 24 hrs/day
The output looks like this:
Number of days: 2
However, this method fails when Daylight Savings is being observed, because the milliseconds don’t always add up according to that simple formula! Let’s take March 13th again:
DateMidnight dstStartMidnight = new DateMidnight(fmtDate.parseDateTime("03/13/2011")); DateMidnight dstEndMidnight = new DateMidnight(fmtDate.parseDateTime("03/15/2011")); Interval dstInterval = new Interval(dstStartMidnight, dstEndMidnight); System.out.println("Here's the DST interval: " + dstInterval); System.out.println("Number of days in DST range, wrong way: " + dstInterval.toDurationMillis()/(1000 * 60 * 60 * 24)); //1000ms/sec*60 secs/min * 60 mins/hr * 24 hrs/day
Uh-oh! 47 hours is only one day?
Number of days in DST range, wrong way: 1
What we really want to do is use the Joda Days class for this…why reinvent the wheel with an overly simplified calculation?
Days days = Days.daysIn(dstInterval); System.out.println("Number of days in DST range, right way: " + days.getDays());
Which provides the proper number of days:
Number of days in DST range, right way: 2
There are plenty of ways to convert between standard Calendar and Date objects and Joda-Time objects; here are a few examples:
java.sql.Date sqlDate = rs.getDate("dateField"); java.sql.Timestamp sqlTimeS = rs.getTimestamp("timestampField"); LocalDate localDate = new LocalDate(sqlDate.getTime()); LocalDateTime localDateTime = new LocalDateTime(sqlTimeS.getTime()); System.out.println("JodaLocalDate = " + localDate.toString() + " --> new SQLDate = " + new java.sql.Date(localDate.toDateTimeAtCurrentTime().getMillis())); System.out.println("JodaLocalDateTime = " + localDateTime.toString() + " --> new SQLTimestamp = " + new java.sql.Timestamp(localDateTime.toDateTime().getMillis())); Calendar cal = Calendar.getInstance(); LocalDate calDate = new LocalDate(cal.getTimeInMillis()); LocalDateTime calDateTime = new LocalDateTime(cal.getTimeInMillis()); //first, we need to create non-local dateTimes...these assume the current system timezone, etc. DateTime jodaDate = new DateTime(calDate.toDateMidnight()); DateTime jodaDateTime = new DateTime(calDateTime.toDateTime().getMillis()); Calendar calendarDateOnly = jodaDate.toCalendar(Locale.US); Calendar calendarDateTime = jodaDateTime.toCalendar(Locale.US); calDateString = df2.format(calendarDateOnly.getTime()); calDtString = df2.format(calendarDateTime.getTime());
And this is only the tip of the iceberg for Joda-Time.
One very good resource, in addition to the Joda-Time web site, is the IBM Developerworks article: http://www.ibm.com/developerworks/java/library/j-jodatime/index.html
It’s important to note that Joda-Time is the basis for a new date/time API to be included in Java: http://jcp.org/en/jsr/detail?id=310 and http://sourceforge.net/apps/mediawiki/threeten/index.php?title=ThreeTen.
Simply put, this is a framework with a future.
— David Kieras, [email protected]