Skip to content

Commit aecb3f6

Browse files
committed
Fix RecurrenceSet result count when iterating RRULEs with a COUNT parameter and an unsynched start (i.e. the start is not part of the rule).
IN such case RecurrenceRuleAdapter has to stop iterating one element earlier to ensure the correct result count. Fixes #34
1 parent 69803da commit aecb3f6

File tree

6 files changed

+309
-6
lines changed

6 files changed

+309
-6
lines changed

.idea/modules/lib-recur.iml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules/lib-recur_main.iml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules/lib-recur_test.iml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2017 Marten Gajda <[email protected]>
3+
*
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.dmfs.rfc5545.recurrenceset;
19+
20+
import java.util.NoSuchElementException;
21+
22+
23+
/**
24+
* An {@link AbstractRecurrenceAdapter.InstanceIterator} which inserts a start instance.
25+
*
26+
* @author Marten Gajda
27+
*/
28+
public final class CountLimitedRecurrenceRuleIterator implements AbstractRecurrenceAdapter.InstanceIterator
29+
{
30+
private final AbstractRecurrenceAdapter.InstanceIterator mDelegate;
31+
private int mRemaining;
32+
33+
34+
public CountLimitedRecurrenceRuleIterator(AbstractRecurrenceAdapter.InstanceIterator delegate, int remaining)
35+
{
36+
mDelegate = delegate;
37+
mRemaining = remaining;
38+
}
39+
40+
41+
@Override
42+
public boolean hasNext()
43+
{
44+
return mRemaining > 0 && mDelegate.hasNext();
45+
}
46+
47+
48+
@Override
49+
public long next()
50+
{
51+
if (!hasNext())
52+
{
53+
throw new NoSuchElementException("No further elements to iterate");
54+
}
55+
mRemaining--;
56+
return mDelegate.next();
57+
}
58+
59+
60+
@Override
61+
public long peek()
62+
{
63+
if (!hasNext())
64+
{
65+
throw new NoSuchElementException("No further elements to iterate");
66+
}
67+
return mDelegate.peek();
68+
}
69+
70+
71+
@Override
72+
public void skip(int count)
73+
{
74+
mRemaining -= count;
75+
mDelegate.skip(count);
76+
}
77+
78+
79+
@Override
80+
public void fastForward(long until)
81+
{
82+
while (hasNext() && peek() < until)
83+
{
84+
next();
85+
}
86+
}
87+
}

src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,14 @@ public RecurrenceRuleAdapter(RecurrenceRule rule)
100100
@Override
101101
AbstractRecurrenceAdapter.InstanceIterator getIterator(TimeZone timezone, long start)
102102
{
103-
return new InstanceIterator(mRrule.iterator(start, timezone));
103+
AbstractRecurrenceAdapter.InstanceIterator iterator = new InstanceIterator(mRrule.iterator(start, timezone));
104+
if (mRrule.getCount() != null && iterator.peek() != start)
105+
{
106+
// we have a count limited rule and an unsynched start date
107+
// since the start date counts as the first element, the RRULE iterator should return one less element.
108+
iterator = new CountLimitedRecurrenceRuleIterator(iterator, mRrule.getCount() - 1);
109+
}
110+
return iterator;
104111
}
105112

