Java Calendar: Getting Difference Between Two Dates/Times - Off by One

58,875

Solution 1

As suggested by other users, you need to also set the millisecond value of the calendars to zero to compare only the dates. This can be achieved with the following code snippet:

someCalendar.set(Calendar.MILLISECOND, 0)

Also, note that timezone changes (e.g. moving from winter time to summer time) may mean there is more or less than 86,400,000 ms in a day.

Solution 2

The Joda Time Library has very good support for such problems:

LocalDate d1 = new LocalDate(calendar1.getTimeInMillis());
LocalDate d2 = new LocalDate(calendar2.getTimeInMillis());
int days = Days.daysBetween(d1, d2).getDays();

UPDATE (feedback from @basil-bourque):

As of Java 8 the new time library java.time has been introduced, now a similar option without external dependencies is available:

int days = Duration.between(calendar1.toInstant(), calendar2.toInstant()).toDays();

Solution 3

tl;dr

ChronoUnit.DAYS.between( then , now )

Naïve calculations

You assume in your code that every day is exactly twenty four hours long. Not true. Anomalies such as Daylight Saving Time (DST) mean that days may vary such as 23 hours long, 25 hours, or other numbers. The very meaning of a time zone is to track a history of these anomalies.

Also, you assume the day starts at midnight. Not true. Because of anomalies, some days start at other time-of-day such as 01:00.

Avoid legacy date-time classes

You are using the troublesome old date-time classes such as java.util.Date, java.util.Calendar, and java.text.SimpleTextFormat are now legacy, supplanted by the java.time classes.

Time zone

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

Do not extend java.time

Do not extend (subclass) the java.time classes (they are marked final).

And do not generalize to their interfaces for your business logic; stick to the concrete classes of this framework. While generalizing makes sense in other frameworks such as Java Collections, not so in java.time.

Using java.time

This work is much easier with the java.time classes.

ZonedDateTime represents a moment on the timeline with an assigned tim zone (ZoneId).

ZoneId z = ZoneId.of( "America/Montreal" );

// of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone)
ZonedDateTime then = ZonedDateTime.of( 2017 , 1 , 23 , 12 , 34 , 56 , 123456789 , z );

ZonedDateTime now = ZonedDateTime.now( z );

The ChronoUnit enum can calculate elapsed time between a pair of moments.

long days = ChronoUnit.DAYS.between( then , now );

See this code run live at IdeOne.com.

then.toString(): 2017-01-23T12:34:56.123456789-05:00[America/Montreal]

now.toString(): 2017-03-01T21:26:04.884-05:00[America/Montreal]

days: 37

Converting legacy instances to java.time

If you are given GregorianCalendar objects, convert to java.time using new methods added to the old classes.

ZonedDateTime zdt = myGregCal.toZonedDateTime() ;

If you know a Calendar instance is actually a GregorianCalendar, cast it.

GregorianCalendar myGregCal = (GregorianCalendar) myCal ;

Half-Open

The java.time classes define spans of time by the Half-Open approach where the beginning is inclusive while the ending is exclusive.


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.

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.

Solution 4

I have written a much more simpler way to tackle this problem, even i faced the same problem of having one day extra for some dates.

This is my approach currently,

    public long getDays(long currentTime, long endDateTime) {

    Calendar endDateCalendar;
    Calendar currentDayCalendar;


    //expiration day
    endDateCalendar = Calendar.getInstance(TimeZone.getTimeZone("EST"));
    endDateCalendar.setTimeInMillis(endDateTime);
    endDateCalendar.set(Calendar.MILLISECOND, 0);
    endDateCalendar.set(Calendar.MINUTE, 0);
    endDateCalendar.set(Calendar.HOUR, 0);

    //current day
    currentDayCalendar = Calendar.getInstance(TimeZone.getTimeZone("EST"));
    currentDayCalendar.setTimeInMillis(currentTime);
    currentDayCalendar.set(Calendar.MILLISECOND, 0);
    currentDayCalendar.set(Calendar.MINUTE, 0);
    currentDayCalendar.set(Calendar.HOUR, 0);

    long remainingDays = Math.round((float) (endDateCalendar.getTimeInMillis() - currentDayCalendar.getTimeInMillis()) / (24 * 60 * 60 * 1000));

    return remainingDays;
}

