Alexei
Alexei

Reputation: 15664

Java. Dates difference in months by GregorianCalendar not work correct

java 6.0

Suppose today is: 8 Aug 2018

Here method return difference of 2 dates in months .

    public static Integer getDiffMonths(Date dateFrom, Date dateTo) {
        Calendar cal1 = new GregorianCalendar();
        cal1.setTime(dateFrom);
        Calendar cal2 = new GregorianCalendar();
        cal2.setTime(dateTo);
        return differenceInMonths(cal1, cal2);
}

 private static Integer differenceInMonths(Calendar dateFrom, Calendar dateTo) {
        int month1 = dateFrom.get(Calendar.YEAR) * 12 + dateFrom.get(Calendar.MONTH);
        int month2 = dateTo.get(Calendar.YEAR) * 12 + dateTo.get(Calendar.MONTH);
        int diff = month2 - month1;
        return diff;
    }

Here results of 4 test cases:

1.

currentDate = Aug 08 2018
DateTo =      Aug 13 2018
diffMonth = 0

2.

currentDate = Aug 08 2018
DateTo =      Oct 14 2018
diffMonth = 2

3.

currentDate = Aug 08 2018
DateTo =      Jan 03 2019
diffMonth = 5

Error: difference must be 4 months

4.

currentDate = Aug 08 2018
DateTo =      Aug 03 2019
diffMonth = 12

Error: difference must be 11 months

As you can see test cases #1 and #2 are correct, but test cases #3 and #4 are incorrect.

Why?

Upvotes: 0

Views: 89

Answers (4)

Anonymous
Anonymous

Reputation: 86276

This is one of the (many) cases where java.time, the modern Java date and time API, excels:

public static long differenceInMonths(LocalDate dateFrom, LocalDate dateTo) {
    return ChronoUnit.MONTHS.between(dateFrom, dateTo);
}

Yes, I seriously suggest that you use this in your old Java 6 code. It requires a little more explanation, of course. Before I get to that, allow me to demonstrate that the one-liner above gives you the results you expect:

    LocalDate currentDate = LocalDate.of(2018, Month.AUGUST, 8);
    System.out.println("1. Months until Aug 13 2018: " 
            + differenceInMonths(currentDate, LocalDate.of(2018, Month.AUGUST, 13)));
    System.out.println("2. Months until Oct 14 2018: " 
            + differenceInMonths(currentDate, LocalDate.of(2018, Month.OCTOBER, 14)));
    System.out.println("3. Months until Jan 03 2019: " 
            + differenceInMonths(currentDate, LocalDate.of(2019, Month.JANUARY, 3)));
    System.out.println("4. Months until Aug 03 2019: " 
            + differenceInMonths(currentDate, LocalDate.of(2019, Month.AUGUST, 3)));

Output:

1. Months until Aug 13 2018: 0
2. Months until Oct 14 2018: 2
3. Months until Jan 03 2019: 4
4. Months until Aug 03 2019: 11

Question: How can I use this method when I have variables of type Date?

As I understand, you have an old method taking two old-fashioned Date arguments and you are asking why it gives incorrect results. So I guess you will also want to fix it to give the results that are considered correct by your users (as your own answer suggests). So rewrite your method to use my method above:

public static Integer getDiffMonths(Date dateFrom, Date dateTo) {
    ZoneId zone = ZoneId.systemDefault();
    LocalDate localDateFrom = DateTimeUtils.toInstant(dateFrom)
            .atZone(zone)
            .toLocalDate();
    LocalDate localDateTo = DateTimeUtils.toInstant(dateTo)
            .atZone(zone)
            .toLocalDate();
    long diff = differenceInMonths(localDateFrom, localDateTo);
    if (diff < Integer.MIN_VALUE || diff > Integer.MAX_VALUE) {
        throw new IllegalArgumentException("Dates are too far apart");
    }
    return (int) diff;
}

Even though the conversions from Date to LocalDate are at least as wordy as the conversions to Calendar in your own code, you may consider it worth is when the difference calculation itself is so simple and clear.

What went wrong in your old code has been explained nicely by others, there is no reason for me to repeat.

Question: How can I use java.time on Java 6?

java.time works nicely on Java 6.

  • In Java 8 and later and on newer Android devices (from API level 26, I’m told) the modern API comes built-in.
  • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom).
  • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

The code above uses ThreeTen Backport and imports the date and time classes from org.threeten.bp with subpackages:

import org.threeten.bp.DateTimeUtils;
import org.threeten.bp.LocalDate;
import org.threeten.bp.Month;
import org.threeten.bp.ZoneId;
import org.threeten.bp.temporal.ChronoUnit;

If you want to use the same code on Java 8 or later and use the built-in java.time, the conversion happens a little differently, for example:

    LocalDate localDateFrom = dateFrom.toInstant()
            .atZone(zone)
            .toLocalDate();

Links

Upvotes: 1

Alexei
Alexei

Reputation: 15664

Here correct code:

   public static Integer getDiffMonths(Date dateFrom, Date dateTo) {
        Calendar cal1 = new GregorianCalendar();
        cal1.setTime(dateFrom);
        Calendar cal2 = new GregorianCalendar();
        cal2.setTime(dateTo);
        return differenceInMonths(cal1, cal2);
    }

   private static Integer differenceInMonths(Calendar dateFrom, Calendar dateTo) {
        if (dateFrom == null || dateTo == null) {
            return 0;
        }
        int month1 = dateFrom.get(Calendar.YEAR) * 12 + dateFrom.get(Calendar.MONTH);
        int month2 = dateTo.get(Calendar.YEAR) * 12 + dateTo.get(Calendar.MONTH);
        int diff = month2 - month1;
        if (diff > 0) {
            if (dateFrom.get(Calendar.DAY_OF_MONTH) > dateTo.get(Calendar.DAY_OF_MONTH)) {
                diff--;
            }
        } else if (diff < 0) {
            if (dateTo.get(Calendar.DAY_OF_MONTH) > dateFrom.get(Calendar.DAY_OF_MONTH)) {
                diff++;
            }
        }
        return diff;
    }

Upvotes: -1

gkgkgkgk
gkgkgkgk

Reputation: 717

Examples 3 and 4 are working as expected. Aug -> Jan is 5 months, Aug -> Aug is 12 months. When you are subtracting the month values, it is strictly dealing with month values, not the days of the month. If you want to account for the day of that month, add this:

if(dateFrom.get(Calendar.DAY_OF_MONTH) > dateTo.get(Calendar.DAY_OF_MONTH) && diff != 0){
    diffMonth -=1;
}

Upvotes: 1

Henry
Henry

Reputation: 43738

You completely ignore the day when calculating the difference. So Jan 1 is the same as Jan 31.

The first two cases happen to have greater day in the second date, the last two a smaller day. But this information is ignored.

Upvotes: 6

Related Questions