Home > Software design >  One second difference when comparing two Date() with difference of one day
One second difference when comparing two Date() with difference of one day

Time:10-15

I wrote simple TimeService with method getDateAfter(int days) and test for it:

@Test
@Order(7)
public void getDateAfterCorrect() throws InterruptedException {
    waitIfNeeded();
    LocalDateTime today = LocalDateTime.now();
    LocalDateTime tomorrow = timeService.getDateAfter(1).toInstant()
        .atZone(ZoneId.systemDefault()).toLocalDateTime();
    long diff = ChronoUnit.SECONDS.between(today, tomorrow);
    long secondsAtDay = 86400;
    Assertions.assertEquals(secondsAtDay, diff);
}

It should be 86400 seconds at day, but diff is 86399. I tried to take that one part of code could be executed in another time than other into account by implementing waitIfNeeded() method

private void waitIfNeeded() throws InterruptedException {
    var currentMillis = Instant.now().get(ChronoField.MILLI_OF_SECOND);
    if (currentMillis > 500) {
        Thread.sleep(1000 - currentMillis);
    }
}

Do You have any idea why I am not able to make this test and other possible things that can be wrong here (I assume things like how programming languages are dealing with step year, etc...)

CodePudding user response:

I managed to get test simplified and working, now it is OK:

@Test
@Order(7)
public void getDateAfterCorrect() throws InterruptedException {
    waitIfNeeded();
    long today = timeService.getDate().toInstant().getEpochSecond();
    long tommorow = timeService.getDateAfter(1).toInstant().getEpochSecond();
    Assertions.assertEquals(86400, tommorow - today);
}

but It is still interesting why using other method of comparing that two dates produced such results, if someone with deep level knowledge can answer it, probably few people will be interested.

CodePudding user response:

The java.util Date-Time API is outdated and error-prone. It is recommended to stop using it completely and switch to the modern Date-Time API*.

Apart from that, instead of performing the calculation (subtraction) yourself, you use Instant#until which returns the duration in the specified ChronoUnit.

import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class Main {
    public static void main(String[] args) {
        // This is a sample Instant. In your case, it will be returned by
        // timeService.getDate().toInstant()
        Instant today = Instant.now();

        // This is a sample Instant after one day. In your case, it will be returned by
        // timeService.getDateAfter(1).toInstant()
        Instant tomorrow = today.plus(1, ChronoUnit.DAYS);

        long seconds = today.until(tomorrow, ChronoUnit.SECONDS);

        // In your case, you will use Assertions.assertEquals(86400, seconds);
        System.out.println(seconds);
    }
}

Output:

86400

ONLINE DEMO

Learn more about the modern Date-Time API from Trail: Date Time.


* If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8 APIs available through desugaring. Note that Android 8.0 Oreo already provides support for java.time.

CodePudding user response:

The explanation: why 86399? Lack of precision

I am assuming that timeService.getDateAfter(1) returns an old-fashioned Date object. You shouldn’t be using Date in your code anymore, it’s been replaced by java.time, the modern Java date and time API that you are also using, many years ago. But we’re still curious why your code using Date didn’t give the expected result of 86 400 seconds in a day.

Date has milliseconds precision. java.time has nanosecond precision, and since Java 9 the now methods of many of the classes have microsecond precision on most platforms. So for example LocalDateTime.now() returns 0.001234 seconds past a whole second. Some microseconds later, maybe at 0.001432 past the whole second, your time service returns a Date worth 0.001 seconds past the same whole second. Between today at 0.001234 and tomorrow at 0.001 is not a whole day, not fully 24 hours, so the difference in seconds is truncated to 86 399.

When I ran your code in a loop, the first time I got 86 400. It must be because I passed a full millisecond of the second between the two calls. Maybe because of JVM warm-up. The following times I got 86 399.

A possible fix

One way to obtain consistent precision is to truncate everything to milliseconds. Or even to seconds. With the following change to your code I got 86 400 consistently.

    LocalDateTime today = LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS);

I got the same result when using ChronoUnit.SECONDS instead of .MILLIS. I believe that this also somewhat explains why the change in your own answer worked. I think you need to be aware, though, that some time does elapse between your two calls, and you cannot control how much. So you may get 86 401 or even a still higher number on rare occasions. Even if I didn’t observe it in my few runs.

I did once work in a place where some of the very many unit tests failed sporadically. It was quite annoying and a source of distrust in the code even when I made it a habit to type comments into the unit tests in question about their sporadic failures. Please do not put yourself and your co-workers in the same situation.

  • Related