简体   繁体   中英

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 = (ba) + 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.

ThreeTen Backport

There are a couple of possibilities using the modern Java date and time API known as JSR-310 or java.time . Can you use this with Java 7? Certainly! Get ThreeTen Backport and start coding. :-)

Difference between date-times

EDIT: If you want to perceive your a and b as points in time, in other words, both dates and times of day, you may use either a suitably small time unit for the difference, or the Duration class. Both will work the same. Since Instant s have nanosecond precision, calculating a difference in nanos will make sure we don't lose any precision:

    Instant a = Instant.parse("2015-08-29T00:00:00.000Z");
    Instant b = Instant.parse("2040-01-01T00:00:00.000Z");
    long delta = ChronoUnit.NANOS.between(a, b);
    a = a.plusNanos(delta);
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    System.out.println("delta = " + delta);
    System.out.println("a.equals(b) = " + a.equals(b));

This prints

a = 2040-01-01T00:00:00Z
b = 2040-01-01T00:00:00Z
delta = 768182400000000000
a.equals(b) = true

The Duration class is meant for durations in hours, minutes, seconds and fraction of second. You can use it for longer durations, but it doesn't know about weeks, months or years.

    Duration delta = Duration.between(a, b);
    a = a.plus(delta);

Otherwise as before. This time the output is

a = 2040-01-01T00:00:00Z
b = 2040-01-01T00:00:00Z
delta = PT213384H
a.equals(b) = true

So the difference is 213384 hours. Only because the minutes and seconds are 0 in this case, they are not printed, otherwise they would be; Duration too has nanosecond precision.

Difference between dates

Taking your title more literally and noticing that your example dates fall on midnight UTC. If you don't care about the time of day and only want to count days, months and years, you may use either the Period class or simply count days. The Period class is for periods of days, months and years and has 1 day precision (that means, cannot be used for hours and smaller units).

    LocalDate a = LocalDate.of(2015, Month.AUGUST, 29);
    LocalDate b = LocalDate.of(2040, Month.JANUARY, 1);
    Period delta = Period.between(a, b);
    a = a.plus(delta);

The printing goes as before, but now it prints

a = 2040-01-01
b = 2040-01-01
delta = P24Y4M3D
a.equals(b) = true

Counting days goes just like nanoseconds above, only with days instead:

    long delta = ChronoUnit.DAYS.between(a, b);
    a = a.plusDays(delta);

This time the output contains:

delta = 8891
a.equals(b) = true

Please note that because months haven't got the same length, even though 24 years 4 months 3 days seems to equal 8891 days, this will more often not be the exact case. This is explained in more detail in @Meno Hochschild's comment and Hugo's answer . So the choice between a Period and a number of days will make a difference, and you should choose based on your more exact requirements.

Combining Period and Duration

It's a bit funny that Java doesn't offer a class for a period or duration of years, months, days, hour, minutes and seconds. If you wanted, you might use a Period instance for the years, months and days, and then a Duration for the hours, minutes and seconds. It's a little bit complicated, so I am not writing the code unless you say you need it, but it is viable. You may of course also work out the details yourself.

I am told that the ThreeTen-extra project includes a PeriodDuration class that combines the two. I haven't got any experience with it myself.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM