From 890c5bf0822b088e60c7d58b508828fd76caf68e Mon Sep 17 00:00:00 2001 From: Marten Gajda Date: Mon, 24 Sep 2018 13:37:03 +0200 Subject: [PATCH] Rewrite YEARLY expansion, implement #44, fixes #40 (#53) This commit introduces code to rewrite the expansion parts before building the iterator. This way many expanders can be simplified significantly. --- .../dmfs/rfc5545/recur/ByWeekNoFilter.java | 74 ++++++ .../recur/ByYearDayWeeklyExpander.java | 2 +- .../dmfs/rfc5545/recur/RecurrenceRule.java | 211 ++++++++++++++++-- .../rfc5545/recur/RecurrenceIteratorTest.java | 38 +++- .../recur/RecurrenceRuleYearlyTest.java | 6 + 5 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/dmfs/rfc5545/recur/ByWeekNoFilter.java diff --git a/src/main/java/org/dmfs/rfc5545/recur/ByWeekNoFilter.java b/src/main/java/org/dmfs/rfc5545/recur/ByWeekNoFilter.java new file mode 100644 index 0000000..ffa4104 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recur/ByWeekNoFilter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 Marten Gajda + * + * + * 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; + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recur/ByYearDayWeeklyExpander.java b/src/main/java/org/dmfs/rfc5545/recur/ByYearDayWeeklyExpander.java index 48d51da..1946cee 100644 --- a/src/main/java/org/dmfs/rfc5545/recur/ByYearDayWeeklyExpander.java +++ b/src/main/java/org/dmfs/rfc5545/recur/ByYearDayWeeklyExpander.java @@ -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) { diff --git a/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java b/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java index b4d7b0e..5d730e0 100644 --- a/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java +++ b/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java @@ -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; @@ -340,29 +342,14 @@ boolean expands(RecurrenceRule rule) *

