Skip to content

Commit 803edb5

Browse files
authored
Fix iteration of exception that falls on last instance, fixes #93 (#94)
This fix ensures an exception that happens to fall on the last instance of a recurrence set is not returned as an instance.
1 parent 5d5c7e8 commit 803edb5

File tree

2 files changed

+140
-84
lines changed

2 files changed

+140
-84
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,15 @@ public class RecurrenceSetIterator
5353
* Create a new recurrence iterator for specific lists of instances and exceptions.
5454
*
5555
* @param instances
56-
* The instances, must not be <code>null</code> or empty.
56+
* The instances, must not be <code>null</code> or empty.
5757
* @param exceptions
58-
* The exceptions, may be null.
58+
* The exceptions, may be null.
5959
*/
6060
RecurrenceSetIterator(List<InstanceIterator> instances, List<InstanceIterator> exceptions)
6161
{
6262
mInstances = instances.size() == 1 ? instances.get(0) : new CompositeIterator(instances);
6363
mExceptions = exceptions == null || exceptions.isEmpty() ? new EmptyIterator() :
64-
exceptions.size() == 1 ? exceptions.get(0) : new CompositeIterator(exceptions);
64+
exceptions.size() == 1 ? exceptions.get(0) : new CompositeIterator(exceptions);
6565
pullNext();
6666
}
6767

@@ -71,7 +71,7 @@ public class RecurrenceSetIterator
7171
* be set before you start iterating, otherwise you may get wrong results.
7272
*
7373
* @param end
74-
* The date at which to stop the iteration in milliseconds since the epoch.
74+
* The date at which to stop the iteration in milliseconds since the epoch.
7575
*/
7676
RecurrenceSetIterator setEnd(long end)
7777
{
@@ -97,7 +97,7 @@ public boolean hasNext()
9797
* @return The time in milliseconds since the epoch of the next instance.
9898
*
9999
* @throws ArrayIndexOutOfBoundsException
100-
* if there are no more instances.
100+
* if there are no more instances.
101101
*/
102102
public long next()
103103
{
@@ -115,7 +115,7 @@ public long next()
115115
* Fast forward to the next instance at or after the given date.
116116
*
117117
* @param until
118-
* The date to fast forward to in milliseconds since the epoch.
118+
* The date to fast forward to in milliseconds since the epoch.
119119
*/
120120
public void fastForward(long until)
121121
{
@@ -148,6 +148,8 @@ private void pullNext()
148148
{
149149
throw new RuntimeException(String.format(Locale.ENGLISH, "Skipped too many (%d) instances", MAX_SKIPPED_INSTANCES));
150150
}
151+
// we've skipped the next instance, this might have bene the last one
152+
next = Long.MAX_VALUE;
151153
}
152154
mNextInstance = next;
153155
mNextException = nextException;

src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIteratorTest.java

Lines changed: 132 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717

1818
package org.dmfs.rfc5545.recurrenceset;
1919

20+
import org.dmfs.iterators.AbstractBaseIterator;
2021
import org.dmfs.rfc5545.DateTime;
2122
import org.dmfs.rfc5545.Duration;
2223
import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException;
2324
import org.dmfs.rfc5545.recur.RecurrenceRule;
2425
import org.junit.Test;
2526

27+
import java.util.NoSuchElementException;
2628
import java.util.TimeZone;
2729
import java.util.concurrent.TimeUnit;
2830

2931
import static java.util.Arrays.asList;
3032
import static org.dmfs.jems.hamcrest.matchers.GeneratableMatcher.startsWith;
33+
import static org.dmfs.jems.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf;
3134
import static org.hamcrest.Matchers.is;
3235
import static org.junit.Assert.assertThat;
3336

@@ -48,8 +51,8 @@ public void testExceptionsAllDay()
4851
TimeZone testZone = TimeZone.getTimeZone("UTC");
4952
DateTime start = DateTime.parse("20180101");
5053
RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator(
51-
asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())),
52-
asList(new RecurrenceList("20180102,20180103", testZone).getIterator(testZone, start.getTimestamp())));
54+
asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())),
55+
asList(new RecurrenceList("20180102,20180103", testZone).getIterator(testZone, start.getTimestamp())));
5356

