Perché sottrarre queste due volte (nel 1927) sta dando uno strano risultato?


6828

Se eseguo il seguente programma, che analizza due stringhe di date che fanno riferimento a volte a 1 secondo di distanza e le confronta:

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

L'output è:

353

Perché ld4-ld3no 1(come mi aspetterei dalla differenza di un secondo nei tempi), ma 353?

Se cambio le date a volte 1 secondo dopo:

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

Allora ld4-ld3lo sarà 1.


Versione 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
Questo potrebbe essere un problema di localizzazione.
Thorbjørn Ravn Andersen,

72
La vera risposta è sempre, usare sempre i secondi da un'epoca per la registrazione, come l'epoca Unix, con rappresentazione a 64 bit interi (firmata, se si desidera consentire timbri prima dell'epoca). Qualsiasi sistema di tempo reale ha un comportamento non lineare, non monotono come ore bisestili o ora legale.
Phil H,

22
Lol. L'hanno riparato per jdk6 nel 2011. Poi, due anni dopo, hanno scoperto che avrebbe dovuto essere riparato anche in jdk7 .... risolto a partire da 7u25, ovviamente, non ho trovato alcun suggerimento nella nota di rilascio. A volte mi chiedo quanti bug corregga Oracle e non lo dice a nessuno per motivi di pubbliche relazioni.
user1050755

8
Un ottimo video su questo genere di cose: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen,

4
@PhilH La cosa bella è che ci saranno ancora secondi bisestili. Quindi anche quello non funziona.
12431234123412341234123

Risposte:


10876

È un cambio di fuso orario il 31 dicembre a Shanghai.

Vedi questa pagina per i dettagli del 1927 a Shanghai. Fondamentalmente a mezzanotte alla fine del 1927, gli orologi risalgono a 5 minuti e 52 secondi. Quindi "1927-12-31 23:54:08" è successo due volte, e sembra che Java lo stia analizzando come l' istante possibile successivo per quella data / ora locale - da qui la differenza.

Solo un altro episodio nel mondo spesso strano e meraviglioso dei fusi orari.

EDIT: Stop premere! La storia cambia ...

La domanda originale non dimostrerebbe più lo stesso comportamento, se ricostruita con la versione 2013a di TZDB . Nel 2013a, il risultato sarebbe di 358 secondi, con un tempo di transizione di 23:54:03 anziché 23:54:08.

L'ho notato solo perché sto raccogliendo domande come questa in Noda Time, sotto forma di test unitari ... Il test ora è stato modificato, ma va solo a mostrare - nemmeno i dati storici sono al sicuro.

EDIT: la storia è cambiata di nuovo ...

In TZDB 2014f, il tempo del cambiamento è passato a 1900-12-31, ed è ora un semplice cambio di 343 secondi (quindi il tempo tra te t+1è 344 secondi, se vedi cosa intendo).

EDIT: Per rispondere a una domanda su una transizione nel 1900 ... sembra che l'implementazione del fuso orario Java tratti tutti i fusi orari come se fossero semplicemente nel loro orario standard per qualsiasi istante prima dell'inizio del 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);
            }
        }
    }
}

Il codice sopra non produce output sul mio computer Windows. Quindi qualsiasi fuso orario che ha un offset diverso da quello standard all'inizio del 1900 lo considererà come una transizione. TZDB stesso ha alcuni dati che risalgono a prima, e non fa affidamento su alcuna idea di un tempo standard "fisso" (che è ciò che getRawOffsetassume essere un concetto valido), quindi altre librerie non devono introdurre questa transizione artificiale.


25
@Jon: Per curiosità, perché hanno riportato i loro orologi a un intervallo così "strano"? Qualcosa come un'ora sarebbe sembrato logico, ma come mai erano le 5: 52min?
Johannes Rudolph,

63
@Johannes: credo che per renderlo un fuso orario più globalmente normale, l' offset risultante sia UTC + 8. Parigi fece lo stesso tipo di cose nel 1911 per esempio: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet

34
@Jon Ti capita di sapere se Java / .NET affronta settembre del 1752? Mi piaceva sempre mostrare alle persone cal 9 1752 su sistemi unix
Mr Moose,

30
Quindi perché diamine è stato Shanghai a 5 minuti fuori di testa in primo luogo?
Igby Largeman,

25
@Charles: molti posti avevano allora offset meno convenzionali. In alcuni paesi, città diverse avevano ciascuna il proprio offset per essere il più vicino possibile geograficamente corretto.
Jon Skeet,

1602

Hai riscontrato una discontinuità nell'ora locale :

Quando l'ora locale locale stava per arrivare a domenica, 1. gennaio 1928, 00:00:00, l'orologio veniva spostato all'indietro dalle 0:05:52 alle ore sabato 31 dicembre 1927, 23:54:08 ora locale standard

Ciò non è particolarmente strano ed è accaduto praticamente ovunque in un momento o nell'altro quando i fusi orari sono stati cambiati o cambiati a causa di azioni politiche o amministrative.


