Чому віднімання цих двох разів (у 1927 р.) Дає дивний результат?


6824

Якщо я запускаю наступну програму, яка аналізує два рядки дати, що посилаються один на один раз, і порівнює їх:

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

23
Це може бути проблема локальної локалізації.
Thorbjørn Ravn Andersen

72
Справжня відповідь полягає в тому, що завжди, завжди використовуйте секунди з епохи для ведення журналу, як епоха Unix, із 64-бітовим цілим представленням (підписано, якщо ви хочете дозволити штампи перед епохою). Будь-яка система в реальному часі має деяку нелінійну, немонотонну поведінку, наприклад, високосні години або економія денного світла.
Філ Х

22
Лол. Вони виправили це за jdk6 у 2011 році. Потім, через два роки, вони виявили, що він має бути зафіксований у jdk7 теж .... виправлений станом на 7u25, я, звичайно, не знайшов жодної підказки в примітці до випуску. Іноді мені цікаво, скільки помилок виправляє Oracle і нікому не повідомляє про це з PR-причин.
користувач1050755

8
Чудове відео про подібні речі: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen

4
@PhilH Приємна річ, що ще будуть високосні секунди. Так що навіть це не працює.
12431234123412341234123

Відповіді:


10870

Це зміна часового поясу 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 секунди, якщо ви розумієте, що я маю на увазі).

EDIT: Щоб відповісти на запитання про перехід о 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 січня 1928 року, 00:00:00 годинники повернулися назад 0:05:52 годин до суботи, 31 грудня 1927, 23:54:08 місцевий стандартний час

Це не особливо дивно, і це трапляється майже скрізь у той чи інший час, коли часові пояси перемикалися чи змінювалися через політичні чи адміністративні дії.


661

Мораль цієї дивацтва:

  • Використовуйте дати та час у UTC, де це можливо.
  • Якщо ви не можете відобразити дату або час у UTC, завжди вказуйте часовий пояс.
  • Якщо ви не можете вимагати дати / часу введення в UTC, вимагайте чітко вказаного часового поясу.

75
Перетворення / зберігання в UTC насправді не допоможе описаній проблемі, оскільки ви зіткнетеся з розривом конверсії в UTC.
unpythonic

23
@Mark Mann: якщо ваша програма всюди використовує UTC внутрішньо, перетворюючись на / з локального часового поясу лише в інтерфейсі, ви б не піклувалися про такі розриви.
Raedwald

66
@Raedwald: Впевнений, що ти - Який час UTC за 1927-12-31 23:54:08? (На даний момент ігноруючи, що UTC навіть не існував у 1927 році). У якийсь момент цей час і дата надходять у вашу систему, і ви повинні вирішити, що з цим робити. Якщо сказати користувачеві, що їм доведеться вводити час у UTC, просто переміщується проблема до користувача, вона не усуває її.
Нік Бастін

72
Я відчуваю себе помстим за активність у цій темі, працюючи над рефакторингом великої програми за датою / часом вже майже рік. Якщо ви робите щось на зразок календаря, ви не можете "просто" зберігати UTC, оскільки визначення часових поясів, у яких воно може бути відображено, змінюватимуться з часом. Ми зберігаємо "час наміру користувача" - місцевий час користувача та його часовий пояс - і UTC для пошуку та сортування, і кожного разу, коли база даних IANA оновлюється, ми перераховуємо всі часи UTC.
taiganaut

366

Збільшуючи час, вам слід перетворити назад в UTC, а потім додати або відняти. Використовуйте місцевий час лише для відображення.

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

Якщо ви перейшли на UTC, додайте кожну секунду та перетворіть на місцевий час для відображення. Ви б пройшли через 23:54:08 вечора LMT - 23:59:59 вечора LMT, а потім 23:54:08 вечора CST - 23:59:59 вечора за літній час .


309

Замість перетворення кожної дати ви можете використовувати такий код:

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

А потім побачите, що результат такий:

1

72
Боюся, що це не так. Ви можете спробувати мій код у вашій системі, він виведе 1, тому що у нас різні локали.
Freewind

