Warum führt das Subtrahieren dieser beiden Male (1927) zu einem seltsamen Ergebnis?

6827

Wenn ich das folgende Programm ausführe, das zwei Datumszeichenfolgen analysiert, die auf Zeiten im Abstand von 1 Sekunde verweisen, und diese vergleicht:

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

Die Ausgabe ist:

353

Warum ld4-ld3nicht 1(wie ich es von dem Zeitunterschied von einer Sekunde erwarten würde), aber 353?

Wenn ich die Daten 1 Sekunde später auf mal ändere:

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

Dann ld4-ld3wird es sein 1.


Java-Version:

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
Freilauf
quelle
23
Dies könnte ein Problem mit dem Gebietsschema sein.
Thorbjørn Ravn Andersen
72
Die eigentliche Antwort lautet, immer, immer Sekunden seit einer Epoche für die Protokollierung zu verwenden, wie die Unix-Epoche, mit einer 64-Bit-Ganzzahldarstellung (signiert, wenn Sie Stempel vor der Epoche zulassen möchten). Jedes Echtzeitsystem weist ein nichtlineares, nicht monotones Verhalten wie Schaltstunden oder Sommerzeit auf.
Phil H
22
Lol. Sie haben es für jdk6 im Jahr 2011 behoben. Dann, zwei Jahre später, stellten sie fest, dass es auch in jdk7 behoben werden sollte. Ab 7u25 behoben, natürlich fand ich keinen Hinweis in der Versionshinweis. Manchmal frage ich mich, wie viele Fehler Oracle aus PR-Gründen behebt und niemandem davon erzählt.
user1050755
8
Ein tolles Video über solche Dinge: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen
4
@PhilH Das Schöne ist, es wird immer noch Schaltsekunden geben. Also auch das funktioniert nicht.
12431234123412341234123

Antworten:

10872

Es ist eine Zeitzonenänderung am 31. Dezember in Shanghai.

Siehe diese Seite für Details von 1927 in Shanghai. Grundsätzlich gingen die Uhren Ende 1927 um Mitternacht 5 Minuten und 52 Sekunden zurück. "1927-12-31 23:54:08" ist also tatsächlich zweimal passiert, und es sieht so aus, als würde Java es als den später möglichen Zeitpunkt für dieses lokale Datum / diese lokale Uhrzeit analysieren - daher der Unterschied.

Nur eine weitere Episode in der oft seltsamen und wundervollen Welt der Zeitzonen.

EDIT: Hör auf zu drücken! Geschichte ändert sich ...

Die ursprüngliche Frage würde nicht mehr das gleiche Verhalten zeigen, wenn sie mit der Version 2013a von TZDB neu erstellt würde . In 2013a wäre das Ergebnis 358 Sekunden mit einer Übergangszeit von 23:54:03 anstelle von 23:54:08.

Ich habe das nur bemerkt, weil ich in Noda Time Fragen wie diese in Form von Komponententests sammle ... Der Test wurde jetzt geändert, aber er zeigt nur, dass nicht einmal historische Daten sicher sind.

EDIT: Die Geschichte hat sich wieder geändert ...

In der TZDB 2014f hat sich die Zeit der Änderung auf 1900-12-31 verschoben und beträgt jetzt nur noch 343 Sekunden (also beträgt die Zeit zwischen tund t+1344 Sekunden, wenn Sie sehen, was ich meine).

BEARBEITEN: Um eine Frage zu einem Übergang um 1900 zu beantworten ... es sieht so aus, als würde die Java-Zeitzonenimplementierung alle Zeitzonen für jeden Moment vor Beginn von 1900 UTC als einfach in ihrer Standardzeit befindlich behandeln :

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

Der obige Code erzeugt keine Ausgabe auf meinem Windows-Computer. Jede Zeitzone, die zu Beginn des Jahres 1900 einen anderen Versatz als den Standardversatz aufweist, zählt dies als Übergang. TZDB selbst hat einige Daten, die früher zurückreichen, und stützt sich nicht auf eine Idee einer "festen" Standardzeit (was getRawOffsetals gültiges Konzept angenommen wird), sodass andere Bibliotheken diesen künstlichen Übergang nicht einführen müssen.

Jon Skeet
quelle
25
@ Jon: Warum haben sie aus Neugier ihre Uhren um so ein "seltsames" Intervall zurückgestellt? Etwas wie eine Stunde wäre logisch gewesen, aber warum war es 5: 52 Minuten?
Johannes Rudolph
63
@Johannes: Um es zu einer global normaleren Zeitzone zu machen, glaube ich - der resultierende Offset ist UTC + 8. Paris hat 1911 zum Beispiel das Gleiche getan: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet
34
@ Jon Weißt du zufällig, ob Java / .NET mit dem September 1752 zurechtkommt? Ich habe es immer geliebt, Menschen cal 9 1752 auf Unix-Systemen zu zeigen
Mr Moose
30
Warum zum Teufel war Shanghai überhaupt 5 Minuten aus dem Ruder gelaufen?
Igby Largeman
25
@ Charles: Viele Orte hatten damals weniger konventionelle Offsets. In einigen Ländern hatten verschiedene Städte jeweils ihren eigenen Versatz, um so nahe wie möglich an der geografischen Korrektheit zu sein.
Jon Skeet
1602

Sie haben eine lokale Zeitdiskontinuität festgestellt :

Als die lokale Standardzeit den Sonntag, den 1. Januar 1928, erreichen sollte, wurden die Uhren um 00:00:00 Uhr auf 0:05:52 Uhr bis Samstag, den 31. Dezember 1927, 23:54:08 Uhr der lokalen Standardzeit zurückgestellt

Dies ist nicht besonders seltsam und ist fast überall zu der einen oder anderen Zeit passiert, als Zeitzonen aufgrund politischer oder administrativer Maßnahmen gewechselt oder geändert wurden.

Michael Borgwardt
quelle
661

Die Moral dieser Fremdheit ist:

  • Verwenden Sie nach Möglichkeit Datum und Uhrzeit in UTC.
  • Wenn Sie in UTC kein Datum oder keine Uhrzeit anzeigen können, geben Sie immer die Zeitzone an.
  • Wenn Sie in UTC kein Datum und keine Uhrzeit eingeben können, benötigen Sie eine explizit angegebene Zeitzone.
Raedwald
quelle
75
Die Konvertierung / Speicherung in UTC würde bei dem beschriebenen Problem wirklich nicht helfen, da bei der Konvertierung in UTC eine Diskontinuität auftreten würde.
unpythonic
23
@ Mark Mann: Wenn Ihr Programm UTC intern überall verwendet und nur in der Benutzeroberfläche in eine lokale Zeitzone konvertiert, interessieren Sie sich nicht für solche Diskontinuitäten.
Raedwald
66
@ Raedwald: Sicher würden Sie - Was ist die UTC-Zeit für 1927-12-31 23:54:08? (Ignoriert im Moment, dass UTC 1927 noch nicht einmal existierte). An einem gewissen Punkt dieser Zeit und das Datum kommen in Ihr System, und Sie müssen entscheiden , was damit zu tun. Wenn Sie dem Benutzer mitteilen, dass er Zeit in UTC eingeben muss, wird das Problem nur auf den Benutzer übertragen, und es wird nicht behoben.
Nick Bastin
72
Ich fühle mich bestätigt über die Menge an Aktivitäten in diesem Thread, da ich seit fast einem Jahr an der Umgestaltung von Datum und Uhrzeit einer großen App arbeite. Wenn Sie beispielsweise Kalender erstellen, können Sie UTC nicht "einfach" speichern, da sich die Definitionen der Zeitzonen, in denen sie gerendert werden können, im Laufe der Zeit ändern. Wir speichern "User Intent Time" - die Ortszeit des Benutzers und seine Zeitzone - und UTC zum Suchen und Sortieren. Bei jeder Aktualisierung der IANA-Datenbank berechnen wir alle UTC-Zeiten neu.
Taiganaut
366

Wenn Sie die Zeit erhöhen, sollten Sie zurück in UTC konvertieren und dann addieren oder subtrahieren. Verwenden Sie die Ortszeit nur zur Anzeige.

Auf diese Weise können Sie alle Zeiträume durchlaufen, in denen Stunden oder Minuten zweimal vorkommen.

Wenn Sie in UTC konvertiert haben, fügen Sie jede Sekunde hinzu und konvertieren Sie zur Anzeige in die Ortszeit. Sie würden durch 23:54:08 Uhr LMT - 23:59:59 Uhr LMT und dann 23:54:08 Uhr CST - 23:59:59 Uhr CST gehen.

PatrickO
quelle
309

Anstatt jedes Datum zu konvertieren, können Sie den folgenden Code verwenden:

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

Und dann sehen Sie, dass das Ergebnis ist:

1
Rajshri
quelle
72
Ich fürchte, das ist nicht der Fall. Sie können meinen Code in Ihrem System ausprobieren, er wird ausgegeben 1, da wir unterschiedliche Gebietsschemas haben.
Freilauf
14
Dies gilt nur, weil Sie das Gebietsschema in der Parser-Eingabe nicht angegeben haben. Das ist ein schlechter Codierungsstil und ein großer Designfehler in Java - die inhärente Lokalisierung. Persönlich setze ich "TZ = UTC LC_ALL = C" überall dort ein, wo ich Java verwende, um dies zu vermeiden. Darüber hinaus sollten Sie jede lokalisierte Version einer Implementierung vermeiden, es sei denn, Sie interagieren direkt mit einem Benutzer und möchten dies ausdrücklich. Verwenden Sie für keine Berechnungen einschließlich Lokalisierungen immer die Zeitzonen Locale.ROOT und UTC, es sei denn, dies ist unbedingt erforderlich.
user1050755
226

Es tut mir leid zu sagen, aber die Zeitdiskontinuität hat sich etwas verschoben

JDK 6 vor zwei Jahren und in JDK 7 erst kürzlich in Update 25 .

Lektion zu lernen: Vermeiden Sie unbedingt Nicht-UTC-Zeiten, außer vielleicht für die Anzeige.

user1050755
quelle
27
Das ist falsch. Die Diskontinuität ist kein Fehler - es ist nur so, dass eine neuere Version von TZDB leicht unterschiedliche Daten enthält. Wenn Sie beispielsweise auf meinem Computer mit Java 8 den Code geringfügig ändern, um "1927-12-31 23:54:02" und "1927-12-31 23:54:03" zu verwenden, wird weiterhin a angezeigt Diskontinuität - aber jetzt 358 Sekunden statt 353. Noch neuere Versionen von TZDB haben noch einen weiteren Unterschied - siehe meine Antwort für Details. Hier gibt es keinen wirklichen Fehler, nur eine Entwurfsentscheidung darüber, wie mehrdeutige Datums- / Zeittextwerte analysiert werden.
Jon Skeet
6
Das eigentliche Problem ist, dass Programmierer nicht verstehen, dass die Konvertierung zwischen lokaler und universeller Zeit (in beide Richtungen) nicht 100% zuverlässig ist und nicht 100% zuverlässig sein kann. Für alte Zeitstempel sind die Daten, die wir über die Ortszeit haben, bestenfalls wackelig. Für zukünftige Zeitstempel können politische Aktionen ändern, auf welche Weltzeit eine bestimmte lokale Zeit abgebildet wird. Bei aktuellen und aktuellen Zeitstempeln in der Vergangenheit kann das Problem auftreten, dass der Prozess der Aktualisierung der tz-Datenbank und der Einführung der Änderungen langsamer sein kann als der Implementierungsplan der Gesetze.
Plugwash
200

Wie von anderen erklärt, gibt es dort eine zeitliche Diskontinuität. Es gibt zwei mögliche Zeitzonenversätze für 1927-12-31 23:54:08at Asia/Shanghai, aber nur einen Versatz für 1927-12-31 23:54:07. Je nachdem, welcher Offset verwendet wird, gibt es entweder einen Unterschied von einer Sekunde oder einen Unterschied von 5 Minuten und 53 Sekunden.

Diese leichte Verschiebung der Offsets anstelle der üblichen einstündigen Sommerzeit (Sommerzeit), die wir gewohnt sind, verschleiert das Problem ein wenig.

Beachten Sie, dass das 2013a-Update der Zeitzonendatenbank diese Diskontinuität einige Sekunden zuvor verschoben hat, der Effekt jedoch weiterhin sichtbar ist.

Mit dem neuen java.timePaket unter Java 8 können Sie dies klarer sehen und Tools bereitstellen, um damit umzugehen. Gegeben:

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

Dann durationAtEarlierOffsetist es eine Sekunde, während durationAtLaterOffsetes fünf Minuten und 53 Sekunden sind.

Auch diese beiden Offsets sind gleich:

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

Aber diese beiden sind unterschiedlich:

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

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

Sie können das gleiche Problem zu vergleichen sehen 1927-12-31 23:59:59mit 1928-01-01 00:00:00, in diesem Fall aber ist es der frühere Zeitpunkt ist versetzt , dass die längere Divergenz erzeugt, und es ist das frühere Datum , die zwei mögliche Offsets hat.

Eine andere Möglichkeit, dies zu erreichen, besteht darin, zu überprüfen, ob ein Übergang stattfindet. Wir können das so machen:

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

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