previously I was using this piece of line, rest is same :-

long remainingDays = TimeUnit.MILLISECONDS.toDays(
            Math.abs(expiryCalendar.getTimeInMillis() - currentDayCalendar.getTimeInMillis()));

But seems to be not working for me.

Solution 5

You recognized the issue in step one by setting the time to midnight: that made sure that all the hours, minutes, and seconds were at zero. But you didn't go far enough! You also have to make sure that the milliseconds are zeroed out, too.

Here's some code to do exactly that.

protected long truncate_to_seconds (long date_in_millis) {
    long l = date_in_millis / 1000L;
    l *= 1000L;
    return l;
} 
Share:
58,875
SMBiggs
Author by

SMBiggs

Computer Scientist originally from Austin, Texas. Worked on computer games and the movie/film industry as both programmer and director/producer since 1991. Android developer with experience for both small and enterprise-level apps. Knowledge of over two-dozen computer languages and operating systems (yes, I learn quickly!).

Updated on August 23, 2020

Comments

  • SMBiggs
    SMBiggs over 3 years

    I have seen many questions and answers on this topic, but none addressed my particular problem. I extended the java Calendar class (standard--no third party libraries), and needed to find the difference in days between two arbitrary dates.

    Method:

    1. Change the time of both dates to midnight.
    2. Convert the dates to milliseconds.
    3. Find the difference between the two dates.
    4. Divide the result by the number of milliseconds in a day (24 * 60 * 60 * 1000).
    5. The result should be the difference in days.

    And it sometimes is, and it sometimes isn't. Even tests on the same date can be off by one. What's going on?

  • Duncan Jones
    Duncan Jones over 11 years
    No need for the truncate method, he can just zero the milliseconds on the Calendar instances with set(Calendar.MILLISECOND, 0).
  • SMBiggs
    SMBiggs over 11 years
    @DuncanJones Even BETTER! Didn't see that aspect of the Calendar library, thanks!!
  • Yogendra Singh
    Yogendra Singh over 11 years
    @ScottBiggs: Good to know. Please don't forget to accept the answer.
  • SMBiggs
    SMBiggs over 11 years
    @DuncanJones: please add this as an answer so I can give you proper credit.
  • SMBiggs
    SMBiggs over 11 years
    The elegant solution! I should have read the docs more thoroughly (like we all have time!).
  • user1052080
    user1052080 over 10 years
    That's really a good solution, I think it's wise to consider some outside library, particularly such a small and nice one like JodaTime. Works perfect and gives exactly what you want.
  • Basil Bourque
    Basil Bourque about 7 years
    FYI, the Joda-Time project is now in maintenance mode, with the team advising migration to the java.time classes.
  • Basil Bourque
    Basil Bourque about 7 years
    FYI, the troublesome old date-time classes such as java.util.Calendar are now legacy, supplanted by the java.time classes.
  • Arne Burmeister
    Arne Burmeister about 7 years
    @BasilBourque thanks, added the version using java.time from JRE 8 libraries
  • Basil Bourque
    Basil Bourque about 7 years
    LocalDateTime is the wrong class for this. That class purposely loses all offset and time zone information. So you will be working with generic 24-hour days and ignoring the anomalies represented in time zones such as Daylight Saving Time (DST). See my Answer for details.
  • doctorram
    doctorram almost 6 years
    Didn't work for me. It says .toInstant() does not exist! What Calendar does it have to be exactly?
  • Arne Burmeister
    Arne Burmeister over 5 years
    @doctorram java.util.Calendar.toInstant() is available from Java 8 runtime libs
  • Felipe
    Felipe over 2 years
    I did this: double remainingDays = (cal.getTimeInMillis() - calNow.getTimeInMillis()) / (24.0 * 60 * 60 * 1000);