¿Por qué restar estas dos veces (en 1927) está dando un resultado extraño?

6828

Si ejecuto el siguiente programa, que analiza dos cadenas de fecha que hacen referencia a 1 segundo de diferencia y las compara:

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

El resultado es:

353

¿Por qué ld4-ld3no lo es 1(como esperaría de la diferencia de un segundo en los tiempos), pero 353?

Si cambio las fechas a veces 1 segundo después:

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

Entonces ld4-ld3será 1.


Versión de 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
Viento libre
fuente
23
Esto podría ser un problema local.
Thorbjørn Ravn Andersen
72
La verdadera respuesta es siempre, siempre usar segundos desde una época para el registro, como la época de Unix, con una representación de enteros de 64 bits (con signo, si desea permitir sellos antes de la época). Cualquier sistema de tiempo real tiene un comportamiento no lineal y no monótono, como las horas bisiestos o el horario de verano.
Phil H
22
Jajaja Lo arreglaron para jdk6 en 2011. Luego, dos años después de eso, descubrieron que también debería arreglarse en jdk7 ... arreglado a partir de 7u25, por supuesto, no encontré ninguna pista en la nota de lanzamiento. A veces me pregunto cuántos errores corrige Oracle y no se lo cuenta a nadie por razones de relaciones públicas.
user1050755
8
Un gran video sobre este tipo de cosas: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen
44
@PhilH Lo bueno es que todavía habrá segundos bisiestos. Así que incluso eso no funciona.
12431234123412341234123

Respuestas:

10876

Es un cambio de zona horaria el 31 de diciembre en Shanghai.

Vea esta página para detalles de 1927 en Shanghai. Básicamente, a medianoche a fines de 1927, los relojes retrocedieron 5 minutos y 52 segundos. Entonces "1927-12-31 23:54:08" en realidad sucedió dos veces, y parece que Java lo analiza como el último instante posible para esa fecha / hora local, de ahí la diferencia.

Solo otro episodio en el mundo a menudo extraño y maravilloso de las zonas horarias.

EDITAR: ¡ Deja de presionar! La historia cambia ...

La pregunta original ya no demostraría el mismo comportamiento, si se reconstruye con la versión 2013a de TZDB . En 2013a, el resultado sería 358 segundos, con un tiempo de transición de 23:54:03 en lugar de 23:54:08.

Solo me di cuenta de esto porque estoy recopilando preguntas como esta en Noda Time, en forma de pruebas unitarias ... La prueba ahora se ha cambiado, pero solo muestra que ni siquiera los datos históricos son seguros.

EDITAR: la historia ha cambiado de nuevo ...

En TZDB 2014f, el tiempo del cambio se ha movido a 1900-12-31, y ahora es solo un cambio de 343 segundos (por lo que el tiempo entre ty t+1es 344 segundos, si entiendes lo que quiero decir).

EDITAR: Para responder una pregunta sobre una transición en 1900 ... parece que la implementación de la zona horaria de Java trata todas las zonas horarias como si estuvieran en su hora estándar durante cualquier instante antes del comienzo de las 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);
            }
        }
    }
}

El código anterior no produce resultados en mi máquina Windows. Por lo tanto, cualquier zona horaria que tenga un desplazamiento diferente al estándar a principios de 1900 contará eso como una transición. TZDB en sí tiene algunos datos que se remontan antes de eso, y no se basa en ninguna idea de un tiempo estándar "fijo" (que es lo que se getRawOffsetsupone que es un concepto válido), por lo que otras bibliotecas no necesitan introducir esta transición artificial.

Jon Skeet
fuente
25
@ Jon: Por curiosidad, ¿por qué retrasaron sus relojes en un intervalo tan "extraño"? Cualquier cosa como una hora habría parecido lógico, pero ¿cómo es que fueron 5: 52mins?
Johannes Rudolph
63
@Johannes: para que sea una zona horaria más normal a nivel mundial, creo que el desplazamiento resultante es UTC + 8. París hizo lo mismo en 1911, por ejemplo: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet
34
@ Jon ¿Sabes si Java / .NET hace frente a septiembre de 1752? Siempre me encantaba mostrarle a la gente el 9 952 en sistemas Unix
Mr Moose
30
Entonces, ¿por qué demonios estaba Shanghai 5 minutos fuera de combate en primer lugar?
Igby Largeman
25
@Charles: Muchos lugares tenían compensaciones menos convencionales en ese entonces. En algunos países, las diferentes ciudades tenían su propio desplazamiento para ser lo más geográficamente correcto posible.
Jon Skeet
1602

Ha encontrado una discontinuidad de hora local :

Cuando la hora estándar local estaba a punto de llegar el domingo 1 de enero de 1928, las 00:00:00 horas se volvieron hacia atrás 0:05:52 horas hasta el sábado 31 de diciembre de 1927, 23:54:08 hora estándar local.