661

La morale di questa stranezza è:

  • Usa le date e gli orari in UTC ove possibile.
  • Se non è possibile visualizzare una data o un'ora in UTC, indicare sempre il fuso orario.
  • Se non è possibile richiedere una data / ora di input in UTC, richiedere un fuso orario esplicitamente indicato.

75
La conversione / archiviazione in UTC non sarebbe davvero d'aiuto per il problema descritto in quanto si incontrerebbe la discontinuità nella conversione in UTC.
unpythonic il

23
@Mark Mann: se il tuo programma utilizza UTC internamente ovunque, convertendolo in / da un fuso orario locale solo nell'interfaccia utente, non ti interesseresti di tali discontinuità.
Raedwald,

66
@Raedwald: Certo che lo faresti - Qual è l'orario UTC per il 1927-12-31 23:54:08? (Ignorando, per il momento, che UTC non esisteva nemmeno nel 1927). Ad un certo punto questa data e ora stanno entrando nel tuo sistema e devi decidere cosa farne. Dire all'utente che devono inserire il tempo in UTC sposta semplicemente il problema all'utente, non lo elimina.
Nick Bastin,

72
Mi sento giustificato dalla quantità di attività su questo thread, lavorando sul refactoring di data / ora di una grande app da quasi un anno ormai. Se stai facendo qualcosa di simile al calendario, non puoi "semplicemente" memorizzare l'UTC, poiché le definizioni dei fusi orari in cui può essere visualizzato cambieranno nel tempo. Archiviamo "ora di intento dell'utente" - l'ora locale dell'utente e il suo fuso orario - e UTC per la ricerca e l'ordinamento e ogni volta che il database IANA viene aggiornato, ricalcoliamo tutti gli orari UTC.
Taiganaut,

366

Quando si incrementa il tempo, è necessario riconvertirlo in UTC e quindi aggiungere o sottrarre. Utilizzare l'ora locale solo per la visualizzazione.

In questo modo sarai in grado di percorrere tutti i periodi in cui ore o minuti si verificano due volte.

Se ti sei convertito in UTC, aggiungi ogni secondo e converti in ora locale per la visualizzazione. Passeresti alle 23:54:08 pm LMT - 23:59:59 pm LMT e poi 23:54:08 pm CST - 23:59:59 CST.


309

Invece di convertire ogni data, puoi usare il seguente codice:

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

E poi vedi che il risultato è:

1

72
Temo che non sia così. Puoi provare il mio codice nel tuo sistema, verrà emesso 1, perché abbiamo impostazioni locali diverse.
Freewind,

14
Questo è vero solo perché non hai specificato la locale nell'input del parser. Questo è un cattivo stile di programmazione e un enorme difetto di progettazione in Java: la sua localizzazione intrinseca. Personalmente, ho messo "TZ = UTC LC_ALL = C" ovunque uso Java per evitarlo. Inoltre, dovresti evitare ogni versione localizzata di un'implementazione a meno che tu non stia interagendo direttamente con un utente e lo desideri esplicitamente. Non eseguire QUALSIASI calcolo incluso la localizzazione, usa sempre i fusi orari Locale.ROOT e UTC a meno che non sia assolutamente necessario.
user1050755,

226

Mi dispiace dirlo, ma la discontinuità temporale si è spostata un po '

JDK 6 due anni fa e in JDK 7 solo recentemente nell'aggiornamento 25 .

Lezione da imparare: evitare orari non UTC a tutti i costi, tranne forse per la visualizzazione.


27
Questo non è corretto La discontinuità non è un bug - è solo che una versione più recente di TZDB ha dati leggermente diversi. Ad esempio, sulla mia macchina con Java 8, se modifichi leggermente il codice per utilizzare "1927-12-31 23:54:02" e "1927-12-31 23:54:03" vedrai comunque un discontinuità - ma ora di 358 secondi, anziché 353. Anche le versioni più recenti di TZDB hanno ancora un'altra differenza - vedi la mia risposta per i dettagli. Non c'è un vero bug qui, solo una decisione di progettazione su come vengono analizzati i valori di testo di data / ora ambigui.
Jon Skeet,

6
Il vero problema è che i programmatori non comprendono che la conversione tra l'ora locale e l'ora universale (in entrambe le direzioni) non è e non può essere affidabile al 100%. Per i vecchi timestamp, i dati che abbiamo sull'ora locale erano al massimo traballanti. Per i timestamp futuri, le azioni politiche possono cambiare a che ora universale viene mappata una data ora locale. Per i timestamp passati attuali e recenti, potresti avere il problema che il processo di aggiornamento del database tz e di implementazione delle modifiche può essere più lento rispetto al programma di implementazione delle leggi.
lavaggio:

200

Come spiegato da altri, c'è una discontinuità temporale lì. Esistono due possibili offset del fuso orario per 1927-12-31 23:54:08at Asia/Shanghai, ma solo un offset per 1927-12-31 23:54:07. Quindi, a seconda dell'offset utilizzato, c'è una differenza di un secondo o una differenza di 5 minuti e 53 secondi.

