Skip to content

Commit 5791fbd

Browse files
authored
Rework recurrence iteration (#102)
Rework recurrence iteration fixes #35 #101 This commit deprecates the old recurrenceset implementation and adds a new one. Handling of the first instance is now determined by chosing the right `InstanceIterable`, `RuleInstances` vs. `FirstAndRuleInstances`. Also in this commit: * Upgrade gradle -> gradle 7.4 * Upgrade jems -> jems2 * Upgrade JUnit -> Jupiter
1 parent 803edb5 commit 5791fbd

File tree

63 files changed

+3529
-2078
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+3529
-2078
lines changed

.idea/compiler.xml

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

.idea/modules.xml

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

README.md

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,44 +36,135 @@ The basic use case is to iterate over all instances of a given rule starting on
3636

3737
The following code iterates over the instances of a recurrence rule:
3838

39-
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
4039
41-
DateTime start = new DateTime(1982, 4 /* 0-based month numbers! */,23);
40+
```java
41+
DateTime start = RecurrenceRuleIterator it = rule.iterator(start);
4242

43-
RecurrenceRuleIterator it = rule.iterator(start);
43+
int maxInstances = 100; // limit instances for rules that recur forever
44+
45+
while (it.hasNext() && (!rule.isInfinite() || maxInstances-- > 0))
46+
{
47+
DateTime nextInstance = it.nextDateTime();
48+
// do something with nextInstance
49+
}
50+
```
4451

45-
int maxInstances = 100; // limit instances for rules that recur forever
52+
### Iterating Recurrence Sets
4653

47-
while (it.hasNext() && (!rule.isInfinite() || maxInstances-- > 0))
48-
{
49-
DateTime nextInstance = it.nextDateTime();
50-
// do something with nextInstance
51-
}
54+
This library also supports processing of EXRULEs, RDATEs and EXDATEs, i.e. complete recurrence sets.
5255

53-
This library also supports processing of EXRULEs, RDATEs and EXDATEs, i.e. complete recurrence sets. To iterate a recurrence set use the following code:
56+
In order to iterate a recurrence set you first compose the set from its components:
5457

55-
// create a recurence set
56-
RecurrenceSet rset = new RecurrenceSet();
58+
```java
59+
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
5760

58-
// add instances from a recurrence rule
59-
// you can add any number of recurrence rules or RDATEs (RecurrenceLists).
60-
rset.addInstances(new RecurrenceRuleAdapter(rule));
61+
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
6162

62-
// optionally add exceptions
63-
// rset.addExceptions(new RecurrenceList(timestamps));
63+
for (DateTime instance:new RecurrenceSet(firstInstance, new RuleInstances(rule))) {
64+
// do something with instance
65+
}
66+
```
6467

65-
// get an iterator
66-
RecurrenceSetIterator iterator = rset.iterator(start.getTimeZone(), start.getTimestamp());
68+
`RecurrenceSet` takes two `InstanceIterable` arguments the first one is expected to iterate the actual
69+
occurrences, the second, optional one iterates exceptions:
6770

68-
while (iterator.hasNext() && --limit >= 0)
69-
{
70-
long nextInstance = iterator.next();
71-
// do something with nextInstance
72-
}
71+
```java
72+
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
7373

74-
Note: at this time RecurrenceSetIterator supports iterating timestamps only. All-day dates will be iterated as timestamps at 00:00 UTC.
74+
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
7575

76-
By default the parser is very tolerant and accepts all rules that comply with RFC 5545. You can use other modes to ensure a certain compliance level:
76+
for (DateTime instance:
77+
new RecurrenceSet(firstInstance,
78+
new RuleInstances(rule),
79+
new InstanceList(exceptions))) {
80+
// do something with instance
81+
}
82+
```
83+
84+
You can compose multiple rules or `InstanceList`s using `Composite` like this
85+
86+
```java
87+
RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
88+
RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20");
89+
90+
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
91+
92+
for (DateTime instance:
93+
new RecurrenceSet(firstInstance,
94+
new Composite(new RuleInstances(rule1), new RuleInstances(rule2)),
95+
new InstanceList(exceptions))) {
96+
// do something with instance
97+
}
98+
```
99+
100+
or simply by providing a `List` of `InstanceIterable`s:
101+
102+
```java
103+
RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
104+
RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20");
105+
106+
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
107+
108+
for (DateTime instance:
109+
new RecurrenceSet(firstInstance,
110+
List.of(new RuleInstances(rule1), new RuleInstances(rule2)),
111+
new InstanceList(exceptions))) {
112+
// do something with instance
113+
}
114+
```
115+
116+
#### Handling first instances that don't match the RRULE
117+
118+
Note that `RuleInstances` does not iterate the start date if it doesn't match the RRULE. If you want to
119+
iterate any non-synchronized first date, use `FirstAndRuleInstances` instead!
120+
121+
```java
122+
new RecurrenceSet(DateTime.parse("19820523"),
123+
new RuleInstances(
124+
new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) {
125+
// do something with instance
126+
}
127+
```
128+
results in
129+
```
130+
19830524,19840524,19850524…
131+
```
132+
Note that `19820523` is not among the results.
133+
134+
However,
135+
136+
```java
137+
new RecurrenceSet(DateTime.parse("19820523"),
138+
new RuleInstances(
139+
new FirstAndRuleInstances("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) {
140+
// do something with instance
141+
}
142+
```
143+
results in
144+
```
145+
19820523,19830524,19840524,19850524…
146+
```
147+
148+
149+
#### Dealing with infinite rules
150+
151+
Be aware that RRULEs are infinite if they specify neither `COUNT` nor `UNTIL`. This might easily result in an infinite loop when you just iterate over the recurrence set like above.
152+
153+
One way to address this is by adding a decorator like `First` from the `jems2` library:
154+
155+
```java
156+
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
157+
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
158+
for (DateTime instance: new First(1000, new RecurrenceSet(firstInstance, new RuleInstances(rule)))) {
159+
// do something with instance
160+
}
161+
```
162+
163+
This will always stop iterating after at most 1000 instances.
164+
165+
### Strict and lax parsing
166+
167+
By default, the parser is very tolerant and accepts all rules that comply with RFC 5545. You can use other modes to ensure a certain compliance level:
77168

78169
RecurrenceRule rule1 = new RecurrenceRule("FREQ=WEEKLY;BYWEEKNO=1,2,3,4;BYDAY=SU", RfcMode.RFC2445_STRICT);
79170
// -> will iterate Sunday in the first four weeks of the year
@@ -148,4 +239,4 @@ There are at least two other implentations of recurrence iterators for Java:
148239

149240
## License
150241

151-
Copyright (c) Marten Gajda 2015, licensed under Apache2.
242+
Copyright (c) Marten Gajda 2022, licensed under Apache2.

benchmark/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
plugins {
22
id 'java'
3-
id "me.champeau.gradle.jmh" version "0.5.1"
3+
id "me.champeau.jmh" version "0.6.8"
44
}
55

66
sourceCompatibility = JavaVersion.VERSION_1_8
77
targetCompatibility = JavaVersion.VERSION_1_8
88

99
dependencies {
10-
jmh 'org.dmfs:jems:1.41'
10+
jmh 'org.dmfs:jems2:2.11.1'
1111
jmh rootProject
1212
}
1313

benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceRuleExpansion.java

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,7 @@
1818
package org.dmfs.rfc5545.recur;
1919

2020
import org.dmfs.rfc5545.DateTime;
21-
import org.openjdk.jmh.annotations.Benchmark;
22-
import org.openjdk.jmh.annotations.BenchmarkMode;
23-
import org.openjdk.jmh.annotations.Fork;
24-
import org.openjdk.jmh.annotations.Measurement;
25-
import org.openjdk.jmh.annotations.Mode;
26-
import org.openjdk.jmh.annotations.Param;
27-
import org.openjdk.jmh.annotations.Scope;
28-
import org.openjdk.jmh.annotations.Setup;
29-
import org.openjdk.jmh.annotations.State;
30-
import org.openjdk.jmh.annotations.Warmup;
21+
import org.openjdk.jmh.annotations.*;
3122

3223
import java.util.TimeZone;
3324

@@ -49,20 +40,20 @@ public static class BenchmarkState
4940
int iterations;
5041

5142
@Param({
52-
"FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=24",
53-
"FREQ=MONTHLY;BYMONTH=12;BYMONTHDAY=24",
54-
"FREQ=YEARLY;BYDAY=-2SU,-3SU,-4SU,-5SU",
55-
"FREQ=MONTHLY;INTERVAL=3;BYDAY=2WE",
56-
"FREQ=YEARLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
57-
"FREQ=MONTHLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
58-
"FREQ=YEARLY;BYDAY=MO",
59-
"FREQ=MONTHLY;BYDAY=MO",
60-
"FREQ=WEEKLY;BYDAY=MO",
61-
"FREQ=DAILY;BYDAY=MO",
62-
"FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
63-
"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
64-
"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
65-
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
43+
"FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=24",
44+
"FREQ=MONTHLY;BYMONTH=12;BYMONTHDAY=24",
45+
"FREQ=YEARLY;BYDAY=-2SU,-3SU,-4SU,-5SU",
46+
"FREQ=MONTHLY;INTERVAL=3;BYDAY=2WE",
47+
"FREQ=YEARLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
48+
"FREQ=MONTHLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
49+
"FREQ=YEARLY;BYDAY=MO",
50+
"FREQ=MONTHLY;BYDAY=MO",
51+
"FREQ=WEEKLY;BYDAY=MO",
52+
"FREQ=DAILY;BYDAY=MO",
53+
"FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
54+
"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
55+
"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
56+
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
6657
})
6758
String rule;
6859

0 commit comments

Comments
 (0)