Дата Java — Високосный год — странное поведение

#java #date #datetime

#java #Дата #дата и время

Вопрос:

Итак, у меня такая странная проблема с датой Java.

Точкой входа для этой проблемы являются 2 строковые даты StartDate («30 января 2016 года») и endate «»29 января 2017 года».

Проблема немного сложнее, но я подвел ее только к этому случаю.

Что мне нужно сделать, так это получить расписание на 1 год, точно такое же, как показано ниже, начиная с этих данных.

Таблица содержит для каждого месяца: первый день, последний день и дни между этими 2 днями.

Ожидаемые результаты:

/**

  • Сб 30 января 00:00:00 GMT 2016 — Вс 28 февраля 23:59:59 GMT 2016 — 30

  • Пн, 29 февраля, 00:00:00 по Гринвичу 2016 — Вт, 29 марта, 23:59:59 по британскому времени 2016 — 30……….. по британскому летнему времени

  • Ср. 30 марта 00:00:00 BST 2016 — Пт. 29 апр. 23:59:59 BST 2016 — 31. По британскому летнему времени по британскому летнему времени

  • Сб, 30 апреля, 00:00:00 BST 2016 — Вс, 29 мая, 23:59:59 BST, 2016 — 30……….. по британскому летнему времени по британскому летнему времени

  • Пн, 30 мая, 00:00:00 по британскому летнему времени 2016 — Ср, 29 июня, 23:59:59 по британскому летнему времени 2016 — 31

  • Чт, 30 июня, 00:00:00 по британскому летнему времени 2016 — Пт, 29 июля, 23:59:59 по британскому летнему времени 2016 — 30

  • Сб, 30 июля, 00:00:00 BST 2016 — Пн, 29 августа, 23:59:59 BST, 2016 — 31……….. по британскому летнему времени по британскому летнему времени

  • Вт, 30 августа, 00:00:00 BST 2016 — Чт, 29 сентября, 23:59:59 BST, 2016 — 31……….. по британскому летнему времени по британскому летнему времени.

  • Пт, 30 сентября, 00:00:00 BST 2016 — Сб, 29 октября, 23:59:59 BST, 2016 — 30……….. по британскому летнему времени по британскому летнему времени.

  • ВС окт 30 00:00:00 по британскому летнему времени 2016 — Вт Ноя 29 23:59:59 по Гринвичу 2016 — 31

  • Ср. 30 ноября 00:00:00 GMT 2016 — Чт. 29 дек. 23:59:59 GMT 2016 — 30

  • Пт Дек 30 00:00:00 GMT 2016 — Вс Янв 29 23:59:59 GMT 2017 — 31

*/

Мои результаты

Сб 30 января 00:00:00 EET 2016 ::::: Вс 28 февраля 23:59:59 EET 2016 ::::: 29

Пн 29 февраля 00:00:00 EET 2016 ::::: Пн 28 марта 23:59:59 EEST 2016 ::::: 28

Tue Mar 29 00:00:00 EEST 2016 ::::: Thu Apr 28 23:59:59 EEST 2016 ::::: 30

Пт 29 апреля 00:00:00 EEST 2016 ::::: Сб 28 мая 23:59:59 EEST 2016 ::::: 29

Вс 29 мая 00:00:00 EEST 2016 ::::: Вт 28 июня 23:59:59 EEST 2016 ::::: 30

Ср. 29 июня 00:00:00 EEST 2016 ::::: Чт. 28 июля 23:59:59 EEST 2016 ::::: 29

Пт 29 июля 00:00:00 EEST 2016 ::::: Вс 28 августа 23:59:59 EEST 2016 ::::: 30

Пн Авг 29 00:00:00 EEST 2016 ::::: Ср 28 сентября 23:59:59 EEST 2016 ::::: 30

Чт 29 сентября 00:00:00 EEST 2016 ::::: Пт 28 октября 23:59:59 EEST 2016 ::::: 29

Sat Oct 29 00:00:00 EEST 2016 ::::: Mon Nov 28 23:59:59 EET 2016 ::::: 31

Tue Nov 29 00:00:00 EET 2016 ::::: Wed Dec 28 23:59:59 EET 2016 ::::: 29

Thu Dec 29 00:00:00 EET 2016 ::::: Sat Jan 28 23:59:59 EET 2017 ::::: 30

So I’m not sure if this happens because of the February month or because 2016 was a leap year that included 29th of February.

What am I missing? I have the same problem run multiple test cases and all others are OK, but this.

Я также пытался сделать это с помощью Javas 8 LocalDate и LocalDateTime, и я получаю точно такие же результаты.

Вот мой код

 import org.apache.commons.lang.time.DateUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


public class Main {

    public static void main(String[] args) throws ParseException {
        String startDate = "30 Jan 2016";
        String endDate = "29 Jan 2017"; // current not using this ?!
        SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy");

        Date start = formatter.parse(startDate);

        List<Item> items = new ArrayList<>();

        for (int i = 0; i < 12; i  ) {
            Date end = DateUtils.addMonths(start, 1);
            end = DateUtils.addSeconds(end, -1);
            items.add(new Item(start, end));
            start = DateUtils.addMonths(start, 1);
        }

        items.forEach(item -> {
            System.out.println(item.getStart()   " ::::: "   item.getEnd()   " ::::: "   getDifferenceDays(item.getStart(), item.getEnd()));
        });

    }

    public static long getDifferenceDays(Date d1, Date d2) {
        return ChronoUnit.DAYS.between(d1.toInstant(), d2.toInstant());
    }

}
  
  • Класс элемента
 import java.util.Date;

public class Item {
    Date start;
    Date end;

    public Item(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return "Item{"  
                "start="   start  
                ", end="   end  
                '}';
    }
}
  

Комментарии:

1. Ну, для начала, вы не должны использовать Date , Calendar и SimpleDateFormat классы. Они доставляют хлопоты. Вместо этого используйте классы из java.time .

2. Как я уже сказал… Я использовал Javas 8 LocalDate и LocalDateTime, и я получаю такое же поведение.

3. Вы пробовали docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html ; установка зоны. О, я вижу, что DateUtils — это внешняя библиотека, вспомните мой комментарий по этому поводу.

4. DateUtils взят из import org.apache.commons.lang.time. DateUtils; Я не понимаю, что именно вы подразумеваете под классом ZonedDateTime. Тем не менее O изменил дату моего компьютера на GTM, и я не получаю те же часовые пояса, но поведение даты остается прежним.

5. PeterMmm правильно: вы неправильно используете DateUtils. Но что более важно, если вы используете Java 8 или выше, вам ГОРАЗДО лучше использовать вместо этого новое Java «time». ПРЕДЛОЖЕНИЕ: Пожалуйста, попробуйте совет Ole V.V.В. Если это работает, пожалуйста, «Проголосуйте» и «Примите» его.

Ответ №1:

java.time

Поскольку вы можете использовать java.time, современный Java date и time API, я рекомендую вам придерживаться этого и оставить старые классы SimpleDateFormat и Date в покое. Тогда вам также не нужен Apache DateUtils . ChronoUnit Перечисление взято из java.time.

     DateTimeFormatter dateFormatter
            = DateTimeFormatter.ofPattern("d MMM u", Locale.ENGLISH);
    
    String startDateString = "30 Jan 2016";
    LocalDate originalStartDate
            = LocalDate.parse(startDateString, dateFormatter);
    for (int i = 0; i < 12; i  ) {
        LocalDate startDate = originalStartDate.plusMonths(i);
        LocalDate nextStartDate = originalStartDate.plusMonths(i   1);
        LocalDate endDate = nextStartDate.minusDays(1);
        long differenceDays = ChronoUnit.DAYS.between(startDate, nextStartDate);
        
        System.out.format("%s - %s : %d%n", startDate, endDate, differenceDays);
    }
  

Вывод:

 2016-01-30 - 2016-02-28 : 30
2016-02-29 - 2016-03-29 : 30
2016-03-30 - 2016-04-29 : 31
2016-04-30 - 2016-05-29 : 30
2016-05-30 - 2016-06-29 : 31
2016-06-30 - 2016-07-29 : 30
2016-07-30 - 2016-08-29 : 31
2016-08-30 - 2016-09-29 : 31
2016-09-30 - 2016-10-29 : 30
2016-10-30 - 2016-11-29 : 31
2016-11-30 - 2016-12-29 : 30
2016-12-30 - 2017-01-29 : 31
  

Что пошло не так в вашем коде?

За вашими наблюдаемыми неожиданными результатами есть несколько причин.

  1. Когда вы добавляете месяц к 30 января, вы получаете 29 февраля, как и ожидали. В невисокосный год вы получили бы 28 февраля. Когда вы добавляете еще один месяц к 29 февраля, вы получаете 29 марта. Удивительно ли это, когда вы думаете об этом? В невисокосный год вы получили бы 28 марта, поэтому 2016 год, являющийся високосным годом, фактически помог вам приблизиться к желаемому результату. В моем коде я решаю эту проблему, добавляя правильное количество месяцев к исходной дате начала, а не добавляя один месяц к предыдущей дате начала.
  2. Как уже сказал PeterMmm, ChronoUnit.DAYS.Between() считается полных 24 часа в сутки. Любой неполный день отбрасывается. Даже 23 часа 59 минут 59 секунд. С Сб 30 января 00:00:00 EET 2016 по Вс 28 февраля 23:59: 59 EET 2016 составляет 30 дней 23 часа 59 минут 59 секунд, поэтому результат, который вы получаете, составляет 30 дней. В моем коде я решаю проблему, считая дни до начала следующего элемента.

В качестве отступления: с понедельника 29 февраля 00:00:00 EET 2016 по понедельник 28 марта 23:59: 59 EEST 2016 из-за перехода на летнее время (летнее время) составляет всего 28 дней 22 часа 59 минут 59 секунд. Таким образом, воздержание от вычитания секунды не решит вашу проблему в этом случае.

Комментарии:

1. Спасибо!!! Я не смотрел на это так. Дата / Время / Високосные годы — это нечто сложное в программировании. Рад, что Java 8 с пакетом java.time решила многие из этих проблем.

Ответ №2:

Это

 end = DateUtils.addSeconds(end, -1);
  

не требуется. Или сделать

 end = DateUtils.addSeconds(end, 0);
  

взгляните на документацию between().
Второй параметр является эксклюзивным. Это означает, что ваш второй параметр не достигает конца дня, и поэтому учитывается на день меньше (учитываются только полные 24-часовые дни).

Ответ №3:

Вот немного другой подход:

 LocalDate startDate = LocalDate.of(2016, 1, 30);
LocalDate endDate = LocalDate.of(2017, 1, 30);

Function<YearMonth, LocalDate> addMonthFunction = ym -> ym
    .atDay(Math.min(startDate.getDayOfMonth(), ym.lengthOfMonth()));

long months = ChronoUnit.MONTHS.between(startDate, endDate);
YearMonth yearMonth = YearMonth.from(startDate);
Stream.iterate(yearMonth, ym -> ym.plusMonths(1))
    .limit(months)
    .map(addMonthFunction)
    .map(LocalDate::atStartOfDay)
    .map(date -> {
        LocalDateTime end = addMonthFunction.apply(YearMonth.from(date.plusMonths(1)))
            .atStartOfDay()
            .minusSeconds(1);
        long between = ChronoUnit.DAYS.between(date, end)   1;
        return String.format("%s to %s (%s days)", date, end, between);
    })
    .forEach(System.out::println);
  

Идея заключается в том, что каждая следующая дата всегда приходится на 30-й день месяца (или, точнее, на тот же день месяца, что и дата начала), за исключением случаев, когда это будет недопустимая дата.

Я использовал LocalDate здесь s, но вы могли бы сделать его чувствительным к часовому поясу, используя ZonedDateTime .

Комментарии:

1. Очень интересно! Спасибо.