Questo leggero spostamento delle compensazioni, invece del solito ora legale di un'ora (ora legale) a cui siamo abituati, oscura un po 'il problema.

Si noti che l'aggiornamento 2013a del database del fuso orario ha spostato questa discontinuità pochi secondi prima, ma l'effetto sarebbe comunque osservabile.

Il nuovo java.timepacchetto su Java 8 consente di vedere questo in modo più chiaro e fornisce strumenti per gestirlo. Dato:

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

Quindi durationAtEarlierOffsetsarà un secondo, mentre durationAtLaterOffsetsaranno cinque minuti e 53 secondi.

Inoltre, questi due offset sono uguali:

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

Ma questi due sono diversi:

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

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

Si può vedere lo stesso problema a confronto 1927-12-31 23:59:59con 1928-01-01 00:00:00, però, in questo caso, è la prima di offset che produce la divergenza più a lungo, ed è la data in precedenza che ha due possibili compensazioni.

Un altro modo per affrontarlo è verificare se è in corso una transizione. Possiamo farlo in questo modo:

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

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

Puoi verificare se la transizione è una sovrapposizione in cui esiste più di un offset valido per quella data / ora o un intervallo in cui quella data / ora non è valida per quell'ID zona - utilizzando i metodi isOverlap()e .isGap()zot4

Spero che questo aiuti le persone a gestire questo tipo di problema una volta che Java 8 sarà diventato ampiamente disponibile, o per coloro che usano Java 7 che adottano il backport JSR 310.


1
Ciao Daniel, ho eseguito il tuo pezzo di codice ma non sta dando l'output come previsto. come durationAtEarlierOffset e durationAtLaterOffset sono entrambi solo 1 secondo e anche zot3 e zot4 sono entrambi nulli. Ho appena copiato ed eseguito questo codice sul mio computer. c'è qualcosa che deve essere fatto qui. Fammi sapere se vuoi vedere un pezzo di codice. Ecco il codice tutorialspoint.com/… puoi farmi sapere cosa sta succedendo qui.
Vineeshchauhan,

2
@vineeshchauhan Dipende dalla versione di Java, perché questa è cambiata in tzdata e diverse versioni di JDK raggruppano versioni diverse di tzdata. Sul mio Java installato, i tempi sono 1900-12-31 23:54:16e 1900-12-31 23:54:17, ma questo non funziona sul sito che hai condiviso, quindi usano una versione Java diversa da I.
Daniel C. Sobral,

167

IMHO la pervasiva, implicita localizzazione in Java è il suo unico più grande difetto di progettazione. Può essere inteso per interfacce utente, ma francamente, chi usa davvero Java per le interfacce utente oggi ad eccezione di alcuni IDE in cui puoi praticamente ignorare la localizzazione perché i programmatori non sono esattamente il pubblico di destinazione per questo. Puoi risolverlo (specialmente sui server Linux):

  • export LC_ALL = C TZ = UTC
  • imposta l'orologio di sistema su UTC
  • non utilizzare mai implementazioni localizzate se non in caso di assoluta necessità (ovvero solo per visualizzazione)

Ai membri di Java Community Process consiglio:

  • rendere i metodi localizzati non predefiniti, ma richiedere all'utente di richiedere esplicitamente la localizzazione.
  • usa invece UTF-8 / UTC come predefinito FISSO perché oggi è semplicemente quello predefinito. Non c'è motivo di fare qualcos'altro, tranne se si desidera produrre thread come questo.

Voglio dire, dai, le variabili statiche globali non sono un modello anti-OO? Nient'altro sono quelle pervasive impostazioni predefinite date da alcune variabili ambientali rudimentali .......


21

Come altri hanno detto, è un cambio di orario nel 1927 a Shanghai.

Quando era 23:54:07a Shanghai, l'ora standard locale, ma dopo 5 minuti e 52 secondi, passò al giorno successivo alle ore 00:00:00, e quindi l'ora solare locale tornò a 23:54:08. Quindi, ecco perché la differenza tra le due volte è di 343 secondi e non di 1 secondo, come ci si aspetterebbe.

Il tempo può anche incasinare in altri luoghi come gli Stati Uniti. Gli Stati Uniti hanno l'ora legale. Quando inizia l'ora legale, l'ora va avanti di 1 ora. Ma dopo un po 'l'ora legale termina e torna indietro di 1 ora al fuso orario standard. Quindi a volte quando si confrontano i tempi negli Stati Uniti la differenza è di circa 3600secondi e non di 1 secondo.

Ma c'è qualcosa di diverso in questi due cambiamenti temporali. Il secondo cambia continuamente e il primo era solo un cambiamento. Non è cambiato né è cambiato di nuovo per lo stesso importo.

È meglio usare UTC dove l'ora non cambia a meno che non sia necessario usare l'ora non UTC come nel display.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.