Skip to content

Commit

Permalink
Rework recurrence iteration (#102)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dmfs authored Nov 6, 2022
1 parent 803edb5 commit 5791fbd
Show file tree
Hide file tree
Showing 63 changed files with 3,529 additions and 2,078 deletions.
10 changes: 10 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 118 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,44 +36,135 @@ The basic use case is to iterate over all instances of a given rule starting on

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

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

RecurrenceRuleIterator it = rule.iterator(start);
int maxInstances = 100; // limit instances for rules that recur forever

while (it.hasNext() && (!rule.isInfinite() || maxInstances-- > 0))
{
DateTime nextInstance = it.nextDateTime();
// do something with nextInstance
}
```

int maxInstances = 100; // limit instances for rules that recur forever
### Iterating Recurrence Sets

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

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

// create a recurence set
RecurrenceSet rset = new RecurrenceSet();
```java
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");

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

// optionally add exceptions
// rset.addExceptions(new RecurrenceList(timestamps));
for (DateTime instance:new RecurrenceSet(firstInstance, new RuleInstances(rule))) {
// do something with instance
}
```

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

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

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

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:
for (DateTime instance:
new RecurrenceSet(firstInstance,
new RuleInstances(rule),
new InstanceList(exceptions))) {
// do something with instance
}
```

You can compose multiple rules or `InstanceList`s using `Composite` like this

```java
RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20");

DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);

for (DateTime instance:
new RecurrenceSet(firstInstance,
new Composite(new RuleInstances(rule1), new RuleInstances(rule2)),
new InstanceList(exceptions))) {
// do something with instance
}
```

or simply by providing a `List` of `InstanceIterable`s:

```java
RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20");

DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);

for (DateTime instance:
new RecurrenceSet(firstInstance,
List.of(new RuleInstances(rule1), new RuleInstances(rule2)),
new InstanceList(exceptions))) {
// do something with instance
}
```

#### Handling first instances that don't match the RRULE

Note that `RuleInstances` does not iterate the start date if it doesn't match the RRULE. If you want to
iterate any non-synchronized first date, use `FirstAndRuleInstances` instead!

```java
new RecurrenceSet(DateTime.parse("19820523"),
new RuleInstances(
new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) {
// do something with instance
}
```
results in
```
19830524,19840524,19850524…
```
Note that `19820523` is not among the results.

However,

```java
new RecurrenceSet(DateTime.parse("19820523"),
new RuleInstances(
new FirstAndRuleInstances("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) {
// do something with instance
}
```
results in
```
19820523,19830524,19840524,19850524…
```


#### Dealing with infinite rules

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.

One way to address this is by adding a decorator like `First` from the `jems2` library:

```java
RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5");
DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23);
for (DateTime instance: new First(1000, new RecurrenceSet(firstInstance, new RuleInstances(rule)))) {
// do something with instance
}
```

This will always stop iterating after at most 1000 instances.

### Strict and lax parsing

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:

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

## License

Copyright (c) Marten Gajda 2015, licensed under Apache2.
Copyright (c) Marten Gajda 2022, licensed under Apache2.
4 changes: 2 additions & 2 deletions benchmark/build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
plugins {
id 'java'
id "me.champeau.gradle.jmh" version "0.5.1"
id "me.champeau.jmh" version "0.6.8"
}

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

dependencies {
jmh 'org.dmfs:jems:1.41'
jmh 'org.dmfs:jems2:2.11.1'
jmh rootProject
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,7 @@
package org.dmfs.rfc5545.recur;

import org.dmfs.rfc5545.DateTime;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.annotations.*;

import java.util.TimeZone;

Expand All @@ -49,20 +40,20 @@ public static class BenchmarkState
int iterations;

@Param({
"FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=24",
"FREQ=MONTHLY;BYMONTH=12;BYMONTHDAY=24",
"FREQ=YEARLY;BYDAY=-2SU,-3SU,-4SU,-5SU",
"FREQ=MONTHLY;INTERVAL=3;BYDAY=2WE",
"FREQ=YEARLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
"FREQ=MONTHLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
"FREQ=YEARLY;BYDAY=MO",
"FREQ=MONTHLY;BYDAY=MO",
"FREQ=WEEKLY;BYDAY=MO",
"FREQ=DAILY;BYDAY=MO",
"FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=24",
"FREQ=MONTHLY;BYMONTH=12;BYMONTHDAY=24",
"FREQ=YEARLY;BYDAY=-2SU,-3SU,-4SU,-5SU",
"FREQ=MONTHLY;INTERVAL=3;BYDAY=2WE",
"FREQ=YEARLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
"FREQ=MONTHLY;INTERVAL=1;BYDAY=WE;BYMONTHDAY=25,26,27,21,22,23,24;BYMONTH=4",
"FREQ=YEARLY;BYDAY=MO",
"FREQ=MONTHLY;BYDAY=MO",
"FREQ=WEEKLY;BYDAY=MO",
"FREQ=DAILY;BYDAY=MO",
"FREQ=YEARLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
"FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR,SA,SU",
})
String rule;

Expand Down
Loading

0 comments on commit 5791fbd

Please sign in to comment.