Java ZonedDateTime.toInstant() behavior

zoneddatetime to date
zoneddatetime utc
zoneddatetime change timezone
java instant
zoneddatetime vs instant
zoneddatetime without zoneid
zoneddatetime parse
instant to zoneddatetime

I'm running the below expressions on December 7th, 2018.

I'm seeing a discrepancy whereby this:

ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(30)

returns (correctly):

2018-11-07T22:44:11.242576-05:00[America/New_York]

whereas conversion to an instant:

ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(30).toInstant()

seems to mess up the result by adding an extra day to it:

2018-11-08T03:58:01.724728Z

I need an instant conversion to use its result in the following code as Date:

... = Date.from(t.toInstant()) 

An equivalent Python code (Django) works correctly:

datetime.datetime.now('America/New_York')+datetime.timedelta(days=-30)

evaluating to: datetime: 2018-11-07 20:13:55.063888-05:00

What's causing the discrepancy?

What should I use so that Java conversion to Date resulted in the November 7th being returned, just like in Python's case? Basically, I'm looking to an equivalent translation of that Python code into Java, or in pseudocode:

`datetime.X = datetime.now(deployment_zone) - (N_days)`,

where `deployment_zone` is configurable (i.e. `America/New_York`)

`N_days` is configurable (i.e. 30)

Update for @Basil Bourque:

When I formulated the original question, I (per SO rules) tried to simplify it to a digestible form which probably destroyed most of the necessary context making it vague. Let me try again.

As I explained in the comments, I'm converting the existing Python code (which is more actively maintained and which client wants to keep intact) to existing Java code (legacy that has not been properly maintained and strayed away from the Python's logic some time back). Both code bases need to be functionally on par with each other. Java needs to do what Python is already doing.

Python code is as follows (I'm lumping all into one place for succinctness, in reality it's distributed across a couple of files):

analytics.time_zone=America/New_York
TIME_ZONE = props.getProperty('analytics.time_zone', 'UTC')
TZ = pytz.timezone(TIME_ZONE)

    def days_back(num_days=0):
            adjusted_datetime = datetime.datetime.now(TZ)+datetime.timedelta(days=-num_days) 
            return DateRangeUtil.get_start_of_day(adjusted_datetime)

class DateRangeUtil():

    @staticmethod
    def get_start_of_day(date):
        return date.astimezone(TZ).replace(hour=0, minute=0, second=0, microsecond=0)

which basically takes the configured time zone, in which it obtains the current instant, subtracts a specified number of days from it, converts it to the beginning of that date and thus receives the lower bound of the range to use while querying the DB, something like Start time: datetime: 2018-11-07 20:13:55.063888-05:00

