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

6828

Если я запускаю следующую программу, которая анализирует две строки даты, ссылаясь на раз в 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нет 1(как и следовало ожидать от разницы во времени в одну секунду), но 353?

Если я изменю даты на время 1 секунду позже:

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
Freewind
источник
23
Это может быть языковой проблемой.
Торбьерн Равн Андерсен
72
Реальный ответ - всегда, всегда используйте секунды для регистрации, как эпоха Unix, с 64-битным целочисленным представлением (со знаком, если вы хотите разрешить штампы до эпохи). Любая система реального времени имеет нелинейное, немонотонное поведение, такое как високосные часы или летнее время.
Фил Х
22
Лол. Они исправили это для jdk6 в 2011 году. Затем, спустя два года, они обнаружили, что это должно быть исправлено и в jdk7 .... исправлено по состоянию на 7u25, конечно, я не нашел никаких подсказок в примечании к выпуску. Иногда я задаюсь вопросом, сколько ошибок Oracle исправляет и никому не говорит об этом по причинам пиара.
user1050755
8
Отличное видео о таких вещах: youtube.com/watch?v=-5wpm-gesOY
Торбьерн Равн Андерсен,
4
@PhilH Хорошо, что все еще будут високосные секунды. Так что даже это не работает.
12431234123412341234123

Ответы:

10876

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

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

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

РЕДАКТИРОВАТЬ: Стоп пресс! История меняется ...

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

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

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

В 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допустимым понятием), поэтому другие библиотеки не должны вводить этот искусственный переход.

Джон Скит
источник
25
@Jon: из любопытства, почему они поставили свои часы назад на такой «странный» интервал? Что-то вроде часа показалось бы логичным, но как получилось, что 5 минут 52 минуты?
Йоханнес Рудольф
63
@Johannes: Чтобы сделать его более нормальным в глобальном масштабе часовым поясом, я считаю - итоговое смещение будет UTC + 8. Париж сделал то же самое в 1911 году, например: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Джон Скит
34
@Jon Вы случайно не знаете, справится ли Java / .NET с сентябрем 1752 года? Я всегда любил показывать людям звони 9 1752 в системах Unix
Мистер Мус
30
Так почему, черт возьми, Шанхай был 5 минут безумным во-первых?
Игби Крупный человек
25
@Charles: тогда у многих мест были менее обычные смещения. В некоторых странах разные города имели свое собственное смещение, чтобы быть как можно ближе к географически правильному.
Джон Скит
1602

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

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

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

Майкл Боргвардт
источник
661

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

  • Используйте даты и время в UTC, где это возможно.
  • Если вы не можете отобразить дату или время в формате UTC, всегда указывайте часовой пояс.
  • Если вам не требуется вводить дату / время в формате UTC, укажите явно указанный часовой пояс.
Raedwald
источник
75
Преобразование / хранение в UTC действительно не поможет описанной проблеме, поскольку вы столкнетесь с разрывом в преобразовании в UTC.
пифы
23
@ Марк Манн: если ваша программа везде использует UTC, конвертируя в / из местного часового пояса только в пользовательском интерфейсе, вас не будут беспокоить такие разрывы.
Raedwald
66
@Raedwald: Конечно, вы бы ... Какое время UTC для 1927-12-31 23:54:08? (На данный момент игнорируя, что UTC даже не существовало в 1927 году). В какой-то момент это время и дата входят в вашу систему, и вы должны решить, что с ней делать. Сообщение пользователю о необходимости ввода времени в формате UTC просто переносит проблему на пользователя, но не устраняет ее.
Ник Бастин
72
Я чувствую себя оправданным из-за активности в этой теме, работая над рефакторингом даты и времени большого приложения уже почти год. Если вы делаете что-то вроде календаря, вы не можете «просто» хранить UTC, так как определения часовых поясов, в которых он может отображаться, со временем будут меняться. Мы сохраняем «время намерения пользователя» - местное время пользователя и его часовой пояс - и UTC для поиска и сортировки, и всякий раз, когда база данных IANA обновляется, мы пересчитываем все время UTC.
Тайганаут
366

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

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

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

PatrickO
источник
309

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

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

И тогда увидите, что результат:

