Почему разница между 30 марта и 1 марта 2020 года ошибочно дает 28 дней вместо 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)

Результат 28, а должно быть 29.

Может ли быть часовой пояс / местоположение?

Джо
источник
17
Примечание: пожалуйста, не используйте SimpleDateFormatбольше, потому что он устарел. java.timeВместо этого используйте пакеты от . В SimpleDateFormatслучае использования DateTimeFormatter. В случае Java 7, см. Комментарий Энди Тернера ниже.
MC Emperor
28
Не делайте математику в разы. Используйте подходящую библиотеку времени ( java.time[хотя я отмечаю, что вы находитесь на Java 7], ThreeTenBp , Joda).
Энди Тернер
14
Я хотел бы побывать на собрании, на котором кто-то говорит: «Хорошо, теперь, когда у нас есть часовые пояса, вроде как, давайте перейдем в режим 100% задницы и реализуем то, что называется летним временем, которое пришло ко мне во сне после моего кислотного путешествия прошлой ночью."
MonkeyZeus
5
@gmauch TimeUnitне делает вид, что ничего не знает о DST. Как говорит Javadoc: наносекунду определяют как одну тысячную микросекунды, микросекунду - одну тысячную миллисекунды, миллисекунду - одну тысячную секунды, минуту - шестьдесят секунд, час - шестьдесят минут, а день - как двадцать четыре часа . --- Так как летнее время приводит к тому, что 2 дня в году не являются ровно 24 часами, TimeUnitэто неправильно, когда речь идет о летнем времени.
Андреас
1
Эта проблема возникает только на компьютерах, которые находятся в часовых поясах с переходом на летнее время. Это дает правильное количество дней (29) в часовых поясах, которые не имеют летнего времени!
Гопинатх

Ответы:

207

Проблема в том, что из-за перехода на летнее время (в воскресенье, 8 марта 2020 г.) между этими датами 28 дней и 23 часа . TimeUnit.DAYS.convert(...) обрезает результат до 28 дней.

Чтобы увидеть проблему (я в восточном часовом поясе США):

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

Вывод

2502000000
Days: 28
Hours: 695
Days: 28.958333333333332

Чтобы исправить это, используйте часовой пояс без DST, например, 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();

Вывод

2505600000
Days: 29
Hours: 696
Days: 29.0
Andreas
источник
121
«Чтобы исправить» не делайте математику со временем. Используйте правильную библиотеку даты / времени.
Энди Тернер
62
@AndyTurner Исправить, используя встроенные Java 7 API. Так как это может быть исправлено, показываю, как это правильный ответ. Заставить кого-либо включить полную библиотеку (Joda-Time, ThreeTen и т. Д.) Просто для выполнения этого одного вычисления было бы излишним. Конечно, рекомендуется использовать библиотеку, но это не обязательно .
Андреас
38
Я с уважением не согласен. Если вы хотите сделать расчет, сделайте это правильно; оплатить стоимость, чтобы сделать это правильно.
Энди Тернер
16
Мои € 0,02: «правильный» способ сделать в Java 7 без какой-либо внешней библиотеки будет использовать GregorianCalendarобъект и добавлять 1 день за раз, пока не будет достигнута конечная дата. Я бы заплатил высокую цену, чтобы избежать этого. И добавление бэкпорта библиотеки, которая уже является частью Java 8, 9, 10, 11, 12, 13,… не является высокой ценой. Напротив, в следующий раз, когда вам нужно будет что-то сделать с датой или временем, это уже будет выгодно.
Оле В.В.
27
Даже в UTC последний день июня иногда бывает на одну секунду короче, без какого-либо реального предупреждения или предсказуемости. Всегда используйте библиотеку даты и времени.
Affe
41

Причина этой проблемы уже упоминается в ответе Андреаса .

Вопрос в том, что именно вы хотите посчитать. Тот факт, что вы утверждаете, что фактическая разница должна быть 29 вместо 28, и спрашиваете, может ли быть «время в зоне / зоне» быть проблемой » , показывает, что вы на самом деле хотите считать. Видимо, вы хотите избавиться от любой разницы часовых поясов.

Я предполагаю, что вы хотите только рассчитать дни без времени и часового пояса.

Java 8

Ниже, в примере того, как правильно рассчитать количество дней между ними, я использую класс, который представляет именно это - дату без времени и часового пояса - 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);

Обратите внимание , что ChronoUnit, DateTimeFormatterи LocalDateтребуют , по крайней мере , Java 8, который не доступен для вас, в соответствии с тега. Тем не менее, это возможно для будущих читателей.

Как упоминал Оле В.В., есть также ThreeTen Backport , который передает функциональность API Java 8 Date and Time на Java 6 и 7.

MC Emperor
источник
2
@ OleV.V. Я знаю, что есть ThreeTen, некоторые пользователи, возможно, упоминали его несколько раз. (Я собирался сделать ссылку на запрос SEDE, возвращающий все сообщения и комментарии пользователя 5772882, содержащие текст ThreeTen;-), но, к сожалению, на момент написания он был отключен.) Я обновлю сообщение.
MC Emperor
2
@OleVV Это совсем не плохо. Я думаю, что большинство людей просто невежественны java.time, потому что в школе они все еще используют старые классы. Но API даты и времени Java 8 очень хорошо спроектирован - было бы неудобно не использовать его.
MC Emperor