12

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?

Kara
  • 6,115
  • 16
  • 50
  • 57
SMBiggs
  • 11,034
  • 6
  • 68
  • 83
  • On a side note, are you sure it was a good idea to *extend* `Calendar` as opposed to producing a helper library that operates on `Calendar` instances? – Duncan Jones Nov 02 '12 at 15:52
  • 2
    One problem is that you assume there are 86400000ms in a day. – jarnbjo Nov 02 '12 at 17:20

7 Answers7

14

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();
Arne Burmeister
  • 20,046
  • 8
  • 53
  • 94
  • 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. – user1052080 Sep 28 '13 at 16:34
  • FYI, the [Joda-Time](http://www.joda.org/joda-time/) project is now in [maintenance mode](https://en.wikipedia.org/wiki/Maintenance_mode), with the team advising migration to the [java.time](http://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html) classes. – Basil Bourque Mar 02 '17 at 02:10
  • @BasilBourque thanks, added the version using `java.time` from JRE 8 libraries – Arne Burmeister Mar 03 '17 at 16:47
  • `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](http://stackoverflow.com/a/42545700/642706) for details. – Basil Bourque Mar 03 '17 at 16:56
  • Didn't work for me. It says .toInstant() does not exist! What Calendar does it have to be exactly? – doctorram Jul 07 '18 at 16:29
  • @doctorram `java.util.Calendar.toInstant()` is available from Java 8 runtime libs – Arne Burmeister Aug 22 '18 at 10:42
6

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.

Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
  • 1
    The elegant solution! I should have read the docs more thoroughly (like we all have time!). – SMBiggs Nov 07 '12 at 06:15
  • FYI, the troublesome old date-time classes such as [`java.util.Calendar`](https://docs.oracle.com/javase/8/docs/api/java/util/Date.html) are now [legacy](https://en.wikipedia.org/wiki/Legacy_system), supplanted by the [java.time](https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html) classes. – Basil Bourque Mar 02 '17 at 02:11
6

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.

Community
  • 1
  • 1
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
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.

Arpit
  • 1,259
  • 11
  • 21
  • I did this: double remainingDays = (cal.getTimeInMillis() - calNow.getTimeInMillis()) / (24.0 * 60 * 60 * 1000); – Felipe Oct 02 '21 at 08:17
3

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;
} 
SMBiggs
  • 11,034
  • 6
  • 68
  • 83
1

I believe you need to truncate the time part from the date before computing the difference as below:

   DateFormat formatter= new SimpleDateFormat("MM/dd/yyyy");
   String truncatedDateString1 = formatter.format(date1);
   Date truncatedDate1 = formatter.parse(truncatedDateString1);

   String truncatedDateString2 = formatter.format(date2);
   Date truncatedDate2 = formatter.parse(truncatedDateString2);

   long timeDifference = truncatedDate2.getTime()- truncatedDate1.getTime();

   int daysInBetween = timeDifference / (24*60*60*1000);

Hope this works.

Yogendra Singh
  • 33,927
  • 6
  • 63
  • 73
0

I have my function as purpose at your request. I was using calendar, i set all the time part to 0 before compare.

    int countDays(Date dateBegin, Date dateEnd) {
    if (dateBegin == null || dateEnd == null)
        return 0;
    Calendar from = asCalendar(dateBegin); // asCalendar() Initialise a Calendar from a Date
    Calendar to = asCalendar(dateEnd);
    // Set the time part to 0
    from.set(Calendar.MILLISECOND, 0);
    from.set(Calendar.SECOND, 0);
    from.set(Calendar.MINUTE, 0);
    from.set(Calendar.HOUR_OF_DAY, 0);
    to.set(Calendar.MILLISECOND, 0);
    to.set(Calendar.SECOND, 0);
    to.set(Calendar.MINUTE, 0);
    to.set(Calendar.HOUR_OF_DAY, 0);
    int nbJours = 0;
    for (Calendar c = from ; c.before(to) ; c.add(Calendar.DATE, +1))
    {
        nbJours++;
    }
    for (Calendar c = from ; c.after(to) ; c.add(Calendar.DATE, -1))
    {
        nbJours--;
    }
    return nbJours;
    }