java.util.Calendar.getActualMaximum returns strange results
By: Peter Schuler, 20 January 2010At the end of last year I encountered something odd in the java.util.Calendar. Now is odd behavior nothing to be surprised of in the Java Calendar but this particular oddness was really hard to spot.
I will therefore share it with you.
The code
Let’s first look a some code dealing with getting the last day of the month:
public class CalendarTest {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, Calendar.FEBRUARY);
int maxDayOfMonth = c.getActualMaximum(Calendar.DAY_OF_MONTH);
System.out.println("last day of month = " + maxDayOfMonth);
}
}
Please read the java doc for Calendar.getActualMaximum():
Returns the maximum value that the specified calendar field could have, given the time value of this Calendar. For example, the actual maximum value of the MONTH field is 12 in some years, and 13 in other years in the Hebrew calendar system.
So the code above can print two different values right …? 28 or 29 depending on the whether this year is a leap year. As you could have guessed that is another unexpected possibility. I can also print 31. Yes… really.
I all depends on the date on which this code is executed.
The problem is that Calendar.getInstance() will return a Calendar filled with the current date/time. Let’s assume the above code runs on the last day of January. The call to getInstance() will return 31-01-2009. The next step puts the month to February. This will result in a overflow as 31-02-2009 is invalid. Because of this Calendar will move the date to 3-03-2000. And March has 31 days.
There is a bug report (status: Closed, Not a Defect) in the SDN which told me that the observed behavior was correct. I was not the first person surprised by this and I’m guessing I will not be the last.
I was …
- … lucky I wrote a decent unit test.
- … lucky to be running said unit test om 31 December.
- … unlucky for having to work on the last day of the year.
Otherwise the bug I created would properly have slipped through the QA cycles and would have ended up in production. There it would only be visible on the last three days of every month if someone would specify a date in February.
What about lenient?
The Calendar.setLenient function does protect against invalid input. Let’s quote the the Calendar java doc about Leniency:
Leniency
Calendar has two modes for interpreting the calendar fields, lenient and non-lenient. When a Calendar is in lenient mode, it accepts a wider range of calendar field values than it produces. When a Calendar recomputes calendar field values for return by get(), all of the calendar fields are normalized. For example, a lenient GregorianCalendar interprets MONTH == JANUARY, DAY_OF_MONTH == 32 as February 1.When a Calendar is in non-lenient mode, it throws an exception if there is any inconsistency in its calendar fields. For example, a GregorianCalendar always produces DAY_OF_MONTH values between 1 and the length of the month. A non-lenient GregorianCalendar throws an exception upon calculating its time or calendar field values if any out-of-range field value has been set.
So lenient does protect against the programmer/user creating a invalid date in a way that the following code will result in an exception:
Calendar c = Calendar.getInstance();
c.setLenient(false);
c.set(Calendar.MONTH, Calendar.FEBRUARY);
c.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(c.getTime());
But the exception is only thrown when c.getTime() is called. Calls to getActualMaximum() still work and return 31. So lenient is not useful here.
Lessons learned
The quick fix for this problem is to set the DAY_OF_MONTH to 1 (or any number between 1 and 28) before setting the month.
I also learned that Calendar.setLenient() will not protect you from this error. It will only stop you from getting an invalid Date. It does not protect against overflows.
Sould I use JODA time?
At the end of this post I have a question for you. I have no experience with JODA time always preferring to use the standard Date/Time API unless there was a problem. But perhaps I should reverse my views and use JODA time unless I’m not allowed too? What do you think … is it time to stop using the default Calendar API and use JODA time instead? Or should I wait for Java 7 with JSR 310?


24 January 2010 om 2:27 pm
I would say, switch to Joda Time. Firstly, because it just works like a charm. (great API)
Second, because it doesn’t have the strange behavior you describe. Check:
Calendar c = Calendar.getInstance(); c.set(Calendar.MONTH, Calendar.JANUARY); c.set(Calendar.DAY_OF_MONTH, 31); c.set(Calendar.YEAR, 2010); // Construct 31-01-2010 date DateTime dateTime = new DateTime(c); // Set month to February dateTime = dateTime.withMonthOfYear(DateTimeConstants.FEBRUARY); // Get the max value int maximumValue = dateTime.dayOfMonth().getMaximumValue(); System.out.println("Joda maximumValue = " + maximumValue);————–
Prints:
Joda maximumValue = 28