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

6283

Если я запускаю следующую программу, которая анализирует две строки даты, ссылаясь на раз в 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
  • 18
    Это может быть языковой проблемой.
  • 1346
    Вы действительно наткнулись на эту конкретную ситуацию в реальном сценарии, или этот вопрос был просто загадкой - просто для удовольствия?
Показать ещё 8 комментариев
Теги:
date
timezone

8 ответов

10650
Лучший ответ

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

  • 646
    @ Гарет: Нет, но проверка деталей переходов часового пояса Шанхая в тот период была моим первым портом захода. И я недавно работал над переходами часовых поясов для Noda Time, так что возможность двусмысленности в любом случае находится на переднем крае моих мыслей ...
  • 23
    @Jon: из любопытства, почему они поставили свои часы назад на такой «странный» интервал? Что-то вроде часа показалось бы логичным, но почему это было 5: 52 минут?
Показать ещё 32 комментария
1441

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

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

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

  • 56
    @Jason: Для чтения перед сном я бы предложил (сейчас) базу данных часовых поясов IANA (которая, как мне кажется, ранее управляла прекрасный парень по имени Олсон), был бы отличным ресурсом: iana.org/time-zones . Насколько я знаю, большинство мира с открытым исходным кодом (так упоминаемые библиотеки) используют это в качестве основного источника данных о часовых поясах.
577

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

  • Использовать даты и время в формате UTC по мере возможности.
  • Если вы не можете отображать дату или время в UTC, всегда указывайте часовой пояс.
  • Если вы не можете вводить дату/время ввода в UTC, вам требуется явно указанная зона времени.
  • 70
    Преобразование / хранение в UTC действительно не помогло бы описанной проблеме, так как вы столкнулись бы с разрывом в преобразовании в UTC.
  • 18
    @ Марк Манн: если ваша программа везде использует UTC, конвертируя в / из локального часового пояса только в пользовательском интерфейсе, вас это не беспокоит .
Показать ещё 2 комментария
320

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

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

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

269

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

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

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

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

Прошу прощения, но разрыв времени немного сдвинулся

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

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

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

Как объясняют другие, там есть разрыв времени. Для 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.

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

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

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

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню