3

Assuming Jan 31, 2000, a leap year, the following two ways of adding a month give me different results. Am I doing something wrong or are these two different philosophies of handling leap-year months? And, if these two approaches are just a philosophy difference, how would you know which method to pick?

Method 1: using LocalDate plusMonths():

LocalDate atestDate = LocalDate.parse("2000-01-31");
System.out.println("One month in future using LocalDate.addMonths() " +   atestDate.plusMonths(1));

Output:

One month in future using LocalDate.addMonths() 2000-02-29

Method 2: using Calendar:

Calendar zcal = Calendar.getInstance();
zcal.set(Calendar.DAY_OF_MONTH, 31);
zcal.set(Calendar.MONTH, 1);
zcal.set(Calendar.YEAR, 2000);
zcal.add(Calendar.MONTH, 0);
System.out.println("ONE MONTH IN FUTURE using Calendar: " 
 + zcal.getTime());

Output:

ONE MONTH IN FUTURE using Calendar: Thu Mar 02 2000 

Why are these two dates' output not the same?

Thanks.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
Morkus
  • 517
  • 7
  • 21
  • Please specify Java Version also. – this_is_om_vm Apr 27 '18 at 17:25
  • 2
    `zcal.add(Calendar.MONTH, 0)` does not change the calendar’s date. You’re adding zero to it. Also, `zcal.set(Calendar.MONTH, 1)` sets the month to February, because [Calendar.FEBRUARY is equal to 1](https://docs.oracle.com/javase/9/docs/api/constant-values.html#java.util.Calendar.FEBRUARY), not 2. Calendar.JANUARY is 0. Many have argued that this was a design error, but regardless, if you use the constants instead of literal int values, you won’t have the problem. – VGR Apr 27 '18 at 17:25
  • Actually, for me, anyway, zcal.add(Calendar.Month, 0) does change the month by one. Try it. – Morkus Apr 27 '18 at 19:19
  • Correction to my comment above, I should have done: zcal.set(Calendar.MONTH, Calendar.JANUARY). Then zcal.add(Calendar.MONTH, 1) would be correct as expected. My mistake. – Morkus Apr 27 '18 at 21:30
  • FYI, the troublesome old date-time classes such as [`java.util.Date`](https://docs.oracle.com/javase/10/docs/api/java/util/Date.html), [`java.util.Calendar`](https://docs.oracle.com/javase/10/docs/api/java/util/Calendar.html), and `java.text.SimpleDateFormat` are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [*java.time*](https://docs.oracle.com/javase/10/docs/api/java/time/package-summary.html) classes built into Java 8 and later. See [*Tutorial* by Oracle](https://docs.oracle.com/javase/tutorial/datetime/TOC.html). – Basil Bourque Apr 27 '18 at 21:36

3 Answers3

3
zcal.set(Calendar.DAY_OF_MONTH, 31);
zcal.set(Calendar.MONTH, 1);
zcal.set(Calendar.YEAR, 2000);

This defines the date of February 31st, 2000, which, due to lenient measuring, is equated to March 2nd, 2000. Month values are zero-indexed.

Setting strict:

zcal.setLenient(false);

We get an exception:

Exception in thread "main" java.lang.IllegalArgumentException: MONTH: 1 -> 2
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2829)
at java.util.Calendar.updateTime(Calendar.java:3393)
at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
at java.util.Calendar.getTime(Calendar.java:1755)
at Employee.Tester.main(Tester.java:19)

So you can see that the interpolation was shifted over to March. If you wish to hard-code dates for testing purposes, I would recommend using strict validation to avoid these cases. There have been other documented issues related to setting values that make things messy.

Compass
  • 5,867
  • 4
  • 30
  • 42
2

tl;dr

Just use LocalDate and forget all about Calendar (troublesome legacy class).

LocalDate.parse( "2000-01-31" ).plusMonths( 1 ).toString()

2000-02-29

Method 1

Method 1: using LocalDate plusMonths():

The LocalDate class documentation explains that it first adds the month-number, and leaves the day-of-month alone. If that day-of-month is not valid in that month (29, 30, or 31), then it adjusts backwards to the last valid day-of-month.

This method adds the specified amount to the months field in three steps:

  1. Add the input months to the month-of-year field
  2. Check if the resulting date would be invalid
  3. Adjust the day-of-month to the last valid day if necessary

For example, 2007-03-31 plus one month would result in the invalid date 2007-04-31. Instead of returning an invalid result, the last valid day of the month, 2007-04-30, is selected instead.

Seems like a smart approach to me.

In your example, LocalDate first changed 2000-01-31 to 2000-02-31. There is no 31st in February, so it walked back to 30. But no Feb 30th either. So it walked further back to 29. Bingo! In that year in that month, there is indeed a 29th day because the year 2000 is a Leap Year. So the answer is 2000-02-29.

LocalDate.parse( "2000-01-31" )
         .plusMonths( 1 )
         .toString()

2000-02-29

Method 2

Method 2: using Calendar:

Don’t bother.

This terribly troublesome old Calendar class is now legacy, supplanted entirely by the java.time classes as defined in JSR 310. Specifically, replaced by ZonedDateTime.

Never touch Calendar class again. Save yourself some pain and headache, and pretend this class never existed.

Furthermore, Calendar was a date-with-time value. Not appropriate for a date-only value like yours in the Question.

Convert legacy <–> modern

If you must inter-operate with code not yet updated to java.time, convert between the legacy classes and java.time by calling new conversion methods added to the old classes. For more info, see the Question, Convert java.util.Date to what “java.time” type?


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
1

The problem I had in the posting above turned out to be a typo.

Calendar zcal = Calendar.getInstance();
zcal.set(Calendar.DAY_OF_MONTH, 31);
zcal.set(Calendar.MONTH, 1);
zcal.set(Calendar.YEAR, 2000);
zcal.add(Calendar.MONTH, 0);
System.out.println("ONE MONTH IN FUTURE using Calendar: " 
 + zcal.getTime());

Should have been:

Calendar zcal = Calendar.getInstance();
zcal.set(Calendar.DAY_OF_MONTH, 31);
zcal.set(Calendar.MONTH, Calendar.JANUARY);
zcal.set(Calendar.YEAR, 2000);
zcal.add(Calendar.MONTH, 1);
System.out.println("ONE MONTH IN FUTURE using Calendar: " 
 + zcal.getTime());

Then I get the expected matching date.

Thanks to all who answered! :)

Morkus
  • 517
  • 7
  • 21