Esto no es particularmente extraño y ha sucedido prácticamente en todas partes en un momento u otro a medida que las zonas horarias se cambiaron o cambiaron debido a acciones políticas o administrativas.

Michael Borgwardt
fuente
661

La moraleja de esta extrañeza es:

  • Use fechas y horas en UTC siempre que sea posible.
  • Si no puede mostrar una fecha u hora en UTC, siempre indique la zona horaria.
  • Si no puede solicitar una fecha / hora de entrada en UTC, solicite una zona horaria indicada explícitamente.
Raedwald
fuente
75
La conversión / almacenamiento en UTC realmente no ayudaría para el problema descrito, ya que encontraría la discontinuidad en la conversión a UTC.
antipático el
23
@ Mark Mann: si su programa usa UTC internamente en todas partes, convirtiendo a / desde una zona horaria local solo en la interfaz de usuario, no le importarían tales discontinuidades.
Raedwald
66
@Raedwald: Claro que sí. ¿Cuál es el horario UTC para 1927-12-31 23:54:08? (Ignorando, por el momento, que UTC ni siquiera existía en 1927). En algún momento esta fecha y hora están llegando a su sistema, y ​​usted tiene que decidir qué hacer con él. Decirle al usuario que tiene que ingresar el tiempo en UTC simplemente traslada el problema al usuario, no lo elimina.
Nick Bastin
72
Me siento reivindicado por la cantidad de actividad en este hilo, después de haber estado trabajando en la refactorización de fecha / hora de una aplicación grande durante casi un año. Si está haciendo algo como calendario, no puede "simplemente" almacenar UTC, ya que las definiciones de zonas horarias en las que se puede representar cambiarán con el tiempo. Almacenamos el "tiempo de intención del usuario", la hora local del usuario y su zona horaria, y UTC para buscar y ordenar, y cada vez que se actualiza la base de datos de la IANA, recalculamos todas las horas UTC.
taiganaut
366

Al incrementar el tiempo, debe volver a convertir a UTC y luego sumar o restar. Use la hora local solo para mostrar.

De esta manera, podrá caminar a través de cualquier período en el que las horas o los minutos sucedan dos veces.

Si se convirtió a UTC, agregue cada segundo y conviértalo a la hora local para mostrarlo. Usted pasaría por las 11:54:08 pm LMT - 11:59:59 pm LMT y luego 11:54:08 pm CST - 11:59:59 pm CST.

PatrickO
fuente
309

En lugar de convertir cada fecha, puede usar el siguiente código:

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

Y luego ver que el resultado es:

1
Rajshri
fuente
72
Me temo que ese no es el caso. Puede probar mi código en su sistema, saldrá 1, porque tenemos diferentes configuraciones regionales.
Freewind
14
Eso solo es cierto porque no ha especificado la configuración regional en la entrada del analizador. Ese es un mal estilo de codificación y una gran falla de diseño en Java: su localización inherente. Personalmente, pongo "TZ = UTC LC_ALL = C" en todas partes donde uso Java para evitar eso. Además, debe evitar todas las versiones localizadas de una implementación a menos que esté interactuando directamente con un usuario y lo desee explícitamente. No realice NINGÚN cálculo, incluidas las localizaciones, utilice siempre las zonas horarias Locale.ROOT y UTC a menos que sea absolutamente necesario.
user1050755
226

Lamento decirlo, pero la discontinuidad del tiempo se ha movido un poco

JDK 6 hace dos años, y en JDK 7 recientemente en la actualización 25 .

Lección que debe aprender: evite a toda costa los horarios que no sean UTC, excepto tal vez para la exhibición.

usuario1050755
fuente
27
Esto es incorrecto. La discontinuidad no es un error, es solo que una versión más reciente de TZDB tiene datos ligeramente diferentes. Por ejemplo, en mi máquina con Java 8, si cambia el código ligeramente para usar "1927-12-31 23:54:02" y "1927-12-31 23:54:03" aún verá un discontinuidad, pero ahora de 358 segundos, en lugar de 353. Incluso las versiones más recientes de TZDB tienen otra diferencia: vea mi respuesta para más detalles. No hay un error real aquí, solo una decisión de diseño sobre cómo se analizan los valores de texto de fecha / hora ambiguos.
Jon Skeet
66
El verdadero problema es que los programadores no entienden que la conversión entre hora local y universal (en cualquier dirección) no es ni puede ser 100% confiable. Para las marcas de tiempo antiguas, los datos que tenemos sobre la hora local son, en el mejor de los casos, inestables. Para futuras marcas de tiempo, las acciones políticas pueden cambiar a qué hora universal se asigna una hora local determinada. Para las marcas de tiempo pasadas actuales y recientes, puede tener el problema de que el proceso de actualización de la base de datos tz y la implementación de los cambios puede ser más lento que el cronograma de implementación de las leyes.
plugwash
200

