Skip to content

Commit

Permalink
Implemented more complete limit/expansion rules according to RFC5545
Browse files Browse the repository at this point in the history
  • Loading branch information
benfortuna committed Dec 14, 2018
1 parent da211ca commit c1a4e59
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 110 deletions.
8 changes: 4 additions & 4 deletions src/main/java/net/fortuna/ical4j/model/Recur.java
Expand Up @@ -320,17 +320,17 @@ public Recur(final Frequency frequency, final int count) {
private void initTransformers() {
transformers = new HashMap<>();
if (secondList != null) {
transformers.put(BYSECOND, new BySecondRule(secondList, Optional.ofNullable(weekStartDay)));
transformers.put(BYSECOND, new BySecondRule(secondList, frequency, Optional.ofNullable(weekStartDay)));
} else {
secondList = new NumberList(0, 59, false);
}
if (minuteList != null) {
transformers.put(BYMINUTE, new ByMinuteRule(minuteList, Optional.ofNullable(weekStartDay)));
transformers.put(BYMINUTE, new ByMinuteRule(minuteList, frequency, Optional.ofNullable(weekStartDay)));
} else {
minuteList = new NumberList(0, 59, false);
}
if (hourList != null) {
transformers.put(BYHOUR, new ByHourRule(hourList, Optional.ofNullable(weekStartDay)));
transformers.put(BYHOUR, new ByHourRule(hourList, frequency, Optional.ofNullable(weekStartDay)));
} else {
hourList = new NumberList(0, 23, false);
}
Expand All @@ -350,7 +350,7 @@ private void initTransformers() {
weekNoList = new NumberList(1, 53, true);
}
if (monthList != null) {
transformers.put(BYMONTH, new ByMonthRule(monthList, Optional.ofNullable(interval),
transformers.put(BYMONTH, new ByMonthRule(monthList, frequency,
Optional.ofNullable(weekStartDay)));
} else {
monthList = new NumberList(1, 12, false);
Expand Down
@@ -0,0 +1,111 @@
package net.fortuna.ical4j.transform.recurrence;

import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.WeekDay;
import net.fortuna.ical4j.transform.Transformer;
import net.fortuna.ical4j.util.Dates;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Optional;

/**
* Subclasses provide implementations to expand (or limit) a list of dates based on rule requirements as
* specified in RFC5545.
*
* <pre>
* 3.3.10. Recurrence Rule
*
* ...
*
* BYxxx rule parts modify the recurrence in some manner. BYxxx rule
* parts for a period of time that is the same or greater than the
* frequency generally reduce or limit the number of occurrences of
* the recurrence generated. For example, "FREQ=DAILY;BYMONTH=1"
* reduces the number of recurrence instances from all days (if
* BYMONTH rule part is not present) to all days in January. BYxxx
* rule parts for a period of time less than the frequency generally
* increase or expand the number of occurrences of the recurrence.
* For example, "FREQ=YEARLY;BYMONTH=1,2" increases the number of
* days within the yearly recurrence set from 1 (if BYMONTH rule part
* is not present) to 2.
*
* If multiple BYxxx rule parts are specified, then after evaluating
* the specified FREQ and INTERVAL rule parts, the BYxxx rule parts
* are applied to the current set of evaluated occurrences in the
* following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY,
* BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are
* evaluated.
*
* The table below summarizes the dependency of BYxxx rule part
* expand or limit behavior on the FREQ rule part value.
*
* The term "N/A" means that the corresponding BYxxx rule part MUST
* NOT be used with the corresponding FREQ value.
*
* BYDAY has some special behavior depending on the FREQ value and
* this is described in separate notes below the table.
*
* +----------+--------+--------+-------+-------+------+-------+------+
* | |SECONDLY|MINUTELY|HOURLY |DAILY |WEEKLY|MONTHLY|YEARLY|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYMONTH |Limit |Limit |Limit |Limit |Limit |Limit |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYWEEKNO |N/A |N/A |N/A |N/A |N/A |N/A |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYYEARDAY |Limit |Limit |Limit |N/A |N/A |N/A |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYMONTHDAY|Limit |Limit |Limit |Limit |N/A |Expand |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYDAY |Limit |Limit |Limit |Limit |Expand|Note 1 |Note 2|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYHOUR |Limit |Limit |Limit |Expand |Expand|Expand |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYMINUTE |Limit |Limit |Expand |Expand |Expand|Expand |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYSECOND |Limit |Expand |Expand |Expand |Expand|Expand |Expand|
* +----------+--------+--------+-------+-------+------+-------+------+
* |BYSETPOS |Limit |Limit |Limit |Limit |Limit |Limit |Limit |
* +----------+--------+--------+-------+-------+------+-------+------+
*
* Note 1: Limit if BYMONTHDAY is present; otherwise, special expand
* for MONTHLY.
*
* Note 2: Limit if BYYEARDAY or BYMONTHDAY is present; otherwise,
* special expand for WEEKLY if BYWEEKNO present; otherwise,
* special expand for MONTHLY if BYMONTH present; otherwise,
* special expand for YEARLY.
* </pre>
*/
public abstract class AbstractDateExpansionRule implements Transformer<DateList>, Serializable {

private final int calendarWeekStartDay;

public AbstractDateExpansionRule() {
// default week start is Monday per RFC5545
this(Optional.of(WeekDay.Day.MO));
}

public AbstractDateExpansionRule(Optional<WeekDay.Day> weekStartDay) {
this.calendarWeekStartDay = WeekDay.getCalendarDay(WeekDay.getWeekDay(weekStartDay.orElse(WeekDay.Day.MO)));
}

/**
* Construct a Calendar object and sets the time.
*
* @param date
* @param lenient
* @return
*/
protected Calendar getCalendarInstance(final Date date, final boolean lenient) {
Calendar cal = Dates.getCalendarInstance(date);
// A week should have at least 4 days to be considered as such per RFC5545
cal.setMinimalDaysInFirstWeek(4);
cal.setFirstDayOfWeek(calendarWeekStartDay);
cal.setLenient(lenient);
cal.setTime(date);

return cal;
}
}

This file was deleted.

Expand Up @@ -15,7 +15,7 @@
* Applies BYDAY rules specified in this Recur instance to the specified date list. If no BYDAY rules are specified
* the date list is returned unmodified.
*/
public class ByDayRule extends AbstractRecurrenceRule {
public class ByDayRule extends AbstractDateExpansionRule {

public enum FilterType {
Daily, Weekly, Monthly, Yearly;
Expand Down
Expand Up @@ -3,27 +3,35 @@
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.NumberList;
import net.fortuna.ical4j.model.Recur.Frequency;
import net.fortuna.ical4j.model.WeekDay;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.util.Dates;

import java.util.Calendar;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;

import static net.fortuna.ical4j.model.Recur.Frequency.*;

/**
* Applies BYHOUR rules specified in this Recur instance to the specified date list. If no BYHOUR rules are
* specified the date list is returned unmodified.
*/
public class ByHourRule extends AbstractRecurrenceRule {
public class ByHourRule extends AbstractDateExpansionRule {

private final NumberList hourList;

public ByHourRule(NumberList hourList) {
private final Frequency frequency;

public ByHourRule(NumberList hourList, Frequency frequency) {
this.hourList = hourList;
this.frequency = frequency;
}

public ByHourRule(NumberList hourList, Optional<WeekDay.Day> weekStartDay) {
public ByHourRule(NumberList hourList, Frequency frequency, Optional<WeekDay.Day> weekStartDay) {
super(weekStartDay);
this.hourList = hourList;
this.frequency = frequency;
}

@Override
Expand All @@ -33,12 +41,51 @@ public DateList transform(DateList dates) {
}
final DateList hourlyDates = Dates.getDateListInstance(dates);
for (final Date date : dates) {
final Calendar cal = getCalendarInstance(date, true);
for (final Integer hour : hourList) {
cal.set(Calendar.HOUR_OF_DAY, hour);
hourlyDates.add(Dates.getInstance(cal.getTime(), hourlyDates.getType()));
if (EnumSet.of(DAILY, WEEKLY, MONTHLY, YEARLY).contains(frequency)) {
hourlyDates.addAll(new ExpansionFilter(hourlyDates.getType()).apply(date));
} else {
hourlyDates.addAll(new LimitFilter(hourlyDates.getType()).apply(date));
}
}
return hourlyDates;
}

private class LimitFilter implements Function<Date, List<Date>> {

private final Value type;

public LimitFilter(Value type) {
this.type = type;
}

@Override
public List<Date> apply(Date date) {
final Calendar cal = getCalendarInstance(date, true);
if (hourList.contains(cal.get(Calendar.HOUR_OF_DAY))) {
return Arrays.asList(date);
}
return Collections.emptyList();
}
}

private class ExpansionFilter implements Function<Date, List<Date>> {

private final Value type;

public ExpansionFilter(Value type) {
this.type = type;
}

@Override
public List<Date> apply(Date date) {
List<Date> retVal = new ArrayList<>();
final Calendar cal = getCalendarInstance(date, true);
// construct a list of possible months..
hourList.forEach(hour -> {
cal.set(Calendar.HOUR_OF_DAY, hour);
retVal.add(Dates.getInstance(cal.getTime(), type));
});
return retVal;
}
}
}
Expand Up @@ -3,27 +3,35 @@
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.NumberList;
import net.fortuna.ical4j.model.Recur.Frequency;
import net.fortuna.ical4j.model.WeekDay;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.util.Dates;

import java.util.Calendar;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;

import static net.fortuna.ical4j.model.Recur.Frequency.*;

/**
* Applies BYMINUTE rules specified in this Recur instance to the specified date list. If no BYMINUTE rules are
* specified the date list is returned unmodified.
*/
public class ByMinuteRule extends AbstractRecurrenceRule {
public class ByMinuteRule extends AbstractDateExpansionRule {

private final NumberList minuteList;

public ByMinuteRule(NumberList minuteList) {
private final Frequency frequency;

public ByMinuteRule(NumberList minuteList, Frequency frequency) {
this.minuteList = minuteList;
this.frequency = frequency;
}

public ByMinuteRule(NumberList minuteList, Optional<WeekDay.Day> weekStartDay) {
public ByMinuteRule(NumberList minuteList, Frequency frequency, Optional<WeekDay.Day> weekStartDay) {
super(weekStartDay);
this.minuteList = minuteList;
this.frequency = frequency;
}

@Override
Expand All @@ -33,12 +41,51 @@ public DateList transform(DateList dates) {
}
final DateList minutelyDates = Dates.getDateListInstance(dates);
for (final Date date : dates) {
final Calendar cal = getCalendarInstance(date, true);
for (final Integer minute : minuteList) {
cal.set(Calendar.MINUTE, minute);
minutelyDates.add(Dates.getInstance(cal.getTime(), minutelyDates.getType()));
if (EnumSet.of(HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY).contains(frequency)) {
minutelyDates.addAll(new ExpansionFilter(minutelyDates.getType()).apply(date));
} else {
minutelyDates.addAll(new LimitFilter(minutelyDates.getType()).apply(date));
}
}
return minutelyDates;
}

private class LimitFilter implements Function<Date, List<Date>> {

private final Value type;

public LimitFilter(Value type) {
this.type = type;
}

@Override
public List<Date> apply(Date date) {
final Calendar cal = getCalendarInstance(date, true);
if (minuteList.contains(cal.get(Calendar.MINUTE))) {
return Arrays.asList(date);
}
return Collections.emptyList();
}
}

private class ExpansionFilter implements Function<Date, List<Date>> {

private final Value type;

public ExpansionFilter(Value type) {
this.type = type;
}

@Override
public List<Date> apply(Date date) {
List<Date> retVal = new ArrayList<>();
final Calendar cal = getCalendarInstance(date, true);
// construct a list of possible minutes..
minuteList.forEach(minute -> {
cal.set(Calendar.MINUTE, minute);
retVal.add(Dates.getInstance(cal.getTime(), type));
});
return retVal;
}
}
}
Expand Up @@ -13,7 +13,7 @@
* Applies BYMONTHDAY rules specified in this Recur instance to the specified date list. If no BYMONTHDAY rules are
* specified the date list is returned unmodified.
*/
public class ByMonthDayRule extends AbstractRecurrenceRule {
public class ByMonthDayRule extends AbstractDateExpansionRule {

private transient Logger log = LoggerFactory.getLogger(ByMonthDayRule.class);

Expand Down

0 comments on commit c1a4e59

Please sign in to comment.