http://www.developer.com/

Back to article

Back to Basics with Dates, Calendars, and Formatters


December 12, 2007

Introduction

Working with dates seems like a simple enough task, but there are some pitfalls for the unwary. This article was sparked, in part, by a forum question that seemed innocent enough on the surface: "How can I compute the number of days between two dates?" It brought to mind a blog entry I read, a couple years ago, where someone was flaming Java for having a bug in the number of days in the month of April—he even had code to "prove" it. And of course, being unwary, he fell deep into one of the pitfalls that surround date computations.

But while you're here, I will also show a couple of classes in the standard API that help format messages. One of them is specifically designed for those situations where you need a different format depending on the value of some number, such as singular/plural word forms in a phrase. (It's like two articles in one. What a bonus!)

Background

The really old-timers in the Java crowd—those who started on JDK 1.0—started with the java.util.Date class. It was simple enough to use, but did little to really help a developer do anything other than get and set the component values in some date. It was left to the developer to keep track of which years were leap years, for example. (It isn't as simple as "any year divisible by 4"; there are exceptions, with exceptions.)

Then JDK 1.1 came out, and most of the constructors and methods in the Date class were deprecated, with directions to use the Calendar class instead. However, in the interim, JDBC code subclassed the java.util.Date class into java.sql.Date (and a couple others) to reflect standard SQL storage types. For good or ill, you're stuck with code that throws a Date object (of some variety) around.

Fortunately, translating between a Date and a Calendar object isn't too bad. Refer to the static Calendar.getInstance() method (synonymous with "new Date()" to get a representation of "right now") and the instance methods getTime() and setTime(Date). The getTimeInMillis() and setTimeInMillis(long) instance methods also are valuable because the Date constructor that takes a long parameter (representing a milliseconds value) is the only constructor (other than the no-arg) that survived the deprecation. And, when you want a Date object initialized to a specific value but don't want to raise your hackles over a deprecation warning, you can use a SimpleDateFormat instance to parse a String directly into one. So, you can go back and forth. What's the big deal, then?

Pitfalls

The blog entry I mentioned reading involved someone calculating the number of days in each month by getting the time, in milliseconds, of the first of each month (at midnight, as I recall), taking a difference of those millisecond values, then dividing by "24 * 60 * 60 * 1000", which would be the number of milliseconds in a day. Where this blogger came up short was that he was told April had 29 days. Here's a code snippet to recreate the event:

try {
   SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy");
   Date d1 = sdf.parse("01-Apr-2005");
   Date d2 = sdf.parse("01-May-2005");
   long millisInDay = 24 * 60 * 60 * 1000;
   int wrongDiff = (int) ((d2.getTime() -
      d1.getTime()) / millisInDay);
   // 29 days?
   System.out.println("Apr 2005 has "+wrongDiff+" days");
} catch (ParseException pe) { /*ignored*/ }

Clearly, you know otherwise. Curiously, when he played a similar game to compute the number of days in a full year, he got the right number! Although the blogger knew something was wrong, he didn't quite arrive at the right conclusion. The Java libraries were accounting for the loss of an hour in Spring when (most of) the United States enters Daylight Saving Time (DST). (Also, note the integer division effect.)

Note: To play the game this year, check how many days this method reports for March, because DST now starts sooner.

The lesson is that the Calendar class is probably smarter than you are. It knows which years are and are not leap years, so it knows how many days are in any given year. It also knows how to adjust for the lose-an-hour, gain-an-hour DST effects. And, when you know how to take advantage of this, you can reliably count the number of days between two arbitrary dates.

The lesson is also that "24*60*60*1000" is a bad idea, and it is unfortunate that it seems so commonplace in date difference calculation code throughout the Internet. (For that matter, I'll even admit that, when my fingers are typing faster than my brain can think, I'll have typed up that very piece of code, only to remember later that it has hidden faults.) So, what's the right way? Read on.

How Many Days Between...

With the background covered, turn your attention to a question that comes up often enough that I thought it was worth writing an article over: How does one compute the number of days between two arbitrary dates? Such a question might come up in a business workflow management application where, knowing a starting date and a projected ending date, one wants to know how many actual days are between them. Another reason this question might come up is the display of "relative date" information as an optional user convenience. Examples include: "file last updated four days ago" or "message received yesterday" or "meeting today at 2:00 pm."

Noting that the Calendar class has methods to indicate whether one instance is "before" or "after" another instance (and treating the hour, minute, and second fields as being equal, such as at midnight) a naïve way to attack this problem would be to simply enter a while loop, incrementing one of the Calendar objects day fields (for example, DAY_OF_MONTH) and accumulating these increments until the two objects are on the same day, and then returning the accumulated value. This works, but the amount of time needed in the loop increases with the number of days between the dates in question. There is a more efficient way to attack the problem:

int computeDayDiff(Calendar calA, Calendar calB) {
   Calendar firstCal = (calA.before(calB) ? (Calendar)calA.clone() :
                       (Calendar)calB.clone());
   Calendar secondCal = (calA.before(calB) ? calB : calA);

   // first adjust the starting date back to the beginning
   // of the year
   int diff = -firstCal.get(Calendar.DAY_OF_YEAR);
   firstCal.set(Calendar.DAY_OF_YEAR, 1);

   // then adjust the years to have the same value, adding the
   // number of days in that year to the difference accumulator
   while(firstCal.get(Calendar.YEAR)
      < secondCal.get(Calendar.YEAR)) {
      diff += firstCal.getActualMaximum(Calendar.DAY_OF_YEAR);
      firstCal.add(Calendar.YEAR, 1);
   }

   // now both calendar objects are in the same year, so add
   // back the ending "day of the year" value.
   diff += secondCal.get(Calendar.DAY_OF_YEAR);
   return diff;
}

The first pair of lines arranges some method-local Calendar objects such that firstCal.before(secondCal) returns true. (The clone operation when setting firstCal prevents the modifications in firstCal from showing up in the caller's copy of calA.) The trick to the computation is to:

  1. Move the start date back to the beginning of its own year while keeping track of this amount in the "diff" accumulator variable as a negative quantity. Not doing this can introduce an off-by-one error during leap years on the following step. (Left as an exercise to the reader.)
  2. Incrementally adjust the year of the first date until it is the same as the year of the second date. The method call getActualMaximum(Calendar.DAY_OF_YEAR) will return the number of days in a year, given the current year value (in other words, it will return either 365 or 366, as appropriate) and this value is added to the "diff" accumulator variable.
  3. Add in the number of days into the year for the end date.

And there you have it. Pick two dates, feed a Calendar object representation of them into this method, and it will tell you how many days they are apart. This method could be modified to work on months instead of days, and there is even a convenient HOUR_OF_DAY field value to do similar comparisons on an hourly basis (which, if you or your code works in one of the US regions with DST, might be worth using at least two days out of the year).

Output Formatting

The standard API has some nice message formatting classes. Perhaps the more notable one is the java.text.MessageFormat class. If you aren't familiar with it, it seems almost too simple to be useful. Given some text with placeholders, and given a list of values to insert into those placeholders, it returns a String with the appropriate substitutions made. For example:

String msg = MessageFormat.format("Some string: {0} and some int:
   {1}", "hello!", 42);
System.out.println(msg);    // "Some string: hello! and some int: 42"
Warning: This static method variation uses a "varargs" parameter list, so you'll need to use one of the other instance methods in Java 1.4 and earlier.

If it doesn't seem like it is any better than a good "printf" or a manually concatenated String, you aren't thinking big enough. The real appeal of MessageFormat is that you can internationalize: put different format patterns in a collection of resource property files and use java.util.ResourceBundle to automatically grab the appropriate format pattern from the right file (depending on the user's locale). This lets the substitution placeholders be in different places (and even different orders), but the same API call builds the resulting string.

However, on its own, MessageFormat doesn't know how to pluralize or use other special words. Consider: "No files", "One file", and "Multiple files". For this, use the java.text.ChoiceFormat class. The concept here is that you specify interval ranges for which a given piece of text is used. I'll refer you to read the API documentation to understand the constructors and pattern formats (especially because the class operates not just on integer values, but on double-precision values) but by way example:

ChoiceFormat cf = new ChoiceFormat("0#No files| 1#One file|
   2#Multiple files");
System.out.println(cf.format(0));    // "No files"
System.out.println(cf.format(1));    // "One file"
System.out.println(cf.format(5));    // "Multiple files"

To get placeholder substitution, chain the result of the ChoiceFormat into a MessageFormat. In the following example, assuming that you are comparing some target date relative to today (and using the above code to compute the value of "dayDiff") :

ChoiceFormat choiceFormat = new ChoiceFormat(
   "-2#was {0} days ago| -1#was Yesterday| 0#is Today| "+
   "1#is Tomorrow| 2#is {0} days away");

int dayDiffAbs = (int)(Math.abs(dayDiff));

System.out.println("That date is " +
   MessageFormat.format(choiceFormat.format(dayDiff), dayDiffAbs));
/*
If you started on December 1st, and you are checking how many days
until Christmas, the response is: "That date is 24 days away".
If you wait until December 24th, the response instead
is: "That date is Tomorrow".
*/

Note in this case that you need the actual day difference, positive or negative, to feed into the ChoiceFormat object, but you want an absolute value to display the friendly message to the user. Of course, if you are worried about internationalization, things get a little trickier. (The literal "That date is", as well as the formatting pattern for the ChoiceFormat call itself, should come from a resource file, and the entire phrase should probably be assembled by yet another MessageFormat call using yet another pattern with placeholders in the resource file, but you should now have the idea.)

Conclusion

The biggest warning above was to never, ever use the "24*60*60*1000" gimmick to compute a date difference. You now have a better, safer approach that will automatically account for locale differences. You also have seen some of Java's built-in formatting tricks that help you give convenient messages to the user while being forward-thinking enough to support internationalization.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date