¿Por qué la diferencia entre el 30 de marzo y el 1 de marzo de 2020 da erróneamente 28 días en lugar 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)

El resultado es 28, mientras que debería ser 29.

¿Podría la zona horaria / ubicación ser el problema?

Joe
fuente
17
Nota: No lo use SimpleDateFormatmás, ya que está obsoleto. Use paquetes de en su java.timelugar. En SimpleDateFormatel caso de, uso DateTimeFormatter. En el caso de Java 7, vea el comentario de Andy Turner a continuación.
MC Emperor
28
No hagas cálculos a veces. Use una biblioteca de tiempo adecuada ( java.time[aunque noto que está en Java 7], ThreeTenBp , Joda).
Andy Turner
14
Me encantaría haber estado en la reunión a la que alguien va "Está bien, ahora que tenemos zonas horarias un poco entendidas, vamos al 100% en modo culo e implementemos esto llamado ahorro de luz que me llegó en un sueño después de mi viaje ácido anoche."
MonkeyZeus
55
@gmauch TimeUnitno pretende saber nada sobre el horario de verano. Como dice el javadoc: un nanosegundo se define como una milésima de microsegundo, un microsegundo como una milésima de milisegundo, un milisegundo como una milésima de segundo, un minuto como sesenta segundos, una hora como sesenta minutos y un día como veinticuatro horas . --- Dado que el horario de verano hace que 2 días del año no sean exactamente 24 horas, TimeUnitse equivoca cuando está involucrado el horario de verano.
Andreas
1
Este problema ocurre solo en computadoras que se encuentran en zonas horarias con horario de verano. ¡Proporciona el número correcto de días (29) en zonas horarias que no tienen horario de verano!
Gopinath

Respuestas:

207

El problema es que debido al cambio de horario de verano (el domingo 8 de marzo de 2020), hay 28 días y 23 horas entre esas fechas. TimeUnit.DAYS.convert(...) trunca el resultado a 28 días.

Para ver el problema (estoy en la zona horaria del este de EE. UU.):

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

Salida

2502000000
Days: 28
Hours: 695
Days: 28.958333333333332

Para solucionarlo, use una zona horaria que no tenga horario de verano, por ejemplo, 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();

Salida

2505600000
Days: 29
Hours: 696
Days: 29.0
Andreas
fuente
121
"Arreglar" no hagas cálculos matemáticos con los tiempos. Use una biblioteca de fecha / hora adecuada.
Andy Turner
6262
@AndyTurner Para solucionarlo, utilizando las API integradas de Java 7. Como se puede arreglar, muestra cómo es una respuesta válida. Obligar a alguien a incluir una biblioteca completa (Joda-Time, ThreeTen, etc.) solo para hacer este cálculo sería excesivo. Claro, se recomienda usar una biblioteca, pero no es obligatorio .
Andreas
38
Respetuosamente no estoy de acuerdo. Si quieres hacer un cálculo, hazlo bien; pagar el costo para hacerlo bien
Andy Turner
16
Mi 0,02 €: la forma "correcta" de hacerlo en Java 7 sin ninguna biblioteca externa sería utilizar un GregorianCalendarobjeto y agregar 1 día a la vez hasta llegar a la fecha de finalización. Pagaría un alto precio para evitar eso. Y agregar el backport de una biblioteca que ya es parte de Java 8, 9, 10, 11, 12, 13, ... no tiene un precio alto. Por el contrario, la próxima vez que necesite hacer algo con fecha u hora, ya será una ganancia.
Ole VV
27
Incluso en UTC, el último día de junio es ocasionalmente un segundo demasiado corto, sin ninguna advertencia real o previsibilidad. Siempre use una biblioteca de fecha y hora.
Affe
41

La causa de este problema ya se menciona en la respuesta de Andreas .

La pregunta es qué es exactamente lo que quieres contar. El hecho de que declare que la diferencia real debe ser 29 en lugar de 28, y pregunte si "la hora de ubicación / zona podría ser un problema" , revela lo que realmente quiere contar. Aparentemente, desea deshacerse de cualquier diferencia de zona horaria.

Supongo que solo desea calcular los días, sin hora ni zona horaria.

Java 8

A continuación, en el ejemplo de cómo se puede calcular correctamente el número de días entre cada uno, estoy usando una clase que representa exactamente eso, una fecha sin hora ni zona horaria 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);

Tenga en cuenta que ChronoUnit, DateTimeFormattery LocalDaterequiere al menos Java 8, que no está disponible para usted, de acuerdo con la etiqueta . Sin embargo, tal vez sea para futuros lectores.

Según lo mencionado por Ole VV, también está ThreeTen Backport , que soporta la funcionalidad de API de fecha y hora de Java 8 a Java 6 y 7.

MC Emperor
fuente
2
@ OleV.V. Sé que hay ThreeTen, algunos usuarios pueden haberlo mencionado algunas veces. (Estaba a punto de vincularme a una consulta de SEDE que devolvía todas las publicaciones y comentarios del usuario 5772882 que contenía el texto ThreeTen;-), pero desafortunadamente está fuera de línea al momento de escribir.) Actualizaré la publicación.
MC Emperor
2
@OleVV No es algo malo en absoluto. Creo que la mayoría de las personas simplemente ignoran java.time, porque en la escuela todavía usan las viejas clases. Pero la API de fecha y hora de Java 8 está muy bien diseñada, sería una pérdida no usarla.
MC Emperor