Por que subtrair essas duas vezes (em 1927) está dando um resultado estranho?

6827

Se eu executar o programa a seguir, que analisa duas seqüências de datas fazendo referência a tempos com um segundo de diferença e as 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);
}

A saída é:

353

Por que ld4-ld3não é 1(como eu esperaria da diferença de um segundo nos tempos), mas 353?

Se eu alterar as datas para horários 1 segundo depois:

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

Então ld4-ld3será 1.


Versão 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
Vento livre
fonte
23
Isso pode ser um problema de localidade.
Thorbjørn Ravn Andersen
72
A resposta real é sempre, sempre use segundos desde uma época para registro, como a época Unix, com representação inteira de 64 bits (assinada, se você deseja permitir carimbos antes da época). Qualquer sistema de tempo do mundo real tem algum comportamento não linear e não monotônico, como horas bissextas ou horário de verão.
21740 Phil
22
Ri muito. Eles o corrigiram para o jdk6 em 2011. Depois, dois anos depois, descobriram que ele também deveria ser corrigido no jdk7 .... corrigido a partir de 7u25, é claro, não encontrei nenhuma dica na nota de lançamento. Às vezes me pergunto quantos bugs o Oracle corrige e não conta a ninguém por motivos de relações públicas.
user1050755
8
Um ótimo vídeo sobre esse tipo de coisa: youtube.com/watch?v=-5wpm-gesOY
Thorbjørn Ravn Andersen
4
@PhilH O bom é que ainda haverá segundos bissextos. Então, mesmo isso não funciona.
12431234123412341234123

Respostas:

10873

É uma mudança de fuso horário no dia 31 de dezembro em Xangai.

Veja esta página para detalhes de 1927 em Xangai. Basicamente, à meia-noite do final de 1927, os relógios retrocederam 5 minutos e 52 segundos. Então "1927-12-31 23:54:08" realmente aconteceu duas vezes, e parece que o Java está analisando-o mais tarde instante possível para a data / hora local - daí a diferença.

Apenas mais um episódio no mundo muitas vezes estranho e maravilhoso dos fusos horários.

EDIT: Pare de pressionar! O histórico muda ...

A pergunta original não demonstraria mais o mesmo comportamento, se reconstruída com a versão 2013a do TZDB . Em 2013a, o resultado seria 358 segundos, com um tempo de transição de 23:54:03 em vez de 23:54:08.

Eu só notei isso porque estou coletando perguntas como essa no Noda Time, na forma de testes de unidade ... O teste agora foi alterado, mas é apenas para mostrar - nem mesmo dados históricos são seguros.

EDIT: História mudou novamente ...

No TZDB 2014f, o tempo da mudança mudou para 1900-12-31, e agora é apenas uma mudança de 343 segundos (portanto, o tempo entre te t+1é 344 segundos, se você entende o que eu quero dizer).

EDIT: Para responder a uma pergunta sobre uma transição em 1900 ... parece que a implementação do fuso horário Java trata todos os fusos horários como estando simplesmente no horário padrão por qualquer instante antes do início de 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);
            }
        }
    }
}

O código acima não produz resultados na minha máquina Windows. Portanto, qualquer fuso horário que possua outro deslocamento que não seja o padrão no início de 1900 contará isso como uma transição. O próprio TZDB possui alguns dados voltando antes disso e não se baseia em nenhuma idéia de um tempo padrão "fixo" (que é o que getRawOffsetassume ser um conceito válido), portanto outras bibliotecas não precisam introduzir essa transição artificial.

Jon Skeet
fonte
25
@ Jon: Por curiosidade, por que eles atrasaram seus relógios em um intervalo tão "estranho"? Qualquer coisa como uma hora teria parecido lógico, mas por que eram 5: 52mins?
Johannes Rudolph
63
@ Johannes: Para torná-lo um fuso horário mais globalmente normal, acredito - o deslocamento resultante é UTC + 8. Paris fez o mesmo tipo de coisa em 1911, por exemplo: timeanddate.com/worldclock/clockchange.html?n=195&year=1911
Jon Skeet
34
@ Jon Você sabe se Java / .NET lida com setembro em 1752? Eu sempre costumava amar mostrando às pessoas cal 9 1752 em sistemas UNIX
Sr. Alce
30
Então, por que diabos Xangai estava 5 minutos fora de moda em primeiro lugar?
Igby Largeman
25
@ Charles: Muitos lugares tiveram menos compensações convencionais naquela época. Em alguns países, cada cidade diferente teve seu próprio deslocamento para estar o mais próximo possível da correção geográfica.
31911 Jon Skeet
1602

Você encontrou uma descontinuidade na hora local :

Quando a hora local padrão estava prestes a chegar ao domingo, 1. janeiro de 1928, 00:00:00 relógios foram revertidos 0:05:52 horas para sábado, 31. dezembro 1927, 23:54:08 hora local

