Dlaczego odejmowanie tych dwóch razy (w 1927 r.) Daje dziwny wynik?

6824

Jeśli uruchomię następujący program, który analizuje dwa ciągi dat odnoszące się do odstępów co 1 sekundę i porównuje je:

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);
}

Dane wyjściowe to:

353

Dlaczego ld4-ld3nie jest 1(jak bym się spodziewał po jednosekundowej różnicy czasów), ale 353?

Jeśli zmienię daty na czasy 1 sekundę później:

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

Wtedy ld4-ld3będzie 1.


Wersja 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
źródło
23
Może to być problem z ustawieniami regionalnymi.
Thorbjørn Ravn Andersen
72
Prawdziwą odpowiedzią jest zawsze, zawsze używaj sekund od epoki do logowania, jak epoka Uniksa, z 64-bitową reprezentacją liczb całkowitych (podpisaną, jeśli chcesz zezwolić na znaczki przed epoką). Każdy system czasu rzeczywistego ma pewne nieliniowe, niemonotoniczne zachowanie, takie jak godziny przestępne lub czas letni.
Phil H
22
Lol. Naprawili go dla jdk6 w 2011 roku. Potem, dwa lata później, odkryli, że powinien zostać naprawiony również w jdk7 .... naprawiony od 7u25, oczywiście, nie znalazłem żadnej wskazówki w informacji o wydaniu. Czasami zastanawiam się, ile błędów Oracle naprawia i nikomu o tym nie mówi z powodów PR.
user1050755
8
Świetny film o tego typu rzeczach: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen
4
@PhilH Fajną rzeczą jest to, że wciąż będą sekundy przestępne. Więc nawet to nie działa.
12431234123412341234123

Odpowiedzi:

10870

To zmiana strefy czasowej 31 grudnia w Szanghaju.

Zobacz tę stronę, aby poznać szczegóły 1927 roku w Szanghaju. Zasadniczo o północy pod koniec 1927 r. Zegary cofnęły się o 5 minut i 52 sekund. Tak więc „1927-12-31 23:54:08” faktycznie wydarzyło się dwa razy i wygląda na to, że Java analizuje go jako późniejszy możliwy moment dla tej lokalnej daty / godziny - stąd różnica.

Kolejny odcinek w często dziwnym i cudownym świecie stref czasowych.

EDYCJA: Przestań naciskać! Historia się zmienia ...

Pierwotne pytanie nie wykazywałoby już takiego samego zachowania, gdyby zostało przebudowane z wersją TZDB 2013a . W 2013a wynik wynosiłby 358 sekund, a czas przejścia to 23:54:03 zamiast 23:54:08.

Zauważyłem to tylko dlatego, że zbieram takie pytania w Czasie Nody, w formie testów jednostkowych ... Test został teraz zmieniony, ale po prostu pokazuje - nawet dane historyczne nie są bezpieczne.

EDYCJA: Historia znów się zmieniła ...

W TZDB 2014f czas zmiany przesunął się do 1900-12-31, a teraz jest to zaledwie 343 sekundowa zmiana (więc czas pomiędzy ti t+1wynosi 344 sekundy, jeśli rozumiesz, co mam na myśli).

EDYCJA: Aby odpowiedzieć na pytanie związane z przejściem o 1900 ... wygląda na to, że implementacja strefy czasowej Java traktuje wszystkie strefy czasowe po prostu w swoim standardowym czasie dla dowolnej chwili przed rozpoczęciem 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);
            }
        }
    }
}

Powyższy kod nie generuje danych wyjściowych na moim komputerze z systemem Windows. Tak więc każda strefa czasowa, która ma dowolne przesunięcie inne niż standardowe na początku 1900 r., Będzie to traktować jako przejście. Sam TZDB ma pewne wcześniejsze dane i nie opiera się na żadnej idei „ustalonego” standardowego czasu (który getRawOffsetzakłada, że ​​jest to poprawna koncepcja), więc inne biblioteki nie muszą wprowadzać tego sztucznego przejścia.