* TODO: validate year days */ - BYYEARDAY(new ListValueConverter(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); } @@ -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. */ @@ -813,6 +930,45 @@ abstract RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, Ca } + private final static Set REWRITE_PARTS = EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY); + + private final static Map, Set> 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 -4SU which means the fourth last Sunday * in the interval or 2MO which means the second Monday in the interval. In addition this class accepts simple weekdays like SU @@ -974,7 +1130,7 @@ public String toString() /** * The parts of this rule. */ - private EnumMap mParts = new EnumMap(Part.class); + private EnumMap 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. @@ -2002,8 +2158,21 @@ else if ((iterator = FastWeeklyIterator.getInstance(this, rScaleCalendarMetrics, // add SanityFilet of not present yet mParts.put(Part._SANITY_FILTER, null); + Set parts = EnumSet.copyOf(mParts.keySet()); + + if (getFreq() == Freq.YEARLY) + { + Set 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) diff --git a/src/test/java/org/dmfs/rfc5545/recur/RecurrenceIteratorTest.java b/src/test/java/org/dmfs/rfc5545/recur/RecurrenceIteratorTest.java index 2336c34..e613b71 100644 --- a/src/test/java/org/dmfs/rfc5545/recur/RecurrenceIteratorTest.java +++ b/src/test/java/org/dmfs/rfc5545/recur/RecurrenceIteratorTest.java @@ -12,7 +12,7 @@ * 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; @@ -498,9 +498,9 @@ else if (!rule.allday) { /* * Add a number of recurrence rules taken from the internet. - */ + */ - /* rules limited by COUNT */ + /* rules limited by COUNT */ mTestRules.add(new TestRule("FREQ=DAILY;COUNT=1").setCount(1)); mTestRules.add(new TestRule("FREQ=DAILY;COUNT=10").setCount(10)); mTestRules.add(new TestRule("FREQ=DAILY;COUNT=5").setCount(5)); @@ -572,7 +572,7 @@ else if (!rule.allday) mTestRules.add(new TestRule("FREQ=YEARLY;COUNT=4;INTERVAL=4").setCount(4)); mTestRules.add(new TestRule("FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYMONTHDAY=1;COUNT=5;WKST=SU").setCount(5).setMonths(1).setMonthdays(1)); - /* rules limited by UNTIL */ + /* rules limited by UNTIL */ mTestRules.add(new TestRule("FREQ=DAILY;INTERVAL=14;UNTIL=20130620T035959Z;WKST=SU").setUntil("20130620T035959Z")); mTestRules.add(new TestRule("FREQ=DAILY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR;UNTIL=20130928T065959Z;WKST=SU").setUntil("20130928T065959Z").setWeekdays( Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY)); @@ -825,7 +825,7 @@ else if (!rule.allday) mTestRules.add(new TestRule("FREQ=YEARLY;UNTIL=21000613T000000Z;INTERVAL=1;BYMONTH=6").setUntil("21000613T000000Z").setMonths(6)); mTestRules.add(new TestRule("UNTIL=20070721T000000Z;FREQ=DAILY").setUntil("20070721T000000Z")); - /* unlimited test rules */ + /* unlimited test rules */ mTestRules.add(new TestRule("FREQ=DAILY")); mTestRules.add(new TestRule("FREQ=DAILY;BYDAY=SU,MO,TU,WE,TH,FR,SA")); mTestRules.add(new TestRule("FREQ=DAILY;BYMINUTE=0,20,40;BYHOUR=9,10,11,12,13,14,15,16")); @@ -1043,9 +1043,9 @@ else if (!rule.allday) "FREQ=MONTHLY;BYMONTH=1,2,3,4,5,6,7,8,9,10,11,12;BYMONTHDAY=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,-11,-12,-13,-14,-15,-16,-17,-18,-19,-20,-21,-22,-23,-24,-25,-26,-27,-28,-29,-30,-31;BYSETPOS=1,2,3") .setMonthdays(1, 2, 3)); - /* + /* * Rules with a specific number of instances - */ + */ // first 9 mondays of 2016 mTestRules.add(new TestRule("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1,2;UNTIL=20161231").setStart("20160104").setUntil("20161231").setInstances(9) @@ -1208,6 +1208,22 @@ else if (!rule.allday) "FREQ=YEARLY;BYMONTHDAY=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31;BYDAY=2TH;UNTIL=20171231") .setStart("20130101").setUntil("20171231").setMonths(1).setWeekdays(Calendar.THURSDAY).setMonthdays(8, 9, 10, 11, 12, 13, 14).setInstances(5)); + mTestRules.add(new TestRule( + "FREQ=YEARLY;BYMONTH=1,3;BYYEARDAY=1,31,59,60,61,62;COUNT=30").setStart("20180101") + .setMonths(1, 3) + .setMonthdays(1, 31, 28, 29, 2, 3) + .setCount(30)); + mTestRules.add(new TestRule( + "FREQ=YEARLY;BYWEEKNO=1,3,4,5,8,9;BYYEARDAY=1,14,31,60;UNTIL=20181231").setStart("20180101") + .setMonths(1, 3) + .setMonthdays(1, 31) + .setInstances(3)); + mTestRules.add(new TestRule( + "FREQ=YEARLY;BYWEEKNO=1,3,4,5,8,9;BYYEARDAY=1,14,31,60;BYDAY=WE;UNTIL=20181231").setStart("20180101") + .setMonths(1) + .setMonthdays(31) + .setInstances(1)); + /** * Interval tests */ @@ -1293,7 +1309,7 @@ else if (!rule.allday) mTestRules.add(new TestRule("FREQ=YEARLY;UNTIL=20120731T000000;BYHOUR=12").setStart("20120707T000000").setInstances(1).setMonths(7).setMonthdays(7) .setHours(12)); - /* Tests for RSCALE */ + /* Tests for RSCALE */ mTestRules.add(new TestRule("FREQ=YEARLY;RSCALE=gregorian;BYMONTH=2;BYMONTHDAY=29;SKIP=FORWARD;UNTIL=29171231", RfcMode.RFC2445_LAX) .setStart("20130301").setUntil("29171231").setMonths(2, 3).setMonthdays(1, 29).setInstances(904)); @@ -1318,12 +1334,12 @@ else if (!rule.allday) mTestRules.add(new TestRule("FREQ=MONTHLY;INTERVAL=48;RSCALE=gregorian;BYMONTH=2;BYMONTHDAY=29;SKIP=BACKWARD;UNTIL=29171231", RfcMode.RFC2445_LAX) .setStart("20130228").setUntil("29171231").setMonths(2).setMonthdays(28).setInstances(227)); - /* Special rules for the skip all but last test */ + /* Special rules for the skip all but last test */ mTestRules.add(new TestRule("FREQ=MONTHLY;INTERVAL=3;COUNT=10;BYMONTHDAY=10,11,12,13,14,15").setStart("20090714").setCount(10) .setMonthdays(10, 11, 12, 13, 14, 15).setLastInstance("20100111")); - /* Special test for fast birthday iterator */ + /* Special test for fast birthday iterator */ mTestRules.add(new TestRule("FREQ=MONTHLY;INTERVAL=1;COUNT=10;BYMONTH=10,BYMONTHDAY=10").setStart("20091010").setCount(10).setMonths(10) .setMonthdays(10).setLastInstance("20181010")); @@ -1348,7 +1364,7 @@ else if (!rule.allday) mTestRules.add(new TestRule("FREQ=MONTHLY;INTERVAL=10;COUNT=10;BYMONTH=10,BYMONTHDAY=10").setStart("20091010").setCount(10).setMonths(10) .setMonthdays(10).setLastInstance("20541010")); - /* Test time zone related issues */ + /* Test time zone related issues */ mTestRules.add(new TestRule("FREQ=DAILY;UNTIL=20160521T060000Z").setStart("20160520T080000", "Europe/Berlin") .setInstances(2) diff --git a/src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleYearlyTest.java b/src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleYearlyTest.java index 2d36afa..73df63f 100644 --- a/src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleYearlyTest.java +++ b/src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleYearlyTest.java @@ -82,6 +82,12 @@ public void test() throws InvalidRecurrenceRuleException instances(are(inMonth(1, 2), inWeekOfYear(1, 7), onWeekDay(MO) /* inherited weekday */)), startingWith("20180101", "20180212", "20190211", "20200210", "20210104", "20210215", "20220103", "20220214")))); + assertThat(new RecurrenceRule("FREQ=YEARLY;BYWEEKNO=1,3,4,5,8,9;BYYEARDAY=1,14,31,60"), + is(validRule(DateTime.parse("20180101"), + walking(), + instances(are(inMonth(12, 1, 2, 3), inWeekOfYear(1, 3, 4, 5, 8, 9), onDayOfYear(1, 14, 31, 60))), + startingWith("20180101", "20180131", "20180301", "20190101", "20190114", "20190131", "20190301", "20200101", "20200114", "20200131")))); + assertThat(new RecurrenceRule("FREQ=YEARLY;BYMONTH=1,3;BYYEARDAY=1,14,31,32,60,61"), is(validRule(DateTime.parse("20100101"), walking(),