Por que a diferença entre 30 de março e 1 de março de 2020 dá erroneamente 28 dias em vez de 29?

124
TimeUnit.DAYS.convert(
   Math.abs(
      new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").parse("30-03-2020 00:00:00").getTime() - 
      new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").parse("1-03-2020 00:00:00").getTime()
   ),
   TimeUnit.MILLISECONDS)

O resultado é 28, enquanto deve ser 29.

O fuso horário / local pode ser o problema?

Joe
fonte
17
Nota: Por favor, não use SimpleDateFormatmais, pois é obsoleto. Use pacotes de java.timevez. No SimpleDateFormatcaso, use DateTimeFormatter. No caso do Java 7, veja o comentário de Andy Turner abaixo.
MC Emperor
28
Não faça matemática de vez em quando. Use uma biblioteca de tempo adequada ( java.time[embora eu note que você esteja no Java 7], ThreeTenBp , Joda).
Andy Turner
14
Eu adoraria estar na reunião em que alguém vai "Ok, agora que descobrimos os fusos horários, vamos usar o modo burro 100% e implementar essa coisa chamada horário de verão que me ocorreu em um sonho depois da minha viagem ácida noite passada."
MonkeyZeus
5
@gmauch TimeUnitnão pretende saber nada sobre horário de verão. Como diz o javadoc: Um nanossegundo é definido como um milésimo de microssegundo, um microssegundo como um milésimo de um milissegundo, um milissegundo como um milésimo de segundo, um minuto como sessenta segundos, uma hora como sessenta segundos e um dia como vinte e quatro horas . --- Como o horário de verão faz com que dois dias do ano não sejam exatamente 24 horas, TimeUnitele erra quando o horário de verão está envolvido.
Andreas
11
Esse problema ocorre apenas em computadores que estão em fusos horários com horário de verão. Fornece o número correto de dias (29) em fusos horários que não têm horário de verão!
Gopinath

Respostas:

207

O problema é que, devido ao horário de verão (no domingo, 8 de março de 2020), há 28 dias e 23 horas entre essas datas. TimeUnit.DAYS.convert(...) trunca o resultado para 28 dias.

Para ver o problema (estou no fuso horário do leste dos EUA):

SimpleDateFormat fmt = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
long diff = fmt.parse("30-03-2020 00:00:00").getTime() -
            fmt.parse("1-03-2020 00:00:00").getTime();

System.out.println(diff);
System.out.println("Days: " + TimeUnit.DAYS.convert(Math.abs(diff), TimeUnit.MILLISECONDS));
System.out.println("Hours: " + TimeUnit.HOURS.convert(Math.abs(diff), TimeUnit.MILLISECONDS));
System.out.println("Days: " + TimeUnit.HOURS.convert(Math.abs(diff), TimeUnit.MILLISECONDS) / 24.0);

Resultado

2502000000
Days: 28
Hours: 695
Days: 28.958333333333332

Para corrigir, use um fuso horário que não tenha horário de verão, por exemplo, UTC :

SimpleDateFormat fmt = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
long diff = fmt.parse("30-03-2020 00:00:00").getTime() -
            fmt.parse("1-03-2020 00:00:00").getTime();

Resultado

2505600000
Days: 29
Hours: 696
Days: 29.0
Andreas
fonte
121
"Para consertar" não faça matemática com o tempo. Use uma biblioteca de data / hora adequada.
Andy Turner
62
@AndyTurner Para corrigir, usando APIs Java 7 internas. Uma vez que pode ser corrigido, mostrando como é uma resposta válida. Forçar alguém a incluir uma biblioteca completa (Joda-Time, ThreeTen etc.) apenas para fazer esse cálculo seria um exagero. Claro, o uso de uma biblioteca seria recomendado, mas não é obrigatório .
Andreas
38
Eu discordo respeitosamente. Se você deseja fazer um cálculo, faça o que é certo; pague o custo para fazê-lo corretamente.
Andy Turner
16
Meu € 0,02: a maneira "correta" de executar o Java 7 sem nenhuma biblioteca externa seria usar um GregorianCalendarobjeto e adicionar 1 dia por vez até que a data final seja atingida. Eu pagaria um preço alto para evitar isso. E adicionar o backport de uma biblioteca que já faz parte do Java 8, 9, 10, 11, 12, 13, ... não é um preço alto. Pelo contrário, da próxima vez que você precisar fazer algo com data ou hora, já será um ganho.
Ole VV
27
Mesmo no UTC, o último dia de junho é ocasionalmente um segundo curto demais, sem nenhum aviso ou previsibilidade real. Sempre use uma biblioteca de data e hora.
Affe
41

A causa desse problema já é mencionada na resposta de Andreas .

A questão é o que exatamente você deseja contar. O fato de você declarar que a diferença real deve ser 29 em vez de 28 e perguntar se "a hora do local / fuso horário pode ser um problema" revela o que você realmente deseja contar. Aparentemente, você quer se livrar de qualquer diferença de fuso horário.

Suponho que você só deseja calcular os dias, sem hora e fuso horário.

Java 8

Abaixo, no exemplo de como o número de dias entre eles pode ser calculado corretamente, estou usando uma classe que representa exatamente isso - uma data sem hora e fuso horário - LocalDate.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d-MM-yyyy HH:mm:ss");
LocalDate start = LocalDate.parse("1-03-2020 00:00:00", formatter);
LocalDate end = LocalDate.parse("30-03-2020 00:00:00", formatter);

long daysBetween = ChronoUnit.DAYS.between(start, end);

Note-se que ChronoUnit, DateTimeFormattere LocalDateexigem pelo menos Java 8, que não está disponível para você, de acordo com o tag. No entanto, talvez seja para futuros leitores.

Conforme mencionado por Ole VV, também há o ThreeTen Backport , que suporta a funcionalidade da API de data e hora do Java 8 para Java 6 e 7.

MC Emperor
fonte
2
@ OleV.V. Eu sei que há ThreeTen, alguns usuários podem ter mencionado isso algumas vezes. (Eu estava prestes a vincular a uma consulta SEDE retornando todas as postagens e comentários do usuário 5772882 contendo o texto ThreeTen;-), mas infelizmente ele está offline no momento da escrita.) Atualizarei a postagem.
MC Emperor
2
@OleVV Não é uma coisa ruim. Eu acho que a maioria das pessoas simplesmente ignora java.time, porque na escola elas ainda usam as classes antigas. Mas a API Java 8 Date and Time é muito bem projetada - seria uma perda não usá-la.
MC Emperor