5

I have noticed strange behavior of date and time in java. I have the following code:

public class TestDateTime {
    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/Helsinki"));

        Calendar calendar = GregorianCalendar.getInstance();

        assert(calendar.getTimeZone().equals(TimeZone.getDefault()));
        //Set 1899-12-30T23:00:00
        calendar.set(1899,11,30,23,0,0);
        calendar.set(Calendar.MILLISECOND,0);

        long timeInMillis = calendar.getTimeInMillis();
        java.util.Date calendarDateTime = new java.util.Date(timeInMillis);

        LocalDateTime localDateTime = LocalDateTime.ofInstant(ofEpochMilli(timeInMillis), ZoneId.systemDefault());
        System.out.println("Time in millis: " + timeInMillis);
        System.out.println("Date: " + calendarDateTime.toString());
        System.out.println("Local DateTime: " + localDateTime.toString());
    }
}

The output is:

Time in millis: -2209086000000
Date: Sat Dec 30 23:00:00 EET 1899
Local DateTime: 1899-12-30T22:39:49

timeInMillis must contain the number of milliseconds passed from 1970-01-01T00:00:00Z. The instance of Date class stores number of milliseconds passed from 1970-01-01T00:00:00Z. Date.toString() method returns local date and time for the default timezone.

So the Date.toString() and LocalDateTime.toString() must return the same date and time, but we see the difference (more than 20 minutes).

Is this a bug of java, or I use date and time incorrectly in Java?

Art Spasky
  • 1,635
  • 2
  • 17
  • 30

4 Answers4

5

LocalDateTime is CORRECT. According to the TZ database, the GMT offset at that date was 1:39:49:

# Zone  NAME        GMTOFF  RULES   FORMAT  [UNTIL]
Zone    Europe/Helsinki 1:39:49 -   LMT 1878 May 31
            1:39:49 -   HMT 1921 May    # Helsinki Mean Time
            2:00    Finland EE%sT   1983
            2:00    EU  EE%sT

Historical timezones are incredibly complex, and prior to standardization offsets were inherited from settings based on things like mean solar noon. When going back that far just about any offset is possible, and the IANA TZ database is the master reference for historical data.

From what I can see in the database, the weird offset did not get standardized to 2:00:00 until 1921 when HMT was replaced with EE(S)T.

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • So does Calendar or Date.toString() contain incorrect behavior? – Art Spasky Mar 17 '17 at 19:38
  • 2
    No, `Date` is intended to hold UTC times only, and has long been recognized as an underpowered, obsolete interface that doesn't cope with time in a rational manner. This is the reason for the existence of libraries like JodaTime, and the addition of the `java.time` package. – Jim Garrison Mar 17 '17 at 19:51
  • The Oracle guys and also S. Colebourne (the primary author of `java.time`-API) have suggested to remove the LMT-information entirely, see my answer. So I conclude that we cannot say. The new API is correct while the old API is wrong. They just behave different (inconsistency). – Meno Hochschild Mar 18 '17 at 05:03
5

This is a weirdness caused by Finland time change, see Clock Changes in Helsinki, Finland (Helsingfors) in 1921:

May 1, 1921 - Time Zone Change (HMT → EET)

When local standard time was about to reach Sunday, May 1, 1921, 12:00:00 midnight clocks were turned forward 0:20:11 hours to Sunday, May 1, 1921, 12:20:11 am local standard time instead

Those 20 minutes 11 seconds seem to be what you're observing.

As Jim Garrison said in his answer, LocalDateTime is correctly handling that, while Calendar is not.

In reality, it seems that the old TimeZone is getting the offset wrong, while the new ZoneId is getting it right, as can be seen in the following test code:

public static void main(String[] args) {
    compare(1800, 1, 1,  0, 0, 0);
    compare(1899,12,31, 23,59,59);
    compare(1900, 1, 1,  0, 0, 0);
    compare(1900,12,30, 23, 0, 0);
    compare(1921, 4,30,  0, 0, 0);
    compare(1921, 5, 1,  0, 0, 0);
    compare(1921, 5, 2,  0, 0, 0);
}
private static void compare(int year, int month, int day, int hour, int minute, int second) {
    Calendar calendar = new GregorianCalendar();
    calendar.clear();
    calendar.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
    calendar.set(year, month-1, day, hour, minute, second);
    Date date = calendar.getTime();
    
    ZonedDateTime zdt = ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.of("Europe/Helsinki"));
    
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z XXX");
    sdf.setTimeZone(TimeZone.getTimeZone("Europe/Helsinki"));
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss z XXX");
    
    System.out.printf("%04d-%02d-%02d %02d:%02d:%02d   %s = %d   %s = %d   %d%n",
                      year, month, day, hour, minute, second,
                      sdf.format(date), date.getTime(),
                      dtf.format(zdt), zdt.toInstant().toEpochMilli(),
                      date.getTime() - zdt.toInstant().toEpochMilli());
}

