Friday, September 23, 2011

Calculating Dates with Java Calendar

Found a tricky problem with Java Calendars today. Essentially, I discovered that you can't reuse Calendar objects in a calling function, because they are singletons. Take this code, for example:

How NOT to do it

// give me the date X days from the day represented by cal
public static String getOffsetDate(Calendar cal, int daysOffset){
    cal.add(Calendar.DATE, daysOffset);
    return dateFormat.format(cal.getTime());
}

This function appears to work until you call it a second and third time, and find out (like I did) that the new calendar value is being retained each time, and so increasing the daysOffset incrementally results in dates that are additively further out:

Today :2011-01-15
1 days ago     : 2011-01-14
2 days ago     : 2011-01-12 
3 days ago     : 2011-01-09
4 days ago     : 2011-01-05
5 days ago     : 2010-12-31 // wait. what?!

Apparently the Calendar.getInstance() gives you a singleton. Try passing it around to other methods, expecting it to make a new instance of itself and you'll run into problems. I found this to be extremely annoying and non-intuitive, and the Java Doc does not warn you about this, either.

Solution

So what's the solution? You have to create a new instance, or clone() the instance of the calendar before making the calculation (see sample code below).

package com.test.calendar;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class CalendarTestDaysAgo {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance(); // today
        
        // set to January 15
        cal.set(Calendar.MONTH, Calendar.JANUARY);
        cal.set(Calendar.DAY_OF_MONTH, 15);
        
        System.out.print("Today :"+getOffsetDate(cal,0) + "\n");
        for (int d = 1; d < 32; d++){
            System.out.print(d + " days ago: " + getOffsetDate(cal, d * -1) + "\n");
        }
    }

    // give me the date X days hence from the day represented by cal
    public static String getOffsetDate(Calendar cal, int daysOffset){
                                          // this clone() call is required
                                          // otherwise subsequent changes are additive 
        Calendar newCal = (Calendar) cal.clone();
  
        newCal.add(Calendar.DATE, daysOffset);
        return dateFormat.format(newCal.getTime());
    }

}
Results in:
Today :2011-01-15
1 days ago     : 2011-01-14
2 days ago     : 2011-01-13
3 days ago     : 2011-01-12
4 days ago     : 2011-01-11
5 days ago     : 2011-01-10 // that's better.
...

2 comments:

  1. "Apparently the Calendar.getInstance() gives you a singleton" are you of it ? I don't think it does.

    ReplyDelete
  2. Just a note: Calendar.getInstance() doesn't return a singleton instance. Actually it's a factory method that returns a new instance each time it's called.
    However, Calendars are mutable, which is considered by many as a design flaw of the API, because a single method can change the content and break the program. All other basic types in java are immutable for this very reason.
    By the way, the solution is correct.

    ReplyDelete