Jon Skeet
źródło
25
@Jon: Z ciekawości dlaczego cofnęli swoje zegary o tak „dziwny” odstęp? Coś jak godzina wydawałoby się logiczne, ale dlaczego to było 5:52 min?
Johannes Rudolph
63
@Johannes: Uważam, że aby była bardziej globalną strefą czasową - wynikowe przesunięcie wynosi UTC + 8. Paryż zrobił to samo w 1911 r. Na przykład: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet
34
@Jon Czy wiesz, czy Java / .NET radzi sobie z wrześniem w 1752 roku? Zawsze lubiłem pokazywać ludziom cal 9 1752 na systemach uniksowych
Mr Moose
30
Więc dlaczego, do cholery, Szanghaj był na pierwszym miejscu 5 minut od wack?
Igby Largeman
25
@Charles: Wiele miejsc miało wówczas mniej konwencjonalnych odsunięć. W niektórych krajach każde z różnych miast miało własne przesunięcie, tak aby było możliwie jak najbliższe geograficznie.
Jon Skeet
1602

Wystąpiła nieciągłość czasu lokalnego :

Kiedy lokalny czas standardowy miał wkrótce nadejść w niedzielę, 1 stycznia 1928 r., 00:00:00 zegary zostały cofnięte o 0:05:52 godziny do soboty 31 grudnia. Zamiast tego 31 grudnia 1927 r., 23:54:08 czasu lokalnego

Nie jest to szczególnie dziwne i zdarzyło się prawie wszędzie w tym samym czasie, ponieważ strefy czasowe zostały zmienione lub zmienione z powodu działań politycznych lub administracyjnych.

Michael Borgwardt
źródło
661

Morał tej dziwności jest następujący:

  • W miarę możliwości używaj dat i godzin w UTC.
  • Jeśli nie możesz wyświetlić daty lub godziny w UTC, zawsze wskaż strefę czasową.
  • Jeśli nie można wymagać wprowadzenia daty / godziny w UTC, należy podać wyraźnie wskazaną strefę czasową.
Raedwald
źródło
75
Konwersja / przechowywanie w UTC naprawdę nie pomogłoby w opisanym problemie, ponieważ napotkałbyś nieciągłość w konwersji do UTC.
niepythonic
23
@Mark Mann: jeśli twój program wszędzie używa UTC wszędzie, konwertując do / z lokalnej strefy czasowej tylko w interfejsie użytkownika, nie przejmujesz się takimi przerwami.
Raedwald
66
@ Raedwald: Pewnie, że byś zrobił - Jaki jest czas UTC dla 1927-12-31 23:54:08? (Na razie ignorując, że UTC nawet nie istniał w 1927 r.). W pewnym momencie ta godzina i data nadejdą do twojego systemu i musisz zdecydować, co z tym zrobić. Poinformowanie użytkownika, że ​​musi wprowadzić czas w UTC, po prostu przenosi problem na użytkownika, ale go nie eliminuje.
Nick Bastin
72
Czuję się usprawiedliwiony ilością aktywności w tym wątku, pracując nad refaktoryzacją daty / godziny dużej aplikacji od prawie roku. Jeśli robisz coś takiego jak kalendarz, nie możesz „po prostu” przechowywać UTC, ponieważ definicje stref czasowych, w których może być renderowany, zmieniają się z czasem. Przechowujemy „czas zamiaru użytkownika” - czas lokalny użytkownika i jego strefę czasową - oraz UTC do wyszukiwania i sortowania, a po każdej aktualizacji bazy danych IANA przeliczamy wszystkie czasy UTC.
taiganaut
366

Przy zwiększaniu czasu powróć do UTC, a następnie dodaj lub odejmij. Użyj czasu lokalnego tylko do wyświetlenia.

W ten sposób będziesz mógł przejść przez wszystkie okresy, w których godziny lub minuty występują dwa razy.

Jeśli dokonałeś konwersji na UTC, dodaj każdą sekundę i przekonwertuj na czas lokalny do wyświetlenia. Przechodziłbyś przez 11:54:08 pm LMT - 11:59:59 pm LMT, a następnie 23:54:08 CST - 23:59:59 CST.

PatrickO
źródło
309

Zamiast konwertować każdą datę, możesz użyć następującego kodu:

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

A potem przekonaj się, że wynikiem jest:

1
Rajshri
źródło
72
Obawiam się, że tak nie jest. Możesz wypróbować mój kod w swoim systemie, wyświetli się on 1, ponieważ mamy różne lokalizacje.
Freewind
14
Jest to prawdą tylko dlatego, że nie określono ustawień narodowych w danych wejściowych analizatora składni. To zły styl kodowania i ogromna wada projektowa w Javie - jej nieodłączna lokalizacja. Osobiście umieszczam „TZ = UTC LC_ALL = C” wszędzie, gdzie używam Javy, aby tego uniknąć. Ponadto należy unikać każdej zlokalizowanej wersji implementacji, chyba że użytkownik bezpośrednio komunikuje się z użytkownikiem i wyraźnie tego chce. Nie wykonuj ŻADNYCH obliczeń, w tym lokalizacji, zawsze używaj stref czasowych Locale.ROOT i UTC, chyba że jest to absolutnie konieczne.
user1050755,
226

Przykro mi to mówić, ale nieciągłość czasu trochę się zmieniła

JDK 6 dwa lata temu, aw JDK 7 niedawno w aktualizacji 25 .

Lekcja do nauczenia się: za wszelką cenę unikaj czasów poza UTC, z wyjątkiem może wyświetlania.

użytkownik1050755
źródło
27
To jest niepoprawne. Nieciągłość nie jest błędem - po prostu nowsza wersja TZDB ma nieco inne dane. Na przykład na mojej maszynie z Javą 8, jeśli nieznacznie zmienisz kod, aby użyć „1927-12-31 23:54:02” i „1927-12-31 23:54:03”, nadal zobaczysz nieciągłość - ale teraz 358 sekund, zamiast 353. Nawet nowsze wersje TZDB mają jeszcze jedną różnicę - szczegóły w mojej odpowiedzi. Nie ma tu prawdziwego błędu, tylko decyzja projektowa dotycząca tego, jak niejednoznaczne wartości daty / godziny są analizowane.
Jon Skeet
6
Prawdziwy problem polega na tym, że programiści nie rozumieją, że konwersja czasu lokalnego i uniwersalnego (w dowolnym kierunku) nie jest i nie może być w 100% niezawodna. W przypadku starych znaczników czasu dane dotyczące czasu lokalnego są co najwyżej niepewne. W przypadku przyszłych znaczników czasu działania polityczne mogą zmienić czas uniwersalny danego czasu lokalnego. W przypadku bieżących i ostatnich znaczników czasu w przeszłości może wystąpić problem polegający na tym, że proces aktualizacji bazy danych tz i wprowadzania zmian może być wolniejszy niż harmonogram wdrażania przepisów.
płyn do płukania
200

Jak wyjaśniają inni, istnieje nieciągłość czasowa. Istnieją dwa możliwe przesunięcia strefy czasowej dla 1927-12-31 23:54:08o Asia/Shanghai, ale tylko jedno przesunięcie dla 1927-12-31 23:54:07. Tak więc, w zależności od zastosowanego przesunięcia, występuje różnica jednej sekundy lub różnica 5 minut i 53 sekund.

Ta niewielka zmiana przesunięć, zamiast zwykłej jednogodzinnej oszczędności światła dziennego (czasu letniego), do której jesteśmy przyzwyczajeni, nieco zaciemnia problem.

Należy zauważyć, że aktualizacja bazy danych strefy czasowej 2013a przesunęła tę nieciągłość kilka sekund wcześniej, ale efekt byłby nadal widoczny.

Nowy java.timepakiet w Javie 8 pozwala lepiej to zobaczyć i zapewnia narzędzia do obsługi. Dany:

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());

Następnie durationAtEarlierOffsetbędzie jedną sekundę, natomiast durationAtLaterOffsetbędzie pięć minut i 53 sekund.

Te dwa przesunięcia są takie same:

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

Ale te dwa są różne:

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

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

Możesz zobaczyć ten sam problem w porównaniu 1927-12-31 23:59:59z tym 1928-01-01 00:00:00, jednak w tym przypadku to wcześniejsze przesunięcie powoduje dłuższe rozbieżności i wcześniejsza data ma dwa możliwe przesunięcia.

Innym sposobem podejścia do tego jest sprawdzenie, czy zachodzi przejście. Możemy to zrobić w następujący sposób:

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

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

Możesz sprawdzić, czy przejście jest pokrywające się, gdy istnieje więcej niż jedno prawidłowe przesunięcie dla tej daty / godziny, czy przerwa, w której ta data / czas nie jest ważna dla tego identyfikatora strefy - przy użyciu metod isOverlap()i .isGap()zot4

Mam nadzieję, że pomoże to ludziom poradzić sobie z tego rodzaju problemami, gdy Java 8 stanie się powszechnie dostępna, lub osobom używającym Java 7, które przyjmą backport JSR 310.

Daniel C. Sobral
źródło
1
Cześć Daniel. Uruchomiłem twój fragment kodu, ale nie daje on oczekiwanych wyników. Podobnie jak durationAtEarlierOffset i durationAtLaterOffset oba mają tylko 1 sekundę, a także Zot3 i Zot4 są zerowe. Właśnie skopiowałem i uruchomiłem ten kod na moim komputerze. czy jest coś do zrobienia w tym miejscu? Daj mi znać, jeśli chcesz zobaczyć fragment kodu. Oto kod tutorialspoint.com/... czy możesz mi powiedzieć, co się tutaj dzieje.
vineeshchauhan
2
@vineeshchauhan Zależy to od wersji Javy, ponieważ zmieniło się to w tzdata, a różne wersje JDK zawierają różne wersje tzdata. Na moim własnym zainstalowanej Javy, czasy są 1900-12-31 23:54:16a 1900-12-31 23:54:17, ale to nie działa na stronie masz wspólnego, więc są one przy użyciu innej wersji Java niż I.
Daniel C. Sobral
167

IMHO wszechobecna, domyślna lokalizacja w Javie jest jej największą pojedynczą wadą projektową. Może być przeznaczony dla interfejsów użytkownika, ale szczerze mówiąc, kto tak naprawdę używa Java dla interfejsów użytkownika, z wyjątkiem niektórych IDE, w których można w zasadzie zignorować lokalizację, ponieważ programiści nie są dokładnie docelowymi odbiorcami. Możesz to naprawić (szczególnie na serwerach Linux):

  • eksport LC_ALL = C TZ = UTC
  • ustaw zegar systemowy na UTC
  • nigdy nie używaj zlokalizowanych implementacji, chyba że jest to absolutnie konieczne (tj. tylko do wyświetlania)

Do Java Community Process członków Polecam:

  • ustaw metody zlokalizowane na inne niż domyślne, ale wymagają, aby użytkownik wyraźnie zażądał lokalizacji.
  • zamiast tego użyj UTF-8 / UTC jako domyślnego NAPRAWIONEGO, ponieważ dzisiaj jest to po prostu domyślny. Nie ma powodu, aby robić coś innego, chyba że chcesz produkować takie wątki.

Chodzi mi o to, czy globalne zmienne statyczne nie są wzorcem anty-OO? Nic więcej nie jest wszechobecnymi wartościami domyślnymi podanymi przez niektóre podstawowe zmienne środowiskowe .......

użytkownik1050755
źródło
21

Jak powiedzieli inni, to zmiana czasu w 1927 r. W Szanghaju.

Kiedy był 23:54:07w Szanghaju, czas lokalny standardowy, ale po 5 minutach i 52 sekundach zmienił się na następny dzień o godzinie 00:00:00, a potem czas lokalny standardowy zmienił się z powrotem na 23:54:08. Dlatego różnica między tymi dwoma czasami wynosi 343 sekundy, a nie 1 sekundę, jak można się spodziewać.

Czas może również popsuć się w innych miejscach, takich jak USA. USA mają czas letni. Kiedy zaczyna się czas letni, czas przesuwa się o 1 godzinę do przodu. Ale po pewnym czasie kończy się czas letni i wraca o 1 godzinę wstecz do standardowej strefy czasowej. Czasami więc przy porównywaniu czasów w USA różnica wynosi około 3600sekund, a nie 1 sekundy.

Ale w tych dwóch zmianach czasu jest coś innego. Ten drugi zmienia się ciągle, a ten pierwszy był tylko zmianą. Nie zmieniło się ani nie zmieniło o tę samą kwotę.

Lepiej jest używać UTC, gdy czas się nie zmienia, chyba że jest to konieczne, aby użyć czasu innego niż UTC, jak na wyświetlaczu.

zixuan
źródło