5457
// note we call hasNext twice to ensure it's idempotent
5558
assertThat(recurrenceSetIterator.hasNext(), is(true));
@@ -72,9 +75,9 @@ public void testMultipleExceptionsAllDay()
7275
TimeZone testZone = TimeZone.getTimeZone("UTC");
7376
DateTime start = DateTime.parse("20180101");
7477
RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator(
75-
asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())),
76-
asList(new RecurrenceList("20180103", testZone).getIterator(testZone, start.getTimestamp()),
77-
new RecurrenceList("20180102", testZone).getIterator(testZone, start.getTimestamp())));
78+
asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())),
79+
asList(new RecurrenceList("20180103", testZone).getIterator(testZone, start.getTimestamp()),
80+
new RecurrenceList("20180102", testZone).getIterator(testZone, start.getTimestamp())));
7881

7982
// note we call hasNext twice to ensure it's idempotent
8083
assertThat(recurrenceSetIterator.hasNext(), is(true));
@@ -97,9 +100,9 @@ public void testExceptions()
97100
TimeZone testZone = TimeZone.getTimeZone("UTC");
98101
DateTime start = DateTime.parse("20180101T120000");
99102
RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator(
100-
asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone,
101-
start.getTimestamp())),
102-
asList(new RecurrenceList("20180102T120000,20180103T120000", testZone).getIterator(testZone, start.getTimestamp())));
103+
asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone,
104+
start.getTimestamp())),
105+
asList(new RecurrenceList("20180102T120000,20180103T120000", testZone).getIterator(testZone, start.getTimestamp())));
103106

104107
// note we call hasNext twice to ensure it's idempotent
105108
assertThat(recurrenceSetIterator.hasNext(), is(true));
@@ -122,10 +125,10 @@ public void testMultipleExceptions()
122125
TimeZone testZone = TimeZone.getTimeZone("UTC");
123126
DateTime start = DateTime.parse("20180101T120000");
124127
RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator(
125-
asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone,
126-
start.getTimestamp())),
127-
asList(new RecurrenceList("20180103T120000", testZone).getIterator(testZone, start.getTimestamp()),
128-
new RecurrenceList("20180102T120000", testZone).getIterator(testZone, start.getTimestamp())));
128+
asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone,
129+
start.getTimestamp())),
130+
asList(new RecurrenceList("20180103T120000", testZone).getIterator(testZone, start.getTimestamp()),
131+
new RecurrenceList("20180102T120000", testZone).getIterator(testZone, start.getTimestamp())));
129132

130133
// note we call hasNext twice to ensure it's idempotent
131134
assertThat(recurrenceSetIterator.hasNext(), is(true));
@@ -156,19 +159,19 @@ public void testMultipleRules() throws InvalidRecurrenceRuleException
156159
RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp());
157160

158161
assertThat(() -> it::next, startsWith(
159-
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
160-
new DateTime(DateTime.UTC, 2019, 1, 1, 5, 0, 0).getTimestamp(),
161-
new DateTime(DateTime.UTC, 2019, 1, 1, 10, 0, 0).getTimestamp(),
162-
new DateTime(DateTime.UTC, 2019, 1, 1, 15, 0, 0).getTimestamp(),
163-
new DateTime(DateTime.UTC, 2019, 1, 1, 20, 0, 0).getTimestamp(),
164-
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
165-
new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(),
166-
new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(),
167-
new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(),
168-
new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(),
169-
new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(),
170-
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
171-
new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp()
162+
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
163+
new DateTime(DateTime.UTC, 2019, 1, 1, 5, 0, 0).getTimestamp(),
164+
new DateTime(DateTime.UTC, 2019, 1, 1, 10, 0, 0).getTimestamp(),
165+
new DateTime(DateTime.UTC, 2019, 1, 1, 15, 0, 0).getTimestamp(),
166+
new DateTime(DateTime.UTC, 2019, 1, 1, 20, 0, 0).getTimestamp(),
167+
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
168+
new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(),
169+
new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(),
170+
new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(),
171+
new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(),
172+
new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(),
173+
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
174+
new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp()
172175
));
173176
}
174177

@@ -191,24 +194,45 @@ public void testMultipleRulesWithSameValues() throws InvalidRecurrenceRuleExcept
191194
RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp());
192195

