IbrahimMitko
IbrahimMitko

Reputation: 1207

Java date comparison off by a day

I have a Java method which compares two Dates and returns the number of days between them, but it's off by a day.

Even after I 0 out the hours, min, and sec the calculation is still off.

public long compareDates(Date exp, Date today){
        TimeZone tzone = TimeZone.getTimeZone("America/New_York");
        Calendar expDate = Calendar.getInstance();
        Calendar todayDate = Calendar.getInstance();

        expDate.setTime(exp);
        todayDate.setTime(today);

        expDate.set(Calendar.HOUR_OF_DAY, 0);
        expDate.set(Calendar.MINUTE, 0);
        expDate.set(Calendar.SECOND, 0);

        todayDate.set(Calendar.HOUR_OF_DAY, 0);
        todayDate.set(Calendar.MINUTE, 0);
        todayDate.set(Calendar.SECOND, 0);

        logger.info("Today = " + Long.toString(todayDate.getTimeInMillis()) + " Expiration = " + Long.toString(expDate.getTimeInMillis()));

        expDate.setTimeZone(tzone);
        todayDate.setTimeZone(tzone);

        return (expDate.getTimeInMillis()-todayDate.getTimeInMillis())/86400000;
    }

Output

Today = 1453939200030 Expiration = 1454544000000

There's 7 days between 1/28 and 2/4 but this returns 6.

Upvotes: 1

Views: 235

Answers (4)

IbrahimMitko
IbrahimMitko

Reputation: 1207

To fix my problems, I have zeroed out the milliseconds as mentioned, as well as casted the longs to doubles in order to maintain accuracy and round when necessary.

expDate.setTime(exp);
todayDate.setTime(today);
expDate.setTimeZone(tzone);
todayDate.setTimeZone(tzone);

expDate.set(Calendar.HOUR_OF_DAY, 0);
expDate.set(Calendar.MINUTE, 0);
expDate.set(Calendar.SECOND, 0);
expDate.set(Calendar.MILLISECOND, 0);

todayDate.set(Calendar.HOUR_OF_DAY, 0);
todayDate.set(Calendar.MINUTE, 0);
todayDate.set(Calendar.SECOND, 0);
todayDate.set(Calendar.MILLISECOND, 0);

double diff = ((double)expDate.getTimeInMillis()-(double)todayDate.getTimeInMillis())/86400000;

return Math.round(diff);

Upvotes: 0

Basil Bourque
Basil Bourque

Reputation: 338795

java.time

The Question and other Answers use outmoded classes. The old date-time classes such as java.util.Date/.Calendar bundled with the earliest versions of Java have proven to be quite troublesome. Those old classes have been supplanted by the java.time framework in Java 8 and later.

As the other Answers point out correctly, the issue is that the start long has 30 on the right side, precluding a whole-day calculation.

Count-Of-Days Definition

Furthermore you must define what you mean by a count-of-days. Do you mean a count by date, so any time on the 3rd of January to any time on the 4th is one day even if the times were a minute before and after midnight? Or do you mean a count of generic 24-hour blocks of time while ignoring the fact that particular days in particular time zones are not always 24-hours long because of Daylight Saving Time (DST) and other anomalies?

Count Days By Date

If you want the former, count by dates, then make use of the LocalDate class (a date-only without time-of-day nor time zone) and the Period class (a span of time defined as a count of years, months, days) found in java.time.

Define your inputs. Use long rather than int. These numbers apparently represent a count of milliseconds since the first moment of 1970 in UTC.

long startMilli = 1_453_939_200_030L;
long stopMilli = 1_454_544_000_000L;

Convert those long numbers into Instant objects, a moment on the timeline in UTC.

Instant startInstant = Instant.ofEpochMilli ( startMilli );
Instant stopInstant = Instant.ofEpochMilli ( stopMilli );

Define the time zone in which you want to consider the calendar dates. Note that time zone is crucial in defining dates. The date is not simultaneously the same around the globe. The date varies by time zone.

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

Apply that time zone to each Instant to produce ZonedDateTime.

ZonedDateTime startZdt = ZonedDateTime.ofInstant ( startInstant , zoneId );
ZonedDateTime stopZdt = ZonedDateTime.ofInstant ( stopInstant , zoneId );

To get a Period, we need “local” dates. By “local” we mean any particular locality, a generic date value. The LocalDate class contains no time zone, but the time zone contained with in the ZonedDateTime is applied when determining a LocalDate.

LocalDate startLocalDate = startZdt.toLocalDate ();;
LocalDate stopLocalDate = stopZdt.toLocalDate ();

Define our span of time as a count of generic days, in Period.

Period period = Period.between ( startLocalDate , stopLocalDate );

Interrogate the Period to ask for the number of generic days contained within.

int days = period.getDays ();

Dump to console.

System.out.println ( "milli: " + startMilli + "/" + stopMilli + " | Instant: " + startInstant + "/" + stopInstant + " | ZonedDateTime: " + startZdt + "/" + stopZdt + " | LocalDate: " + startLocalDate + "/" + stopLocalDate + " | period: " + period + " | days: " + days );

milli: 1453939200030/1454544000000 | Instant: 2016-01-28T00:00:00.030Z/2016-02-04T00:00:00Z | ZonedDateTime: 2016-01-27T19:00:00.030-05:00[America/Montreal]/2016-02-03T19:00-05:00[America/Montreal] | LocalDate: 2016-01-27/2016-02-03 | period: P7D | days: 7

Count Of Whole Days

If you want a count of whole days, use the Days class from ThreeTen-Extra. Notice in the output below that we get a count of six (6) days rather than seven (7) as seen above.

ThreeTen-Extra

The ThreeTen-Extra project extends java.time. Run by the same folks who built java.time.

The behavior of the between method is not documented clearly. Experimenting shows that it seems to based on 24-hour chunks of time, not dates. Replace the 030 with 000, and also try replacing in the stopMilli the last 000 with 030, to see the behavior for yourself.

Days daysObject = Days.between ( startZdt , stopZdt );
int daysObjectCount = daysObject.getAmount ();

Dump to console. The P6D string you see in the output was generated according to the formats defined in the ISO 8601 standard. This standard is used by default in java.time for all parsing and generating of textual representations of date-time values. These standard formats are quite sensible and useful so do glance at that linked Wikipedia page.

System.out.println ( "daysObject: " + daysObject + " | daysObjectCount: " + daysObjectCount );

daysObject: P6D | daysObjectCount: 6

Upvotes: 0

rgettman
rgettman

Reputation: 178263

Today = 1453939200030

The times are given in milliseconds, and it looks like somehow your inputted Date has 30 extra milliseconds on it.

When I subtract the 30 milliseconds, then do the math on a calculator, I get 7 days. With your figures as is, I get 6.9999996527777777777777777777778, and in long math, the decimal figures get truncated to 6.

Zero out the milliseconds also.

expDate.set(Calendar.MILLISECOND, 0);
todayDate.set(Calendar.MILLISECOND, 0);

Upvotes: 3

Andreas
Andreas

Reputation: 159114

Well, as you can see, you didn't clear the milliseconds, and 1454544000000 - 1453939200030 = 604799970 and dividing by 86400000 gets you 6.99999965277777..., which means 6 when truncated to int.

Now, if you clear the milliseconds too, today becomes 1453939200000, which will lead to you answer 7.

Note: This doesn't mean you're done, because of Daylight Savings Time. With DST, one of the timestamps may be ±1 hour from the other, so you may still get that truncation issue.

This was an answer to your particular issue. Try searching for how to correctly find days between dates in Java.

Upvotes: 5

Related Questions