Sie können überprüfen, ob der Übergang eine Überlappung ist, bei der es mehr als einen gültigen Versatz für dieses Datum / diese Uhrzeit gibt, oder eine Lücke, bei der dieses Datum / diese Uhrzeit für diese Zonen-ID nicht gültig ist, indem Sie die Methoden isOverlap()und isGap()verwenden zot4.

Ich hoffe, dies hilft den Leuten, diese Art von Problem zu lösen, sobald Java 8 allgemein verfügbar ist, oder für diejenigen, die Java 7 verwenden und den JSR 310-Backport verwenden.

Daniel C. Sobral
quelle
1
Hallo Daniel, ich habe Ihren Code ausgeführt, aber er gibt nicht die erwartete Ausgabe aus. Wie DurationAtEarlierOffset und DurationAtLaterOffset sind beide nur 1 Sekunde und auch Zot3 und Zot4 sind beide Null. Ich habe gerade kopiert und diesen Code auf meinem Computer ausgeführt. Gibt es etwas, was hier getan werden muss? Lassen Sie mich wissen, ob Sie einen Code sehen möchten. Hier ist Code tutorialspoint.com/… können Sie mich wissen lassen, was hier los ist.
Vineeshchauhan
2
@vineeshchauhan Dies hängt von der Java-Version ab, da sich dies in tzdata geändert hat und verschiedene Versionen von JDK verschiedene Versionen von tzdata bündeln. Auf meinem selbst installierten Java sind die Zeiten 1900-12-31 23:54:16und 1900-12-31 23:54:17, aber das funktioniert auf der von Ihnen freigegebenen Site nicht, sodass sie eine andere Java-Version als I verwenden.
Daniel C. Sobral
167

Meiner Meinung nach ist die allgegenwärtige implizite Lokalisierung in Java der größte Konstruktionsfehler. Es mag für Benutzeroberflächen gedacht sein, aber ehrlich gesagt, wer verwendet Java heute wirklich für Benutzeroberflächen, mit Ausnahme einiger IDEs, bei denen Sie die Lokalisierung grundsätzlich ignorieren können, da Programmierer nicht genau die Zielgruppe dafür sind. Sie können das Problem beheben (insbesondere auf Linux-Servern), indem Sie:

  • export LC_ALL = C TZ = UTC
  • Stellen Sie Ihre Systemuhr auf UTC
  • Verwenden Sie niemals lokalisierte Implementierungen, es sei denn, dies ist unbedingt erforderlich (dh nur zur Anzeige).

Den Mitgliedern des Java Community Process empfehle ich:

  • Machen Sie lokalisierte Methoden nicht zum Standard, sondern fordern Sie den Benutzer auf, die Lokalisierung explizit anzufordern.
  • Verwenden Sie stattdessen UTF-8 / UTC als FIXED- Standard, da dies heute einfach der Standard ist. Es gibt keinen Grund, etwas anderes zu tun, außer wenn Sie solche Threads erstellen möchten.

Ich meine, komm schon, sind globale statische Variablen kein Anti-OO-Muster? Nichts anderes sind diese allgegenwärtigen Standardeinstellungen, die von einigen rudimentären Umgebungsvariablen vorgegeben werden .......

user1050755
quelle
21

Wie andere sagten, ist es eine Zeitumstellung im Jahr 1927 in Shanghai.

Wenn es 23:54:07in Shanghai war, der lokalen Standardzeit, aber nach 5 Minuten und 52 Sekunden, drehte es sich zum nächsten Tag um 00:00:00und dann änderte sich die lokale Standardzeit zurück zu 23:54:08. Deshalb beträgt der Unterschied zwischen den beiden Zeiten 343 Sekunden und nicht 1 Sekunde, wie Sie es erwartet hätten.

Die Zeit kann auch an anderen Orten wie den USA durcheinander geraten. Die USA haben Sommerzeit. Wenn die Sommerzeit beginnt, läuft die Zeit 1 Stunde vorwärts. Aber nach einer Weile endet die Sommerzeit und sie geht 1 Stunde zurück in die Standardzeitzone. Beim Vergleich der Zeiten in den USA beträgt der Unterschied manchmal etwa 3600Sekunden und nicht 1 Sekunde.

Diese beiden Zeitänderungen haben jedoch etwas anderes. Letzteres ändert sich ständig und Ersteres war nur eine Änderung. Es hat sich nicht um den gleichen Betrag zurück oder wieder geändert.

Es ist besser, UTC zu verwenden, wenn sich die Zeit nicht ändert, es sei denn, Sie benötigen eine Nicht-UTC-Zeit wie in der Anzeige.

Zixuan
quelle