7

I was writing some testcode for java-8 conversion between java.util.Date and java.time.LocalDateTime, and discovered an anomaly seems to occur in the hour after the transition from normaltime-to-summertime, when the year is 2038 or higher.

I just wanted to know if this is a bug in jdk8, or if I am doing something wrong?

Note: I am on Windows-7, 64-bit jdk, so should not be affected by the 2038-unix bug, which would have a much worse effect.

Here my demo-code:

package conversiontest;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;

public class ConversionTest {

    public static void main(String[] args) {
        new ConversionTest().testDateConversion();
    }

    // Method under test:
    public java.util.Date toJavaUtilDate(LocalDateTime localDateTime) {
        return java.util.Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }

    // Test-code:
    public void testDateConversion() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        LocalDate localDate = LocalDate.of(2016, 1, 1);
        LocalTime localTime = LocalTime.of(3, 22, 22); // 03:22:22
        while (!localDate.toString().startsWith("2045-")) {
            LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
            java.util.Date date = toJavaUtilDate(localDateTime);
            String sLocalDateTime = localDateTime.toString().replace("T", " ");
            String sJavaUtilDate = sdf.format(date);
            if (!sLocalDateTime.equals(sJavaUtilDate)) {
                System.out.println(String.format("FAILURE: '%s' != '%s'", sLocalDateTime, sJavaUtilDate));
            }
            localDate = localDate.plusDays(1);
        }
    }

}

Output:

FAILURE: '2038-03-28 03:22:22' != '2038-03-28 02:22:22'
FAILURE: '2039-03-27 03:22:22' != '2039-03-27 02:22:22'
FAILURE: '2040-03-25 03:22:22' != '2040-03-25 02:22:22'
FAILURE: '2041-03-31 03:22:22' != '2041-03-31 02:22:22'
FAILURE: '2042-03-30 03:22:22' != '2042-03-30 02:22:22'
FAILURE: '2043-03-29 03:22:22' != '2043-03-29 02:22:22'
FAILURE: '2044-03-27 03:22:22' != '2044-03-27 02:22:22'

As you can see from the output, LocalDateTime(2038-03-28 03:22:22) gets converted to java.util.Date(2038-03-28 02:22:22), etc. But not when the year is lower than 2038.

Anyone has some input to this?

EDIT:

My ZoneId.systemDefault() gives: "Europe/Berlin"

C:\>java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b15, mixed mode)

