29

I'm looking for a solution to calculate the months between two date. I think joda or java8 time can do it. But when I compare them I found something really weird.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import org.joda.time.Months;

public class DateMain {
    public static void main(String[] args) throws ParseException {

    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    Date d1 = simpleDateFormat.parse("2017-01-28 00:00:00.000");
    Date d2 = simpleDateFormat.parse("2017-02-28 00:00:00.000");

    System.out.println("Test Cast 1");
    System.out.println("joda time api result: " + monthsBetweenJoda(d1, d2) + " month");
    System.out.println("java8 time api result: " + monthsBetweenJava8(d1, d2) + " month");

    Date dd1 = simpleDateFormat.parse("2017-01-29 00:00:00.000");
    Date dd2 = simpleDateFormat.parse("2017-02-28 00:00:00.000");

    System.out.println("Test Cast 2");
    System.out.println("joda time api result: " + monthsBetweenJoda(dd1, dd2) + " month");
    System.out.println("java8 time api result: " + monthsBetweenJava8(dd1, dd2) + " month");
}

public static int monthsBetweenJoda(Date fromDate, Date toDate) {
    if (fromDate == null || toDate == null) {
        throw new IllegalArgumentException();
    }
    org.joda.time.LocalDateTime fromLocalDateTime = org.joda.time.LocalDateTime
        .fromDateFields(fromDate);
    org.joda.time.LocalDateTime toLocalDateTime = org.joda.time.LocalDateTime
        .fromDateFields(toDate);
    Months months = Months.monthsBetween(fromLocalDateTime, toLocalDateTime);
    return months.getMonths();
}

public static long monthsBetweenJava8(Date fromDate, Date toDate) {
    if (fromDate == null || toDate == null) {
        throw new IllegalArgumentException();
    }
    LocalDateTime ldt1 = fromDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    LocalDateTime ldt2 = toDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    return ChronoUnit.MONTHS.between(ldt1, ldt2);
}

}

the output is below:

Test Cast 1
joda time api result: 1 month
java8 time api result: 1 month
Test Cast 2
joda time api result: 1 month
java8 time api result: 0 month

I feel very confused with the test case 2, and which one is reasonable?

Sorry, it's my first question here. Completed code attached.

Jiaqi
  • 301
  • 2
  • 6
  • 3
    Joda looks wrong to me. Its documentation says the number of whole months, but your test is inconsistent with that. – Joe C Sep 15 '17 at 05:55
  • but the result of joda looks more reasonable, when plus 1 month to 2017-01-29 or 2017-01-30 , it always return 2017-02-28, so months between 2017-01-29 and 2017-02-28 should be 1 rignt? – Jiaqi Sep 15 '17 at 06:07
  • 2
    @JoeC: I suspect it's taking the largest number such that `fromDate.plusMonths(x) <= toDate`, which is reasonable when adding 1 month to January 29th still gives February 28th. Basically calendar arithmetic is weird. – Jon Skeet Sep 15 '17 at 06:12
  • Does this occur on other months. It could be that joda time it looking at the length of the current month (feb) with 28d and saying the it's still 1 month. – user3206236 Sep 15 '17 at 06:29
  • 3
    Are you using `java.sql.Date` or `java.util.Date`? Please document this fact, and show how you generate your `Date` objects being passed as the pair of arguments. And show how your generated your output strings. **Your output does not match that of the `toString` method on either `java.sql.Date` or `java.util.Date`.** Provide us with a [MCVE](https://stackoverflow.com/help/mcve). Your code as shown is not complete, so we cannot replicate your scenario. – Basil Bourque Sep 15 '17 at 06:35
  • Shouldn't the joda result be more correct? There is no 29th in February 2017 - so there should be exactly 1 month between 29th January 2017 until 28th February 2017? Do we have a bug in the JDK? `LocalDate.of(2017, 1, 29).plusMonths(1L)` results in `java.time.LocalDate = 2017-02-28` but getting the month interval between them results in 0. Could be a bug – ailveen Sep 15 '17 at 07:04
  • 1
    Hm. The [JavaDocs ofr ChonoUnit.between](https://docs.oracle.com/javase/8/docs/api/java/time/temporal/ChronoUnit.html#between-java.time.temporal.Temporal-java.time.temporal.Temporal-) don't explicitly mention how the method deals with units of variable length (like `DAYS`, `MONTHS` or `YEARS` or almost all of them actually). – Hulk Sep 15 '17 at 07:59
  • 1
    My implementation ends up invoking [LocalDateTime.until](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html#until-java.time.temporal.Temporal-java.time.temporal.TemporalUnit-) which (for non-time-based units) in turn invokes [LocalDate.until](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html#until-java.time.temporal.Temporal-java.time.temporal.TemporalUnit-). However, it may reduce the end date by one day if `end.time.isBefore(time)` - maybe this is the case in your test (although it doesn't look like it from your output)? – Hulk Sep 15 '17 at 08:43

1 Answers1

38

Joda-Time and Java 8 java.time.* have different algorithms for determining the number of months between.

java.time.* only declares a month has passed once the day-of-month is greater (or in the next month). From 2017-01-29 to 2017-02-28 it is clear that the second day-of-month (28) is less than the first (29) so a month has not yet been completed.

Joda-Time declares a month has passed because adding 1 month to 2017-01-29 will yield 2017-02-28.

Both are plausible algorithms, but I believe the java.time.* algorithm to be more in tune with what people expect (which is why I chose a different algorithm when writing java.time.*).

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
  • 1
    Makes me uneasy that this was the decision in Java 8 time. So every time we have a February 28, 1 month will only have elapsed by 1 March from January 29 - seems wrong to me, especially since adding a month to January 29 results in February 28. How do I deal with this so I can just tell myself - "let it go"? – ailveen Sep 15 '17 at 14:15