Повторяющаяся логика событий

Я работаю над приложением Groovy/Java calendar-type, которое позволяет пользователю вводить события с датой начала и необязательным повторением. Если это повторяющееся событие, оно может повторяться:

  • ежемесячно в день месяца, который соответствует дате начала.
  • еженедельно в день недели, соответствующем дате начала.
  • каждые 2 недели в день недели, который соответствует дате начала.
  • и др.

Я изначально планировал использовать API календаря Google для выполнения всей логики повторения, но это оказалось огромной PITA, по причинам, которые я буду обсуждать дальше, если кто-то заботится.

Итак, теперь я решил опрокинуть свое решение. Учитывая дату, я хочу выяснить, происходит ли повторное событие в эту дату. Моя логика (в псевдокоде) будет следующей:

public boolean occursOnDate(def date, def event) {

  def firstDate = event.startDate

  if (firstDate > date) {
    return false;

  } else if (event.isWeekly()) {
    return event.dayOfWeek() == date.dayOfWeek()

  } else if (event.isMonthly()) {
    return event.dayOfMonth() == date.dayOfMonth()

  } else {
    // At this point we know the event occurs every X weeks where X > 1
    // Increment firstDate by adding X weeks to it as many times as possible, without
    // going past date
    return firstDate == date
  }  
}

Эта логика кажется разумной, но на самом деле будет довольно много усилий для реализации, если вы рассмотрите все странные случаи кросс (например, как обрабатывать ежемесячное повторяющееся событие в феврале, первое появление которого - 31 января).

Есть ли библиотека, которая может помочь мне реализовать это? Некоторые особенности будут высоко оценены (например, ни один кредит не будет присуждаться за "Использовать время Джоды" ).

Спасибо, Дон

Ответ 1

Соответствующие правила повторения, которые вы хотите, достаточно хорошо указаны в RFC-2445 (в основном, спецификация iCal). Получение мелочей этого правильного может быть довольно привлекательным. Я бы предложил использовать библиотеку google-rfc-2445 для этого или другую реализацию этой спецификации, например iCal4J.

Ответ 2

Я ничего не знаю о Groovy, и моим первым предложением будет Джода, но вы знаете об этом.

Я знаю, что это может показаться излишним для вас и, возможно, даже неприменимо, но Quartz Scheduler отлично справляется со всеми этими правилами повторения и событиями, Вы не могли использовать свои возможности планирования и просто использовать классы Trigger (например, CronTrigger), чтобы рассчитать даты событий для вас.

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

"0 0 12 L *?" - инициировать событие в середине дня каждый последний день месяца (без головных болей с високосными годами и т.д.)

Проблемы с летним временем также обрабатываются.

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

Date firstFireTime = myTrigger.getNextFireTime();
...
while (...) {
    Date nextFireTime = myTrigger.getFireTimeAfter(previousFireTime);
    ...
}

Надеюсь, это может быть полезно.

Ответ 3

Я не очень хорошо знаком с библиотекой Groovy, но поскольку Groovy работает на JVM, мы предполагаем, что вы также сможете использовать библиотеку Java/Scala.

Здесь вам нужна профессиональная библиотека генерации расписания, такая как Lamma (http://lamma.io) вместо библиотеки времени времени общего назначения, такой как Joda.

    // monthly on a date of the month that corresponds to the start date
    // output: [Date(2014,6,10), Date(2014,7,10), Date(2014,8,10), Date(2014,9,10), Date(2014,10,10)]
    System.out.println(Dates.from(2014, 6, 10).to(2014, 10, 10).byMonth().build());

    // weekly on a day of the week of that corresponds to the start date
    // output: [Date(2014,6,10), Date(2014,6,17), Date(2014,6,24), Date(2014,7,1), Date(2014,7,8)]
    System.out.println(Dates.from(2014, 6, 10).to(2014, 7, 10).byWeek().build());

    // every 2 weeks on a day of the week of that corresponds to the start date
    // output: [Date(2014,6,10), Date(2014,6,24), Date(2014,7,8)]
    System.out.println(Dates.from(2014, 6, 10).to(2014, 7, 10).byWeeks(2).build());

    // edge cases are handled properly, for example, leap day
    // output: [Date(2012,2,29), Date(2013,2,28), Date(2014,2,28), Date(2015,2,28), Date(2016,2,29)]
    System.out.println(Dates.from(2012, 2, 29).to(2016, 2, 29).byYear().build());