Dlaczego różnica między 30 marca a 1 marca 2020 r. Błędnie daje 28 dni zamiast 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)

Wynik to 28, a powinno być 29.

Czy strefa czasowa / lokalizacja mogą stanowić problem?

Joe
źródło
17
Uwaga: nie używaj SimpleDateFormatwięcej, ponieważ jest przestarzały. java.timeZamiast tego użyj pakietów . W takim SimpleDateFormatprzypadku użyj DateTimeFormatter. W przypadku Java 7 zobacz komentarz Andy Turner poniżej.
MC Emperor
28
Nie rób matematyki na czas. Użyj odpowiedniej biblioteki czasu ( java.time[zauważam, że korzystasz z Java 7], ThreeTenBp , Joda).
Andy Turner
14
Chciałbym być na spotkaniu, na którym ktoś idzie. „Ok, teraz, kiedy mamy strefy czasowe, trochę się zorientowaliśmy, chodźmy w 100% w trybie ass i zastosujmy tę rzecz zwaną czasem letnim, która przyszła mi do głowy we śnie po mojej podróży po kwasie Ostatnia noc."
MonkeyZeus
5
@gmauch TimeUnitnie udaje, że nic nie wie o DST. Jak mówi javadoc: nanosekunda jest definiowana jako jedna tysięczna mikrosekundy, mikrosekunda jako jedna tysięczna milisekundy, milisekunda jako jedna tysięczna sekundy, minuta jako sześćdziesiąt sekund, godzina jako sześćdziesiąt minut i dzień jako dwadzieścia cztery godziny . --- Ponieważ DST powoduje, że 2 dni w roku nie są dokładnie 24 godziny, TimeUnitrobi się źle, gdy DST jest zaangażowany.
Andreas
1
Ten problem występuje tylko na komputerach znajdujących się w strefach czasowych, w których obowiązuje czas letni. Daje prawidłową liczbę dni (29) w strefach czasowych, które nie mają czasu letniego!
Gopinath

Odpowiedzi:

207

Problem polega na tym, że z powodu zmiany czasu letniego (w niedzielę 8 marca 2020 r.) Między tymi datami jest 28 dni i 23 godziny . TimeUnit.DAYS.convert(...) skraca wynik do 28 dni.

Aby zobaczyć problem (jestem w strefie czasowej USA Wschodniej):

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

Wynik

2502000000
Days: 28
Hours: 695
Days: 28.958333333333332

Aby to naprawić, użyj strefy czasowej, która nie ma czasu letniego, np. 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();

Wynik

2505600000
Days: 29
Hours: 696
Days: 29.0
Andreas
źródło
121
„Naprawić”, nie rób matematyki z czasem. Użyj odpowiedniej biblioteki daty / godziny.
Andy Turner
62
@AndyTurner Aby naprawić, używając wbudowanych interfejsów API języka Java 7. Ponieważ można to naprawić, pokazując, jak jest poprawna odpowiedź. Zmuszanie kogoś do dołączenia pełnej biblioteki (Joda-Time, ThreeTen itp.) Tylko do wykonania tej jednej kalkulacji byłoby przesadą. Oczywiście korzystanie z biblioteki byłoby zalecane, ale nie jest wymagane .
Andreas
38
Z szacunkiem się nie zgadzam. Jeśli chcesz wykonać obliczenia, zrób to dobrze; ponieść koszty, aby zrobić to dobrze.
Andy Turner
16
Moje 0,02 €: „Właściwym” sposobem na zrobienie w Javie 7 bez jakiejkolwiek zewnętrznej biblioteki byłoby użycie GregorianCalendarobiektu i dodawanie 1 dnia na raz, aż do osiągnięcia daty końcowej. Zapłaciłbym wysoką cenę, aby tego uniknąć. A dodanie backportu biblioteki, która jest już częścią Java 8, 9, 10, 11, 12, 13,… nie jest wysoką ceną. Przeciwnie, następnym razem, gdy będziesz musiał zrobić cokolwiek z datą lub godziną, będzie to już zysk.
Ole VV
27
Nawet w UTC ostatni dzień czerwca jest czasami o jedną sekundę za krótki, bez żadnego prawdziwego ostrzeżenia lub przewidywalności. Zawsze używaj biblioteki daty i godziny.
Affe
41

Przyczyna tego problemu jest już wspomniana w odpowiedzi Andreasa .

Pytanie brzmi, co dokładnie chcesz liczyć. Fakt, że stwierdzasz, że rzeczywista różnica powinna wynosić 29 zamiast 28, i pytasz, czy „czas / lokalizacja może być problemem” , ujawnia, co naprawdę chcesz policzyć. Najwyraźniej chcesz pozbyć się różnicy stref czasowych.

Zakładam, że chcesz obliczyć tylko dni, bez czasu i strefy czasowej.

Java 8

Poniżej, w przykładzie, w jaki sposób można poprawnie obliczyć liczbę dni między, używam klasy, która dokładnie to reprezentuje - datę bez godziny i strefy czasowej 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);

Należy zauważyć, że ChronoUnit, DateTimeFormatteri LocalDatewymaga co najmniej Java 8, która nie jest dostępna dla użytkownika, zgodnie z tag. Być może jest to jednak dla przyszłych czytelników.

Jak wspomniano w Ole VV, istnieje również ThreeTen Backport , który backportuje funkcjonalność API Java 8 Date and Time do Java 6 i 7.

MC Emperor
źródło
2
@ OleV.V. Wiem, że jest ThreeTen, niektórzy użytkownicy mogli wspomnieć o nim kilka razy. (Miałem już link do zapytania SEDE zwracającego wszystkie posty i komentarze użytkownika 5772882 zawierające tekst ThreeTen;-), ale niestety jest on offline w momencie pisania.) Zaktualizuję ten post.
MC Cesarz
2
@OleVV To wcale nie jest takie złe. Myślę, że większość ludzi jest po prostu ignorantami java.time, ponieważ w szkole wciąż korzystają ze starych klas. Ale Java 8 Data i czas API jest bardzo dobrze zaprojektowany - byłaby to strata nie go używać.
MC Emperor