Search code examples
javadatedatetimejodatimedate-difference

Finding the real difference between dates in JDK7


How can I find the difference between two dates in JDK7, such that the difference can be added back to make the two dates equal?

I tried to use the solution from a similar StackOverflow question, subtracting one date's milliseconds from the other, but this can result in an incorrect difference between dates.

In this example, I try to make a equal to b by setting a = (b-a) + a

private void makeTwoDatesEqual(GregorianCalendar a, GregorianCalendar b, DatatypeFactory datatypeFactory) {
    long b_minus_a = b.getTimeInMillis() - a.getTimeInMillis();
    Duration delta = datatypeFactory.newDuration(b_minus_a);
    delta.addTo(a);
}

But the results are unexpected:

Before

a = "2015-08-29T00:00:00.000Z"
b = "2040-01-01T00:00:00.000Z"
delta = "P24Y4M5DT0H0M0.000S" // 2 days longer than it should be.

After

a = "2040-01-03T00:00:00.000Z"
b = "2040-01-01T00:00:00.000Z"
delta = "P24Y4M5DT0H0M0.000S"
a.equals(b) = false

Joda-Time has this same issue:

private void makeTwoDatesEqual(GregorianCalendar a, GregorianCalendar b) {
    DateTime date1 = new DateTime(a);
    DateTime date2 = new DateTime(b);
    Interval interval = new Interval(a.getTimeInMillis(), b.getTimeInMillis());
    Period offsetPeriod = interval.toPeriod();
    date1 = date1.plus(offsetPeriod);
    date1.equals(date2); // false in my example
}

Before

date1 = "2015-08-29T00:00:00.000-05:00"
date2 = "2040-01-01T00:00:00.000-05:00"
offsetPeriod = "P24Y4M2DT23H"
date1.equals(date2) = false

After

date1 = "2039-12-31T23:00:00.000-05:00"
date2 = "2040-01-01T00:00:00.000-05:00"
offsetPeriod = "P24Y4M2DT23H"
date1.equals(date2) = false

Before it's asked, there should be no need to account for daylight savings or leap years because in each example I'm adding the interval back over the exact same time period from which it was derived.


Solution

  • As @Meno explained in the comments, date arithmetic is weird, because months and years can have different lenghts. In our ISO calendar, a month can have 28, 29, 30 or 31 days, and a year can have 365 or 366 days.

    So, a Period like P24Y4M5D (24 years, 4 months and 5 days) can represent a totally different number of days, depending on when you start counting it.

    If I start at 2017-01-01T00:00Z, for example. If I add 24 years, I get 2041-01-01, then adding 4 months, I get 2041-05-01, then adding 5 days I get 2041-05-06. The total difference is equivalent to 8891 days.

    But if I start at 2016-01-01T00:00Z and add the same period (24 years, 4 months and 5 days): adding 24 years I get 2040-01-01, then adding for 4 months I get 2040-05-01, then adding 5 days I get 2040-05-06. But if I add 8891 days, I get 2040-05-05.

    That's because a month and year don't have fixed lenghts, so the equivalent total in days will always depend on the dates involved (not to mention Daylight Saving Time and leap seconds effects, which also changes the total of hours, minutes, seconds and milliseconds).

    To not rely on that, you must calculate using the duration in milliseconds (and not in non-fixed-length variables such as years and months). In Joda-Time, you can use the org.joda.time.Duration class:

    public void makeTwoDatesEqual(GregorianCalendar a, GregorianCalendar b) {
        DateTime date1 = new DateTime(a);
        DateTime date2 = new DateTime(b);
        System.out.println("date1=" + date1);
        System.out.println("date2=" + date2);
    
        Duration duration = new Duration(date1, date2);
        DateTime date3 = date1.plus(duration);
        System.out.println("date3=" + date3);
        System.out.println(date3.equals(date2));
    }
    

    Assuming that a is equivalent to 2015-08-29T00:00:00Z and b is 2040-01-01T00:00:00Z, this code will output:

    date1=2015-08-29T00:00:00.000Z
    date2=2040-01-01T00:00:00.000Z
    date3=2040-01-01T00:00:00.000Z
    true


    ThreeTen Backport

    Joda-Time is in maintainance mode and is being replaced by the new APIs, so I don't recommend start a new project with it. Even in joda's website it says: "Note that Joda-Time is considered to be a largely “finished” project. No major enhancements are planned. If using Java SE 8, please migrate to java.time (JSR-310).".

    If you can't (or don't want to) migrate from Joda-Time to the new API, you can ignore this section.

    If you're using Java 6 or 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, you'll also need the ThreeTenABP (more on how to use it here).

    You can use the org.threeten.bp.DateTimeUtils class to convert the GregorianCalendar to org.threeten.bp.Instant and then calculate the difference between them, resulting in a org.threeten.bp.Duration:

    public void makeTwoDatesEqual(GregorianCalendar a, GregorianCalendar b) {
        Instant instantA = DateTimeUtils.toInstant(a);
        Instant instantB = DateTimeUtils.toInstant(b);
        System.out.println(instantA);
        System.out.println(instantB);
    
        Duration duration = Duration.between(instantA, instantB);
    
        Instant instantC = instantA.plus(duration);
        System.out.println(instantC);
        System.out.println(instantC.equals(instantB)); // true
    }
    

    Assuming that a is equivalent to 2015-08-29T00:00:00Z and b is 2040-01-01T00:00:00Z, this code will output:

    2015-08-29T00:00:00Z
    2040-01-01T00:00:00Z
    2040-01-01T00:00:00Z
    true