When I started on the Java side, it had:

    public final static DateRange parse(String dateRange) {
                //.....
                        int days = ...
                        return new DateRange(toBeginningOfDay(daysBack(days)), toEndOfDay(daysBack(0)));

    private final static Date daysBack(int days) {
                return toDate(LocalDateTime.now().minusDays(days));
            }

private final static Date toBeginningOfDay(Date d)
        {
            Calendar c=Calendar.getInstance();
            c.setTime(d);
            c.set(HOUR_OF_DAY,0);
            c.set(MINUTE,0);
            c.set(SECOND,0);
            c.set(MILLISECOND, 0);
            return c.getTime();
        }

        private final static Date toDate(LocalDateTime t) {
            return Date.from(t.atZone(ZoneId.systemDefault()).toInstant());
        }

That code didn't work and introduced the discrepancy which I describe in my original question. I started experimenting and introduced ZonedDateTime into the picture. While investigating, I found that it's the call to .toInstant() that seems to be a culprit and wanted to understand what's behind it in more depth.

In his answer, @ernest_k suggested a solution which seemed to have worked, but I still didn't quite understood which is clear from questions in the comments to his response.

The changes I made based on @ernest_k response are as follows:

private final static Date daysBack(int days) {

            return toDate(ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(days).toLocalDateTime());

private final static Date toDate(LocalDateTime t) {
            return Date.from(t.toInstant(ZoneOffset.UTC));
        }

This seems to produce the desired outcome: However conversion from local to zoned and then back again seemed too much, so I experimented a bit more and found that simply the LocalDateTime does the trick as well:

private final static Date toDate(LocalDateTime t) {
            return Date.from(t.toInstant(ZoneOffset.UTC));
        }

private final static Date daysBack(int days) {
            return toDate(LocalDateTime.now().minusDays(days));
        }

I can see that LocalDate (and perhaps LocalDateTime) has a convenient atStartOfDay() which seems to be a fitting candidate for elimination of Dates out of the picture while replacing the legacy toBeginningOfDay(Date d) method above. Not sure it's doing the same thing - I haven't yet experimented with that idea, so the suggestions are most welcome.

So, with all of the tribulations above, my question started around toInstant() behavior, and when it's passed a zone id, whether it converts TO an instant in that zone, or FROM it, or what?

I guess for the situation I'm describing we only care that the lower time bound in the DB query is formed by comparing some consistent marker of current time (its upper bound) to what it was in the same place (time zone?) in the past N days, so comparing it with UTC should server the purpose.

Does that then make passing the zone in unnecessary?

Now, that a solution seems to have been found, the question revolves around the soundness of the approach described above and the solution that's been stumbled upon - is it the most optimal one, best practices around Java timing libs, etc. The code needs to work for any time zone in which the code bases will end up being deployed, that's why the zone is passed in via configuration.

Also, I wonder if things change when/if the DB itself is deployed off-premise from the rest of the codebase and is configured to persist data in some other time zone. But that might be another question.

ZonedDateTime (Java Platform SE 8 ), Java Code Examples for java.time.ZonedDateTime.toInstant(). The following are Jave code examples for showing how to use toInstant() of the java  ZonedDateTime is an immutable representation of a date-time with a time-zone. This class stores all date and time fields, to a precision of nanoseconds, and a time-zone, with a zone offset used to handle ambiguous local date-times.

That "extra day" is not really an extra day. 2018-11-07T22:44:11 in New York is equivalent to 2018-11-08T03:58:01 in UTC (it's the same point in time). The difference is just 5 hours, not a day (and when I google this, I see New York is GMT-5).

ZonedDateTime#toInstant returns an Instant instance representing the same point in time (in UTC):

Converts this date-time to an Instant. This returns an Instant representing the same point on the time-line as this date-time. The calculation combines the local date-time and offset.

If you want to not use the offset when converting to instant, then you should perhaps use LocalDateTime:

ZonedDateTime.now(ZoneId.of("America/New_York"))
      .toLocalDateTime()
      .toInstant(ZoneOffset.UTC) 

This tells it to convert as though it were already UTC time (but a warning is appropriate here: this changes the date/time value)

Java Code Examples java.time.ZonedDateTime.toInstant, Java 8 - Convert Date to LocalDate and LocalDateTime. Date date = new Date(​); ZonedDateTime zonedDateTime = date.toInstant(). toInstant(); System.out.​println("instant : " + instant); //Zone : UTC+0 //2. Instant + system default time zone + toLocalDate() = LocalDate LocalDate localDate = instant. The new Java 8 java.time.Instant is the equivalent class to the classic java.util.Date The idea of the date conversion is to convert to an instant with a time zone. Date -> Instant + System default time zone = LocalDate Date -> Instant + System default time zone = LocalDateTime Date -> Instant + System default time zone = ZonedDateTime

First, avoid the need for an old-fashioned Date if you can. java.time, the modern Java date and time API, gives you all the functionality you need.

Sometimes we do need a Date for a legacy API that we cannot change or don’t want to upgrade just now. Java is giving you what I think you want. Demonstration:

    ZonedDateTime nov7 = ZonedDateTime.of(2018, 11, 7, 22, 44, 0, 0,
            ZoneId.of("America/New_York"));
    Instant inst = nov7.toInstant();
    System.out.println("As Instant: " + inst);
    Date oldFashionedDate = Date.from(inst);
    System.out.println("As Date: " + oldFashionedDate);

Output from this was:

As Instant: 2018-11-08T03:44:00Z
As Date: Wed Nov 07 22:44:00 EST 2018

Admitted, to get this output I had to change my JVM’s default time zone to America/New_York first.

Date and Instant are roughly equivalent but print differently. Meaning their toString methods behave differently, which may be confusing. Each is a point in time, none of them is a date (despite the name of one of them). It is never the same date in all time zones.

Date.toString picks up your JVM’s time zone setting and uses it for generating the string it returns. Instant.toString on the other hand always uses UTC for this purpose. This is why the same point in time is printed with different date and time. Fortunately they both also print a bit of time zone information so the difference is at least visible. Date prints EST, which, albeit ambiguous, in this case means Eastern Standard Time. Instant prints Z for offset zero from UTC or "Zulu time".

Java 8 - Convert Date to LocalDate and LocalDateTime, javaType. /**. * Simulate the value of now() while executing [block]. @@ -39,13 +​42,53 @@ inline fun <T ofInstant((value as ZonedDateTime).toInstant(), zoneId​) toInstant(), zoneId) it("Should reverse to default behavior after execution") {. val Boolean totalSolarEclipseThisWeek = new Interval(new Duration(java.util.concurrent.TimeUnit.DAYS.toMillis(7)),new DateTime((Sun_Eclipse_Total.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli)).contains(now) Probably not helpful for you, but your line could be written to use an Interval like this…

For issue #790, mock now(zoneId) for ZonedDateTime , ZonedDateTime holds a date with time and with a time-zone in the ISO-8601 Java 8 has added toInstant() method which helps to convert existing java.util. Note. This behavior has been a design flaw since JDK1.1, it creates a lot of confusion. Again, the java.util.Date doesn’t store any time zone info, but if you print it out, the system default

Still using java.util.Date? Don't!, JDK 8 has added convenient toInstant() method on java.util. from LocalDateTime to ZonedDateTime has the potential to introduce unexpected behavior. The following examples show how to use java.time.ZonedDateTime.These examples are extracted from open source projects. You can vote up the examples you like and your votes will be used in our system to produce more good examples.

How to convert Date to LocalDateTime in Java 8, Be able to work with computer time and human time using the Java time library. ZonedDateTime.of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) · java.time. toInstant() · java.time. The difference in how the two behave can be see when adding a single day when  Posts about java written by cvguntur. Published: 2019-07 (July 2019) Relevant for: JUnit 5.5.0 JUnit5 Blog Series. Part 1 – Introduction Part 2 – Test Basics

Comments
  • Do you just need November 7th, or do you also need the same time of day as now?
  • The logic is to subtract certain (configurable) amount of days from the current day. The zone id ('America/New_York') is also parameterized and configured, I simply hardcode those values here for simplicity. So, if run on Dec-8th, I'm expecting Nov-8th to be returned, on Dec 7th - Nov 7th will be returned, and so forth.
  • For example, if run on Fri, Dec-7, 8:14pm Python would return: datetime: 2018-11-07 20:13:55.063888-05:00 but Java wouldn't. I'm expecting similar result on the Java side as well.
  • For those of us who don’t know (enough) Python, maybe if you explain what you need the date for, we can better suggest.
  • I've updated the question with pseudocode of what I'm after. This is a refactoring of the existing codebase to bring it on par with Python, so I'm trying to do it with minimal amount of changes, hence removing their (heavy) usage of Date is undesirable (at least at this point).
  • Perhaps it's unclear: I'm subtracting the 30 (or N) days (from now) in order to form a date range to use in a DB query (in case no explicit start and end date to that query were provided), not because I'm just trying to change the date to a different value. Such is the application logic on the Python side of things from which I'm converting.
  • @SimeonLeyzerzon Your confusion has nothing to do with subtracting days. The problem is not understanding time zone handling, and conflating dates with days.
  • @SimeonLeyzerzon Your question is vague. Saying "30 days ago" can mean at least three different things: (a) 30 * 24 hours, (b) a range from 22:44 thirty calendar days ago in New York time zone to 22:44 now in New York time, (c) The entire day today as seen in New York and the entire days going back 30 days on the calendar as seen in New York. I covered all three in my Answer with example code. Take your pick.
  • That moment is used to form a lower bound in the DB query, so it tries to count the provided number of days back to get the same moment in the corresponding past day and then starts from the beginning of that found day. I put more context into my original query, hopefully the context is clearer now.
  • That suggestion seems to work, but I still don't understand why. What's the need to convert to LocalDateTime. What's the difference between that and the ZonedDateTime with a specified zone id?
  • @SimeonLeyzerzon ZonedDateTime.toInstant() takes the offset (of the relevant time zone) into account, but LocalDateTime.toInstant(ZoneOffset.UTC) doesn't (LocalDateTime doesn't have timezone or offset information, and specifying UTC forces it to treat the time as though it were already UTC time, so that it won't add/remove any offset value). But you need to be sure that this is what you want, as it effectively changes the "point in time" value.
  • Would these 2 expressions be equivalent: LocalDateTime.now().minusDays(days) vs. ZonedDateTime.now(ZoneId.of("America/New_York")).minusDays(days).toLocalDateTime()? What do you mean by saying: it effectively changes the "point in time" value?
  • I'm sure they will, but only if they run on a computer in New York. (so that LocalDateTime.now() picks up the desired values)
  • Just to clarify, I'm looking for a functionally equivalent translation of the Python expression above. On the Java side, as soon as I tack toInstance() to the (run date minus days offset) expression, I get back to the bumped up time value again (from the 7th to the 8th in my case), which was undesirable. I do have some code that then takes the Nov 7th date time and converts it to the beginning of that day. It's excluded for clarity. I wasn't sure what was causing that behavior and weather it had anything to do with time zones or not. Hope that makes sense.
  • I am sorry that I don’t know the Django/Python type. What I do know is that a Java Date cannot give you a date and time of day — only a point in time not tied to any time zone or UTC offset.