Search code examples
datedatetimejava-8java-timezoneddatetime

Getting time range between midnight and current time JDK 8


I have this method to calculate midnigt and current time as long values:

/**
 * Returns the time range between the midnight and current time in milliseconds.
 *
 * @param zoneId time zone ID.
 * @return a {@code long} array, where at index: 0 - midnight time; 1 - current time.
 */

public static long[] todayDateRange(ZoneId zoneId) {
    long[] toReturn = new long[2];

    LocalTime midnight = LocalTime.MIDNIGHT;
    LocalDate today = LocalDate.now(zoneId);
    LocalDateTime todayMidnight = LocalDateTime.of(today, midnight);
    ZonedDateTime todayMidnightZdt = todayMidnight.atZone(zoneId);
    toReturn[0] = todayMidnightZdt.toInstant().toEpochMilli();

    ZonedDateTime nowZdt = LocalDateTime.now().atZone(zoneId);
    toReturn[1] = nowZdt.toInstant().toEpochMilli();
    return toReturn;
}

Perhaps there is the simpler way to do that?


Solution

  • You could also do:

    ZonedDateTime nowZdt = ZonedDateTime.now(zoneId);
    
    ZonedDateTime todayAtMidnightZdt = nowZdt.with(LocalTime.MIDNIGHT);
    

    I can't think of a simpler way to do it.


    LocalDateTime vs ZonedDateTime

    There's a (tricky) difference between LocalDateTime.now().atZone(zoneId) and ZonedDateTime.now(zoneId).

    For the code below, I'm using a JVM in which the default timezone is America/Sao_Paulo and will try to get the current date and time in another timezone (Europe/London). At the moment I run this code, it's August 20th 2017, but in São Paulo the time is 17:56 and in London is 21:56.

    When I do:

    LocalDateTime nowLdt = LocalDateTime.now();
    

    It creates a LocalDateTime with the current date and time in the JVM's default timezone. In this case, it'll get the current date and time in São Paulo's timezone (which is August 20th 2017, at 17:56):

    2017-08-20T17:56:05.159

    When I call the atZone method, it creates a ZonedDateTime that corresponds to this date and time in the specified zone:

    ZoneId zoneId = ZoneId.of("Europe/London");
    ZonedDateTime nowAtZone = nowLdt.atZone(zoneId);
    

    The nowAtZone variable will be:

    2017-08-20T17:56:05.159+01:00[Europe/London]

    The same date (August 20th 2017) and time (17:56) in London timezone. Note that it's not the current date/time in London. If I get the equivalent epochMilli:

    System.out.println(nowAtZone.toInstant().toEpochMilli());
    

    It will be:

    1503248165159

    Now, if I don't use the LocalDateTime and direclty use the ZonedDateTime instead:

    ZonedDateTime nowZdt = ZonedDateTime.now(zoneId);
    

    It will get the current date and time in London, which will be:

    2017-08-20T21:56:05.170+01:00[Europe/London]

    Note that the time changed (it's 21:56). That's because right now, at this moment, that's the current time in London. If I get the epochMilli value:

    System.out.println(nowZdt.toInstant().toEpochMilli());
    

    The value will be:

    1503262565170

    Note that it's different from the first case using LocalDateTime (even if you ignore the difference in the milliseconds value, because the hour is different). If you want the current date and time at the specified timezone, you must use ZonedDateTime.now(zoneId).

    Using LocalDateTime.now().atZone() not only gives a different result, but it will also change if you run in different JVM's, or if the JVM default timezone changes (someone might misconfigure it, or another application running in the same VM calls TimeZone.setDefault()).


    Daylight Saving Time

    Just remind about corner cases due to DST (Daylight Saving Time) issues. I'm gonna use the timezone I live in as example (America/Sao_Paulo).

    In São Paulo, DST started at October 16th 2016: at midnight, clocks shifted 1 hour forward from midnight to 1 AM (and the offset changes from -03:00 to -02:00). So all local times between 00:00 and 00:59 didn't exist in this timezone (you can also think that clocks changed from 23:59:59.999999999 directly to 01:00). If I create a local date in this interval, it's adjusted to the next valid moment:

    ZoneId zone = ZoneId.of("America/Sao_Paulo");
    
    // October 16th 2016 at midnight, DST started in Sao Paulo
    LocalDateTime d = LocalDateTime.of(2016, 10, 16, 0, 0, 0, 0);
    ZonedDateTime z = d.atZone(zone);
    System.out.println(z);// adjusted to 2017-10-15T01:00-02:00[America/Sao_Paulo]
    

    When DST ends: in February 19th 2017 at midnight, clocks shifted back 1 hour, from midnight to 23 PM of 18th (and the offset changes from -02:00 to -03:00). So all local times from 23:00 to 23:59 existed twice (in both offsets: -03:00 and -02:00), and you must decide which one you want. By default, it uses the offset before DST ends, but you can use the withLaterOffsetAtOverlap() method to get the offset after DST ends:

    // February 19th 2017 at midnight, DST ends in Sao Paulo
    // local times from 23:00 to 23:59 at 18th exist twice
    LocalDateTime d = LocalDateTime.of(2017, 2, 18, 23, 0, 0, 0);
    // by default, it gets the offset before DST ends
    ZonedDateTime beforeDST = d.atZone(zone);
    System.out.println(beforeDST); // before DST end: 2018-02-17T23:00-02:00[America/Sao_Paulo]
    
    // get the offset after DST ends
    ZonedDateTime afterDST = beforeDST.withLaterOffsetAtOverlap();
    System.out.println(afterDST); // after DST end: 2018-02-17T23:00-03:00[America/Sao_Paulo]
    

    Note that the dates before and after DST ends have different offsets (-02:00 and -03:00). This affects the value of epochMilli.

    The above can also happen if you adjust the time using with method.