1

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.

marathon
  • 7,881
  • 17
  • 74
  • 137

1 Answers1

5

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?).

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • thank you. yes, this is exactly the problem. Seeing these two were different made me not even think about current-time UTC being actually tomorrow. BTW, in testing your answer, I found that `c.setTimeZone` has to come even before `c.setTime`. – marathon Feb 14 '21 at 03:03
  • 3
    @marathon Really? Putting it after `setTime` works for me though... I guess that's one more reason not to use `Calendar` :D – Sweeper Feb 14 '21 at 03:05