Skip to content

Commit

Permalink
Rewrite YEARLY expansion, implement #44, fixes #40
Browse files Browse the repository at this point in the history
This commit introduces code to rewrite the expansion parts before building the iterator. This way many expanders can be simplified significantly.
  • Loading branch information
dmfs committed Sep 24, 2018
1 parent 11cb71a commit d76e579
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 33 deletions.
74 changes: 74 additions & 0 deletions src/main/java/org/dmfs/rfc5545/recur/ByWeekNoFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2018 Marten Gajda <[email protected]>
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.dmfs.rfc5545.recur;

import org.dmfs.rfc5545.Instance;
import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics;
import org.dmfs.rfc5545.recur.RecurrenceRule.Part;


/**
* A filter that limits recurrence rules by week number. Note, neither RFC 5545 nor RFC 2445 specify filtering by week number. This is meant for internal use
* only.
*
* @author Marten Gajda
*/
final class ByWeekNoFilter implements ByFilter
{
/**
* An array of the week numbers.
*/
private final int[] mWeekNumbers;

/**
* The {@link CalendarMetrics} to use.
*/
final CalendarMetrics mCalendarMetrics;


public ByWeekNoFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics)
{
mCalendarMetrics = calendarMetrics;
mWeekNumbers = StaticUtils.ListToSortedArray(rule.getByPart(Part.BYWEEKNO));
}


@Override
public boolean filter(long instance)
{
int year = Instance.year(instance);
int week = mCalendarMetrics.getWeekOfYear(year, Instance.month(instance), Instance.dayOfMonth(instance));
int weeks;
if (week > 10 && Instance.month(instance) == 1)
{
// week belongs to the previous iso year
weeks = mCalendarMetrics.getWeeksPerYear(year - 1);
}
else if (week == 1 && Instance.month(instance) > 1)
{
// week belongs to the next iso year
weeks = mCalendarMetrics.getWeeksPerYear(year + 1);
}
else
{
weeks = mCalendarMetrics.getWeeksPerYear(year);
}

return (StaticUtils.linearSearch(mWeekNumbers, week) < 0 && StaticUtils.linearSearch(mWeekNumbers, week - 1 - weeks) < 0) || week > weeks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void expand(long instance, long start)
if (0 < actualDay && actualDay <= yearDays && newWeek == oldWeek)
{
int monthAndDay = mCalendarMetrics.getMonthAndDayOfYearDay(year, actualDay);
addInstance(Instance.setMonthAndDayOfMonth(year, CalendarMetrics.packedMonth(monthAndDay), CalendarMetrics.dayOfMonth(monthAndDay)));
addInstance(Instance.setMonthAndDayOfMonth(instance, CalendarMetrics.packedMonth(monthAndDay), CalendarMetrics.dayOfMonth(monthAndDay)));
}
else if (0 < nextYearDay && nextYearDay <= nextYearDays && nextYearDay < 7)
{
Expand Down
211 changes: 190 additions & 21 deletions src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;


Expand Down Expand Up @@ -340,29 +342,14 @@ boolean expands(RecurrenceRule rule)
* <p>
* TODO: validate year days
*/
BYYEARDAY(new ListValueConverter<Integer>(new IntegerConverter(-366, 366).noZero()))
BYYEARDAY(new ListValueConverter<>(new IntegerConverter(-366, 366).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{

ByExpander.Scope scope = rule.getFreq() == Freq.WEEKLY || rule.hasPart(Part.BYWEEKNO) ? rule.hasPart(
Part.BYMONTH) ? ByExpander.Scope.WEEKLY_AND_MONTHLY : ByExpander.Scope.WEEKLY : rule
.getFreq() == Freq.YEARLY && !rule.hasPart(Part.BYMONTH) ? ByExpander.Scope.YEARLY : ByExpander.Scope.MONTHLY;

switch (scope)
{
case WEEKLY:
return new ByYearDayWeeklyExpander(rule, previous, calendarMetrics, start);
case WEEKLY_AND_MONTHLY:
return new ByYearDayWeeklyAndMonthlyExpander(rule, previous, calendarMetrics, start);
case MONTHLY:
return new ByYearDayMonthlyExpander(rule, previous, calendarMetrics, start);
case YEARLY:
return new ByYearDayYearlyExpander(rule, previous, calendarMetrics, start);
default:
throw new Error("Illegal scope");
}
// RFC 5545 only allows BYYEARDAY expansion for YEARLY rules
// We'll expand it the same way for WEEKLY and MONTHLY though and filter afterwards for other frequencies if allowed by the mode
return new ByYearDayYearlyExpander(rule, previous, calendarMetrics, start);
}


Expand Down Expand Up @@ -506,6 +493,136 @@ boolean expands(RecurrenceRule rule)
}
},

/**
* A special BYMONTH filter for expander rewriting
*/
_BYMONTH_FILTER(new ListValueConverter<>(new MonthConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expander request");
}


@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new TrivialByMonthFilter(rule);
}


@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},

/**
* A special BYWEEKNO filter for expander rewriting
*/
_BYWEEKNO_FILTER(new ListValueConverter<>(new IntegerConverter(-53, 53).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected Expansion request");
}


@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByWeekNoFilter(rule, calendarMetrics);
}


@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},

