Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bug in jdk8 date-conversion?

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
like image 600
Rop Avatar asked May 31 '16 16:05

Rop


1 Answers

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…

like image 158
Holger Avatar answered Sep 20 '22 13:09

Holger