Skip to main content

Calendar - calculateDaysBetween

1 reply [Last post]
kid_wonder
Offline
Joined: 2011-04-09
Points: 0

I have run across an issue during unit testing with Calendar calculations (surprise!)
Can someone tell me why this code:

public static int calculateDaysBetween ( Calendar start, Calendar end )
{
    long startTime = start.getTimeInMillis();
    long endTime = end.getTimeInMillis();
    long diffTime = endTime - startTime;
    // divide the milliseconds by # of milliseconds in a day to get days difference
    return (int)( diffTime / (1000 * 60 * 60 * 24) );
}

Will only pass if I use an Assert.assertEquals() if I include a delta of 1 with this test code:
@DataProvider (name = "dp-calcdaysbetween")
public static Object[][] dataProviderCalculateDays ()
{
    Object[][] sendback = new Object[ 100 ][];
    Random rand = new Random( System.currentTimeMillis() );
    for ( int idx = 0; idx < 100; idx++ )
    {
        Calendar start = GregorianCalendar.getInstance();
        Calendar end = GregorianCalendar.getInstance();
        int days = Math.abs( rand.nextInt() % 3650 );
        start.add( Calendar.DAY_OF_YEAR, -days );

        sendback[ idx ] = new Object[] { start, end, new Integer( days ) };
    }

    return sendback;
}

And just for completeness, here is my unit test
@Test (dataProvider = "dp-calcdaysbetween")
public void testDaysBetween ( final Calendar start, final Calendar end, final Integer expDays )
{
    int days = Util.calculateDaysBetween( start, end );

    // FIXME: This calculation is typically correct, but can be off by +/-1 day
    Assert.assertEquals( "Incorrect calculation of days between.", expDays.intValue(), days,
                    // FIXME: This is how much it can be off by
                    1 );
}

This calendar stuff I know can be a bit weird, but since I am creating my two test dates withing milliseconds of each other I would expect this to work without the need for a delta value in my assertEquals
Mahalo
kid_wonder

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
martinval
Offline
Joined: 2011-04-11
Points: 0

Hi,
since these things seem not very well designed (which I dislike) I changed at least the way you set up the end date to
Calendar start = GregorianCalendar.getInstance();
end = (Calendar)start.clone();
This way I won't have to second guess if the short time between creating 2 Calendar objects is part of the problem. And I found that the time difference in milliseconds, as you compute it, is an exact multiple of
1000L * 60 * 60 * 24 (millis per day)
for many subesquent days and then it lacks exactly 3600000 millis (one hour) for many subsequent days more to test and then it's back to the exact multiples that we'd expect.
Obviously, this is an effect of daylight savings change and, since the add() method that you use just changes the DAY_OF_YEAR field, not the HOUR field, that same hour is NOT a multiple of 24 hours apart if start and end date have different daylight savings! The calendar's time zone seems to be at some DST-aware setting, if you create the calendar without setting a timezone that does not take DST into account. Let's do something like
Object[][] sendback = new Object[len][];
Random rand = new Random(System.currentTimeMillis());
for (int idx=0; idx<len; idx++)
{
Calendar start = GregorianCalendar.getInstance();
start.setTimeZone(TimeZone.getTimeZone("Europe/Berlin")); // EST EST5EDT Europe Europe/Berlin
start.set(2004, 3, 3); // just for example...
Calendar end; // = GregorianCalendar.getInstance(); => this could differ by a few msecs in time
end = (Calendar)start.clone();
int days = rand.nextInt() % 3650;
start.add(Calendar.DAY_OF_YEAR, -days);
sendback[idx] = new Object[] { start, end, new Integer(days) };
}
I can recreate your problem going back for more than 6 days from 2004, 3, 3 above since there's a change in DST then.
The problem occurs with e.g. timezones EST5EDT and Europe/Berlin but not with EST and Berlin: the latter are obviously not "DST-sensitive".
Use someTimeZone.useDaylightTime() to determine if a zone is DST-sensitive. Valid names can be queried via static method TimeZone.getAvailableIDs()
The error was, of course, in the sample data generated for unit testing, not in the tested method: Just changing the day-of-year with the Calendar.add() method is not the same as adding/removing a multiple of N millis (where N is the amount of millis for a single day). Mind that just allowing for an extra hour difference in the tested method is not a clean solution: I think there are countries with 2 hours DST-offset, too.
The simplest idea for this test scenario is perhaps to use a timezone without such DST-offset. Is there a standard way of deriving such a timezone from an existing, i.e. if we'd get Europe/Berlin by default on some machine that has this default, is there a query that is guaranteed to give us Berlin, i.e. basically the same zone but without DST handling?
On the other hand, for perfectly adding/subtracting a number of days to/from a date with given timezone we should add/subtract a corresponding offset if there is such a DST-difference between the two dates. I guess, this is the case if method TimeZone.inDaylightTime(Date) returns different results for the two dates in question (but I'm too tired to verify this now, lest check when to add and when to subtract that offset ;-)
On the other hand: If I just assume that adding a multiple of N millis means adding N days, why would I not use that if I want to skip exactly that many periods of 24 hours? Ok, your wrist watch could then be an hour off if you do the time travel, so that might not be the expected result - but the expected result would, in turn, not skip a multiple of 24 hours (also "not quite expected" ;-)
That may all sound somewhat confusing but nevertheless I hope that clarifies a bit...
Greetings :)