Несоответствия часовых поясов при преобразовании XMLGregorianCalendar в LocalDateTime

Итак, у меня есть ответ XML Soap с полями даты / времени, которые представлены следующим образом:

<BusStopTime>
    <BusStopId>1023</BusStopId>
    <Order>1</Order>
    <PassingTime>1899-12-30T07:20:00</PassingTime>
</BusStopTime>

Меня не интересует дата (поскольку это какое-то устаревшее представление, которое я не могу контролировать), но время. Поле трансформируется в XMLGregorianCalendar с помощью инструментов WS, и я собираюсь сделать преобразование.

var date = DatatypeFactory.newInstance()
    .newXMLGregorianCalendar("1899-12-30T07:20:00")
    .toGregorianCalendar().toInstant()

Преобразование в LocalDateTime это просто Я устанавливаю TimeZone явно, чтобы избежать путаницы

LocalDateTime.ofInstant(date, ZoneId.of("Europe/Warsaw"))

что приводит к 1899-12-30T07:44

LocalDateTime.ofInstant(date, ZoneId.of("Europe/Berlin"))

дает мне другой вывод 1899-12-30T07:20

Когда даты начинаются в наши дни (после 1900 года и после) - все работает отлично. Итак, вопрос: что именно произошло между Берлином и Варшавой на рубеже XIX века? Или, если выразиться более четко - почему изменение во времени так странно?

Я бегу на обоих JDK8 и JDK11 (наблюдая за тем же поведением)

{ ~ }  » java -version                                                                                                                                              
openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment 18.9 (build 11.0.1+13)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode)

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

1 ответ

Решение

LocalDateTime.parse()

Если вы можете получить строку из XML без особых проблем, для предсказуемого результата используйте

    LocalDateTime.parse("1899-12-30T07:20:00")

Редактировать: без прямого доступа к строке, я полагаю, что решение состоит в том, чтобы установить смещение от GMT/UTC на вашем XMLGregorianCalendar чтобы избежать какой-либо зависимости от часового пояса JVM по умолчанию:

    XMLGregorianCalendar xgc = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar("1899-12-30T07:20:00");
    xgc.setTimezone(0);
    LocalTime time = xgc.toGregorianCalendar()
            .toZonedDateTime()
            .toLocalTime();
    System.out.println(time);

Так как так называемый "часовой пояс" XMLGregorianCalendar на самом деле ничего, кроме фиксированного смещения, не имеет значения, какое значение мы устанавливаем. Результат из этого фрагмента:

7:20

Я проверил с девятью различными часовыми поясами по умолчанию, включая Европу / Варшаву.

Поскольку вы сказали, что вас интересует только время суток, а не дата, я перевел на LocalTime, Если вы хотите LocalDateTime как в вашем вопросе, просто используйте toLocalDateTime вместо toLocalTime,

Кроме того, это было ваше простое решение из вашего комментария:

    LocalDateTime.parse(xmllGregoriaCalendar.toXMLFormat​())

toXMLFormat​() воссоздает строку из XML, что XMLGregorianCalendar объект был создан из (документы гарантируют, что вы получите ту же строку обратно). Таким образом, этот способ также избегает всех проблем с часовыми поясами.

Редактировать: разногласия между старым и новым классами

Мне кажется, что суть проблемы лежит в старом и устаревшем TimeZone класс и современный ZoneId класс не согласен с историческими смещениями из GMT/UTC.

Я сделал пару экспериментов. Давайте сначала попробуем часовой пояс, который, кажется, работает правильно, Берлин. Берлин был по смещению +01:00 с 1894 по 1915 год. Ява знает, что:

    LocalDate baseDate = LocalDate.of(1899, Month.DECEMBER, 30);

    ZoneId berlin = ZoneId.of("Europe/Berlin");
    TimeZone tzb = TimeZone.getTimeZone(berlin);
    GregorianCalendar gcb = new GregorianCalendar(tzb);
    gcb.set(1899, Calendar.DECEMBER, 30);
    ZonedDateTime zdtb = baseDate.atStartOfDay(berlin);
    System.out.println("" + berlin + ' ' + tzb.getOffset(gcb.getTimeInMillis())
            + ' ' + berlin.getRules().getOffset(zdtb.toInstant())
            + ' ' + berlin.getRules().getOffset(zdtb.toInstant()).getTotalSeconds());

Выход из этого фрагмента:

Европа / Берлин 3600000 +01:00 3600

Смещение за 30 декабря 1899 года правильно указано как +01:00. TimeZone класс говорит 3 600 000 миллисекунд, ZoneId говорит 3600 секунд, поэтому они согласны.

Беда с Варшавой. Варшава все время находилась в смещении по Гринвичу +01:24 до 1915 года. Посмотрим, сможет ли Java узнать:

    ZoneId warsaw = ZoneId.of("Europe/Warsaw");
    TimeZone tzw = TimeZone.getTimeZone(warsaw);
    GregorianCalendar gcw = new GregorianCalendar(tzw);
    gcw.set(1899, Calendar.DECEMBER, 30);
    ZonedDateTime zdtw = baseDate.atStartOfDay(warsaw);
    System.out.println("" + warsaw + ' ' + tzw.getOffset(gcw.getTimeInMillis())
            + ' ' + warsaw.getRules().getOffset(zdtw.toInstant())
            + ' ' + warsaw.getRules().getOffset(zdtw.toInstant()).getTotalSeconds());

Европа / Варшава 3600000 +01:24 5040

ZoneId правильно говорит +01:24 или 5040 секунд, но здесь TimeZone говорит 3 600 000 миллисекунд, так же, как в берлинском случае. Это неверно

Старый GregorianCalendar класс опирается на старое TimeZone класс и, следовательно, дает неправильные результаты при использовании часового пояса Европа / Варшава (либо явно, либо по умолчанию). В частности, вы ошибаетесь Instant от Calendar.toInstant(), И именно потому, что LocalDateTime.ofInstant использует современный ZoneId, ошибка переносится в ваш LocalDateTime,

Также из часовых поясов Европы / Дублина, Европы / Парижа, Европы / Москвы и Азии / Калькутты я получаю противоречивые результаты.

Я запустил свои фрагменты на Java 1.8.0_131, Java 9.0.4 и Java 11. Результаты были одинаковыми для всех версий.

связи

Другие вопросы по тегам