javadatetimeepochnanotime

Using Instant/ChronoUnit to get the time in nanos-since-epoch at midnight today is 24 hours ahead of using GregorianCalendar/Date


I'm attempting to get today's date in nanos since Epoch at midnight (e.g. 13 Feb 2021 00:00:00).

Using GregorianCalendar/Date appears to get the right result.

The way I'm using Instant/ChronoUnit is giving me tomorrow at midnight (e.g 14 Feb 2021 00:00:00).

What is wrong here?

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Calendar;
import java.util.TimeZone;


class scratch {

    public static void main(String[] args) {
        Calendar c = new GregorianCalendar();
        c.setTime(new Date());
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        c.setTimeZone(TimeZone.getTimeZone("UTC"));
        long dateNanos = c.getTimeInMillis() * 1000000L;

        Instant i = Instant.now().truncatedTo(ChronoUnit.DAYS);
        long instantNanos = ChronoUnit.NANOS.between(Instant.EPOCH, i);

        System.out.println("dateMillis = " + dateNanos);
        System.out.println("instantMillis = " + instantNanos);
        System.out.println("instantMillis - dateMillis = " + (instantNanos - dateNanos));
    }
}

It appears that trucatedTo is rounding up. The javadocs says it rounds down.

For example, truncating with the MINUTES unit will round down to the nearest minute, setting the seconds and nanoseconds to zero.


Solution

  • These give different results because you are "truncating to the day" in different timezones. Note that "truncating to the day" gives you different answers when you are in different timezones, because the instant of "midnight" is different in different timezones. Some timezones don't even have the time 00:00:00 due to DST changes.

    In the Calendar version, you are truncating to the day in your system timezone. Note that the setTimeZone call happens after the truncation, so it doesn't actually do anything useful.

    In the Instant version, you are truncating to the day in UTC, because Instants don't have the concept of a "timezone" in them.

    Instant is only "wrong" because you expected it to truncate to a day in your system timezone. Instant isn't really meant to do that. You should use a ZonedDateTime:

    ZonedDateTime now = ZonedDateTime.now(); // this gets the current ZonedDateTime, you can also specify a specific zone
    System.out.println(now.truncatedTo(ChronoUnit.DAYS).toEpochSecond() * 1_000_000_000L);
    

    On the other hand, if you want Calendar to truncate in the UTC timezone, put the setTimeZone call before you truncate:

    Calendar c = new GregorianCalendar();
    c.setTime(new Date());
    c.setTimeZone(TimeZone.getTimeZone("UTC"));
    c.set(Calendar.HOUR_OF_DAY, 0);
    c.set(Calendar.MINUTE, 0);
    c.set(Calendar.SECOND, 0);
    c.set(Calendar.MILLISECOND, 0);
    

    But I would still suggest you use java.time whenever you can :)


    What you should do now is decide which "midnight" you mean (midnight in which timezone?).