/**
* A special BYYEARDAY filter for expander rewriting
*/
_BYYEARDAY_FILTER(new ListValueConverter<>(new IntegerConverter(-366, 366).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expander request");
}


@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByYearDayFilter(rule, calendarMetrics);
}


@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},

/**
* A special BYMONTHDAY filter for expander rewriting
*/
_BYMONTHDAY_FILTER(new ListValueConverter<>(new IntegerConverter(-31, 31).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("This filter does not expand.");
}


@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByMonthDayFilter(rule, calendarMetrics);
}


@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},

/**
* A special BYDAY filter for expander rewriting
*/
_BYDAY_FILTER(new ListValueConverter<>(new WeekdayNumConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expansion request");
}


@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByDayFilter(rule, calendarMetrics);
}


@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},

/**
* The hours on which the event recurs. The value must be a list of integers in the range 0 to 23.
*/
Expand Down Expand Up @@ -813,6 +930,45 @@ abstract RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, Ca
}


private final static Set<Part> REWRITE_PARTS = EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY);

private final static Map<Set<Part>, Set<Part>> YEAR_REWRITE_MAP = new HashMap<>(32);

static
{
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));

YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));

YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
}


/**
* This class represents the position of a {@link Weekday} in a specific range. It parses values like <code>-4SU</code> which means the fourth last Sunday
* in the interval or <code>2MO</code> which means the second Monday in the interval. In addition this class accepts simple weekdays like <code>SU</code>
Expand Down Expand Up @@ -974,7 +1130,7 @@ public String toString()
/**
* The parts of this rule.
*/
private EnumMap<Part, Object> mParts = new EnumMap<Part, Object>(Part.class);
private EnumMap<Part, Object> mParts = new EnumMap<>(Part.class);

/**
* A map of x-parts. This is only used in RFC 2445 modes, RFC 5554 doesn't support X-parts.
Expand Down Expand Up @@ -2002,8 +2158,21 @@ else if ((iterator = FastWeeklyIterator.getInstance(this, rScaleCalendarMetrics,
// add SanityFilet of not present yet
mParts.put(Part._SANITY_FILTER, null);

Set<Part> parts = EnumSet.copyOf(mParts.keySet());

if (getFreq() == Freq.YEARLY)
{
Set<Part> rewritableParts = EnumSet.copyOf(parts);
rewritableParts.retainAll(REWRITE_PARTS);
if (YEAR_REWRITE_MAP.containsKey(rewritableParts))
{
parts.removeAll(rewritableParts);
parts.addAll(YEAR_REWRITE_MAP.get(rewritableParts));
}
}

// since FREQ is the first part anyway we don't have to create it separately
for (Part p : mParts.keySet())
for (Part p : parts)
{
// add a filter for each rule part
if (p != Part.INTERVAL && p != Part.WKST && p != Part.RSCALE)
Expand Down
Loading

0 comments on commit d76e579

Please sign in to comment.