1
Rajshri
источник
72
Боюсь, что это не так. Вы можете попробовать мой код в вашей системе, он выведет 1, потому что у нас разные локали.
Freewind
14
Это верно только потому, что вы не указали локаль во входных данных парсера. Это плохой стиль кодирования и огромный недостаток дизайна в Java - присущая ему локализация. Лично я ставлю "TZ = UTC LC_ALL = C" везде, где я использую Java, чтобы избежать этого. Кроме того, вам следует избегать каждой локализованной версии реализации, если только вы не взаимодействуете непосредственно с пользователем и явно не хотите этого. Не для ЛЮБЫХ расчетов, включая локализации, всегда используйте часовые пояса Locale.ROOT и UTC, если в этом нет крайней необходимости.
user1050755
226

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

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

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

user1050755
источник
27
Это неверно Прерывистость не является ошибкой, просто более свежая версия TZDB содержит немного другие данные. Например, на моей машине с Java 8, если вы слегка измените код для использования «1927-12-31 23:54:02» и «1927-12-31 23:54:03», вы все равно увидите разрыв - но теперь 358 секунд, а не 353. Даже в более поздних версиях TZDB есть еще одно отличие - подробности см. в моем ответе. Здесь нет никакой реальной ошибки, только дизайнерское решение о том, как анализируются неоднозначные текстовые значения даты / времени.
Джон Скит
6
Настоящая проблема заключается в том, что программисты не понимают, что преобразование между местным и универсальным временем (в любом направлении) не является и не может быть на 100% надежным. Для старых меток времени у нас есть данные о том, какое местное время было в лучшем случае шатким. Для будущих временных отметок политические действия могут изменить то, к чему относится универсальное время данного локального времени. Для текущих и недавних прошлых временных отметок у вас может быть проблема, что процесс обновления базы данных tz и развертывания изменений может быть медленнее, чем график выполнения законов.
plugwash
200

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

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

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

Новый 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, которые используют JSR 310 backport.

Даниэль С. Собрал
источник
1
Привет, Даниэль, я запустил твой код, но он не выдает ожидаемого результата. как durationAtEarlierOffset и durationAtLaterOffset имеют значение только 1 секунда, а также zot3 и zot4 оба равны нулю. Я установил только что скопированный и запускаю этот код на моей машине. Есть ли что-нибудь, что нужно сделать здесь. Дайте мне знать, если вы хотите увидеть кусок кода. Вот код tutorialspoint.com/… Вы можете сообщить мне, что здесь происходит.
vineeschchauhan
2
@vineeshchauhan Это зависит от версии Java, потому что это изменилось в tzdata, и разные версии JDK объединяют разные версии tzdata. На моей собственной установленной Java времена 1900-12-31 23:54:16и есть 1900-12-31 23:54:17, но это не работает на сайте, которым вы делитесь, поэтому они используют версию Java, отличную от
Даниэль С. Собрал,
167

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

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

К процессу Java Community членов Я рекомендую:

  • сделать локализованные методы не по умолчанию, но требовать от пользователя явного запроса локализации.
  • вместо этого используйте UTF-8 / UTC в качестве ИСПРАВЛЕННОГО значения по умолчанию, потому что сегодня это просто значение по умолчанию. Нет причин делать что-то еще, кроме как если вы хотите создавать такие потоки.

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

user1050755
источник
21

Как говорили другие, это изменение времени в 1927 году в Шанхае.

Когда это было 23:54:07в Шанхае, по местному стандартному времени, но через 5 минут и 52 секунды оно перешло на следующий день в 00:00:00, а затем местное стандартное время изменилось на 23:54:08. Вот почему разница между двумя значениями составляет 343 секунды, а не 1 секунда, как вы и ожидали.

Время также может испортить в других местах, таких как США. В США есть летнее время. Когда начинается летнее время, время идет вперед на 1 час. Но через некоторое время летнее время заканчивается, и оно возвращается назад на 1 час назад к стандартному часовому поясу. Так что иногда при сравнении времени в США разница составляет около 3600секунды, а не 1 секунды.

Но есть что-то другое в этих двух временных изменениях. Последнее постоянно меняется, а первое было просто изменением. Это не изменилось назад или изменилось снова на ту же сумму.

Лучше использовать UTC, где время не меняется, если только не нужно использовать время не UTC, как на дисплее.

Zixuan
источник