Isso não é particularmente estranho e aconteceu praticamente em todos os lugares ao mesmo tempo, pois os fusos horários foram trocados ou alterados devido a ações políticas ou administrativas.

Michael Borgwardt
fonte
661

A moral dessa estranheza é:

  • Use datas e horas no UTC sempre que possível.
  • Se você não pode exibir uma data ou hora no UTC, sempre indique o fuso horário.
  • Se você não pode exigir uma data / hora de entrada no UTC, exija um fuso horário indicado explicitamente.
Raedwald
fonte
75
A conversão / armazenamento no UTC realmente não ajudaria no problema descrito, pois você encontraria a descontinuidade na conversão para o UTC.
Unpythonic
23
@ Mark Mann: se o seu programa usa o UTC internamente em qualquer lugar, convertendo para / de um fuso horário local apenas na interface do usuário, você não se importaria com essas descontinuidades.
Raedwald 26/08
66
@ Raedwald: Claro que você faria - Qual é o horário UTC de 1927-12-31 23:54:08? (Ignorando, no momento, que o UTC nem existia em 1927). Em algum momento, esta hora e data estão chegando ao seu sistema e você precisa decidir o que fazer com ele. Dizer ao usuário que ele deve inserir o tempo no UTC apenas move o problema para o usuário, não o elimina.
Nick Bastin
72
Sinto-me justificado pela quantidade de atividade nesse segmento, tendo trabalhado na refatoração de data / hora de um aplicativo grande há quase um ano. Se você estiver fazendo algo como calendário, não poderá "simplesmente" armazenar o UTC, pois as definições dos fusos horários nos quais ele pode ser renderizado serão alteradas ao longo do tempo. Armazenamos "hora de intenção do usuário" - a hora local do usuário e seu fuso horário - e o UTC para pesquisa e classificação e, sempre que o banco de dados da IANA é atualizado, recalculamos todos os horários do UTC.
precisa saber é o seguinte
366

Ao incrementar o tempo, você deve voltar ao UTC e adicionar ou subtrair. Use a hora local apenas para exibição.

Dessa forma, você poderá percorrer todos os períodos em que horas ou minutos acontecem duas vezes.

Se você tiver convertido para UTC, adicione cada segundo e converta para a hora local para exibição. Você passaria das 23h54m - 23h59m da LMT - 23h59m59m da LMT e, em seguida, das 23:54:08 pm da CST - 23h59m59s da CST.

PatrickO
fonte
309

Em vez de converter cada data, você pode usar o seguinte código:

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

E então veja que o resultado é:

1
Rajshri
fonte
72
Receio que não seja esse o caso. Você pode tentar o meu código no seu sistema, ele será exibido 1, porque temos localidades diferentes.
Freewind
14
Isso é verdade apenas porque você não especificou o código do idioma na entrada do analisador. Esse é um estilo de codificação ruim e uma enorme falha de design em Java - sua localização inerente. Pessoalmente, coloquei "TZ = UTC LC_ALL = C" em todos os lugares em que uso o Java para evitar isso. Além disso, você deve evitar todas as versões localizadas de uma implementação, a menos que esteja interagindo diretamente com um usuário e deseje explicitamente. Não faça QUALQUER cálculos, incluindo localizações, use sempre fusos horários Locale.ROOT e UTC, a menos que seja absolutamente necessário.
user1050755
226

Lamento dizer, mas a descontinuidade do tempo mudou um pouco

JDK 6 há dois anos e no JDK 7 recentemente na atualização 25 .

Lição a aprender: evite horários fora do UTC a todo custo, exceto talvez para exibição.

user1050755
fonte
27
Isto está incorreto. A descontinuidade não é um bug - é apenas que uma versão mais recente do TZDB possui dados ligeiramente diferentes. Por exemplo, na minha máquina com Java 8, se você alterar um pouco o código para usar "1927-12-31 23:54:02" e "1927-12-31 23:54:03", ainda verá um descontinuidade - mas agora há 358 segundos, em vez de 353. Versões ainda mais recentes do TZDB têm outra diferença - veja minha resposta para obter detalhes. Não há nenhum bug real aqui, apenas uma decisão de design sobre como os valores de texto de data / hora ambíguos são analisados.
Jon Skeet
6
O verdadeiro problema é que os programadores não entendem que a conversão entre o horário local e o universal (em qualquer direção) não é e não pode ser 100% confiável. Para timestamps antigos, os dados que temos sobre a hora local são, na melhor das hipóteses, instáveis. Para timestamps futuros, as ações políticas podem mudar para qual horário universal uma determinada hora local é mapeada. Para registros de data e hora atuais e recentes anteriores, você pode ter o problema de que o processo de atualização do banco de dados tz e implementação das alterações possa ser mais lento que o cronograma de implementação das leis.
plugwash
200

