Почему вычитание этих двух раз (в 1927 году) дает странный результат?

Если я запустил следующую программу, которая анализирует две строки даты, ссылаясь на раз в 1 секунду и сравнивая их:

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    String str3 = "1927-12-31 23:54:07";  
    String str4 = "1927-12-31 23:54:08";  
    Date sDt3 = sf.parse(str3);  
    Date sDt4 = sf.parse(str4);  
    long ld3 = sDt3.getTime() /1000;  
    long ld4 = sDt4.getTime() /1000;
    System.out.println(ld4-ld3);
}

Вывод:

353

Почему ld4-ld3 not 1 (как я ожидал бы от односекундного разницы во времени), но 353?

Если я изменил даты на несколько секунд позже:

String str3 = "1927-12-31 23:54:08";  
String str4 = "1927-12-31 23:54:09";  

Тогда ld4-ld3 будет 1.


Версия Java:

java version "1.6.0_22"
Java(TM) SE Runtime Environment (build 1.6.0_22-b04)
Dynamic Code Evolution Client VM (build 0.2-b02-internal, 19.0-b04-internal, mixed mode)

Timezone(`TimeZone.getDefault()`):

sun.util.calendar.ZoneInfo[id="Asia/Shanghai",
offset=28800000,dstSavings=0,
useDaylight=false,
transitions=19,
lastRule=null]

Locale(Locale.getDefault()): zh_CN
6054
задан Freewind 27 июля '11 в 11:15
источник поделиться

8 ответов

Это изменение часового пояса 31 декабря в Шанхае.

Подробнее см. эту страницу для подробностей 1927 года в Шанхае. В основном в полночь в конце 1927 года часы возвращались на 5 минут и 52 секунды. Итак, "1927-12-31 23:54:08" на самом деле произошло дважды, и похоже, что Java разбирает его как более поздний возможный момент для этой локальной даты/времени - отсюда разница.

Еще один эпизод в часто странном и чудесном мире часовых поясов.

РЕДАКТИРОВАТЬ: Остановить нажмите! История изменений...

Оригинальный вопрос больше не будет демонстрировать совершенно то же поведение, если он будет перестроен с версией 2013a TZDB. В 2013 году результат составит 358 секунд с временем перехода 23:54:03 вместо 23:54:08.

Я только заметил это, потому что я собираю вопросы, подобные этому в Noda Time, в форме модульных тестов... Тест теперь он был изменен, но он просто показывает, что даже исторические данные не являются безопасными.

EDIT: История снова изменилась...

В TZDB 2014f время изменения переместилось на 1900-12-31, и теперь это всего лишь 343 секунды (так что время между t и t+1 составляет 344 секунды, если вы видите, что я среднее значение).

РЕДАКТИРОВАТЬ: Чтобы ответить на вопрос о переходе в 1900 году... похоже, что реализация Java-часового пояса рассматривает все часовые пояса как просто находящиеся в стандартном для любого момента времени до начала 1900 года UTC:

import java.util.TimeZone;

public class Test {
    public static void main(String[] args) throws Exception {
        long startOf1900Utc = -2208988800000L;
        for (String id : TimeZone.getAvailableIDs()) {
            TimeZone zone = TimeZone.getTimeZone(id);
            if (zone.getRawOffset() != zone.getOffset(startOf1900Utc - 1)) {
                System.out.println(id);
            }
        }
    }
}

Приведенный выше код не выводит на мой компьютер Windows. Таким образом, любой часовой пояс, который имеет любое смещение, отличное от его стандартного в начале 1900 года, будет считать это как переход. Сам TZDB имеет некоторые данные, возвращающиеся ранее, и не полагается на какую-либо идею "фиксированного" стандартного времени (что означает, что getRawOffset считается допустимым понятием), поэтому другим библиотекам не нужно вводить этот искусственный переход.

9756
ответ дан Jon Skeet 27 июля '11 в 11:31
источник поделиться

Вы столкнулись с локальным временным разрывом:

Когда местное стандартное время достигло воскресенья, 1. января 1928 года, 00:00:00 часы были повернуты назад 0:05:52 часов до субботы, 31. Декабрь 1927, 23:54:08 вместо обычного локального времени

Это не особенно странно и произошло повсюду в тот или иной момент, когда временные интервалы были изменены или изменены из-за политических или административных действий.

1468
ответ дан Michael Borgwardt 27 июля '11 в 11:38
источник поделиться

Мораль этой странности такова:

  • Использовать даты и время в формате UTC по мере возможности.
  • Если вы не можете отображать дату или время в UTC, всегда указывайте часовой пояс.
  • Если вы не можете вводить дату/время ввода в UTC, вам требуется явно указанная зона времени.
583
ответ дан Raedwald 28 июля '11 в 14:50
источник поделиться

При увеличении времени вы должны вернуться в UTC, а затем добавить или вычесть. Используйте локальное время только для отображения.

Таким образом, вы сможете проходить через любые периоды, когда часы или минуты происходят дважды.

Если вы конвертировали в UTC, добавьте каждую секунду и конвертируйте в локальное время для отображения. Вы пройдете через 11:54:08 вечера. LMT - 11:59:59 пм. LMT, а затем 11:54:08. CST - 11:59:59 с. CST.