193196
assertThat(() -> it::next, startsWith(
194-
new DateTime(2019, 1, 2).getTimestamp(), // SA
195-
new DateTime(2019, 1, 5).getTimestamp(), // TU
196-
new DateTime(2019, 1, 6).getTimestamp(), // WE
197-
new DateTime(2019, 1, 9).getTimestamp(), // SA
198-
new DateTime(2019, 1, 12).getTimestamp(), // TU
199-
new DateTime(2019, 1, 13).getTimestamp(), // WE
200-
new DateTime(2019, 1, 16).getTimestamp(), // SA
201-
new DateTime(2019, 1, 19).getTimestamp(), // TU
202-
new DateTime(2019, 1, 20).getTimestamp(), // WE
203-
new DateTime(2019, 1, 23).getTimestamp(), // SA
204-
new DateTime(2019, 1, 26).getTimestamp(), // TU
205-
new DateTime(2019, 1, 27).getTimestamp(), // WE
206-
new DateTime(2019, 2, 2).getTimestamp(), // SA
207-
new DateTime(2019, 2, 5).getTimestamp() // TU
197+
new DateTime(2019, 1, 2).getTimestamp(), // SA
198+
new DateTime(2019, 1, 5).getTimestamp(), // TU
199+
new DateTime(2019, 1, 6).getTimestamp(), // WE
200+
new DateTime(2019, 1, 9).getTimestamp(), // SA
201+
new DateTime(2019, 1, 12).getTimestamp(), // TU
202+
new DateTime(2019, 1, 13).getTimestamp(), // WE
203+
new DateTime(2019, 1, 16).getTimestamp(), // SA
204+
new DateTime(2019, 1, 19).getTimestamp(), // TU
205+
new DateTime(2019, 1, 20).getTimestamp(), // WE
206+
new DateTime(2019, 1, 23).getTimestamp(), // SA
207+
new DateTime(2019, 1, 26).getTimestamp(), // TU
208+
new DateTime(2019, 1, 27).getTimestamp(), // WE
209+
new DateTime(2019, 2, 2).getTimestamp(), // SA
210+
new DateTime(2019, 2, 5).getTimestamp() // TU
208211
));
209212
}
210213

211214

215+
/**
216+
* See https://github.com/dmfs/lib-recur/issues/93
217+
*/
218+
@Test
219+
public void testGithubIssue93() throws InvalidRecurrenceRuleException
220+
{
221+
DateTime start = DateTime.parse("20200414T160000Z");
222+
223+
// Combine all Recurrence Rules into a RecurrenceSet
224+
RecurrenceSet ruleSet = new RecurrenceSet();
225+
ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=WEEKLY;UNTIL=20200511T000000Z;BYDAY=TU")));
226+
ruleSet.addExceptions(new RecurrenceList("20200421T160000Z,20200505T160000Z", DateTime.UTC));
227+
228+
// Create an iterator using the RecurrenceSet
229+
assertThat(() -> new RecurrenceAdapter(ruleSet.iterator(start.getTimeZone(), start.getTimestamp())),
230+
iteratorOf(
231+
DateTime.parse("20200414T160000Z").getTimestamp(),
232+
DateTime.parse("20200428T160000Z").getTimestamp()));
233+
}
234+
235+
212236
@Test
213237
public void testMultipleRulesWithSameValuesAndCount() throws InvalidRecurrenceRuleException
214238
{
@@ -227,22 +251,22 @@ public void testMultipleRulesWithSameValuesAndCount() throws InvalidRecurrenceRu
227251
RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp());
228252