Como explicaron otros, hay una discontinuidad de tiempo allí. Hay dos posibles compensaciones de zona horaria para 1927-12-31 23:54:08at Asia/Shanghai, pero solo una compensación para 1927-12-31 23:54:07. Entonces, dependiendo de qué desplazamiento se use, hay una diferencia de un segundo o una diferencia de 5 minutos y 53 segundos.

Este ligero cambio de compensaciones, en lugar del habitual horario de verano de una hora (horario de verano), oscurece un poco el problema.

Tenga en cuenta que la actualización 2013a de la base de datos de zonas horarias movió esta discontinuidad unos segundos antes, pero el efecto aún sería observable.

El nuevo java.timepaquete en Java 8 permite ver esto más claramente y proporciona herramientas para manejarlo. Dado:

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

Luego durationAtEarlierOffsetserá un segundo, mientras durationAtLaterOffsetque serán cinco minutos y 53 segundos.

Además, estas dos compensaciones son las mismas:

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

Pero estos dos son diferentes:

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

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

Se puede ver el mismo problema comparando 1927-12-31 23:59:59con 1928-01-01 00:00:00, sin embargo, en este caso, es la compensación temprana que produce la divergencia más tiempo, y es la fecha más temprana que tiene dos posibles desplazamientos.

Otra forma de abordar esto es verificar si hay una transición en curso. Podemos hacer esto así:

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

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

Puede verificar si la transición es una superposición donde hay más de un desplazamiento válido para esa fecha / hora o una brecha donde esa fecha / hora no es válida para esa identificación de zona, utilizando los métodos isOverlap()y isGap()en zot4.

Espero que esto ayude a las personas a manejar este tipo de problemas una vez que Java 8 esté ampliamente disponible, o para aquellos que usan Java 7 que adopten el backport JSR 310.

Daniel C. Sobral
fuente
1
Hola Daniel, he ejecutado tu código pero no está dando el resultado esperado. like DurationAtEarlierOffset y DurationAtLaterOffset ambos son solo 1 segundo y zot3 y zot4 son nulos. Acabo de copiar y ejecutar este código en mi máquina. ¿Hay algo que deba hacerse aquí? Avísame si quieres ver un código. Aquí está el código tutorialspoint.com/…, ¿ puede decirme qué sucede aquí?
vineeshchauhan
2
@vineeshchauhan Depende de la versión de Java, porque esto ha cambiado en tzdata, y las diferentes versiones de JDK incluyen diferentes versiones de tzdata. En mi propio Java instalado, los tiempos son 1900-12-31 23:54:16y 1900-12-31 23:54:17, pero eso no funciona en el sitio que compartiste, por lo que están usando una versión de Java diferente a la mía.
Daniel C. Sobral
167

En mi humilde opinión, la localización generalizada e implícita en Java es su mayor defecto de diseño. Puede estar destinado a interfaces de usuario, pero, francamente, quién realmente usa Java para interfaces de usuario hoy en día, excepto algunos IDE donde básicamente puede ignorar la localización porque los programadores no son exactamente el público objetivo para ella. Puede solucionarlo (especialmente en servidores Linux):

  • exportar LC_ALL = C TZ = UTC
  • configura el reloj de tu sistema en UTC
  • nunca use implementaciones localizadas a menos que sea absolutamente necesario (es decir, solo para visualización)

A los miembros de Java Community Process les recomiendo:

  • hacer que los métodos localizados no sean los predeterminados, pero requieren que el usuario solicite explícitamente la localización.
  • use UTF-8 / UTC como el valor predeterminado FIJO en su lugar porque ese es simplemente el valor predeterminado hoy. No hay ninguna razón para hacer otra cosa, excepto si desea producir hilos como este.

Quiero decir, vamos, ¿no son las variables estáticas globales un patrón anti-OO? Nada más son esos valores predeterminados generalizados dados por algunas variables de entorno rudimentarias .......

usuario1050755
fuente
21

Como otros dijeron, es un cambio de hora en 1927 en Shanghai.

Cuando estaba 23:54:07en Shanghai, la hora estándar local, pero después de 5 minutos y 52 segundos, pasó al día siguiente a las 00:00:00, y luego la hora estándar local volvió a cambiar 23:54:08. Por eso, la diferencia entre los dos tiempos es 343 segundos, no 1 segundo, como era de esperar.

El tiempo también puede arruinar en otros lugares como los Estados Unidos. Estados Unidos tiene horario de verano. Cuando comienza el horario de verano, el tiempo avanza 1 hora. Pero después de un tiempo, el horario de verano termina y retrocede 1 hora a la zona horaria estándar. Entonces, a veces, cuando se comparan los tiempos en los EE. UU., La diferencia es aproximadamente 3600segundos, no 1 segundo.

Pero hay algo diferente en estos dos cambios de tiempo. El último cambia continuamente y el primero fue solo un cambio. No volvió a cambiar ni volvió a cambiar en la misma cantidad.

Es mejor usar UTC donde el tiempo no cambia a menos que sea necesario usar un tiempo que no sea UTC como en la pantalla.

zixuan
fuente