320
ответ дан PatrickO 30 июля '11 в 19:55
источник поделиться

Вместо преобразования каждой даты вы используете следующий код

long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);

И посмотрите результат:

1
265
ответ дан Rajshri 16 мая '12 в 8:31
источник поделиться

Мне жаль это говорить, но разрыв времени немного переместился в

JDK 6 два года назад, и в JDK 7 недавно в обновление 25.

Урок для изучения: избегайте времени без UTC любой ценой, кроме, может быть, для отображения.

188
ответ дан user1050755 18 февр. '14 в 1:44
источник поделиться

Как объясняют другие, там есть разрыв времени. Для 1927-12-31 23:54:08 в Asia/Shanghai есть два возможных смещения временной шкалы, но только одно смещение для 1927-12-31 23:54:07. Таким образом, в зависимости от того, какое смещение используется, существует либо разница в одну секунду, либо разница в 5 минут и 53 секунды.

Это небольшое смещение смещений вместо обычной одночасовой дневной сбережения (летнее время), с которой мы привыкли, немного затушевывает проблему.

Обратите внимание, что обновление базы данных часовых поясов 2013 года переместило этот разрыв на несколько секунд раньше, но эффект все равно будет наблюдаемым.

Новый пакет java.time на Java 8 позволяет использовать это более четко и предоставлять инструменты для его обработки. Дано:

DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
dtfb.append(DateTimeFormatter.ISO_LOCAL_DATE);
dtfb.appendLiteral(' ');
dtfb.append(DateTimeFormatter.ISO_LOCAL_TIME);
DateTimeFormatter dtf = dtfb.toFormatter();
ZoneId shanghai = ZoneId.of("Asia/Shanghai");

String str3 = "1927-12-31 23:54:07";  
String str4 = "1927-12-31 23:54:08";  

ZonedDateTime zdt3 = LocalDateTime.parse(str3, dtf).atZone(shanghai);
ZonedDateTime zdt4 = LocalDateTime.parse(str4, dtf).atZone(shanghai);

Duration durationAtEarlierOffset = Duration.between(zdt3.withEarlierOffsetAtOverlap(), zdt4.withEarlierOffsetAtOverlap());

Duration durationAtLaterOffset = Duration.between(zdt3.withLaterOffsetAtOverlap(), zdt4.withLaterOffsetAtOverlap());

Тогда durationAtEarlierOffset будет на одну секунду, а durationAtLaterOffset будет пять минут и 53 секунды.

Кроме того, эти два смещения одинаковы:

// Both have offsets +08:05:52
ZoneOffset zo3Earlier = zdt3.withEarlierOffsetAtOverlap().getOffset();
ZoneOffset zo3Later = zdt3.withLaterOffsetAtOverlap().getOffset();

Но эти две разные:

// +08:05:52
ZoneOffset zo4Earlier = zdt4.withEarlierOffsetAtOverlap().getOffset();

// +08:00
ZoneOffset zo4Later = zdt4.withLaterOffsetAtOverlap().getOffset();

Вы можете увидеть ту же проблему, сравнивая 1927-12-31 23:59:59 с 1928-01-01 00:00:00, хотя в этом случае это более раннее смещение, которое приводит к более длинной дивергенции, и это более ранняя дата, которая имеет два возможных смещения.

Другой способ приблизиться к этому - проверить, происходит ли переход. Мы можем сделать это следующим образом:

// Null
ZoneOffsetTransition zot3 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

// An overlap transition
ZoneOffsetTransition zot4 = shanghai.getRules().getTransition(ld3.toLocalDateTime);

Вы можете проверить, является ли переход перекрытием - в этом случае имеется более одного допустимого смещения для этой даты/времени или пробела - в этом случае эта дата/время недействительна для этого идентификатора зоны - с помощью isOverlap() и isGap() на zot4.

Я надеюсь, что это поможет людям справиться с такой проблемой, как только Java 8 станет широко доступным, или тем, кто использует Java 7, которые используют backport JSR 310.

168
ответ дан Daniel C. Sobral 03 янв. '14 в 17:43
источник поделиться

ИМХО распространенная, неявная локализация в Java - это один из самых больших недостатков дизайна. Он может быть предназначен для пользовательских интерфейсов, но, честно говоря, кто действительно использует Java для пользовательских интерфейсов сегодня, за исключением некоторых IDE, где вы можете в основном игнорировать локализацию, потому что программисты не являются целевой аудиторией. Вы можете исправить это (особенно на серверах Linux):

  • экспорт LC_ALL = C TZ = UTC
  • установите системные часы на UTC
  • никогда не используйте локализованные реализации, если это абсолютно необходимо (т.е. только для отображения)

Являясь членами сообщества Java, я рекомендую:

  • сделать локализованные методы не стандартными, но требуют, чтобы пользователь явно запрашивал локализацию.
  • используйте UTF-8/UTC как значение FIXED по умолчанию, потому что это просто по умолчанию сегодня. Нет причин делать что-то еще, за исключением случаев, когда вы хотите создавать такие потоки.

Я имею в виду, давайте, не являются глобальными статическими переменными анти-OO-образцом? Ничто другое не является распространенным дефолтом, данным некоторыми элементарными переменными среды.......

137
ответ дан user1050755 26 нояб. '14 в 18:58
источник поделиться

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