106113

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright 2017 Marten Gajda <[email protected]>
3+
*
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.dmfs.rfc5545.recurrenceset;
19+
20+
import org.dmfs.rfc5545.DateTime;
21+
import org.dmfs.rfc5545.recur.RecurrenceRule;
22+
import org.junit.Test;
23+
24+
import java.util.TimeZone;
25+
26+
import static org.hamcrest.CoreMatchers.is;
27+
import static org.junit.Assert.assertThat;
28+
29+
30+
/**
31+
* @author marten
32+
*/
33+
public class RecurrenceRuleAdapterTest
34+
{
35+
@Test
36+
public void testGetIteratorSyncedStartInfinite() throws Exception
37+
{
38+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY"))
39+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
40+
41+
assertThat(iterator.hasNext(), is(true));
42+
assertThat(iterator.hasNext(), is(true));
43+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
44+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
45+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
46+
assertThat(iterator.hasNext(), is(true));
47+
assertThat(iterator.hasNext(), is(true));
48+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
49+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
50+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
51+
assertThat(iterator.hasNext(), is(true));
52+
assertThat(iterator.hasNext(), is(true));
53+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
54+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
55+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
56+
assertThat(iterator.hasNext(), is(true));
57+
assertThat(iterator.hasNext(), is(true));
58+
}
59+
60+
61+
@Test
62+
public void testGetIteratorSyncedStartWithCount() throws Exception
63+
{
64+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=3"))
65+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
66+
67+
assertThat(iterator.hasNext(), is(true));
68+
assertThat(iterator.hasNext(), is(true));
69+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
70+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
71+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
72+
assertThat(iterator.hasNext(), is(true));
73+
assertThat(iterator.hasNext(), is(true));
74+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
75+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
76+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
77+
assertThat(iterator.hasNext(), is(true));
78+
assertThat(iterator.hasNext(), is(true));
79+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
80+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
81+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
82+
assertThat(iterator.hasNext(), is(false));
83+
assertThat(iterator.hasNext(), is(false));
84+
}
85+
86+
87+
@Test
88+
public void testGetIteratorSyncedStartWithUntil() throws Exception
89+
{
90+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20170312T113012Z"))
91+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
92+
93+
assertThat(iterator.hasNext(), is(true));
94+
assertThat(iterator.hasNext(), is(true));
95+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
96+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
97+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()));
98+
assertThat(iterator.hasNext(), is(true));
99+
assertThat(iterator.hasNext(), is(true));
100+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
101+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
102+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp()));
103+
assertThat(iterator.hasNext(), is(true));
104+
assertThat(iterator.hasNext(), is(true));
105+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
106+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
107+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp()));
108+
assertThat(iterator.hasNext(), is(false));
109+
assertThat(iterator.hasNext(), is(false));
110+
}
111+
112+
113+
@Test
114+
public void testGetIteratorUnsyncedStartInfinite() throws Exception
115+
{
116+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=11"))
117+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
118+
119+
// note the unsynced start is not a result, it's added separately by `RecurrenceSet`
120+
assertThat(iterator.hasNext(), is(true));
121+
assertThat(iterator.hasNext(), is(true));
122+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
123+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
124+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
125+
assertThat(iterator.hasNext(), is(true));
126+
assertThat(iterator.hasNext(), is(true));
127+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
128+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
129+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
130+
assertThat(iterator.hasNext(), is(true));
131+
assertThat(iterator.hasNext(), is(true));
132+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
133+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
134+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
135+
assertThat(iterator.hasNext(), is(true));
136+
assertThat(iterator.hasNext(), is(true));
137+
}
138+
139+
140+
@Test
141+
public void testGetIteratorUnsyncedStartWithCount() throws Exception
142+
{
143+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=3;BYMONTHDAY=11"))
144+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
145+
146+
// note the unsynced start is not a result, it's added separately by `RecurrenceSet`
147+
assertThat(iterator.hasNext(), is(true));
148+
assertThat(iterator.hasNext(), is(true));
149+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
150+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
151+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
152+
assertThat(iterator.hasNext(), is(true));
153+
assertThat(iterator.hasNext(), is(true));
154+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
155+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
156+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
157+
assertThat(iterator.hasNext(), is(false));
158+
assertThat(iterator.hasNext(), is(false));
159+
}
160+
161+
162+
@Test
163+
public void testGetIteratorUnsyncedStartWithUntil() throws Exception
164+
{
165+
AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20170312T113012Z;BYMONTHDAY=11"))
166+
.getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp());
167+
168+
// note the unsynced start is not a result, it's added separately by `RecurrenceSet`
169+
assertThat(iterator.hasNext(), is(true));
170+
assertThat(iterator.hasNext(), is(true));
171+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
172+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
173+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp()));
174+
assertThat(iterator.hasNext(), is(true));
175+
assertThat(iterator.hasNext(), is(true));
176+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
177+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
178+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp()));
179+
assertThat(iterator.hasNext(), is(true));
180+
assertThat(iterator.hasNext(), is(true));
181+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
182+
assertThat(iterator.peek(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
183+
assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp()));
184+
assertThat(iterator.hasNext(), is(false));
185+
assertThat(iterator.hasNext(), is(false));
186+
}
187+
188+
189+
@Test
190+
public void testIsInfinite() throws Exception
191+
{
192+
assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY")).isInfinite(), is(true));
193+
assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=10")).isInfinite(), is(false));
194+
assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20171212")).isInfinite(), is(false));
195+
}
196+
197+
198+
@Test
199+
public void testGetLastInstance() throws Exception
200+
{
201+
assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=10"))
202+
.getLastInstance(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()),
203+
is(DateTime.parse("Europe/Berlin", "20171010T113012").getTimestamp()));
204+
assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20171212T101010Z"))
205+
.getLastInstance(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()),
206+
is(DateTime.parse("Europe/Berlin", "20171210T113012").getTimestamp()));
207+
}
208+
}

0 commit comments

Comments
 (0)