C:\>javac -version
javac 1.8.0_91
Rop
  • 3,359
  • 3
  • 38
  • 59
  • 4
    Hint: Did you google `java.util.date 2038`. This yields eg http://stackoverflow.com/questions/15014589/java-util-date-bug and for the fun of reading also http://stackoverflow.com/questions/4313707/why-should-a-java-programmer-care-about-year-2038-bug – Dilettant May 31 '16 at 16:32
  • Java is based on Unix time, and year 2038 is the Unix version of Windows y2k bug – yonisha May 31 '16 at 16:35
  • Thanks for the links, but reading them carefully, this looks different. Java is **not** supposed to have the 2038-bug on a 64-bit Windows system, which I am using. Forgot to mention that, sorry. – Rop May 31 '16 at 16:39
  • Did you also make sure to compile for x64? – yonisha May 31 '16 at 16:43
  • Ummm... I have a 64-bit jdk installation. Doesnt it compile to x64 by default? – Rop May 31 '16 at 16:45
  • 3
    Whether Java classes use `int` or `long` to represent a particular value (e.g. (milli-)seconds since a particular point of time), is completely unrelated to the operating system or the actual CPU. Doing `long` calculations on an 8 bit CPU may require more CPU cycles, but that doesn’t affect the result that a valid JVM/JRE has to produce to meet the specification. – Holger May 31 '16 at 16:50
  • Isn't the output on the left LocalDateTime, and the output on the right Date? It looks like LocalDateTime is always 03:22:22, and it's Date that is converted. – DavidS May 31 '16 at 16:52
  • @DavisS -- yes, it is. But the conversion does not look correct, does it? – Rop May 31 '16 at 16:53
  • 2
    What are your system time zone and java version (with subversion)? At least we need to know these details for reproducibility. – Meno Hochschild May 31 '16 at 16:53
  • I can't explain the Date conversion, Rop, but it's an important distinction. Since it's happening to Date and not LocalDateTime, that means you should be able to reproduce this "bug" under earlier version of JDK than 8 (making it even less likely that it's an actual bug, IMO). – DavidS May 31 '16 at 16:56
  • @Meno -- I added the details at the end of my question. – Rop May 31 '16 at 16:59
  • @DavidS -- I believe the method **java.util.Date.from(Instant instant)** must be **new** in JDK-8, isnt it? That could be where the issue is....? – Rop May 31 '16 at 17:02
  • Good point, `Instant` was added in 8. – DavidS May 31 '16 at 17:03
  • 3
    I can reproduce your problem. The `java.time`-part is okay, but there seems to be a bug in `GregorianCalendar` for year 2038 or later. The old `Calendar`-based API seems to calculate that there is only one instead of two hours ahead-of-UTC-offset. You can see it if you print the `Date`-variable using its `toString()`-method (producing CET instead of CEST) or if you convert it to `GregorianCalendar` (what `SimpleDateFormat` internally does). My advise: Use `java.time.format.DateTimeFormatter` instead of `SimpleDateFormat`. – Meno Hochschild May 31 '16 at 17:29
  • 2
    @Meno Hochschild: there is a mismatch within one hour. At 2028-03-28, `java.util.Date` changes to summer time at `3:00CET`/`4:00CEST` whereas `java.time` does it already at `2:00CET`/`3:00CEST`. – Holger May 31 '16 at 18:20
  • @Meno -- thanks, nice job! :) --- If you post as an Answer, I will mark it. – Rop May 31 '16 at 18:23
  • 2
    `sun.util.calendar.ZoneInfo` has a [table of hardcoded timezone transitions](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/sun/util/calendar/ZoneInfo.java#140) which happens to cover the years until 2038, then, the implementation [switches to `SimpleTimeZone`](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/sun/util/calendar/ZoneInfo.java#284), which explains the behavioral change at that point of time. – Holger May 31 '16 at 18:41
  • I've just noticed this problem myself. What makes 2038 the magic year is that there are built-in transition tables for time zones with DST up until 2037. After that, a rules-based SimpleTimeZone takes over. Either the rules are specified incorrectly, or there's a bug in interpreting the rules that's off by one hour (probably a confusion between the wall-time hour before and after turning the clock forward). I'm seeing this with, for example, America/New_York and America/Chicago. The switch to DST should be 01:59:59 -> 03:00:00, wall time. This is what I get up until 2037. Starting in 2038, the – kshetline Jan 15 '17 at 05:53

1 Answers1

5

The different results stem from a mismatch in switching to summer time, which starts at dates in 2038. You can visualize the difference using the following code:

// for reproducible results
System.setProperty("user.timezone", "Europe/Berlin");

LocalDate[] dates = {LocalDate.of(2037, 3, 29), LocalDate.of(2038, 3, 28)};
LocalTime[] time  = { LocalTime.of(0, 59, 59), LocalTime.of(1, 00, 01),
                      LocalTime.of(1, 59, 59), LocalTime.of(2, 00, 01) };
for(LocalDate localDate : dates) {
    for(LocalTime localTime1 : time) {
        ZonedDateTime zoned = LocalDateTime.of(localDate, localTime1)
                             .atZone(ZoneId.of("UTC"))
                             .withZoneSameInstant(ZoneId.systemDefault());
        System.out.println(zoned);
        System.out.println(new java.util.Date(zoned.toEpochSecond()*1000));
    }
    System.out.println();
}

which will print:

2037-03-29T01:59:59+01:00[Europe/Berlin]
Sun Mar 29 01:59:59 CET 2037
2037-03-29T03:00:01+02:00[Europe/Berlin]
Sun Mar 29 03:00:01 CEST 2037
2037-03-29T03:59:59+02:00[Europe/Berlin]
Sun Mar 29 03:59:59 CEST 2037
2037-03-29T04:00:01+02:00[Europe/Berlin]
Sun Mar 29 04:00:01 CEST 2037

2038-03-28T01:59:59+01:00[Europe/Berlin]
Sun Mar 28 01:59:59 CET 2038
2038-03-28T03:00:01+02:00[Europe/Berlin]
Sun Mar 28 02:00:01 CET 2038
2038-03-28T03:59:59+02:00[Europe/Berlin]
Sun Mar 28 02:59:59 CET 2038
2038-03-28T04:00:01+02:00[Europe/Berlin]
Sun Mar 28 04:00:01 CEST 2038

As we can see, both implementations agree on the instant at which to switch to summer time in 2037, whereas the java.util.* implementation switches one hour later in 2038.

This behavioral change stems from a table of hardcoded transition times in sun.util.calendar.ZoneInfo which has a finite size. As we can see at this point, the code branches depending on the index return by the method getTransitionIndex. If the index is equal or higher than the table length, it falls over to using a SimpleTimeZone implementation.

We can verify that this happens:

long l1 = LocalDateTime.of(LocalDate.of(2037, 3, 29), LocalTime.of(1, 00, 01))
                       .atZone(ZoneId.of("UTC")).toInstant().getEpochSecond()*1000;
long l2 = LocalDateTime.of(LocalDate.of(2038, 3, 28), LocalTime.of(1, 00, 01))
                       .atZone(ZoneId.of("UTC")).toInstant().getEpochSecond()*1000;

TimeZone zone=TimeZone.getTimeZone("Europe/Berlin");
Field table=zone.getClass().getDeclaredField("transitions");
table.setAccessible(true);
System.out.println("table length="+((long[])table.get(zone)).length);

Method getTransitionIndex = zone.getClass()
    .getDeclaredMethod("getTransitionIndex", long.class, int.class);
getTransitionIndex.setAccessible(true);
final Integer UTC_TIME = 0;
int indexFor2037 = (Integer)getTransitionIndex.invoke(zone, l1, UTC_TIME);
System.out.println("index for 2037="+indexFor2037);
int indexFor2038 = (Integer)getTransitionIndex.invoke(zone, l2, UTC_TIME);
System.out.println("index for 2038="+indexFor2038);

prints on my system:

table length=143
index for 2037=141
index for 2038=143

I don’t know of any plans to change the summer time switching in 2038, so I suppose the java.time implementation to be correct. It’s also obvious that any implementation based on a finite table of hardcoded values has a natural limitation…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 3
    About your last sentence, the JSR-310 also knows a "finite table of hardcoded values" AND a set of rules like the old API represented by [ZoneRules.getTransitions()](http://docs.oracle.com/javase/8/docs/api/java/time/zone/ZoneRules.html#getTransitions--) or `getTransitionRules()` This fact only is not a limitation. But I strongly suspect that the `Calendar`-based API has become wrong around the switch from table values to rules in 2038 when replacing the old zi-format by tzdb.dat. (2038 can be explained by the fact that the original ZoneInfo-format (zi-files) was limited to int-range.) – Meno Hochschild Jun 01 '16 at 06:55
  • 3
    @Meno Hochschild: I wasn’t using the term “limitation” as “must become wrong at a certain point”. It implies a behavioral switch and has a higher maintenance effort. As the documentation of `getTransitions()` says, they are usually historical, which makes sense, as history tends to be full of changes and exceptions whereas for the future, we have only one current rule (so it’s unclear why `SimpleTimeZone` does it wrong, perhaps, it’s untested as the table reaches to 2037). Of course, once when we look back from the year 2039, we may have lots of historical data, changes and exceptions as well… – Holger Jun 01 '16 at 07:46