Почему вычитание этих двух раз (в 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
не 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
14 ответов
Это изменение часового пояса 31 декабря в Шанхае.
Смотрите эту страницу для деталей 1927 года в Шанхае. В основном в полночь в конце 1927 года часы вернулись на 5 минут и 52 секунды. Таким образом, "1927-12-31 23:54:08" фактически происходило дважды, и похоже, что Java анализирует его как более поздний возможный момент для этой локальной даты / времени - отсюда и разница.
Просто еще один эпизод в часто странном и чудесном мире часовых поясов.
РЕДАКТИРОВАТЬ: Стоп пресс! История меняется...
Исходный вопрос больше не будет демонстрировать совершенно такое же поведение, если его перестроить с версией TZDB2013a. В 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
предполагается, что это правильное понятие), поэтому другие библиотеки не должны вводить этот искусственный переход.
Вы столкнулись с разрывом местного времени:
Когда местное стандартное время приближалось к воскресенью, 1. января 1928 года, часы 00:00:00 были переведены назад на 0:05:52 часов, к субботе, 31. Декабрь 1927 года, 23:54:08 вместо этого по местному стандартному времени
Это не особенно странно и происходило практически везде когда-то, поскольку часовые пояса менялись или менялись из-за политических или административных действий.
Мораль этой странности такова:
- Используйте даты и время в UTC, где это возможно.
- Если вы не можете отобразить дату или время в формате UTC, всегда указывайте часовой пояс.
- Если вам не требуется вводить дату / время в формате UTC, укажите явно указанный часовой пояс.
При увеличении времени вы должны преобразовать обратно в UTC, а затем добавить или вычесть. Используйте местное время только для отображения.
Таким образом, вы сможете пройти любые периоды, когда часы или минуты встречаются дважды.
Если вы конвертировали в UTC, добавляйте каждую секунду и переводите в местное время для отображения. Вы должны пройти до 23:54:08 вечера LMT - 11:59:59 вечера LMT и затем 11:54:08 вечера CST - 11:59:59 вечера CST.
Вместо того, чтобы конвертировать каждую дату, вы используете следующий код
long difference = (sDt4.getTime() - sDt3.getTime()) / 1000;
System.out.println(difference);
И посмотрим на результат:
1
Мне жаль это говорить, но разрыв времени немного сдвинулся
JDK 6 два года назад, а в JDK 7 совсем недавно в обновлении 25.
Урок, который нужно выучить: избегать времени, отличного от UTC, любой ценой, за исключением, может быть, демонстрации.
Как объяснили другие, там есть временной разрыв. Есть два возможных смещения часового пояса для 1927-12-31 23:54:08
в 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.
ИМХО, повсеместная, неявная локализация в Java - это ее самый большой недостаток дизайна. Это может быть предназначено для пользовательских интерфейсов, но, честно говоря, кто действительно использует Java для пользовательских интерфейсов сегодня, за исключением некоторых IDE, где вы можете в основном игнорировать локализацию, потому что программисты не являются целевой аудиторией для нее. Вы можете исправить это (особенно на серверах Linux):
- экспорт LC_ALL=C TZ=UTC
- установите системные часы на UTC
- никогда не используйте локализованные реализации, за исключением случаев, когда это абсолютно необходимо (т.е. только для отображения)
Участникам процесса сообщества Java я рекомендую:
- сделать локализованные методы не по умолчанию, но требовать от пользователя явного запроса локализации.
- вместо этого используйте UTF-8/UTC в качестве ФИКСИРОВАННОГО значения по умолчанию, потому что это просто значение по умолчанию сегодня. Нет причин делать что-то еще, кроме как если вы хотите создавать такие потоки.
Я имею в виду, да ладно, разве глобальные статические переменные не являются шаблоном анти-OO? Ничто другое не является теми распространенными значениями по умолчанию, заданными некоторыми элементарными переменными среды.......
Это изменение времени в 1927 году в Шанхае, как говорили другие.
В основном это 23:54:07
в Шанхае по местному стандартному времени, через некоторое время он перешел на следующий день в 00:00:00
, Но потом он снова переключился на 23:54:08
по местному стандартному времени. Вот почему разница составляет 343 секунды, а не 1 секунду.
Это также может испортить другие места, такие как США. В США у них есть летнее время. Когда начинается летнее время, время идет вперед на 1 час. Но через некоторое время летнее время заканчивается, оно возвращается назад на 1 час назад к стандартному часовому поясу. Так что иногда при сравнении времени в США разница составляет около 3600
секунд не 1 секунда.
Лучше использовать UTC, где время не меняется, если только не нужно использовать время не UTC, как на дисплее.
Это происходит потому, что это правила часовых поясов на 31 декабря 1927 года в Шанхае. Если вы перейдете на эту страницу и выберете "Изменение часового пояса для 1900 - 1924", вы увидите, что в 1900 году дата и время равны "UTC +8:05:43 часа за весь период".
Итак, Java просто показывает время, настроенное для этого часового пояса в этом году.
если вы измените часовой пояс по умолчанию на Гонконг, он будет показывать правильные результаты.
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Hong_Kong"));
Обратите внимание, что часовой пояс изменился с CST (китайское стандартное время, "трехбуквенный эквивалент" Азии / Шанхая) на HKT (трехбуквенное название часового пояса Гонконга).
Но изменение часового пояса - не лучшее решение. Поэтому вместо этого по возможности используйте системное время UTC. После преобразования всегда будет указано местное время.
Чтобы избежать этой проблемы, при увеличении времени вы должны преобразовать обратно в UTC, а затем добавить или вычесть.
Таким образом, вы сможете пройти через любые периоды, когда часы или минуты встречаются дважды.
Если вы преобразовали в UTC, добавьте каждую секунду и конвертируйте в местное время для отображения. Вы должны пройти 23:54:08 LMT - 23:59:59 LMT, а затем 23:54:08 CST - 23:59:59 CST.
В результате не может быть «1», потому что getTime() возвращает длинные миллисекунды, а не секунды (из которых 353 миллисекунды являются справедливой точкой, но эпоха для Date начинается в 1970 году, а не в 1920-х годах).cmmnt: используемый вами раздел API в основном считается устаревшим. http://windsolarhybridaustralia.x10.mx/httpoutputtools-tomcat-java.html
Таким образом, вы сможете пройти через любые периоды, когда часы или минуты встречаются дважды.
Если вы преобразовали в UTC, добавьте каждую секунду и конвертируйте в местное время для отображения. Вы должны пройти через 23:54:08 LMT - 23:59:59 LMT, а затем 23:54:08 CST - 23:59:59 CST.
Дополнительная информация
Это из-за перехода на летнее время, как упоминали другие в своем ответе. Я хочу указать на другие факты о дате и календаре, чтобы показать вам:
Почему вы НИКОГДА не должны предполагать дату
В дополнение ко многим существующим календарям и правилам, таким как високосные годы и переход на летнее время, на протяжении всей истории в календаре происходило множество изменений, которые не подчинялись никакому порядку.
Например, в 1752 году календарь, используемый в Англии и ее колониях, на 11 дней не синхронизировался с григорианским календарем, который использовался в большинстве других частей Европы. ♂️
Поэтому они изменили его в несколько этапов:
- За 31 декабря 1750 г. последовало 1 января 1750 г. (по «старому стилю» декабрь был 10-м месяцем, а 11 января).
- За 24 марта 1750 г. последовало 25 марта 1751 г. (25 марта было первым днем «старого стиля» года)
- За 31 декабря 1751 г. последовало 1 января 1752 г. (переход с 25 марта на 1 января как первый день года)
- За 2 сентября 1752 г. последовало 14 сентября 1752 г. (сокращение на 11 дней для соответствия григорианскому календарю).
- Дополнительная информация здесь
Другим примером являются лунные календари, такие как дата Хиджры, которую можно менять каждый год в зависимости от фазы луны в каждом месяце.
Так что в некоторых календарях даже есть пропуски более 10 дней и некоторая несогласованность в некоторых календарях из года в год! При работе с календарями мы всегда должны обращать внимание на множество параметров (например, часовой пояс, локаль, летнее время, сам стандарт календаря и т. д.) и всегда стараться использовать проверенную и точную систему дат.
⚠️ Предпочтителен онлайн-календарь, поскольку некоторые календари, такие как иранский календарь хиджры, активно меняются без возможности прогнозирования.