14
Це справедливо лише тому, що ви не вказали локал у вході аналізатора. Це поганий стиль кодування та величезна вада дизайну в Java - притаманна йому локалізація. Особисто я кладу "TZ = UTC LC_ALL = C" скрізь, де я використовую Java, щоб уникнути цього. Крім того, ви повинні уникати кожної локалізованої версії програми, якщо ви безпосередньо не взаємодієте з користувачем і не бажаєте цього прямо. Не приймайте жодних обчислень, включаючи локалізації, завжди використовуйте часові пояси Locale.ROOT та UTC, якщо це абсолютно не потрібно.
користувач1050755

226

Вибачте, але скасування часу трохи змінилося

JDK 6 два роки тому, а JDK 7 зовсім недавно в оновлення 25 .

Урок для засвоєння: уникайте часів, що не були UTC, будь-якою ціною, крім, можливо, для показу.


27
Це неправильно. Перерваність не є помилкою - це лише те, що в новітній версії TZDB є дещо інші дані. Наприклад, на моїй машині з Java 8, якщо ви дуже незначно зміните код, щоб використовувати "1927-12-31 23:54:02" та "1927-12-31 23:54:03", ви все одно побачите розрив - але зараз 358 секунд замість 353. Навіть новіші версії TZDB мають ще одну різницю - детальну інформацію див. у моїй відповіді. Тут немає справжньої помилки, лише дизайнерське рішення про те, як розбір неоднозначних значень тексту дати та часу.
Джон Скіт

6
Справжня проблема полягає в тому, що програмісти не розуміють, що перетворення між локальним і універсальним часом (в будь-якому напрямку) не є і не може бути на 100% надійним. Для старих часових позначок дані, які ми маємо про місцевий час, в кращому випадку хиткі. Для майбутніх часових позначок політичні дії можуть змінити, на який загальний час заданий локальний час. Для поточних та останніх часових позначок у вас може виникнути проблема, що процес оновлення бази даних tz та викорінення змін може бути повільнішим, ніж графік впровадження законів.
підключіть

200

Як пояснили інші, там спостерігається розрив часу. Є два можливих зміщення часових поясів для 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, який приймає репортер JSR 310.


1
Привіт, Даніелю, я запустив твій фрагмент коду, але він не дає результату, як очікувалося. наприклад, durationAtEarlierOffset і durationAtLaterOffset мають лише 1 секунду, а також zot3 і zot4 є недійсними. Я встановив тільки що скопіював і запустив цей код на своїй машині. чи є щось, що тут потрібно зробити. Повідомте мене, якщо ви хочете побачити фрагмент коду. Ось код tutorialspoint.com/… чи можете ви повідомити мені, що тут відбувається.
vinoeshchauhan

2
@vineeshchauhan Це залежить від версії Java, оскільки це змінилося в tzdata, і різні версії пакету JDK включають різні версії tzdata. На моїй встановленої Java, що часи 1900-12-31 23:54:16і 1900-12-31 23:54:17, але це не працює на сайті ви поділилися, тому вони використовують іншу версію Java , ніж I.
Daniel C. Зібрав

167

IMHO, широко розповсюджена неявна локалізація в Java, є її найбільшою вадою дизайну. Він може бути призначений для користувальницьких інтерфейсів, але відверто кажучи, хто сьогодні справді використовує Java для інтерфейсів користувача, за винятком деяких ІДЕ, де ви в основному можете ігнорувати локалізацію, оскільки програмісти не є для неї цільовою аудиторією. Ви можете виправити це (особливо на серверах Linux):

  • експорт LC_ALL = C TZ = UTC
  • встановіть ваш системний годинник на UTC
  • ніколи не використовуйте локалізовані реалізації, якщо це абсолютно не потрібно (тобто лише для відображення)

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

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

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


21

Як говорили інші, час зміни в 1927 році в Шанхаї.

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

Час може зіпсуватись і в інших місцях, таких як США. США мають літній час. Коли починається літній час, час продовжується на 1 годину. Але через деякий час літній час закінчується, і він повертається назад на 1 годину назад до стандартного часового поясу. Тому іноді при порівнянні разів у США різниця становить приблизно 3600секунди, а не 1 секунду.

Але в цих двох часових змінах є щось інше. Останній змінюється постійно, і перший був просто зміною. Він не змінився і не змінився знову на ту саму суму.

Краще використовувати UTC, коли час не змінюється, якщо не потрібно використовувати час, що не є UTC, як на дисплеї.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.