How to format java.util.Date with DateTimeFormatter portable?

How to format java.util.Date with DateTimeFormatter portable?

I can't use

Date in = readMyDateFrom3rdPartySource();
LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());
ldt.format(dateTimeFormatter);

because I afraid that usage of ZoneId.systemDefault() can introduce some changes.

I need to format exactly that object I have.

UPDATE

Note: time is time. Not space. Timezone is very rough measure of longitude, i.e. space. I don't need it. Only time (and date).

UPDATE 2

I wrote the following program, proving, that Date DOES NOT only contain correct "instant":

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DataNature2 {

   public static void main(String[] args) throws ParseException {

      SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

      String dateTimeString = "1970-01-01 00:00:01";

      Date date = simpleDateFormat.parse(dateTimeString);

      System.out.println("1 second = " + date.getTime());

   }
}

The output is follows:

1 second = -10799000

While it should be

1 second = 1000

if Date was "Instant".

The number 10799000 is 3*60*60*1000-1000 - the timezone offset of my local time.

This means, that Date class is dual. It's millisecond part may be shifted relatively to hh mm ss part by timezone offset.

This means, that if any utility returns Date object in terms of it's parts (hh mm ss) then it implicitly converted to local time. And getTime() means DIFFERENT time simultaneously. I mean on different machines if this program run at the same time, getTime() will be the same, while time parts will be different.

So, the code example in the beginning is correct: it takes "instant" part of Date, and supplies system timezone part, which was implicitly used inside Date. I.e. it converts dual Date object into explicit LocalDateTime object with the same parts. And hence, formatting after that, is correct.

UPDATE 3

Event funnier:

Date date = new Date(70, 0, 1, 0, 0, 1);
assertEquals(1000, date.getTime());

this test fails.

UDPATE 4

New code. Dedicated to all believers.

public class DataNature3 {

   public static class TZ extends java.util.TimeZone {


      private int offsetMillis;

      public TZ(int offsetHours) {
         this.offsetMillis = offsetHours * 60 * 60 * 1000;
      }

      @Override
      public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {
         throw new UnsupportedOperationException();
      }

      @Override
      public void setRawOffset(int offsetMillis) {
         this.offsetMillis = offsetMillis;
      }

      @Override
      public int getRawOffset() {
         return offsetMillis;
      }

      @Override
      public boolean useDaylightTime() {
         return false;
      }

      @Override
      public boolean inDaylightTime(Date date) {
         return false;
      }
   }

   public static void main(String[] args) {

      Date date = new Date(0);

      for(int i=0; i<10; ++i) {

         TimeZone.setDefault(new TZ(i));

         if( i<5 ) {
            System.out.println("I am date, I am an instant, I am immutable, my hours property is " + date.getHours() + ", Amen!");
         }
         else {
            System.out.println("WTF!? My hours property is now " + date.getHours() + " and changing! But I AM AN INSTANT! I AM IMMUTABLE!");
         }

      }

      System.out.println("Oh, please, don't do that, this is deprecated!");

   }
}

Output:

I am date, I am an instant, I am immutable, my hours property is 0, Amen!
I am date, I am an instant, I am immutable, my hours property is 1, Amen!
I am date, I am an instant, I am immutable, my hours property is 2, Amen!
I am date, I am an instant, I am immutable, my hours property is 3, Amen!
I am date, I am an instant, I am immutable, my hours property is 4, Amen!
WTF!? My hours property is now 5 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 6 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 7 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 8 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
WTF!? My hours property is now 9 and changing! But I AM AN INSTANT! I AM IMMUTABLE!
Oh, please, don't do that, this is deprecated!
Jon Skeet
people
quotationmark

TL;DR: You're right to be concerned about the use of the system local time zone, but you should have been concerned earlier in the process, when you used the system local time zone to construct a Date in the first place.

If you just want the formatted string to have the same components that Date.getDate(), Date.getMonth(), Date.getYear() etc return then your original code is appropriate:

LocalDateTime ldt = LocalDateTime.ofInstant(in.toInstant(), ZoneId.systemDefault());

You say you're "afraid that usage of ZoneId.systemDefault() can introduce some changes" - but that's precisely what Date.getDate() etc use.

Date doesn't have any kind of "dual contract" that lets you view it as a time-zone-less representation. It is just an instant in time. Almost every single method that lets you construct or deconstruct it into components is clearly documented to use the system default time zone, just like your use of ZoneId.systemDefault(). (One notable exception is the UTC method.)

Implicitly using the system default time zone is not the same as Date being a valid time-zone-less representation, and it's easy to demonstrate why: it can lose data, very easily. Consider the time-zone-free date and time of "March 26th 2017, 1:30am". You may well want to be able to take a text representation of that, parse it, and then later reformat it. If you do that in the Europe/London time zone, you'll have problems, as demonstrated below:

import java.util.*;
import java.time.*;
import java.time.format.*;

public class Test {

    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/London"));
        Date date = new Date(2017 - 1900, 3 - 1, 26, 1, 30);

        Instant instant = date.toInstant();
        ZoneId zone = ZoneId.systemDefault();
        LocalDateTime ldt = LocalDateTime.ofInstant(instant, zone);
        System.out.println(ldt); // Use ISO-8601 by default
    }
}

The output is 2017-03-26T02:30. It's not that there's an off-by-one error in the code - if you change it to display 9:30am, that will work just fine.

The problem is that 2017-03-26T01:30 didn't exist in the Europe/London time zone due to DST - at 1am, the clock skipped forward to 2am.

So if you're happy with that sort of brokenness, then sure, use Date and the system local time zone. Otherwise, don't try to use Date for this purpose.

If you absolutely have to use Date in this broken way, using methods that have been deprecated for about 20 years because they're misleading, but you're able to change the system time zone, then change it to something that doesn't have - and never has had - DST. UTC is the obvious choice here. At that point, you can convert between a local date/time and Date without losing data. It's still a bad use of Date, which is just an instant in time like Instant, but at least you won't lose data.

Or you could make sure that whenever you construct a Date from a local date/time, you use UTC to do the conversion, of course, instead of the system local time zone... whether that's via the Date.UTC method, or by parsing text using a SimpleDateFormat that's in UTC, or whatever it is. Unfortunately you haven't told us anything about where your Date value is coming from to start with...

people

See more on this question at Stackoverflow