简体   繁体   中英

Converting LocalDateTime to Instant give different values

I am trying to use LocalDateTime to manipulate dates in my application but I have noticed that getting epoch seconds from it returns different values from what I expected

val now1 = Instant.now().epochSecond - 60
val now2 = Instant.now().minusSeconds(60).epochSecond
val now3 = LocalDateTime.now().minusSeconds(60).toEpochSecond(ZoneOffset.UTC)
val now4 = System.currentTimeMillis() / 1000 - 60

Output

Now1 = 1674501451
Now2 = 1674501451
Now3 = 1674512251
Now4 = 1674501451

Notice how Now3 has a different value. What is happening?

tl;dr

  • Your JVM's current default time zone is three hours ahead of UTC. Thus your result.
    ( 1_674_512_251L - 1_674_501_451L ) = 3 hours.
  • You are making inappropriate use of LocalDateTime class.

Moment versus not-a-moment

I am trying to use LocalDateTime to manipulate dates

Don't.

If you are representing a moment, a specific point on the timeline, do not use LocalDateTime . That class holds a date with a time-of-day but lacks the context of a time zone or offset-from-UTC. To represent a moment use:

  • Instant — A moment as seen in UTC (an offset from UTC of zero hours-minutes-seconds).
  • OffsetDateTime - A moment as seen with a particular offset.
  • ZonedDateTime - A moment as seen through a particular time zone.

I cannot imagine any scenario where calling LocalDateTime.now is the right thing to do.

Example code:

Instant instant = Instant.now() ;

ZoneOffset offset = ZoneId.of( "Africa/Dar_es_Salaam" ).getRules().getOffset( instant ) ;
OffsetDateTime odt = OffsetDateTime.now( offset ) ;

ZoneId zone = ZoneId.of( "Africa/Dar_es_Salaam" ) ;
ZonedDateTime zonedDateTime = ZonedDateTime.now( zone ) 

Dump to console.

System.out.println( instant + " | " + instant.getEpochSecond() ) ;
System.out.println( odt + " | " + odt.toEpochSecond() ) ;
System.out.println( zonedDateTime + " | " + zonedDateTime.toEpochSecond() ) ;  

See this code run at Ideone.com . Note that all three are within the same second, give or take a second as the clock may rollover to the next second by the second or third call to now .

2023-01-24T14:27:06.416597Z | 1674570426

2023-01-24T17:27:06.478680+03:00 | 1674570426

2023-01-24T17:27:06.487289+03:00[Africa/Dar_es_Salaam] | 1674570426

LocalDateTime#toEpochSecond

Calling the toEpochSecond method on LocalDateTime is tricky conceptually. As a concept, LocalDateTime has no epoch reference point. It is just a date with a time. For example, “noon on January 23 of 2023” by itself has no specific meaning, cannot be nailed down to a point on the timeline. So it is meaningless to compare “noon on January 23 of 2023” to a reference point such as the first moment of 1970 as seen in UTC, 1970-01-01T00:00Z.

Practically, the LocalDateTime class is implemented as if it were in UTC. So internally, the class counts a number of whole seconds since 1970-01-01T00:00Z plus a count of nanoseconds for a fractional second. But you should not be thinking of this implementation detail when writing code for business logic . For business logic, calling LocalDateTime#toEpochSecond makes no sense.

Technically speaking

So the technical answer to your Question is that your delta of 3 hours (1_674_501_451L - 1_674_512_251L) came from the fact that your JVM's current default time zone at that moment used an offset of +03:00, three hours ahead of UTC. So your call to LocalDateTime.now captured a date and time three hours ahead of UTC. But then you asked for toEpochSecond which treats that very same date and time as if it were in UTC .

Logically speaking

Of course that date and time were not meant to represent a moment in UTC. So logically speaking, your code is nonsensical. You should not be comparing a count from epoch for a LocalDateTime (which is a non-moment) to other classes such as Instant (which is a moment).

In other words, you are comparing apples and oranges .

Serializing date-time data

So, if LocalDateTime#toEpochSecond is inappropriate for business logic, why does that class offer such a method?

That method is useful for serializing a LocalDateTime value for storage or data-exchange. Some other systems may present date-with-time values in this manner.

ISO 8601

However, using a count-from-reference is a poor way to communicate date-time values. The values are ambiguous, as their granularity is implicit, and their particular epoch reference point is also implicit. And, such values make validation and debugging difficult for human readers.

I strongly recommend using standard ISO 8601 text rather than a count when storing or exchanging date-time values outside Java. Regarding your example standard textual format would be:

2023-01-23T22:17:31


General advice: Avoid LocalDateTime class unless you are very clear on its appropriate use.

For business apps, we are generally tracking moments. For example, "when does this contract come into effect", "when was this employee hired", "when does the purchase of this house close". So LocalDateTime is not called for.

The two business scenarios for LocalDateTime are (a) making appointments where we want the moment to float if time zone rules change, such as clinic, salon, and studio bookings, and (b) where we mean several moments, each with the same wall-clock time but occurring at different points on the timeline, such as "Announce this corporate message at noon in each of our company factories in Delhi, Düsseldorf, and Detroit.". Search Stack Overflow to learn more on this, as I and others have posted multiple times already.


On Ideone.com , see some code I noodled around with in writing this Answer.

You need to use LocalTime#now(ZoneId zone) with ZoneOffset.UTC in order to get the local time at UTC; otherwise, the system picks up the local time of system's timezone.

Demo :

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

class Main {
    public static void main(String[] args) {
        Instant now = Instant.now();
        var now1 = TimeUnit.SECONDS.convert(now.toEpochMilli(), TimeUnit.MILLISECONDS) - 60;
        var now2 = TimeUnit.SECONDS.convert(now.minusSeconds(60).toEpochMilli(), TimeUnit.MILLISECONDS);
        var now3 = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(60).toEpochSecond(ZoneOffset.UTC);
        var now4 = System.currentTimeMillis() / 1000 - 60;
        System.out.println(now1);
        System.out.println(now2);
        System.out.println(now3);
        System.out.println(now4);
    }
}

Output from a sample run :

1674506413
1674506413
1674506413
1674506413

ONLINE DEMO

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM