Как рассчитать ежемесячную встречаемость в Java

Мне нужно иметь возможность гибко управлять датами для расчета ежемесячных событий.

Если я использую

    SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");

    Calendar calendar = Calendar.getInstance();
    calendar.set(2017, Calendar.AUGUST, 31, 9, 30, 15);
    Calendar next = calendar;
    for (int i = 1; i++<10;) {
        next = (Calendar) calendar.clone();
        next.add(Calendar.MONTH, i);
        System.out.println(sdf.format(next.getTime()));
    }

в результате вы можете увидеть

31-10-2017 09:30:15

30-11-2017 09:30:15

31-12-2017 09:30:15

31-1-2018 09:30:15

28-2-2018 09:30:15

31-3-2018 09:30:15

30-4-2018 09:30:15

31-5-2018 09:30:15

30-6-2018 09:30:15

Кажется, что семантический месяц следующего месяца, если мы добавляем один месяц к дате, мы должны получить в следующем месяце (это правильно), но после семантики вхождения у него есть проблема, потому что принятие должно получить тот же день, но в следующем месяце (как вы можете видеть это). не то же самое иногда. Есть 31, 30, 28). Если нет такой же даты, мы должны null например.

Предоставляет ли java возможность рассчитывать месяцы после семантической стратегии возникновения для решения моей проблемы?

Больше примеров объяснения.

Мне НЕ ТРЕБУЕТСЯ ПОСЛЕДНИЙ МЕСЯЦ. Мне нужны происшествия только из первого объяснения дня 31-го месяца. Я упростил вопрос, насколько это было возможно.

Я могу сделать обходные пути с calendar но я ожидал более элегантного решения.

Если вы используете ical4j для вычисления вхождений, вы получите этот результат. И расчет вхождений должен быть исправлен (это ошибка из-за rfc5545).

import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.component.VEvent

void testCalculateRecurrenceSetOn31th() {
    VEvent event = new ContentBuilder().vevent {
        dtstart('20100831T061500Z', parameters: parameters() {
            value('DATETIME')})
        dtend('20100831T064500Z', parameters: parameters() {
            value('DATETIME')})
        rrule('FREQ=MONTHLY;UNTIL=20110101')
    }

    def dates = event.calculateRecurrenceSet(new Period('20100831T000000/20110131T000000'))

    def expected = new PeriodList(true)
    expected.add new Period('20100831T061500Z/PT30M')
    expected.add new Period('20101031T061500Z/PT30M')
    expected.add new Period('20101231T061500Z/PT30M')

    println dates
    assert dates == expected

    /*
    Assertion failed:

    assert dates == expected
    |     |  |
    |     |  [20100831T061500Z/PT30M, 20101031T061500Z/PT30M, 20101231T061500Z/PT30M]
    |     false
    [20100831T061500Z/PT30M, 20100930T061500Z/PT30M, 20101030T061500Z/PT30M, 20101130T061500Z/PT30M, 20101230T061500Z/PT30M]
    */
}

но если я использую ту же библиотеку с небольшими изменениями:

      rrule('FREQ=MONTHLY;UNTIL=20110101')

->

      rrule('FREQ=MONTHLY;UNTIL=20110101;BYMONTHDAY=31')

рассчитывается чем правильно. Но мне нужно одинаковое вычисление для обоих случаев из-за rfc5545.

Основная проблема в ical4j заключается в Recur.java:

private void increment(final Calendar cal) {
    final int calInterval = (getInterval() >= 1) ? getInterval() : 1;
    cal.add(calIncField, calInterval);
}

2 ответа

java.time

Используйте современные классы java.time, а не старые классы.

Ваш вопрос перегружен и неясен. Вот несколько общих указателей и примеров, которые помогут вам двигаться в правильном направлении.

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

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) ) ;

Если вы хотите добавить месяц, позвоните plusMonths,

LocalDate monthLater = ld.plusMonths( 1 ) ;

Если вы хотите добавить 30 дней, позвоните plusDays,

LocalDate thirtyDaysLater = ld.plusDays( 30 ) ;

Если вы хотите последний день следующего месяца, используйте TemporalAdjuster чтобы получить первое число следующего месяца, затем другое, чтобы получить конец месяца этого дня. Вы найдете такие реализации в TemporalAdjusters класс (обратите внимание на множественное число s).

LocalDate firstOfNextMonth = ld.with( TemporalAdjusters.firstOfNextMonth() ) ;
LocalDate lastOfNextMonth = firstOfNextMonth.with( TemporalAdjusters.lastOfMonth() ) ;

Если вы заботитесь только о тех месяцах, у которых есть 31 день, используйте Month Перечислите, чтобы определить длину. Вы можете написать свою собственную реализацию TemporalAdjuster, как "lastOfTheNextMonthConisting31Days".

if( ld.getMonth().maxLength() == 31 ) { … }

Если вы хотите определенный день каждого месяца, используйте YearMonth учебный класс. Ловушка для DateTimeException если в этом месяце отсутствует этот день месяца, например, 31 февраля.

YearMonth ym = YearMonth.from( ld ) ;
LocalDate thirteenth = ym.atDay( 13 ) ;

Получить тот же день месяца в следующем месяце.

int dayOfMonth = ld.getDayOfMonth() ;
YearMonth my = YearMonth.from( ld ) ;
LocalDate sameDayOfMonthInNextMonth = ym.plusMonths( 1 ).atDay( dayOfMonth ) ; 

При необходимости вы можете добавить время суток и часовой пояс, чтобы получить ZonedDateTime, Если время суток окажется недействительным в эту дату, оно будет скорректировано автоматически. Обязательно изучите корректировочное поведение, чтобы убедиться, что оно соответствует вашим потребностям.

LocalTime lt = LocalTime.of( 9 , 30 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;

Основываясь на комментариях, где было сказано, что вы хотите указать дату последнего дня месяца с 31 днем, вы можете сделать:

SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.AUGUST, 31, 9, 30, 15);
Calendar next = calendar;
for (int i = 1; i < 10; i++) {
    next = (Calendar) calendar.clone();
    next.add(Calendar.MONTH, i);
    // check if month has 31 days
    switch (next.get(Calendar.MONTH)) {
        // months that have 31 days
        case Calendar.JANUARY:
        case Calendar.MARCH:
        case Calendar.MAY:
        case Calendar.JULY:
        case Calendar.AUGUST:
        case Calendar.OCTOBER:
        case Calendar.DECEMBER:
            // set to last day of month
            next.set(Calendar.DAY_OF_MONTH, 31);
            System.out.println(sdf.format(next.getTime()));
            break;
        default:
            // month doesn't have 31 days, do what? (return null? exception? do nothing?)
    }
}

Выход будет:

31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15


Java новый API даты / времени

Старые занятия (Date, Calendar а также SimpleDateFormat) есть много проблем и проблем дизайна, и они заменяются новыми API.

Если вы используете Java 8, рассмотрите возможность использования нового API java.time. Это проще, менее прослушивается и менее подвержено ошибкам, чем старые API.

Если вы используете Java <= 7, вы можете использовать ThreeTen Backport, отличный бэкпорт для новых классов даты / времени в Java 8. А для Android есть ThreeTenABP (подробнее о том, как его использовать здесь).

Код ниже работает для обоих. Разница лишь в именах пакетов (в Java 8 java.time и в ThreeTen Backport (или Android в ThreeTenABP) является org.threeten.bp), но имена классов и методов совпадают.

Чтобы работать только с датой и временем, вы можете использовать LocalDateTime:

// August 31th 2017, at 09:30:15
LocalDateTime d = LocalDateTime.of(2017, 8, 31, 9, 30, 15);

Затем вы создаете TemporalAdjuster это заключает в себе логику получения последнего дня следующего месяца, который имеет 31 день:

// adjust the date to the next month that has 31 days (Java 8 syntax)
TemporalAdjuster lastDayOfNext31daysMonth = temporal -> {
    Temporal t = temporal;
    do {
        // next month
        t = t.plus(1, ChronoUnit.MONTHS);
        // check if month has 31 days
    } while (Month.from(t).maxLength() != 31);
    // adjust to the end of the month
    return t.with(TemporalAdjusters.lastDayOfMonth());
};

Или используя синтаксис Java <= 7:

TemporalAdjuster lastDayOfNext31daysMonth = new TemporalAdjuster() {
    @Override
    public Temporal adjustInto(Temporal temporal) {
        Temporal t = temporal;
        do {
            // next month
            t = t.plus(1, ChronoUnit.MONTHS);
            // check if month has 31 days
        } while (Month.from(t).maxLength() != 31);
        // adjust to the end of the month
        return t.with(TemporalAdjusters.lastDayOfMonth());
    }
};

Наконец, вы используете DateTimeFormatter отформатировать вывод и перебрать даты, корректируя их с помощью TemporalAdjuster:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-M-yyyy hh:mm:ss");
for (int i = 1; i <= 5; i++) {
    // adjust to last day of next month that has 31 days
    d = d.with(lastDayOfNext31daysMonth);
    // format the date
    System.out.println(d.format(fmt));
}

Вывод будет таким же:

31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15

В этом коде я игнорирую месяцы, в которых нет 31 дня. Если вы хотите сделать какое-то действие в этих случаях (например, возврат null или что-то еще), вы можете создать такой метод:

// check if next n-th month has 31 days
public LocalDateTime nextMonth(int n, LocalDateTime d) {
    // get next n-th month
    LocalDateTime date = d.plusMonths(n);
    // if month has 31 days, return it
    if (date.getMonth().maxLength() == 31) {
        return date;
    }
    // otherwise, return null
    return null;
}

Тогда вы бы сделали:

LocalDateTime d = LocalDateTime.of(2017, 8, 31, 9, 30, 15);
for (int i = 1; i < 10; i++) {
    LocalDateTime next = nextMonth(i, d);
    if (next != null) {
        System.out.println(next.format(fmt));
    }
}

Выход также:

31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15

Другие вопросы по тегам