Como explicado por outros, há uma descontinuidade no tempo lá. Existem dois desvios de fuso horário possíveis para 1927-12-31 23:54:08at Asia/Shanghai, mas apenas um deslocamento para1927-12-31 23:54:07 . Portanto, dependendo de qual deslocamento é usado, há uma diferença de um segundo ou de 5 minutos e 53 segundos.

Essa ligeira mudança de compensações, em vez do horário de verão habitual de uma hora (horário de verão) a que estamos acostumados, obscurece um pouco o problema.

Observe que a atualização de 2013 do banco de dados de fuso horário moveu essa descontinuidade alguns segundos antes, mas o efeito ainda seria observável.

O novo java.timepacote no Java 8 permite ver isso mais claramente e fornece ferramentas para lidar com isso. 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());

Em seguida durationAtEarlierOffset, será um segundo, enquanto durationAtLaterOffsetserão cinco minutos e 53 segundos.

Além disso, essas duas compensações são as mesmas:

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

Mas estes dois são diferentes:

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

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

Você pode ver o mesmo problema em comparação 1927-12-31 23:59:59com 1928-01-01 00:00:00, no entanto, nesse caso, é o deslocamento anterior que produz a divergência mais longa e é a data anterior que tem duas compensações possíveis.

Outra maneira de abordar isso é verificar se há uma transição em andamento. Podemos fazer isso assim:

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

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

Você pode verificar se a transição é uma sobreposição em que há mais de um deslocamento válido para essa data / hora ou um intervalo em que essa data / hora não é válida para esse ID de zona - usando os métodos isOverlap()e .isGap()zot4

Espero que isso ajude as pessoas a lidar com esse tipo de problema quando o Java 8 estiver amplamente disponível ou para aqueles que usam o Java 7 que adotam o backport JSR 310.

Daniel C. Sobral
fonte
1
Oi Daniel, Eu executei seu pedaço de código, mas ele não está dando a saída como esperado. como durationAtEarlierOffset e durationAtLaterOffset, ambos são apenas 1 segundo e também zot3 e zot4 são nulos. Eu configurei apenas copiei e executei esse código na minha máquina. existe algo que precisa ser feito aqui. Deixe-me saber se você deseja ver um pedaço de código. Aqui está o código tutorialspoint.com/…, você pode me informar o que está acontecendo aqui.
vineeshchauhan
2
@vineeshchauhan Depende da versão do Java, porque isso foi alterado no tzdata, e diferentes versões do JDK agrupam versões diferentes do tzdata. No meu próprio Java instalado, os horários são 1900-12-31 23:54:16e 1900-12-31 23:54:17, mas isso não funciona no site que você compartilhou, portanto eles estão usando uma versão Java diferente da que eu.
Daniel C. Sobral
167

IMHO, a localização generalizada e implícita em Java é sua maior falha de design. Pode ser destinado a interfaces de usuário, mas, francamente, quem realmente usa Java para interfaces de usuário hoje, exceto em alguns IDEs, nos quais você pode basicamente ignorar a localização porque os programadores não são exatamente o público-alvo para ela. Você pode corrigi-lo (especialmente em servidores Linux):

  • exportar LC_ALL = C TZ = UTC
  • ajuste o relógio do sistema para UTC
  • nunca use implementações localizadas, a menos que seja absolutamente necessário (por exemplo, apenas para exibição)

Para os membros do Java Community Process , eu recomendo:

  • torne os métodos localizados não o padrão, mas exija que o usuário solicite explicitamente a localização.
  • use UTF-8 / UTC como o padrão FIXED, porque esse é simplesmente o padrão hoje. Não há razão para fazer outra coisa, exceto se você deseja produzir threads como este.

Quero dizer, vamos lá, as variáveis ​​estáticas globais não são um padrão anti-OO? Nada mais são os padrões generalizados dados por algumas variáveis ​​de ambiente rudimentares .......

user1050755
fonte
21

Como outros disseram, é uma mudança de horário em 1927 em Xangai.

Quando estava 23:54:07em Xangai, o horário padrão local, mas após 5 minutos e 52 segundos, passou para o dia seguinte às 00:00:00, e o horário padrão local mudou novamente para 23:54:08. Portanto, é por isso que a diferença entre os dois tempos é de 343 segundos e não 1 segundo, como você esperaria.

O tempo também pode atrapalhar em outros lugares como os EUA. Os EUA têm horário de verão. Quando o horário de verão começa, o horário avança 1 hora. Mas, depois de um tempo, o horário de verão termina e volta 1 hora para o fuso horário padrão. Então, às vezes, ao comparar os tempos nos EUA, a diferença é de 3600segundos e não de 1 segundo.

Mas há algo diferente nessas duas mudanças de tempo. O último muda continuamente e o primeiro foi apenas uma mudança. Não mudou de volta ou mudou de novo na mesma quantidade.

É melhor usar o UTC onde o tempo não muda, a menos que seja necessário usar o horário não UTC, como no visor.

zixuan
fonte