229253
assertThat(() -> it::next, startsWith(
230-
new DateTime(2019, 1, 2).getTimestamp(), // SA
231-
new DateTime(2019, 1, 5).getTimestamp(), // TU
232-
new DateTime(2019, 1, 6).getTimestamp(), // WE
233-
new DateTime(2019, 1, 9).getTimestamp(), // SA
234-
new DateTime(2019, 1, 12).getTimestamp(), // TU
235-
new DateTime(2019, 1, 13).getTimestamp(), // WE
236-
//new DateTime(2019, 1, 16).getTimestamp(), // SA
237-
new DateTime(2019, 1, 19).getTimestamp(), // TU
238-
new DateTime(2019, 1, 20).getTimestamp(), // WE
239-
//new DateTime(2019, 1, 23).getTimestamp(), // SA
240-
new DateTime(2019, 1, 25).getTimestamp(), // MO
241-
new DateTime(2019, 1, 26).getTimestamp(), // TU
242-
new DateTime(2019, 1, 27).getTimestamp(), // WE
243-
//new DateTime(2019, 2, 2).getTimestamp(), // SA
244-
new DateTime(2019, 2, 4).getTimestamp(), // MO
245-
new DateTime(2019, 2, 5).getTimestamp() // TU
254+
new DateTime(2019, 1, 2).getTimestamp(), // SA
255+
new DateTime(2019, 1, 5).getTimestamp(), // TU
256+
new DateTime(2019, 1, 6).getTimestamp(), // WE
257+
new DateTime(2019, 1, 9).getTimestamp(), // SA
258+
new DateTime(2019, 1, 12).getTimestamp(), // TU
259+
new DateTime(2019, 1, 13).getTimestamp(), // WE
260+
//new DateTime(2019, 1, 16).getTimestamp(), // SA
261+
new DateTime(2019, 1, 19).getTimestamp(), // TU
262+
new DateTime(2019, 1, 20).getTimestamp(), // WE
263+
//new DateTime(2019, 1, 23).getTimestamp(), // SA
264+
new DateTime(2019, 1, 25).getTimestamp(), // MO
265+
new DateTime(2019, 1, 26).getTimestamp(), // TU
266+
new DateTime(2019, 1, 27).getTimestamp(), // WE
267+
//new DateTime(2019, 2, 2).getTimestamp(), // SA
268+
new DateTime(2019, 2, 4).getTimestamp(), // MO
269+
new DateTime(2019, 2, 5).getTimestamp() // TU
246270
));
247271
}
248272

@@ -267,14 +291,14 @@ public void testMultipleRulesWithFastForward() throws InvalidRecurrenceRuleExcep
267291
it.fastForward(new DateTime(DateTime.UTC, 2019, 1, 1, 22, 0, 0).getTimestamp());
268292

269293
assertThat(() -> it::next, startsWith(
270-
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
271-
new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(),
272-
new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(),
273-
new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(),
274-
new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(),
275-
new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(),
276-
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
277-
new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp()
294+
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
295+
new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(),
296+
new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(),
297+
new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(),
298+
new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(),
299+
new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(),
300+
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
301+
new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp()
278302
));
279303
}
280304

@@ -299,11 +323,11 @@ public void testFastForwardToStart() throws InvalidRecurrenceRuleException
299323
it.fastForward(start.getTimestamp());
300324

301325
assertThat(() -> it::next, startsWith(
302-
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
303-
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
304-
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
305-
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
306-
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp()
326+
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
327+
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
328+
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
329+
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
330+
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp()
307331
));
308332
}
309333

@@ -323,11 +347,11 @@ public void testFastForwardToPast() throws InvalidRecurrenceRuleException
323347
it.fastForward(start.getTimestamp() - TimeUnit.DAYS.toMillis(100));
324348

325349
assertThat(() -> it::next, startsWith(
326-
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
327-
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
328-
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
329-
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
330-
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp()
350+
new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(),
351+
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
352+
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
353+
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
354+
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp()
331355
));
332356
}
333357

@@ -347,12 +371,42 @@ public void testFastForwardToNext() throws InvalidRecurrenceRuleException
347371
it.fastForward(start.getTimestamp() + 1);
348372

349373
assertThat(() -> it::next, startsWith(
350-
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
351-
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
352-
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
353-
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp(),
354-
new DateTime(DateTime.UTC, 2019, 1, 6, 0, 0, 0).getTimestamp()
374+
new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(),
375+
new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(),
376+
new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(),
377+
new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp(),
378+
new DateTime(DateTime.UTC, 2019, 1, 6, 0, 0, 0).getTimestamp()
355379
));
356380
}
357381

382+
383+
private final static class RecurrenceAdapter extends AbstractBaseIterator<Long>
384+
{
385+
386+
private final RecurrenceSetIterator mDelegate;
387+
388+
389+
private RecurrenceAdapter(RecurrenceSetIterator delegate)
390+
{
391+
mDelegate = delegate;
392+
}
393+
394+
395+
@Override
396+
public boolean hasNext()
397+
{
398+
return mDelegate.hasNext();
399+
}
400+
401+
402+
@Override
403+
public Long next()
404+
{
405+
if (!hasNext())
406+
{
407+
throw new NoSuchElementException();
408+
}
409+
return mDelegate.next();
410+
}
411+
}
358412
}

0 commit comments

Comments
 (0)