Output

1800-01-01 00:00:00   1800-01-01 00:00:00 EET +02:00 = -5364669600000   1800-01-01 00:00:00 EET +01:39 = -5364668389000   -1211000
1899-12-31 23:59:59   1899-12-31 23:59:59 EET +02:00 = -2208996001000   1899-12-31 23:59:59 EET +01:39 = -2208994790000   -1211000
1900-01-01 00:00:00   1900-01-01 00:00:00 EET +02:00 = -2208996000000   1900-01-01 00:00:00 EET +01:39 = -2208994789000   -1211000
1900-12-30 23:00:00   1900-12-30 23:00:00 EET +01:39 = -2177548789000   1900-12-30 23:00:00 EET +01:39 = -2177548789000   0
1921-04-30 00:00:00   1921-04-30 00:00:00 EET +01:39 = -1536025189000   1921-04-30 00:00:00 EET +01:39 = -1536025189000   0
1921-05-01 00:00:00   1921-05-01 00:20:11 EET +02:00 = -1535938789000   1921-05-01 00:20:11 EET +02:00 = -1535938789000   0
1921-05-02 00:00:00   1921-05-02 00:00:00 EET +02:00 = -1535853600000   1921-05-02 00:00:00 EET +02:00 = -1535853600000   0
Community
  • 1
  • 1
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • But Calendar class is used by oracle jdbc driver to handle DATE type. So we cannot guarantee the correct conversion from java.sql.Date to java.time.LocalDateTime? – Art Spasky Mar 17 '17 at 19:49
  • If you extract the date from the `ResultSet` using [`getDate(String, Calendar)`](https://docs.oracle.com/javase/7/docs/api/java/sql/ResultSet.html#getDate(java.lang.String,%20java.util.Calendar)), passing in a calendar with UTC TZ, it should? – Mick Mnemonic Mar 17 '17 at 21:39
1

As others pointed out, the difference is because the LMT (local mean time) value is not being taken into account by the Date object. This has been discussed before here, with regard to Joda-Time - the precursor to Java 8's time package.

Additionally, the Joda-Time FAQ says the following:

Why is the offset for a time-zone different to the JDK?

There are two main reasons for this.

The first reason is that both the JDK and Joda-Time have time-zone data files. It is important to keep both up to date and in sync if you want to compare the offset between the two.

The second reason affects date-times before the modern time-zone system was introduced. The time-zone data is obtained from the time-zone database. The database contains information on "Local Mean Time" (LMT) which is the local time that would have been observed at the location following the Sun's movements.

Joda-Time uses the LMT information for all times prior to the first time-zone offset being chosen in a location. By contrast, the JDK ignores the LMT information. As such, the time-zone offset returned by the JDK and Joda-Time are different for date-times before the modern time-zone system.

The last part (which I bolded) is relavent to both Joda-Time and Java 8, even though Java 8 has one set of time zone data files (unlike Joda-Time).

Community
  • 1
  • 1
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • About the statement given in Joda-time ("the JDK ignores the LMT information"), this is not quite correct, see also my answer citating Xueming Shen from Oracle. – Meno Hochschild Mar 18 '17 at 05:00
1

To be more precise about API-inconsistency:

While the new java.time-API always uses the LMT-informations of TZDB, we have also to state that the old JDK-class java.util.TimeZone makes a cut in year 1900 with the consequence that LMT-informations are not taken into account before the year 1900, but after 1900, yes, it is still taken into account! Just make your experiments with an appropriate zone... (Asia/Kamchatka for example)

We cannot really say that either the LMT-strategy of java.time-API is correct or the traditional 1900-strategy. Keep also in mind that there is an open JDK-issue to abolish the LMT-strategy. Citation:

The current TimeZone code does not use LMT. Joda-Time does, as does JSR-310. This is wrong.

Recent discussion on the tzdb mailing list has indicated that the data is not properly maintained or reliably linked to the city of the zone ID. It is also relatively meaningless, being a notional value for a single city within a large region.

Removing LMT is a good thing.

And Xueming Shen from Oracle says as comment in this issue:

The current j.u.TimeZone implementation DOES use LMT. If the LMT is defined/used cross the 1900.1.1 j.u.TimeZone cutoff date (by the tzdb data). For example the offset for Asia/Kamchatka from 1900.1.1 to the 1922.11.10 will be the LMT 10.34.36. Yes, if the LMT end date is before 1900.1.1, the LMT will not be used by the j.u.TZ.

As additional historical note, the JDK-issue was originally suggested by the main author of java.time-API S. Colebourne, see also the ancestor on threeten-issue-tracker.

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126