diff --git a/.gitignore b/.gitignore index 82296b7..b9dd0de 100644 --- a/.gitignore +++ b/.gitignore @@ -6,26 +6,7 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: -.idea/workspace.xml -.idea/tasks.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml - -# Sensitive or high-churn files: -.idea/dataSources.ids -.idea/dataSources.xml -.idea/dataSources.local.xml -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml - -# Gradle: -.idea/gradle.xml -.idea/libraries - -# Mongo Explorer plugin: -.idea/mongoSettings.xml +.idea ## File-based project format: *.iws diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index 09cd245..0000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index e49b50b..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/Apache.xml b/.idea/copyright/Apache.xml deleted file mode 100644 index ae8b931..0000000 --- a/.idea/copyright/Apache.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index 8427c95..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 0548357..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 56c749f..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 92b1ab7..aec6d5b 100644 --- a/README.md +++ b/README.md @@ -27,140 +27,179 @@ The iterator has support for [RSCALE](https://tools.ietf.org/html/rfc7529). At t RSCALE is supported in all RFC2445 and RFC5545 modes. -## Example code +## Recurrence Set API -### Iterating instances +In addition to interpreting recurrence rules, this library provides a set of classes to determine the result of any combination of rrules, rdates and exdates (and exrules, for that matter) as specified in RFC 5545. -The basic use case is to iterate over all instances of a given rule starting on a specific day. Note that some rules may recur forever. In that case you must limit the number of instances in order to avoid an infinite loop. +Version 0.16.0 introduces a new API that is slightly different from the previous one. The new API fixes a few design issues that +made the code more complex than necessary. -The following code iterates over the instances of a recurrence rule: +There is a new interface called `RecurrenceSet` that is implemented by a couple of adapters, decorators and composites. A `RecurrenceSet` +represents the set of occurrences of a recurrence rule or list or any combination of them (including exclusions). - -```java -DateTime start = RecurrenceRuleIterator it = rule.iterator(start); +`RecurrenceSet` extends the `Iterable` interface, so it can be used with any `Iterable` decorator from the jems2 library and in `for` loops. -int maxInstances = 100; // limit instances for rules that recur forever +### Iterating RRules -while (it.hasNext() && (!rule.isInfinite() || maxInstances-- > 0)) -{ - DateTime nextInstance = it.nextDateTime(); - // do something with nextInstance -} -``` +The most common use case is probably just iterating the occurrences of recurrence rules. Although you still can do this using the `RecurrenceRuleIterator` +returned by `RecurrenceRule.iterator(DateTime)`, you may be better off using the `OfRule` adapter that implements the +`Iterable` interface. -### Iterating Recurrence Sets +#### Examples -This library also supports processing of EXRULEs, RDATEs and EXDATEs, i.e. complete recurrence sets. +```java +RecurrenceSet occurrences = new OfRule(rrule, startDate); +``` -In order to iterate a recurrence set you first compose the set from its components: +You can combine this with the `First` or `While` decorators from the jems2 library to guard against infinite rules and use it to +loop over the occurrences. ```java -RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5"); - -DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23); +for (DateTime occurrence:new First<>(1000, // iterate at most/the first 1000 occurrences + new OfRule(rrule, startDate))) { + // do something with occurrence +} +``` -for (DateTime instance:new RecurrenceSet(firstInstance, new RuleInstances(rule))) { - // do something with instance +```java +for (DateTime occurrence:new While<>(endDate::isAfter, // stop at "endDate" + new OfRule(rrule, startDate))) { + // do something with occurrence } ``` -`RecurrenceSet` takes two `InstanceIterable` arguments the first one is expected to iterate the actual -occurrences, the second, optional one iterates exceptions: +#### Handling first instances that don't match the RRULE + +Note that `OfRule` does not iterate the start date if it doesn't match the RRULE. If you want to +iterate any non-synchronized first date, use `OfRuleAndFirst` instead! ```java -RecurrenceRule rule = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5"); +new OfRule( + new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5"), + DateTime.parse("19820523")) +``` +results in +``` +19820524,19830524,19840524,19850524… +``` +Note that `19820523` is not among the results because it doesn't match the rule as it doesn't fall on the 24th. -DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23); +However, -for (DateTime instance: - new RecurrenceSet(firstInstance, - new RuleInstances(rule), - new InstanceList(exceptions))) { - // do something with instance -} +```java +new OfRuleAndFirst( + new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5"), + DateTime.parse("19820523")) +``` +results in +``` +19820523,19820524,19830524,19840524,19850524… ``` -You can compose multiple rules or `InstanceList`s using `Composite` like this +### Iterating RDates and ExDates -```java -RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5"); -RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20"); +Similarly, iterating comma separated Date or DateTime lists (i.e. `RDATE` and `EXDATE` ) can be done with the `OfList` adapter. -DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23); +#### Example -for (DateTime instance: - new RecurrenceSet(firstInstance, - new Composite(new RuleInstances(rule1), new RuleInstances(rule2)), - new InstanceList(exceptions))) { - // do something with instance +```java +for (DateTime occurrence:new OfList(timeZone, rdates)) { + // do something with occurrence } ``` -or simply by providing a `List` of `InstanceIterable`s: +### Combining multiple Rules and/or Lists -```java -RecurrenceRule rule1 = new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=23;BYMONTH=5"); -RecurrenceRule rule2 = new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=20"); +You can merge the occurrences of multiple sets with the `Merged` class. A `Merged` `RecurrenceSet` iterates the occurrences +of all given `RecurrenceSet`s in chronological order. -DateTime firstInstance = new DateTime(1982, 4 /* 0-based month numbers! */,23); +#### Example -for (DateTime instance: - new RecurrenceSet(firstInstance, - List.of(new RuleInstances(rule1), new RuleInstances(rule2)), - new InstanceList(exceptions))) { - // do something with instance -} +```java +RecurrenceSet merged = new Merged( + new OfRule(rule, start), + new OfList(timezone, rdates) +); ``` -#### Handling first instances that don't match the RRULE +The result iterates the occurrences of both, the rule and the rdates in chronological order. -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! +### Excluding Exceptions + +Exceptions can be excluded by composing occurrences and exceptions using `Difference` like in ```java -new RecurrenceSet(DateTime.parse("19820523"), - new RuleInstances( - new RecurrenceRule("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) { - // do something with instance -} -``` -results in -``` -19820524,19830524,19840524,19850524… +RecurrenceSet withoutExceptions = new Difference( + new OfRule(rule, start), + new OfList(timezone, exdates)); ``` -Note that `19820523` is not among the results. -However, +This `RecurrenceSet` contains all the occurrences iterated by the given rule, except those in the exdates list. Note that these must be exact matches, +i.e. the exdate `20240216` does *not* result in the exclusion of `20240216T120000` nor of `20240216T000000`. + +### Fast forwarding + +Sometimes you might want to skip all the instances prior to a given date. This can be achieved by applying the `FastForwarded` decorator like in ```java -new RecurrenceSet(DateTime.parse("19820523"), - new RuleInstances( - new FirstAndRuleInstances("FREQ=YEARLY;BYMONTHDAY=24;BYMONTH=5")))) { - // do something with instance -} -``` -results in -``` -19820523,19820524,19830524,19840524,19850524… +RecurrenceSet merged = new FastForwarded( + fastForwardToDate, + new Merged( + new OfRule(rule, start), + new OfList(timezone, rdates))); ``` +Note, that `new FastForwarded(fastForwardTo, new OfRule(rrule, start))` and `new OfRule(rrule, fastForwardTo)` are not necessarily the same +set of occurrences. + -#### 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. +### Dealing with infinite rules -One way to address this is by adding a decorator like `First` from the `jems2` library: +Be aware that RRULEs are infinite if they specify neither `COUNT` nor `UNTIL`. This might easily result in an infinite loop if not taken care of. + +As stated above, a simple way to deal with this is by applying a decorator like `First` or `While` 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 +DateTime start = new DateTime(1982, 4 /* 0-based month numbers! */,23); +for (DateTime occurrence:new First<>(1000, new OfRule(rule, start))) { + // do something with occurrence } ``` This will always stop iterating after at most 1000 instances. +### Determining the last instance of a RecurrenceSet + +Finite, non-empty `RecurrenceSet`s have a last instance that can be determined with the `LastInstance` adapter. +`LastInstance` is an `Optional` of a `DateTime` value that's present when the given `RecurrenceSet` is finite and +non-empty. + +#### Example + +```java +new LastInstance(new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=10"), startDate)); +``` + +### RFC 5545 Instance Iteration Example + +In a recurring `VEVENT` you might find `RRULE`s, `RDATE`s, `EXDATE`s and (in RFC 2445) `EXRULE`s. Assuming you have all +these in variables with these respective names the `RecurrenceSet` might be constructed like in + +```java +RecurrenceSet occurrences = new Difference( + new Merged( + new OfRule(new RecurrenceRule(rrule), dtstart), + new OfList(timezone, rdates) + ), + new Merged( + new OfRule(new RecurrenceRule(exrule), dtstart), + new OfList(timezone, exdates) + ) +); +``` + ### 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: @@ -233,4 +272,4 @@ There are at least two other implentations of recurrence iterators for Java: ## License -Copyright (c) Marten Gajda 2022, licensed under Apache2. +Copyright (c) Marten Gajda 2024, licensed under Apache2. diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 7aaf94b..30740a3 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -7,7 +7,7 @@ sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 dependencies { - jmh 'org.dmfs:jems2:2.11.1' + jmh libs.jems2 jmh rootProject } diff --git a/benchmark/results/results-0.11.6-1-g4ccdb6d.json b/benchmark/results/results-0.11.6-1-g4ccdb6d.json index 55eb3c2..16433d1 100644 --- a/benchmark/results/results-0.11.6-1-g4ccdb6d.json +++ b/benchmark/results/results-0.11.6-1-g4ccdb6d.json @@ -2323,6 +2323,9 @@ "secondaryMetrics" : { } }, + + + ::::::::::::::::::::::::::::::::::::::::: { "jmhVersion" : "1.25", "benchmark" : "org.dmfs.rfc5545.recur.RecurrenceSetExpansion.testExpansion", diff --git a/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceRuleExpansion.java b/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceRuleExpansion.java index 8314df3..eff75fb 100644 --- a/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceRuleExpansion.java +++ b/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceRuleExpansion.java @@ -72,7 +72,7 @@ public void setup() throws InvalidRecurrenceRuleException } - @Benchmark + // @Benchmark public long benchmarkExpansion(BenchmarkState state) { RecurrenceRuleIterator iterator = state.recurrenceRule.iterator(state.start); diff --git a/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceSetExpansion.java b/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceSetExpansion.java index 93791db..4cd593d 100644 --- a/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceSetExpansion.java +++ b/benchmark/src/jmh/java/org/dmfs/rfc5545/recur/RecurrenceSetExpansion.java @@ -17,28 +17,19 @@ package org.dmfs.rfc5545.recur; -import org.dmfs.iterables.decorators.DelegatingIterable; -import org.dmfs.iterables.decorators.Sieved; -import org.dmfs.jems.function.FragileFunction; -import org.dmfs.jems.function.Function; -import org.dmfs.jems.iterable.decorators.Mapped; -import org.dmfs.jems.iterable.elementary.Seq; -import org.dmfs.jems.predicate.composite.Not; -import org.dmfs.jems.procedure.composite.ForEach; +import org.dmfs.jems2.FragileFunction; +import org.dmfs.jems2.Function; +import org.dmfs.jems2.iterable.DelegatingIterable; +import org.dmfs.jems2.iterable.Mapped; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.jems2.iterable.Sieved; +import org.dmfs.jems2.predicate.Not; import org.dmfs.rfc5545.DateTime; -import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter; -import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; -import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator; -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.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.recurrenceset.Difference; +import org.dmfs.rfc5545.recurrenceset.Merged; +import org.dmfs.rfc5545.recurrenceset.OfRule; +import org.openjdk.jmh.annotations.*; import java.util.TimeZone; @@ -60,54 +51,66 @@ public static class BenchmarkState int iterations; @Param({ - "FREQ=DAILY;BYDAY=MO,TU,WE", - "FREQ=DAILY;BYDAY=MO,TU,WE" + ":" + "FREQ=DAILY;BYDAY=WE,TH,FR" + ":" + "FREQ=DAILY;BYDAY=SU,SO" }) + "FREQ=DAILY;BYDAY=MO,TU,WE", + "FREQ=DAILY;BYDAY=MO,TU,WE" + ":" + "FREQ=DAILY;BYDAY=WE,TH,FR" + ":" + "FREQ=DAILY;BYDAY=SU,SO" }) String instances; @Param({ - "", - "FREQ=DAILY;BYDAY=MO", - "FREQ=DAILY;BYDAY=MO" + ":" + "FREQ=DAILY;BYDAY=WE" + ":" + "FREQ=DAILY;BYDAY=WE,FR" }) + "", + "FREQ=DAILY;BYDAY=MO", + "FREQ=DAILY;BYDAY=MO" + ":" + "FREQ=DAILY;BYDAY=WE" + ":" + "FREQ=DAILY;BYDAY=WE,FR" }) String exceptions; RecurrenceSet recurrenceSet; + DateTime start = new DateTime(TimeZone.getTimeZone("Europe/Berlin"), 2024, 1, 16, 14, 1, 0); @Setup public void setup() { - recurrenceSet = new RecurrenceSet(); - new ForEach<>(new RuleAdapters(instances)).process(recurrenceSet::addInstances); - new ForEach<>(new RuleAdapters(exceptions)).process(recurrenceSet::addExceptions); + if (!exceptions.isEmpty()) + { + recurrenceSet = new Difference( + new Merged(new RuleSets(instances, start)), + new Merged(new RuleSets(exceptions, start)) + ); + } + else + { + recurrenceSet = new Merged(new RuleSets(instances, start)); + } } } @Benchmark - public long testExpansion(BenchmarkState state) + public DateTime testExpansion(BenchmarkState state) { - RecurrenceSetIterator r = state.recurrenceSet.iterator(TimeZone.getDefault(), DateTime.now().getTimestamp()); - long last = 0L; + DateTime last = null; int count = state.iterations; - while (r.hasNext() && count-- > 0) + for (DateTime dt : state.recurrenceSet) { - last = r.next(); + last = dt; + if (--count == 0) + { + break; + } } return last; } - private static class RuleAdapters extends DelegatingIterable + private static class RuleSets extends DelegatingIterable { - public RuleAdapters(String ruleString) + public RuleSets(String ruleString, DateTime start) { super(new Mapped<>( - RecurrenceRuleAdapter::new, - new Mapped<>( - new Unchecked<>(RecurrenceRule::new), - new Sieved<>(new Not<>(String::isEmpty), - new Seq<>(ruleString.split(":")))))); + rule -> new OfRule(rule, start), + new Mapped<>( + new Unchecked<>(RecurrenceRule::new), + new Sieved<>(new Not<>(String::isEmpty), + new Seq<>(ruleString.split(":")))))); } } diff --git a/build.gradle b/build.gradle index 7fe7cff..c109ece 100644 --- a/build.gradle +++ b/build.gradle @@ -71,12 +71,16 @@ if (project.hasProperty('SONATYPE_USERNAME') && project.hasProperty('SONATYPE_PA } dependencies { + compileOnly 'org.eclipse.jdt:org.eclipse.jdt.annotation:2.2.600' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' api 'org.dmfs:rfc5545-datetime:0.3' - implementation 'org.dmfs:jems2:2.11.1' + api libs.jems2 testImplementation project("lib-recur-hamcrest") - testImplementation 'org.dmfs:jems2-testing:2.11.1' + testImplementation project("lib-recur-confidence") + testImplementation 'org.dmfs:jems2-testing:2.22.0' + testImplementation libs.jems2.confidence + testImplementation 'org.saynotobugs:confidence-core:0.42.0' } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f1eefad --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +eclipse-jdt = "2.2.600" +hamcrest = "2.2" +jems2 = "2.22.0" +junit = "5.8.2" +junit-testkit = "1.9.2" +srcless = "0.3.0" +confidence = "0.42.0" + +[libraries] +srcless-annotations = { module = "org.dmfs:srcless-annotations", version.ref = "srcless" } +srcless-processors = { module = "org.dmfs:srcless-processors", version.ref = "srcless" } +eclipse-jdt-anntation = { module = 'org.eclipse.jdt:org.eclipse.jdt.annotation', version.ref = "eclipse-jdt" } +nullless-processors = { module = "org.dmfs:nullless-processors", version.ref = "srcless" } + +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } + +jems2 = { module = "org.dmfs:jems2", version.ref = "jems2" } +jems2-testing = { module = "org.dmfs:jems2-testing", version.ref = "jems2" } +jems2-confidence = { module = "org.dmfs:jems2-confidence", version.ref = "jems2" } + +confidence-core = { module = "org.saynotobugs:confidence-core", version.ref = "confidence" } +confidence-test = { module = "org.saynotobugs:confidence-test", version.ref = "confidence" } + +[bundles] +srcless-processors = ["srcless-processors", "nullless-processors"] + +[plugins] diff --git a/lib-recur-confidence/build.gradle b/lib-recur-confidence/build.gradle new file mode 100644 index 0000000..790a966 --- /dev/null +++ b/lib-recur-confidence/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java-library' +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + compileOnly libs.eclipse.jdt.anntation + compileOnly libs.srcless.annotations + annotationProcessor libs.bundles.srcless.processors + api libs.confidence.core + implementation libs.jems2 + implementation libs.jems2.confidence + api rootProject + + testImplementation libs.confidence.test + testImplementation libs.jems2.testing + testImplementation libs.junit.jupiter.api + testRuntimeOnly libs.junit.jupiter.engine +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSet.java b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSet.java new file mode 100644 index 0000000..7ece674 --- /dev/null +++ b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSet.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.srcless.annotations.staticfactory.StaticFactories; +import org.saynotobugs.confidence.quality.composite.AllOf; +import org.saynotobugs.confidence.quality.composite.Not; +import org.saynotobugs.confidence.quality.composite.QualityComposition; +import org.saynotobugs.confidence.quality.grammar.Is; +import org.saynotobugs.confidence.quality.iterable.EmptyIterable; + +@StaticFactories(value = "Recur", packageName = "org.dmfs.rfc5545.confidence") +public final class EmptyRecurrenceSet extends QualityComposition +{ + public EmptyRecurrenceSet() + { + super(new AllOf<>( + new Is<>(new EmptyIterable()), + new Is<>(new Not<>(new Infinite())))); + } +} diff --git a/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/Infinite.java b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/Infinite.java new file mode 100644 index 0000000..ef7ae19 --- /dev/null +++ b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/Infinite.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.srcless.annotations.staticfactory.StaticFactories; +import org.saynotobugs.confidence.description.Text; +import org.saynotobugs.confidence.quality.composite.QualityComposition; +import org.saynotobugs.confidence.quality.object.Satisfies; + +@StaticFactories(value = "Recur", packageName = "org.dmfs.rfc5545.confidence") +public final class Infinite extends QualityComposition +{ + public Infinite() + { + super(new Satisfies<>(RecurrenceSet::isInfinite, new Text("infinite"))); + } +} diff --git a/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/StartsWith.java b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/StartsWith.java new file mode 100644 index 0000000..5b64f64 --- /dev/null +++ b/lib-recur-confidence/src/main/java/org/dmfs/rfc5545/confidence/quality/StartsWith.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.jems2.iterable.First; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.srcless.annotations.staticfactory.StaticFactories; +import org.saynotobugs.confidence.quality.composite.Has; +import org.saynotobugs.confidence.quality.composite.QualityComposition; +import org.saynotobugs.confidence.quality.iterable.Iterates; + +@StaticFactories(value = "Recur", packageName = "org.dmfs.rfc5545.confidence") +public final class StartsWith extends QualityComposition +{ + public StartsWith(DateTime... instances) + { + super(new Has<>("first " + instances.length + " instances", + candidate -> new First<>(instances.length, candidate), new Iterates<>(instances))); + } +} diff --git a/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSetTest.java b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSetTest.java new file mode 100644 index 0000000..54351e5 --- /dev/null +++ b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/EmptyRecurrenceSetTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.jems2.iterator.Seq; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.instanceiterator.EmptyIterator; +import org.dmfs.rfc5545.instanceiterator.FastForwardable; +import org.junit.jupiter.api.Test; + +import static org.dmfs.jems2.mockito.Mock.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.allOf; +import static org.saynotobugs.confidence.test.quality.Test.fails; + +class EmptyRecurrenceSetTest +{ + @Test + void test() + { + assertThat(new EmptyRecurrenceSet(), + allOf( + org.saynotobugs.confidence.test.quality.Test.passes(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(false)), + with(RecurrenceSet::iterator, returning(new EmptyIterator())))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(true)), + with(RecurrenceSet::iterator, returning(new EmptyIterator())))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(false)), + with(RecurrenceSet::iterator, returning( + new FastForwardable( + DateTime.parse("20240101"), + new Seq<>(DateTime.parse("20240102"), DateTime.parse("20240103"))))))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(true)), + with(RecurrenceSet::iterator, returning( + new FastForwardable( + DateTime.parse("20240101"), + new Seq<>(DateTime.parse("20240102"), DateTime.parse("20240103"))))))))); + } + +} \ No newline at end of file diff --git a/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/InfiniteTest.java b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/InfiniteTest.java new file mode 100644 index 0000000..5776afa --- /dev/null +++ b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/InfiniteTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; +import org.junit.jupiter.api.Test; + +import static org.dmfs.jems2.mockito.Mock.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.allOf; +import static org.saynotobugs.confidence.test.quality.Test.fails; + +class InfiniteTest +{ + @Test + void test() + { + assertThat(new Infinite(), + allOf( + org.saynotobugs.confidence.test.quality.Test.passes(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(true)), + with(RecurrenceSet::iterator, returning(mock(InstanceIterator.class))))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::isInfinite, returning(false)), + with(RecurrenceSet::iterator, returning(mock(InstanceIterator.class))))))); + } + +} \ No newline at end of file diff --git a/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/StartsWithTest.java b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/StartsWithTest.java new file mode 100644 index 0000000..d5fdcd1 --- /dev/null +++ b/lib-recur-confidence/src/test/java/org/dmfs/rfc5545/confidence/quality/StartsWithTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 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.confidence.quality; + +import org.dmfs.jems2.iterator.Seq; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.instanceiterator.EmptyIterator; +import org.dmfs.rfc5545.instanceiterator.FastForwardable; +import org.junit.jupiter.api.Test; + +import static org.dmfs.jems2.mockito.Mock.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.allOf; +import static org.saynotobugs.confidence.test.quality.Test.fails; + +class StartsWithTest +{ + @Test + void test() + { + assertThat(new StartsWith(DateTime.parse("20240101"), DateTime.parse("20240102")), + allOf( + org.saynotobugs.confidence.test.quality.Test.passes(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning(new FastForwardable( + DateTime.parse("20240101"), + new Seq<>(DateTime.parse("20240102"))))))), + + org.saynotobugs.confidence.test.quality.Test.passes(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning(new FastForwardable( + DateTime.parse("20240101"), + new Seq<>( + DateTime.parse("20240102"), + DateTime.parse("20240103"), + DateTime.parse("20240104"))))))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning(new EmptyIterator())))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning( + new FastForwardable( + DateTime.parse("20240101"), + new EmptyIterator()))))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning( + new FastForwardable( + DateTime.parse("20240101"), + new Seq<>(DateTime.parse("20240103"), DateTime.parse("20240104"))))))), + + fails(mock(RecurrenceSet.class, + with(RecurrenceSet::iterator, returning( + new FastForwardable( + DateTime.parse("20240102"), + new Seq<>(DateTime.parse("20240103"), DateTime.parse("20240104"))))))))); + + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9f61e52..b744f16 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'lib-recur' +include 'lib-recur-confidence' include 'lib-recur-hamcrest' include 'benchmark' diff --git a/src/main/java/org/dmfs/rfc5545/InstanceIterator.java b/src/main/java/org/dmfs/rfc5545/InstanceIterator.java new file mode 100644 index 0000000..79b6811 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/InstanceIterator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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; + +import java.util.Iterator; + +public interface InstanceIterator extends Iterator +{ + /** + * Skip all occurrences until {@code until}. If {@code until} is an occurrence itself it will be the next iterated occurrence. + * If the rule doesn't recur till that date the next call to {@link #hasNext()} will return {@code false}. + */ + void fastForward(DateTime until); +} diff --git a/src/main/java/org/dmfs/rfc5545/RecurrenceSet.java b/src/main/java/org/dmfs/rfc5545/RecurrenceSet.java new file mode 100644 index 0000000..f5bcad0 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/RecurrenceSet.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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; + + +import org.dmfs.jems2.Optional; + +/** + * A set of instances. + */ +public interface RecurrenceSet extends Iterable +{ + /** + * Returns an {@link InstanceIterator} for this {@link RecurrenceSet}. + */ + InstanceIterator iterator(); + + /** + * Returns whether this {@link RecurrenceSet} is infinite or not. + */ + boolean isInfinite(); +} \ No newline at end of file diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/CountLimitedRecurrenceRuleIterator.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/CountLimitedRecurrenceRuleIterator.java similarity index 74% rename from src/main/java/org/dmfs/rfc5545/recurrenceset/CountLimitedRecurrenceRuleIterator.java rename to src/main/java/org/dmfs/rfc5545/instanceiterator/CountLimitedRecurrenceRuleIterator.java index a58ff81..8b7a839 100644 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/CountLimitedRecurrenceRuleIterator.java +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/CountLimitedRecurrenceRuleIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Marten Gajda + * Copyright 2022 Marten Gajda * * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,20 +15,19 @@ * limitations under the License. */ -package org.dmfs.rfc5545.recurrenceset; +package org.dmfs.rfc5545.instanceiterator; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; import org.dmfs.rfc5545.recur.RecurrenceRuleIterator; import java.util.NoSuchElementException; /** - * An {@link AbstractRecurrenceAdapter.InstanceIterator} which inserts a start instance. - * - * @author Marten Gajda + * An {@link InstanceIterator} which limits the number of iterated instances. */ -@Deprecated -public final class CountLimitedRecurrenceRuleIterator implements AbstractRecurrenceAdapter.InstanceIterator +public final class CountLimitedRecurrenceRuleIterator implements InstanceIterator { private final RecurrenceRuleIterator mDelegate; private int mRemaining; @@ -49,21 +48,21 @@ public boolean hasNext() @Override - public long next() + public DateTime next() { if (!hasNext()) { throw new NoSuchElementException("No further elements to iterate"); } mRemaining--; - return mDelegate.nextMillis(); + return mDelegate.nextDateTime(); } @Override - public void fastForward(long until) + public void fastForward(DateTime until) { - while (hasNext() && mDelegate.peekMillis() < until) + while (hasNext() && mDelegate.peekMillis() < until.getTimestamp()) { next(); } diff --git a/src/main/java/org/dmfs/rfc5545/instanceiterator/EffectiveInstancesIterator.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/EffectiveInstancesIterator.java new file mode 100644 index 0000000..8e6c0ce --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/EffectiveInstancesIterator.java @@ -0,0 +1,136 @@ +/* + * Copyright 2022 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.instanceiterator; + + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; + +import java.util.Locale; + + +/** + * An iterator for recurrence sets. It takes a number of {@link InstanceIterator}s for instances and exceptions and iterates all resulting instances + * (i.e. only the instances, not the exceptions). + */ +public final class EffectiveInstancesIterator implements InstanceIterator +{ + private final static DateTime MAX = new DateTime(4000, 0, 0, 0, 0, 0); + private final static DateTime MIN = new DateTime(0, 0, 1, 0, 0, 0); + + /** + * Throw if we skipped this many instances in a line, because they were exceptions. + */ + private final static int MAX_SKIPPED_INSTANCES = 1000; + + private final InstanceIterator mInstances; + + private final InstanceIterator mExceptions; + + private DateTime mNextInstance = MIN; + + private DateTime mNextException = MIN; + + + /** + * Create a new recurrence iterator for specific lists of instances and exceptions. + * + * @param instances The instances, must not be null or empty. + * @param exceptions The exceptions, may be null. + */ + public EffectiveInstancesIterator(InstanceIterator instances, InstanceIterator exceptions) + { + mInstances = instances; + mExceptions = exceptions; + pullNext(); + } + + + /** + * Check if there is at least one more instance to iterate. + * + * @return true if the next call to {@link #next()} will return another instance, false otherwise. + */ + @Override + public boolean hasNext() + { + return mNextInstance != MAX; + } + + + /** + * Get the next instance of this set. Do not call this if {@link #hasNext()} returns false. + * + * @return The time in milliseconds since the epoch of the next instance. + * @throws ArrayIndexOutOfBoundsException if there are no more instances. + */ + @Override + public DateTime next() + { + if (!hasNext()) + { + throw new ArrayIndexOutOfBoundsException("no more elements"); + } + DateTime result = mNextInstance; + pullNext(); + return result; + } + + + /** + * Fast-forward to the next instance at or after the given date. + */ + @Override + public void fastForward(DateTime until) + { + if (mNextInstance.before(until)) + { + mInstances.fastForward(until); + mExceptions.fastForward(until); + pullNext(); + } + } + + + private void pullNext() + { + DateTime next = MAX; + DateTime nextException = mNextException; + int skipableInstances = MAX_SKIPPED_INSTANCES; + while (mInstances.hasNext()) + { + next = mInstances.next(); + while (nextException.before(next)) + { + nextException = mExceptions.hasNext() ? mExceptions.next() : MAX; + } + if (nextException.after(next)) + { + break; + } + if (--skipableInstances <= 0) + { + throw new RuntimeException(String.format(Locale.ENGLISH, "Skipped too many (%d) instances", MAX_SKIPPED_INSTANCES)); + } + // we've skipped the next instance, this might have been the last one + next = MAX; + } + mNextInstance = next; + mNextException = nextException; + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/EmptyIterator.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/EmptyIterator.java similarity index 64% rename from src/main/java/org/dmfs/rfc5545/recurrenceset/EmptyIterator.java rename to src/main/java/org/dmfs/rfc5545/instanceiterator/EmptyIterator.java index cf95be1..3142d26 100644 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/EmptyIterator.java +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/EmptyIterator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Marten Gajda + * Copyright 2022 Marten Gajda * * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,17 +15,23 @@ * limitations under the License. */ -package org.dmfs.rfc5545.recurrenceset; +package org.dmfs.rfc5545.instanceiterator; + + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; import java.util.NoSuchElementException; /** - * An {@link AbstractRecurrenceAdapter.InstanceIterator} without any instances. + * An {@link InstanceIterator} without any instances. */ -@Deprecated -public final class EmptyIterator implements AbstractRecurrenceAdapter.InstanceIterator +public final class EmptyIterator implements InstanceIterator { + public static final InstanceIterator INSTANCE = new EmptyIterator(); + + @Override public boolean hasNext() { @@ -34,14 +40,15 @@ public boolean hasNext() @Override - public long next() + public DateTime next() { throw new NoSuchElementException("No elements to iterate"); } @Override - public void fastForward(long until) + public void fastForward(DateTime until) { + /* nothing to do */ } } diff --git a/src/main/java/org/dmfs/rfc5545/instanceiterator/FastForwardable.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/FastForwardable.java new file mode 100644 index 0000000..2eb98b1 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/FastForwardable.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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.instanceiterator; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +public final class FastForwardable implements InstanceIterator +{ + private DateTime mNextInstance; + private final Iterator mDelegate; + private boolean mHasNext = true; + + public FastForwardable( + DateTime firstInstance, + Iterator delegate) + { + mNextInstance = firstInstance; + mDelegate = delegate; + } + + @Override + public void fastForward(DateTime until) + { + while (mHasNext && until.after(mNextInstance)) + { + moveToNext(); + } + } + + @Override + public boolean hasNext() + { + return mHasNext; + } + + @Override + public DateTime next() + { + if (!mHasNext) + { + throw new NoSuchElementException("No more elements to iterate"); + } + DateTime next = mNextInstance; + moveToNext(); + return next; + } + + private void moveToNext() + { + if (mDelegate.hasNext()) + { + mNextInstance = mDelegate.next(); + } + else + { + mHasNext = false; + } + } +} diff --git a/src/main/java/org/dmfs/rfc5545/instanceiterator/Merged.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/Merged.java new file mode 100644 index 0000000..dfb6a74 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/Merged.java @@ -0,0 +1,204 @@ +/* + * Copyright 2022 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.instanceiterator; + +import org.dmfs.jems2.iterable.Ascending; +import org.dmfs.jems2.iterable.Mapped; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.jems2.iterable.Sieved; +import org.dmfs.jems2.single.Collected; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + + +/** + * An {@link InstanceIterator} which iterates the elements of other {@link InstanceIterator} in chronological order. + */ +public final class Merged implements InstanceIterator +{ + private final List mIteratorHolders; + + + public Merged(InstanceIterator... delegates) + { + this(new Seq<>(delegates)); + } + + + public Merged(Iterable delegates) + { + mIteratorHolders = new Collected<>( + ArrayList::new, + new Ascending( + new Mapped<>( + IteratorHolder::new, + new Sieved<>( + InstanceIterator::hasNext, + delegates)))).value(); + + // resolve duplicates + for (int i = mIteratorHolders.size(); i > 1; --i) + { + if (mIteratorHolders.get(i - 1).mNextValue.equals(mIteratorHolders.get(i - 2).mNextValue)) + { + if (mIteratorHolders.get(i - 1).mIterator.hasNext()) + { + bubbleUp(i - 1); + } + else + { + mIteratorHolders.remove(i - 1); + } + } + } + } + + + @Override + public boolean hasNext() + { + return mIteratorHolders.size() > 0; + } + + + @Override + public DateTime next() + { + if (!hasNext()) + { + throw new NoSuchElementException("No more elements to iterate"); + } + DateTime result = mIteratorHolders.get(0).mNextValue; + advance(); + return result; + } + + + @Override + public void fastForward(DateTime until) + { + for (int i = mIteratorHolders.size() - 1; i >= 0; --i) + { + IteratorHolder it = mIteratorHolders.get(i); + if (it.mNextValue.getTimestamp() < until.getTimestamp()) + { + it.mIterator.fastForward(until); + if (it.mIterator.hasNext()) + { + it.mNextValue = it.mIterator.next(); + } + else + { + mIteratorHolders.remove(i); + } + } + } + Collections.sort(mIteratorHolders); + } + + + private void advance() + { + if (mIteratorHolders.size() == 1) + { + IteratorHolder iteratorHolder = mIteratorHolders.get(0); + if (iteratorHolder.mIterator.hasNext()) + { + iteratorHolder.mNextValue = iteratorHolder.mIterator.next(); + } + else + { + mIteratorHolders.clear(); + } + } + else + { + IteratorHolder iteratorHolder = mIteratorHolders.get(0); + if (iteratorHolder.mIterator.hasNext()) + { + bubbleUp(0); + } + else + { + mIteratorHolders.remove(0); + } + } + } + + + private void bubbleUp(int pos) + { + // pull the next element and let it bubble up to its position + final List iteratorHolders = mIteratorHolders; + IteratorHolder first = iteratorHolders.get(pos); + DateTime next = first.mIterator.next(); + while (pos < iteratorHolders.size() - 1 && !next.before(iteratorHolders.get(pos + 1).mNextValue)) + { + if (next.equals(iteratorHolders.get(pos + 1).mNextValue)) + { + // value already present, skip this one + if (first.mIterator.hasNext()) + { + next = first.mIterator.next(); + } + else + { + // this one has no more elements + iteratorHolders.remove(pos); + return; + } + } + iteratorHolders.set(pos, iteratorHolders.get(pos + 1)); + pos++; + } + first.mNextValue = next; + iteratorHolders.set(pos, first); + } + + + private final static class IteratorHolder implements Comparable + { + private DateTime mNextValue; + private final InstanceIterator mIterator; + + + private IteratorHolder(InstanceIterator iterator) + { + this(iterator.next(), iterator); + } + + + private IteratorHolder(DateTime nextValue, InstanceIterator iterator) + { + mNextValue = nextValue; + mIterator = iterator; + } + + @Override + public int compareTo(IteratorHolder o) + { + long diff = mNextValue.getTimestamp() - o.mNextValue.getTimestamp(); + return diff == 0 ? 0 : diff < 0 ? -1 : 1; + } + } +} diff --git a/src/main/java/org/dmfs/rfc5545/instanceiterator/package-info.java b/src/main/java/org/dmfs/rfc5545/instanceiterator/package-info.java new file mode 100644 index 0000000..f633e13 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/instanceiterator/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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. + */ + +@NonNullByDefault +package org.dmfs.rfc5545.instanceiterator; + +import org.eclipse.jdt.annotation.NonNullByDefault; \ No newline at end of file diff --git a/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterable.java b/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterable.java index 6a34ecc..e27e1d0 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterable.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterable.java @@ -22,7 +22,10 @@ /** * An {@link Iterable} of recurring instances. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.RecurrenceSet} */ +@Deprecated public interface InstanceIterable { /** diff --git a/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterator.java b/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterator.java index a471460..b7a5827 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterator.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/InstanceIterator.java @@ -17,6 +17,10 @@ package org.dmfs.rfc5545.iterable; +/** + * @deprecated in favour of {@link org.dmfs.rfc5545.InstanceIterator} + */ +@Deprecated public interface InstanceIterator { /** diff --git a/src/main/java/org/dmfs/rfc5545/iterable/ParsedDates.java b/src/main/java/org/dmfs/rfc5545/iterable/ParsedDates.java new file mode 100644 index 0000000..def20f7 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/iterable/ParsedDates.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.iterable; + +import org.dmfs.jems2.iterable.DelegatingIterable; +import org.dmfs.jems2.iterable.Mapped; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.jems2.iterable.Sieved; +import org.dmfs.jems2.predicate.Not; +import org.dmfs.rfc5545.DateTime; + +import java.util.TimeZone; + +/** + * An {@link Iterable} of {@link DateTime}s parsed from a {@link String} of comma separated RFC 5545 date or + * datetime values. + */ +public final class ParsedDates extends DelegatingIterable +{ + public ParsedDates(TimeZone timeZone, String datesList) + { + super(new Mapped<>(dateString -> DateTime.parse(timeZone, dateString), + new Sieved<>(new Not<>(String::isEmpty), + new Seq<>(datesList.split(","))))); + } + + + public ParsedDates(String datesList) + { + super(new Mapped<>(DateTime::parse, + new Sieved<>(new Not<>(String::isEmpty), + new Seq<>(datesList.split(","))))); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/iterable/RecurrenceSet.java b/src/main/java/org/dmfs/rfc5545/iterable/RecurrenceSet.java index 1d9e767..dea3550 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/RecurrenceSet.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/RecurrenceSet.java @@ -21,6 +21,7 @@ import org.dmfs.rfc5545.iterable.instanceiterable.Composite; import org.dmfs.rfc5545.iterable.instanceiterable.EmptyIterable; import org.dmfs.rfc5545.iterable.instanceiterator.EffectiveInstancesIterator; +import org.dmfs.rfc5545.recurrenceset.Difference; import java.util.Iterator; import java.util.TimeZone; @@ -30,7 +31,10 @@ * An {@link Iterable} of a recurrence set. *

* The recurrence set is determined from a number of {@link InstanceIterable}s providing instances and exceptions. + * + * @deprecated in favour of {@link Difference} */ +@Deprecated public final class RecurrenceSet implements Iterable { private final DateTime mFirst; diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/Composite.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/Composite.java index 0e44c08..dfda4f6 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/Composite.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/Composite.java @@ -27,7 +27,10 @@ /** * A composite {@link InstanceIterable} composed of other {@link InstanceIterable}s. This {@link InstanceIterator} * returned by this class returns the instances of all given {@link InstanceIterable}s in chronological order. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.recurrenceset.Merged} */ +@Deprecated public final class Composite implements InstanceIterable { private final InstanceIterable mDelegate; diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/EmptyIterable.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/EmptyIterable.java index c7d9c10..4f50d6f 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/EmptyIterable.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/EmptyIterable.java @@ -25,7 +25,10 @@ /** * An {@link InstanceIterable} that doesn't have any instances. + * + * @deprecated without replacement */ +@Deprecated public final class EmptyIterable implements InstanceIterable { public static final InstanceIterable INSTANCE = new EmptyIterable(); diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FastForwarded.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FastForwarded.java index f4cffe3..e8b4014 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FastForwarded.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FastForwarded.java @@ -24,7 +24,10 @@ /** * An {@link InstanceIterable} that fast forwards the iteration to a given instant. All instances prior to that instant will be skipped. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.recurrenceset.FastForwarded} */ +@Deprecated public final class FastForwarded implements InstanceIterable { private final long mTimeStamp; diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FirstAndRuleInstances.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FirstAndRuleInstances.java index f188ebf..026f477 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FirstAndRuleInstances.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/FirstAndRuleInstances.java @@ -24,11 +24,15 @@ import org.dmfs.rfc5545.iterable.instanceiterator.CountLimitedRecurrenceRuleIterator; import org.dmfs.rfc5545.recur.RecurrenceRule; import org.dmfs.rfc5545.recur.RecurrenceRuleIterator; +import org.dmfs.rfc5545.recurrenceset.OfRuleAndFirst; /** * Implements {@link InstanceIterable} for a {@link RecurrenceRule} that also returns any non-synchronized first instance. + * + * @deprecated in favour of {@link OfRuleAndFirst} */ +@Deprecated public final class FirstAndRuleInstances implements InstanceIterable { /** @@ -40,8 +44,7 @@ public final class FirstAndRuleInstances implements InstanceIterable /** * Create a new adapter for the given rule and start. * - * @param rule - * The recurrence rule to adapt to. + * @param rule The recurrence rule to adapt to. */ public FirstAndRuleInstances(RecurrenceRule rule) { diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/InstanceList.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/InstanceList.java index 0212440..c43c2f9 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/InstanceList.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/InstanceList.java @@ -21,6 +21,7 @@ import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics; import org.dmfs.rfc5545.iterable.InstanceIterable; import org.dmfs.rfc5545.iterable.InstanceIterator; +import org.dmfs.rfc5545.recurrenceset.OfList; import java.util.Arrays; import java.util.TimeZone; @@ -28,7 +29,10 @@ /** * An {@link InstanceIterable} of a given list of instances. + * + * @deprecated in favour of {@link OfList} */ +@Deprecated public final class InstanceList implements InstanceIterable { @@ -46,10 +50,8 @@ public final class InstanceList implements InstanceIterable /** * Create an adapter for the instances in list. * - * @param list - * A comma separated list of instances using the date-time format as defined in RFC 5545. - * @param timeZone - * The time zone to apply to the instances. + * @param list A comma separated list of instances using the date-time format as defined in RFC 5545. + * @param timeZone The time zone to apply to the instances. */ public InstanceList(String list, TimeZone timeZone) { @@ -60,12 +62,9 @@ public InstanceList(String list, TimeZone timeZone) /** * Create an adapter for the instances in list. * - * @param calendarMetrics - * The calendar scale to use. - * @param list - * A comma separated list of instances using the date-time format as defined in RFC 5545. - * @param timeZone - * The time zone to apply to the instances. + * @param calendarMetrics The calendar scale to use. + * @param list A comma separated list of instances using the date-time format as defined in RFC 5545. + * @param timeZone The time zone to apply to the instances. */ public InstanceList(CalendarMetrics calendarMetrics, String list, TimeZone timeZone) { @@ -94,8 +93,7 @@ public InstanceList(CalendarMetrics calendarMetrics, String list, TimeZone timeZ /** * Create an adapter for the instances in list. * - * @param instances - * An array of instance time stamps in milliseconds. + * @param instances An array of instance time stamps in milliseconds. */ public InstanceList(long[] instances) { diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/RuleInstances.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/RuleInstances.java index 1edf3a2..60d43d9 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/RuleInstances.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterable/RuleInstances.java @@ -22,12 +22,16 @@ import org.dmfs.rfc5545.iterable.InstanceIterator; import org.dmfs.rfc5545.recur.RecurrenceRule; import org.dmfs.rfc5545.recur.RecurrenceRuleIterator; +import org.dmfs.rfc5545.recurrenceset.OfRule; /** * Implements {@link InstanceIterable} for a {@link RecurrenceRule}. That only iterates instances that match the {@link RecurrenceRule}. * Any non-synchronized first instance is not returned. + * + * @deprecated in favour of {@link OfRule} */ +@Deprecated public final class RuleInstances implements InstanceIterable { @@ -40,8 +44,7 @@ public final class RuleInstances implements InstanceIterable /** * Create a new adapter for the given rule and start. * - * @param rule - * The recurrence rule to adapt to. + * @param rule The recurrence rule to adapt to. */ public RuleInstances(RecurrenceRule rule) { diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/Composite.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/Composite.java index ac7d3de..0ba17a5 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/Composite.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/Composite.java @@ -32,7 +32,10 @@ /** * An {@link InstanceIterator} which iterates the elements of other {@link InstanceIterator} in chronological order. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.instanceiterator.Merged} */ +@Deprecated public final class Composite implements InstanceIterator { private List mHelpers; diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/CountLimitedRecurrenceRuleIterator.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/CountLimitedRecurrenceRuleIterator.java index ced99a7..79ed453 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/CountLimitedRecurrenceRuleIterator.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/CountLimitedRecurrenceRuleIterator.java @@ -25,7 +25,10 @@ /** * An {@link InstanceIterator} which limits the number of iterated instances. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.instanceiterator.CountLimitedRecurrenceRuleIterator} */ +@Deprecated public final class CountLimitedRecurrenceRuleIterator implements InstanceIterator { private final RecurrenceRuleIterator mDelegate; diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EffectiveInstancesIterator.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EffectiveInstancesIterator.java index ef949eb..2ae7ac1 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EffectiveInstancesIterator.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EffectiveInstancesIterator.java @@ -25,7 +25,10 @@ /** * An iterator for recurrence sets. It takes a number of {@link InstanceIterator}s for instances and exceptions and iterates all resulting instances * (i.e. only the instances, not the exceptions). + * + * @deprecated in favour of {@link org.dmfs.rfc5545.instanceiterator.EffectiveInstancesIterator} */ +@Deprecated public final class EffectiveInstancesIterator implements InstanceIterator { /** @@ -45,10 +48,8 @@ public final class EffectiveInstancesIterator implements InstanceIterator /** * Create a new recurrence iterator for specific lists of instances and exceptions. * - * @param instances - * The instances, must not be null or empty. - * @param exceptions - * The exceptions, may be null. + * @param instances The instances, must not be null or empty. + * @param exceptions The exceptions, may be null. */ public EffectiveInstancesIterator(InstanceIterator instances, InstanceIterator exceptions) { @@ -74,9 +75,7 @@ public boolean hasNext() * Get the next instance of this set. Do not call this if {@link #hasNext()} returns false. * * @return The time in milliseconds since the epoch of the next instance. - * - * @throws ArrayIndexOutOfBoundsException - * if there are no more instances. + * @throws ArrayIndexOutOfBoundsException if there are no more instances. */ @Override public long next() @@ -94,8 +93,7 @@ public long next() /** * Fast-forward to the next instance at or after the given date. * - * @param until - * The date to fast-forward to in milliseconds since the epoch. + * @param until The date to fast-forward to in milliseconds since the epoch. */ @Override public void fastForward(long until) diff --git a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EmptyIterator.java b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EmptyIterator.java index 3948dc0..0df51a9 100644 --- a/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EmptyIterator.java +++ b/src/main/java/org/dmfs/rfc5545/iterable/instanceiterator/EmptyIterator.java @@ -24,7 +24,10 @@ /** * An {@link InstanceIterator} without any instances. + * + * @deprecated in favour of {@link org.dmfs.rfc5545.instanceiterator.EmptyIterator} */ +@Deprecated public final class EmptyIterator implements InstanceIterator { public static final InstanceIterator INSTANCE = new EmptyIterator(); diff --git a/src/main/java/org/dmfs/rfc5545/optional/Last.java b/src/main/java/org/dmfs/rfc5545/optional/Last.java new file mode 100644 index 0000000..59a0ac1 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/optional/Last.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.optional; + +import org.dmfs.jems2.Optional; + +import java.util.Iterator; + +/** + * TODO: move to jems2 + */ +final class Last implements Optional +{ + private final Iterable mIterable; + + public Last(Iterable iterable) + { + this.mIterable = iterable; + } + + @Override + public boolean isPresent() + { + return mIterable.iterator().hasNext(); + } + + @Override + public T value() + { + Iterator iterator = mIterable.iterator(); + T result = iterator.next(); + while (iterator.hasNext()) + { + result = iterator.next(); + } + return result; + } +} diff --git a/src/main/java/org/dmfs/rfc5545/optional/LastInstance.java b/src/main/java/org/dmfs/rfc5545/optional/LastInstance.java new file mode 100644 index 0000000..a037f98 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/optional/LastInstance.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.optional; + +import org.dmfs.jems2.Optional; +import org.dmfs.jems2.optional.DelegatingOptional; +import org.dmfs.jems2.optional.Restrained; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.RecurrenceSet; + +/** + * The {@link Optional} last {@link DateTime} of a {@link RecurrenceSet}. + * The value is absent when the {@link RecurrenceSet} is empty or infinite. + */ +public final class LastInstance extends DelegatingOptional +{ + public LastInstance(RecurrenceSet recurrenceSet) + { + super(new Restrained<>(() -> !recurrenceSet.isInfinite(), new Last<>(recurrenceSet))); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/optional/package-info.java b/src/main/java/org/dmfs/rfc5545/optional/package-info.java new file mode 100644 index 0000000..750644f --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/optional/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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. + */ + +@NonNullByDefault +package org.dmfs.rfc5545.optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; \ No newline at end of file diff --git a/src/main/java/org/dmfs/rfc5545/package-info.java b/src/main/java/org/dmfs/rfc5545/package-info.java new file mode 100644 index 0000000..826547c --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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. + */ + +@NonNullByDefault +package org.dmfs.rfc5545; + +import org.eclipse.jdt.annotation.NonNullByDefault; \ No newline at end of file diff --git a/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRuleIterator.java b/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRuleIterator.java index f80083b..6852111 100644 --- a/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRuleIterator.java +++ b/src/main/java/org/dmfs/rfc5545/recur/RecurrenceRuleIterator.java @@ -12,14 +12,16 @@ * 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.DateTime; import org.dmfs.rfc5545.Instance; +import org.dmfs.rfc5545.InstanceIterator; import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics; +import org.eclipse.jdt.annotation.NonNull; import java.util.TimeZone; @@ -30,7 +32,7 @@ * * @author Marten Gajda */ -public final class RecurrenceRuleIterator +public final class RecurrenceRuleIterator implements InstanceIterator { /** * The previous iterator instance. This is null for the {@link FreqIterator}. @@ -75,10 +77,8 @@ public final class RecurrenceRuleIterator /** * Creates a new {@link RecurrenceRuleIterator} that gets its input from ruleIterator. * - * @param ruleIterator - * The last {@link RuleIterator} in the chain of iterators. - * @param start - * The first instance to iterate. + * @param ruleIterator The last {@link RuleIterator} in the chain of iterators. + * @param start The first instance to iterate. */ RecurrenceRuleIterator(RuleIterator ruleIterator, DateTime start, CalendarMetrics calendarMetrics) { @@ -148,13 +148,13 @@ public DateTime nextDateTime() if (mAllDay) { return new DateTime(mCalendarMetrics, Instance.year(nextInstance), Instance.month(nextInstance), - Instance.dayOfMonth(nextInstance)); + Instance.dayOfMonth(nextInstance)); } else { return new DateTime(mCalendarMetrics, mTimeZone, Instance.year(nextInstance), Instance.month(nextInstance), - Instance.dayOfMonth(nextInstance), - Instance.hour(nextInstance), Instance.minute(nextInstance), Instance.second(nextInstance)); + Instance.dayOfMonth(nextInstance), + Instance.hour(nextInstance), Instance.minute(nextInstance), Instance.second(nextInstance)); } } @@ -164,6 +164,12 @@ public boolean hasNext() return mNextInstance != Long.MIN_VALUE; } + @Override + public DateTime next() + { + return nextDateTime(); + } + /** * Peek at the next instance to be returned by {@link #nextMillis()} without actually iterating it. Calling this method (even multiple times) won't affect @@ -207,14 +213,14 @@ public DateTime peekDateTime() if (mAllDay) { return mNextDateTime = new DateTime(mCalendarMetrics, Instance.year(nextInstance), - Instance.month(nextInstance), Instance.dayOfMonth(nextInstance)); + Instance.month(nextInstance), Instance.dayOfMonth(nextInstance)); } else { return mNextDateTime = new DateTime(mCalendarMetrics, mTimeZone, Instance.year(nextInstance), - Instance.month(nextInstance), - Instance.dayOfMonth(nextInstance), Instance.hour(nextInstance), Instance.minute(nextInstance), - Instance.second(nextInstance)); + Instance.month(nextInstance), + Instance.dayOfMonth(nextInstance), Instance.hour(nextInstance), Instance.minute(nextInstance), + Instance.second(nextInstance)); } } @@ -223,8 +229,7 @@ public DateTime peekDateTime() * Skip the given number of instances.

Note: After calling this method you should call {@link #hasNext()} before you continue because * there might be less than skip instances left when you call this.

* - * @param skip - * The number of instances to skip. + * @param skip The number of instances to skip. */ public void skip(int skip) { @@ -258,8 +263,7 @@ public void skip(int skip) * Skip all instances up to a specific date.

Note: After calling this method you should call {@link #hasNext()} before you continue * because there might no more instances left if there is an UNTIL or COUNT part in the rule.

* - * @param until - * The time stamp of earliest date to be returned by the next call to {@link #nextMillis()} or {@link #nextDateTime()}. + * @param until The time stamp of earliest date to be returned by the next call to {@link #nextMillis()} or {@link #nextDateTime()}. */ public void fastForward(long until) { @@ -298,17 +302,16 @@ public void fastForward(long until) * Skip all instances up to a specific date.

Note: After calling this method you should call {@link #hasNext()} before you continue * because there might no more instances left if there is an UNTIL or COUNT part in the rule.

* - * @param until - * The earliest date to be returned by the next call to {@link #nextMillis()} or {@link #nextDateTime()}. + * @param until The earliest date to be returned by the next call to {@link #nextMillis()} or {@link #nextDateTime()}. */ - public void fastForward(DateTime until) + public void fastForward(@NonNull DateTime until) { if (!hasNext()) { return; } - DateTime untilDate = until.shiftTimeZone(mTimeZone); + DateTime untilDate = until.isAllDay() ? until.startOfDay().shiftTimeZone(mTimeZone) : until.shiftTimeZone(mTimeZone); // convert until to an instance long untilInstance = untilDate.getInstance(); diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/AbstractRecurrenceAdapter.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/AbstractRecurrenceAdapter.java deleted file mode 100644 index 5da9ad2..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/AbstractRecurrenceAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2013 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.recurrenceset; - -import java.util.TimeZone; - - -/** - * An abstract adapter for recurrence instance sets. This represents the instances of a specific instance set (e.g. an rrule, an exrule, a list of rdates or - * exdates) - * - * @author Marten Gajda - */ -@Deprecated -public abstract class AbstractRecurrenceAdapter -{ - - interface InstanceIterator - { - - /** - * Check if there is at least one more instance to iterate. - * - * @return true if the next call to {@link #next()} will return another instance, false otherwise. - */ - abstract boolean hasNext(); - - /** - * Get the next instance of this set. Do not call this if {@link #hasNext()} returns false. - * - * @return The time in milliseconds since the epoch of the next instance. - * - * @throws ArrayIndexOutOfBoundsException - * if there are no more instances. - */ - abstract long next(); - - /** - * Skip all instances till until. If until is an instance itself it will be the next iterated instance. If the rule doesn't - * recur till that date the next call to {@link #hasNext()} will return false. - * - * @param until - * A time stamp of the date to fast forward to. - */ - abstract void fastForward(long until); - - } - - - /** - * Get an iterator for this adapter. - * - * @param timezone - * The {@link TimeZone} of the first instance. - * @param start - * The start date in milliseconds since the epoch. - */ - abstract InstanceIterator getIterator(TimeZone timezone, long start); - - /** - * Returns whether this adapter iterates an infinite number of instances. - * - * @return true if the instances in this adapter are not limited, false otherwise. - */ - abstract boolean isInfinite(); - - /** - * Returns the last instance this adapter will iterate or {@link Long#MAX_VALUE} if {@link #isInfinite()} returns true. - * - * @param timezone - * The {@link TimeZone} of the first instance. - * @param start - * The start date in milliseconds since the epoch. - * - * @return The last instance in milliseconds since the epoch. - */ - abstract long getLastInstance(TimeZone timezone, long start); -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/CompositeIterator.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/CompositeIterator.java deleted file mode 100644 index 6ab8a8f..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/CompositeIterator.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2020 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.recurrenceset; - -import org.dmfs.jems2.comparator.By; -import org.dmfs.jems2.iterable.Mapped; -import org.dmfs.jems2.iterable.Sieved; -import org.dmfs.jems2.single.Collected; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; - - -/** - * An {@link AbstractRecurrenceAdapter.InstanceIterator} which iterates the elements of other {@link AbstractRecurrenceAdapter.InstanceIterator} in sorted - * order. - */ -@Deprecated -public final class CompositeIterator implements AbstractRecurrenceAdapter.InstanceIterator -{ - private List mHelpers; - - - public CompositeIterator(Iterable delegates) - { - mHelpers = new Collected<>( - ArrayList::new, - new Mapped<>( - Helper::new, - new Sieved<>( - AbstractRecurrenceAdapter.InstanceIterator::hasNext, - delegates))).value(); - Collections.sort(mHelpers, new By<>(helper -> helper.nextValue)); - - // resolve duplicates - for (int i = mHelpers.size(); i > 1; --i) - { - if (mHelpers.get(i - 1).nextValue == mHelpers.get(i - 2).nextValue) - { - if (mHelpers.get(i - 1).iterator.hasNext()) - { - bubbleUp(i - 1); - } - else - { - mHelpers.remove(i - 1); - } - } - } - } - - - @Override - public boolean hasNext() - { - return mHelpers.size() > 0; - } - - - @Override - public long next() - { - if (!hasNext()) - { - throw new NoSuchElementException("No more elements to iterate"); - } - long result = mHelpers.get(0).nextValue; - advance(); - return result; - } - - - @Override - public void fastForward(long until) - { - for (int i = mHelpers.size() - 1; i >= 0; --i) - { - Helper it = mHelpers.get(i); - if (it.nextValue < until) - { - it.iterator.fastForward(until); - if (it.iterator.hasNext()) - { - it.nextValue = it.iterator.next(); - } - else - { - mHelpers.remove(i); - } - } - } - Collections.sort(mHelpers, new By<>(helper -> helper.nextValue)); - } - - - private void advance() - { - if (mHelpers.size() == 1) - { - Helper helper = mHelpers.get(0); - if (helper.iterator.hasNext()) - { - helper.nextValue = helper.iterator.next(); - } - else - { - mHelpers.clear(); - } - } - else - { - Helper helper = mHelpers.get(0); - if (helper.iterator.hasNext()) - { - bubbleUp(0); - } - else - { - mHelpers.remove(0); - } - } - } - - - private void bubbleUp(int pos) - { - // pull the next element and let it bubble up to its position - final List helpers = mHelpers; - Helper first = helpers.get(pos); - long next = first.iterator.next(); - while (pos < helpers.size() - 1 && next >= helpers.get(pos + 1).nextValue) - { - if (next == helpers.get(pos + 1).nextValue) - { - // value already present, skip this one - if (first.iterator.hasNext()) - { - next = first.iterator.next(); - } - else - { - // this one has no more elements - helpers.remove(pos); - return; - } - } - helpers.set(pos, helpers.get(pos + 1)); - pos++; - } - first.nextValue = next; - helpers.set(pos, first); - } - - - private final static class Helper - { - private long nextValue; - private final AbstractRecurrenceAdapter.InstanceIterator iterator; - - - private Helper(AbstractRecurrenceAdapter.InstanceIterator iterator) - { - this(iterator.next(), iterator); - } - - - private Helper(long nextValue, AbstractRecurrenceAdapter.InstanceIterator iterator) - { - this.nextValue = nextValue; - this.iterator = iterator; - } - } -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/Difference.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/Difference.java new file mode 100644 index 0000000..bfaf2f5 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/Difference.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.instanceiterator.EffectiveInstancesIterator; + +/** + * A {@link RecurrenceSet} that contains all the instances of a given {@link RecurrenceSet} except the ones that are + * in the exceptions {@link RecurrenceSet}. + */ +public final class Difference implements RecurrenceSet +{ + private final RecurrenceSet mMinuend; + private final RecurrenceSet mSubtrahend; + + public Difference(RecurrenceSet minuend, RecurrenceSet subtrahend) + { + mMinuend = minuend; + mSubtrahend = subtrahend; + } + + @Override + public InstanceIterator iterator() + { + return new EffectiveInstancesIterator(mMinuend.iterator(), mSubtrahend.iterator()); + } + + @Override + public boolean isInfinite() + { + return mMinuend.isInfinite(); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/FastForwarded.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/FastForwarded.java new file mode 100644 index 0000000..05aff8c --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/FastForwarded.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; + + +/** + * A {@link RecurrenceSet} that fast forwards the iteration to a given instant. + * All instances prior to that instant will be skipped. + */ +public final class FastForwarded implements RecurrenceSet +{ + private final DateTime mTimeStamp; + private final RecurrenceSet mDelegate; + + + public FastForwarded(DateTime fastForwardTo, RecurrenceSet... delegate) + { + this(fastForwardTo, new Seq<>(delegate)); + } + + + public FastForwarded(DateTime fastForwardTo, Iterable delegate) + { + this(fastForwardTo, new Merged(delegate)); + } + + + public FastForwarded(DateTime timeStamp, RecurrenceSet delegate) + { + mTimeStamp = timeStamp; + mDelegate = delegate; + } + + + @Override + public InstanceIterator iterator() + { + InstanceIterator iterator = mDelegate.iterator(); + iterator.fastForward(mTimeStamp); + return iterator; + } + + @Override + public boolean isInfinite() + { + return mDelegate.isInfinite(); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/Merged.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/Merged.java new file mode 100644 index 0000000..6b7a6f2 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/Merged.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.iterable.Mapped; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.jems2.iterable.Sieved; +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; + + +/** + * A composite {@link RecurrenceSet} composed of other {@link RecurrenceSet}s. This {@link RecurrenceSet} + * iterates the instances of all given {@link RecurrenceSet}s in chronological order. + */ +public final class Merged implements RecurrenceSet +{ + private final RecurrenceSet mDelegate; + + + public Merged(RecurrenceSet... delegates) + { + this(new Seq<>(delegates)); + } + + + public Merged(Iterable delegates) + { + this( + new RecurrenceSet() + { + @Override + public InstanceIterator iterator() + { + return new org.dmfs.rfc5545.instanceiterator.Merged(new Mapped<>(RecurrenceSet::iterator, delegates)); + } + + @Override + public boolean isInfinite() + { + return new Sieved<>(RecurrenceSet::isInfinite, delegates).iterator().hasNext(); + } + + }); + } + + + private Merged(RecurrenceSet delegate) + { + mDelegate = delegate; + } + + + @Override + public InstanceIterator iterator() + { + return mDelegate.iterator(); + } + + @Override + public boolean isInfinite() + { + return mDelegate.isInfinite(); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/OfList.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfList.java new file mode 100644 index 0000000..969ad84 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfList.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.comparator.By; +import org.dmfs.jems2.iterable.Expanded; +import org.dmfs.jems2.iterable.Frozen; +import org.dmfs.jems2.iterable.Seq; +import org.dmfs.jems2.iterable.Sorted; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.instanceiterator.EmptyIterator; +import org.dmfs.rfc5545.instanceiterator.FastForwardable; +import org.dmfs.rfc5545.iterable.ParsedDates; + +import java.util.Iterator; +import java.util.TimeZone; + + +/** + * A {@link RecurrenceSet} of a given list of instances, typically provided in the form of {@code RDATE}s or {@code EXDATE}s. + *

+ * Note, that this class does not support parsing the {@code PERIOD} type specified in + * RFC 5545, section 3.3.9. + */ +public final class OfList implements RecurrenceSet +{ + private final Iterable mInstances; + + + public OfList(TimeZone timeZone, String... instances) + { + this(timeZone, new Seq<>(instances)); + } + + public OfList(TimeZone timeZone, Iterable instances) + { + this(new Expanded<>(list -> new ParsedDates(timeZone, list), instances)); + } + + public OfList(TimeZone timeZone, String instances) + { + this(new ParsedDates(timeZone, instances)); + } + + public OfList(String... instances) + { + this(new Expanded<>(ParsedDates::new, new Seq<>(instances))); + } + + public OfList(String instances) + { + this(new ParsedDates(instances)); + } + + public OfList(DateTime... instances) + { + this(new Seq<>(instances)); + } + + public OfList(Iterable instances) + { + mInstances = new Frozen<>(new Sorted<>(new By<>(DateTime::getTimestamp), instances)); + } + + @Override + public InstanceIterator iterator() + { + Iterator delegate = mInstances.iterator(); + return delegate.hasNext() + ? new FastForwardable(delegate.next(), delegate.hasNext() ? delegate : EmptyIterator.INSTANCE) + : EmptyIterator.INSTANCE; + } + + @Override + public boolean isInfinite() + { + return false; + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRule.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRule.java new file mode 100644 index 0000000..39a18e8 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRule.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.recur.RecurrenceRule; + +/** + * The {@link RecurrenceSet} of a single {@link RecurrenceRule}. + */ +public final class OfRule implements RecurrenceSet +{ + private final RecurrenceRule mRecurrenceRule; + private final DateTime mFirstInstance; + + public OfRule(RecurrenceRule recurrenceRule, DateTime firstInstance) + { + mRecurrenceRule = recurrenceRule; + mFirstInstance = firstInstance; + } + + @Override + public InstanceIterator iterator() + { + return mRecurrenceRule.iterator(mFirstInstance); + } + + @Override + public boolean isInfinite() + { + return mRecurrenceRule.isInfinite(); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirst.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirst.java new file mode 100644 index 0000000..cf4ad8f --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirst.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.InstanceIterator; +import org.dmfs.rfc5545.RecurrenceSet; +import org.dmfs.rfc5545.instanceiterator.CountLimitedRecurrenceRuleIterator; +import org.dmfs.rfc5545.instanceiterator.Merged; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.dmfs.rfc5545.recur.RecurrenceRuleIterator; + + +/** + * The {@link RecurrenceSet} of a single {@link RecurrenceRule} that also returns any non-synchronized first instance. + */ +public final class OfRuleAndFirst implements RecurrenceSet +{ + private final RecurrenceRule mRecurrenceRule; + private final DateTime mFirst; + + /** + * Create a new adapter for the given rule and start. + * + * @param rule The recurrence rule to adapt to. + */ + public OfRuleAndFirst(RecurrenceRule rule, DateTime first) + { + mRecurrenceRule = rule; + mFirst = first; + } + + @Override + public InstanceIterator iterator() + { + RecurrenceRuleIterator ruleIterator = mRecurrenceRule.iterator(mFirst); + if (!ruleIterator.peekDateTime().equals(mFirst)) + { + return new Merged( + new OfList(mFirst).iterator(), + mRecurrenceRule.getCount() != null + // we have a count limited rule and an unsynced start date + // since the start date counts as the first element, the RRULE iterator should return one less element. + ? new CountLimitedRecurrenceRuleIterator(ruleIterator, mRecurrenceRule.getCount() - 1) + : ruleIterator); + } + return ruleIterator; + } + + @Override + public boolean isInfinite() + { + return mRecurrenceRule.isInfinite(); + } +} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceList.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceList.java deleted file mode 100644 index 768c277..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceList.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2013 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.recurrenceset; - -import org.dmfs.rfc5545.DateTime; -import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics; - -import java.util.Arrays; -import java.util.TimeZone; - - -/** - * A concrete {@link AbstractRecurrenceAdapter} for lists of instances. You can provide the instances in a String or in an array of longs. - * - * @author Marten Gajda - */ -@Deprecated -public final class RecurrenceList extends AbstractRecurrenceAdapter -{ - class InstanceIterator implements AbstractRecurrenceAdapter.InstanceIterator - { - private int mNext; - - - @Override - public boolean hasNext() - { - return mNext < mCount; - } - - - @Override - public long next() - { - if (mNext >= mCount) - { - throw new ArrayIndexOutOfBoundsException("No more instances to iterate."); - } - return mInstances[mNext++]; - } - - - @Override - public void fastForward(long until) - { - int count = mCount; - int next = mNext; - long[] instances = mInstances; - while (next < count && instances[next] < until) - { - ++next; - } - mNext = next; - } - } - - - /** - * The instances to iterate. - */ - private final long[] mInstances; - - /** - * The number of instances in {@link #mInstances}. - */ - private final int mCount; - - - /** - * Create an adapter for the instances in list. - * - * @param list - * A comma separated list of instances using the date-time format as defined in RFC 5545. - * @param timeZone - * The time zone to apply to the instances. - */ - public RecurrenceList(String list, TimeZone timeZone) - { - this(DateTime.GREGORIAN_CALENDAR_SCALE, list, timeZone); - } - - - /** - * Create an adapter for the instances in list. - * - * @param calendarMetrics - * The calendar scale to use. - * @param list - * A comma separated list of instances using the date-time format as defined in RFC 5545. - * @param timeZone - * The time zone to apply to the instances. - */ - public RecurrenceList(CalendarMetrics calendarMetrics, String list, TimeZone timeZone) - { - if (list == null || list.length() == 0) - { - mInstances = null; - mCount = 0; - return; - } - - String[] instances = list.split(","); - mInstances = new long[instances.length]; - int count = 0; - - for (String instanceString : instances) - { - DateTime instance = DateTime.parse(calendarMetrics, timeZone, instanceString); - mInstances[count] = instance.getTimestamp(); - ++count; - } - mCount = count; - Arrays.sort(mInstances); - } - - - /** - * Create an adapter for the instances in list. - * - * @param instances - * An array of instance time stamps in milliseconds. - */ - public RecurrenceList(long[] instances) - { - mInstances = new long[instances.length]; - System.arraycopy(instances, 0, mInstances, 0, instances.length); - mCount = instances.length; - Arrays.sort(mInstances); - } - - - @Override - InstanceIterator getIterator(TimeZone timezone, long start) - { - return new InstanceIterator(); - } - - - @Override - boolean isInfinite() - { - // the given lists are always finite - return false; - } - - - @Override - long getLastInstance(TimeZone timezone, long start) - { - long[] instances = mInstances; - return instances[instances.length - 1]; - } -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapter.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapter.java deleted file mode 100644 index 9b27578..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapter.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2013 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.recurrenceset; - -import org.dmfs.rfc5545.recur.RecurrenceRule; -import org.dmfs.rfc5545.recur.RecurrenceRuleIterator; - -import java.util.TimeZone; - - -/** - * Implements {@link AbstractRecurrenceAdapter} for a {@link RecurrenceRule}. - * - * @author Marten Gajda - */ -@Deprecated -public final class RecurrenceRuleAdapter extends AbstractRecurrenceAdapter -{ - - class InstanceIterator implements AbstractRecurrenceAdapter.InstanceIterator - { - private final RecurrenceRuleIterator mIterator; - - - public InstanceIterator(RecurrenceRuleIterator iterator) - { - mIterator = iterator; - } - - - @Override - public boolean hasNext() - { - return mIterator.hasNext(); - } - - - @Override - public long next() - { - return mIterator.nextMillis(); - } - - - @Override - public void fastForward(long until) - { - mIterator.fastForward(until); - } - - } - - - /** - * The recurrence rule. - */ - private final RecurrenceRule mRrule; - - - /** - * Create a new adapter for the given rule and start. - * - * @param rule - * The recurrence rule to adapt to. - */ - public RecurrenceRuleAdapter(RecurrenceRule rule) - { - mRrule = rule; - } - - - @Override - AbstractRecurrenceAdapter.InstanceIterator getIterator(TimeZone timezone, long start) - { - RecurrenceRuleIterator ruleIterator = mRrule.iterator(start, timezone); - AbstractRecurrenceAdapter.InstanceIterator iterator = new InstanceIterator(ruleIterator); - if (mRrule.getCount() != null && ruleIterator.peekMillis() != start) - { - // we have a count limited rule and an unsynched start date - // since the start date counts as the first element, the RRULE iterator should return one less element. - iterator = new CountLimitedRecurrenceRuleIterator(ruleIterator, mRrule.getCount() - 1); - } - return iterator; - } - - - @Override - boolean isInfinite() - { - return mRrule.isInfinite(); - } - - - @Override - long getLastInstance(TimeZone timezone, long start) - { - if (isInfinite()) - { - return Long.MAX_VALUE; - } - - RecurrenceRuleIterator iterator = mRrule.iterator(start, timezone); - iterator.skipAllButLast(); - - long lastInstance = Long.MIN_VALUE; - if (iterator.hasNext()) - { - lastInstance = iterator.nextMillis(); - } - return lastInstance; - } -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSet.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSet.java deleted file mode 100644 index fdee376..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSet.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (C) 2013 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.recurrenceset; - -import org.dmfs.rfc5545.recurrenceset.AbstractRecurrenceAdapter.InstanceIterator; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.TimeZone; - - -/** - * A recurrence set. A recurrence set consists of all instances defined by a recurrence rule or a list if instances except for exception instances. Exception - * instances are defined by exceptions rules or lists of exception instances.

This class allows you to add any number of recurrence rules, recurrence - * instances, exception rules and exception instance. It returns an {@link Iterator} that iterates all resulting instances.

- * - * @author Marten Gajda - */ -@Deprecated -public class RecurrenceSet -{ - - /** - * All the instances in the set. Not all of them may be iterated, since instances that are exceptions will be skipped. - */ - private final List mInstances = new ArrayList(); - - /** - * All exceptions in the set. - */ - private List mExceptions = null; - - /** - * Indicates if the recurrence set is infinite. - */ - private boolean mIsInfinite = false; - - - /** - * Add instances to the set of instances. - * - * @param adapter - * An {@link AbstractRecurrenceAdapter} that defines instances. - */ - public void addInstances(AbstractRecurrenceAdapter adapter) - { - mInstances.add(adapter); - - // the entire set is infinite if there is at least one infinite instance set - mIsInfinite |= adapter.isInfinite(); - } - - - /** - * Add exceptions to the set of instances (i.e. effectively remove instances from the instance set). - * - * @param adapter - * An {@link AbstractRecurrenceAdapter} that defines instances. - */ - public void addExceptions(AbstractRecurrenceAdapter adapter) - { - if (mExceptions == null) - { - mExceptions = new ArrayList(); - } - mExceptions.add(adapter); - } - - - /** - * Get an iterator for the specified start time. - * - * @param timezone - * The {@link TimeZone} of the first instance. - * @param start - * The start time in milliseconds since the epoch. - * - * @return A {@link RecurrenceSetIterator} that iterates all instances. - */ - public RecurrenceSetIterator iterator(TimeZone timezone, long start) - { - return iterator(timezone, start, Long.MAX_VALUE); - } - - - /** - * Return a new {@link RecurrenceSetIterator} for this recurrence set. - * - * @param timezone - * The {@link TimeZone} of the first instance. - * @param start - * The start time in milliseconds since the epoch. - * @param end - * The end of the time range to iterate in milliseconds since the epoch. - * - * @return A {@link RecurrenceSetIterator} that iterates all instances. - */ - public RecurrenceSetIterator iterator(TimeZone timezone, long start, long end) - { - List instances = new ArrayList(mInstances.size()); - // make sure we add the start as the first instance - instances.add(new RecurrenceList(new long[] { start }).getIterator(timezone, start)); - for (AbstractRecurrenceAdapter adapter : mInstances) - { - instances.add(adapter.getIterator(timezone, start)); - } - - List exceptions = null; - if (mExceptions != null) - { - exceptions = new ArrayList(mExceptions.size()); - for (AbstractRecurrenceAdapter adapter : mExceptions) - { - exceptions.add(adapter.getIterator(timezone, start)); - } - } - return new RecurrenceSetIterator(instances, exceptions).setEnd(end); - } - - - /** - * Returns whether this {@link RecurrenceSet} contains an infinite number of instances. - * - * @return true if the instances in this {@link RecurrenceSet} is infinite, false otherwise. - */ - public boolean isInfinite() - { - return mIsInfinite; - } - - - public long getLastInstance(TimeZone timezone, long start) - { - if (isInfinite()) - { - throw new IllegalStateException("can not calculate the last instance of an infinite recurrence set"); - } - - if (mExceptions != null && mExceptions.size() > 0) - { - /* - * This is the difficult case. - * - * The last instance of the given rules might be an exception. Unfortunately there doesn't seem to be an easier way to get the very last instance - * than by iterating all instances. - */ - long last = Long.MIN_VALUE; - - RecurrenceSetIterator iterator = iterator(timezone, start); - while (iterator.hasNext()) - { - last = iterator.next(); - } - return last; - } - - if (mInstances.size() == 1) - { - // simple case, only one set of instances - return mInstances.get(0).getLastInstance(timezone, start); - } - - // We have multiple instance sets, but no exceptions. That means we just have to determine the maximum instance over all sets. - long last = Long.MIN_VALUE; - for (AbstractRecurrenceAdapter adapter : mInstances) - { - long lastOfAdapter = adapter.getLastInstance(timezone, start); - if (lastOfAdapter > last) - { - last = lastOfAdapter; - } - } - - return last; - } -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIterator.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIterator.java deleted file mode 100644 index 59eada8..0000000 --- a/src/main/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIterator.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2013 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.recurrenceset; - -import org.dmfs.rfc5545.recurrenceset.AbstractRecurrenceAdapter.InstanceIterator; - -import java.util.List; -import java.util.Locale; - - -/** - * An iterator for recurrence sets. It takes a number of {@link AbstractRecurrenceAdapter}s for instances and exceptions and iterates all resulting instances - * (i.e. only the instances, not the exceptions).

This class doesn't implement the {@link InstanceIterator} interface for one reasons:

  • An - * {@link InstanceIterator} always returns an {@link Object}, so instead of a primitive long we would have to return a {@link Long}. That is an - * additional object which doesn't have any advantage.
- * - * @author Marten Gajda - */ -@Deprecated -public class RecurrenceSetIterator -{ - /** - * Throw if we skipped this many instances in a line, because they were exceptions. - */ - private final static int MAX_SKIPPED_INSTANCES = 1000; - - private final InstanceIterator mInstances; - - private final InstanceIterator mExceptions; - - private long mIterateEnd = Long.MAX_VALUE; - - private long mNextInstance = Long.MIN_VALUE; - - private long mNextException = Long.MIN_VALUE; - - - /** - * Create a new recurrence iterator for specific lists of instances and exceptions. - * - * @param instances - * The instances, must not be null or empty. - * @param exceptions - * The exceptions, may be null. - */ - RecurrenceSetIterator(List instances, List exceptions) - { - mInstances = instances.size() == 1 ? instances.get(0) : new CompositeIterator(instances); - mExceptions = exceptions == null || exceptions.isEmpty() ? new EmptyIterator() : - exceptions.size() == 1 ? exceptions.get(0) : new CompositeIterator(exceptions); - pullNext(); - } - - - /** - * Set the iteration end. The iterator will stop if the next instance is after the given date, no matter how many instances are still to come. This needs to - * be set before you start iterating, otherwise you may get wrong results. - * - * @param end - * The date at which to stop the iteration in milliseconds since the epoch. - */ - RecurrenceSetIterator setEnd(long end) - { - mIterateEnd = end; - return this; - } - - - /** - * Check if there is at least one more instance to iterate. - * - * @return true if the next call to {@link #next()} will return another instance, false otherwise. - */ - public boolean hasNext() - { - return mNextInstance < mIterateEnd; - } - - - /** - * Get the next instance of this set. Do not call this if {@link #hasNext()} returns false. - * - * @return The time in milliseconds since the epoch of the next instance. - * - * @throws ArrayIndexOutOfBoundsException - * if there are no more instances. - */ - public long next() - { - if (!hasNext()) - { - throw new ArrayIndexOutOfBoundsException("no more elements"); - } - long result = mNextInstance; - pullNext(); - return result; - } - - - /** - * Fast forward to the next instance at or after the given date. - * - * @param until - * The date to fast forward to in milliseconds since the epoch. - */ - public void fastForward(long until) - { - if (mNextInstance < until) - { - mInstances.fastForward(until); - mExceptions.fastForward(until); - pullNext(); - } - } - - - private void pullNext() - { - long next = Long.MAX_VALUE; - long nextException = mNextException; - int skipableInstances = MAX_SKIPPED_INSTANCES; - while (mInstances.hasNext()) - { - next = mInstances.next(); - while (nextException < next) - { - nextException = mExceptions.hasNext() ? mExceptions.next() : Long.MAX_VALUE; - } - if (nextException > next) - { - break; - } - if (--skipableInstances <= 0) - { - throw new RuntimeException(String.format(Locale.ENGLISH, "Skipped too many (%d) instances", MAX_SKIPPED_INSTANCES)); - } - // we've skipped the next instance, this might have bene the last one - next = Long.MAX_VALUE; - } - mNextInstance = next; - mNextException = nextException; - } -} diff --git a/src/main/java/org/dmfs/rfc5545/recurrenceset/package-info.java b/src/main/java/org/dmfs/rfc5545/recurrenceset/package-info.java new file mode 100644 index 0000000..e26d540 --- /dev/null +++ b/src/main/java/org/dmfs/rfc5545/recurrenceset/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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. + */ + +@NonNullByDefault +package org.dmfs.rfc5545.recurrenceset; + +import org.eclipse.jdt.annotation.NonNullByDefault; \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/iterable/ParsedDatesTest.java b/src/test/java/org/dmfs/rfc5545/iterable/ParsedDatesTest.java new file mode 100644 index 0000000..a05549c --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/iterable/ParsedDatesTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024 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.iterable; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.iterable.ParsedDates; +import org.junit.jupiter.api.Test; +import org.saynotobugs.confidence.quality.Core; + +import static java.util.TimeZone.getTimeZone; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class ParsedDatesTest +{ + @Test + void testEmpty() + { + assertThat(new ParsedDates(""), is(emptyIterable())); + } + + @Test + void testSingletonDate() + { + assertThat(new ParsedDates("20240216"), Core.iterates(DateTime.parse("20240216"))); + } + + @Test + void testMultipleDates() + { + assertThat(new ParsedDates("20240216,20240217,20240218"), + iterates( + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"))); + } + + @Test + void testSingletonDateTime() + { + assertThat(new ParsedDates("20240216T123456"), iterates(DateTime.parse("20240216T123456"))); + } + + @Test + void testMultipleDateTimes() + { + assertThat(new ParsedDates("20240216T123456,20240217T123456,20240218T123456"), + iterates( + DateTime.parse("20240216T123456"), + DateTime.parse("20240217T123456"), + DateTime.parse("20240218T123456"))); + } + + @Test + void testAbsoluteSingletonDateTime() + { + assertThat(new ParsedDates(getTimeZone("Europe/Berlin"), "20240216T123456"), + iterates(DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T123456"))); + } + + @Test + void testAbsoluteMultipleDateTimes() + { + assertThat(new ParsedDates(getTimeZone("Europe/Berlin"), "20240216T123456,20240217T123456,20240218T123456"), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T123456"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240217T123456"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240218T123456"))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/optional/LastInstanceTest.java b/src/test/java/org/dmfs/rfc5545/optional/LastInstanceTest.java new file mode 100644 index 0000000..c602c62 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/optional/LastInstanceTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.optional; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.dmfs.rfc5545.recurrenceset.OfList; +import org.dmfs.rfc5545.recurrenceset.OfRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.jems2.confidence.Jems2.absent; +import static org.dmfs.jems2.confidence.Jems2.present; +import static org.dmfs.jems2.iterable.EmptyIterable.emptyIterable; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.is; + +class LastInstanceTest +{ + @Test + void testEmptyList() + { + assertThat(new LastInstance(new OfList(emptyIterable())), + is(absent())); + } + + + @Test + void testSingleton() + { + assertThat(new LastInstance(new OfList(DateTime.parse("20240215"))), + is(present(DateTime.parse("20240215")))); + } + + @Test + void testFiniteRule() throws InvalidRecurrenceRuleException + { + assertThat(new LastInstance( + new OfRule( + new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + is(present(DateTime.parse("20240219")))); + } + + + @Test + void testInfiniteRule() throws InvalidRecurrenceRuleException + { + assertThat(new LastInstance( + new OfRule( + new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215"))), + is(absent())); + } + +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/DifferenceTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/DifferenceTest.java new file mode 100644 index 0000000..c0acb65 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/DifferenceTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.iterable.EmptyIterable; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.rfc5545.confidence.Recur.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class DifferenceTest +{ + @Test + void testEmptyInstancesAndExceptions() + { + assertThat(new Difference( + new OfList(new EmptyIterable<>()), + new OfList(new EmptyIterable<>()) + ), + is(emptyRecurrenceSet())); + } + + @Test + void testEmptyInstancesAndNonEmptyExceptions() throws InvalidRecurrenceRuleException + { + assertThat(new Difference( + new OfList(new EmptyIterable<>()), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240224T120000"))), + is(emptyRecurrenceSet())); + } + + @Test + void testInstancesEqualExceptions() throws InvalidRecurrenceRuleException + { + assertThat(new Difference( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240224T120000")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240224T120000"))), + is(emptyRecurrenceSet())); + } + + + @Test + void testInstancesAndExceptions() throws InvalidRecurrenceRuleException + { + assertThat(new Difference( + new OfRule(new RecurrenceRule("FREQ=HOURLY;INTERVAL=12;COUNT=10"), DateTime.parse("20240224T120000")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240224T000000"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240224T120000"), + DateTime.parse("20240225T120000"), + DateTime.parse("20240226T120000"), + DateTime.parse("20240227T120000"), + DateTime.parse("20240228T120000"), + DateTime.parse("20240229T000000")))); + } + + + /** + * See Issue 93 + */ + @Test + void test_github_issue_93() throws InvalidRecurrenceRuleException + { + assertThat( + new Difference( + new OfRule(new RecurrenceRule("FREQ=WEEKLY;UNTIL=20200511T000000Z;BYDAY=TU"), DateTime.parse("20200414T160000Z")), + new OfList(DateTime.parse("20200421T160000Z"), DateTime.parse("20200505T160000Z"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20200414T160000Z"), + DateTime.parse("20200428T160000Z")))); + } + + + @Test + void test_multiple_rules_with_same_values_and_count() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(2019, 1, 1); + + assertThat( + new Difference( + new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TU,WE"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,TH,FR;COUNT=10"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,FR,SA;COUNT=5"), start) + ), + new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TH;UNTIL=20190212"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=MO;COUNT=4"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;BYDAY=TH,FR"), start) + ) + ), + allOf( + is(infinite()), + startsWith( + new DateTime(2019, 1, 2), // SA + new DateTime(2019, 1, 5), // TU + new DateTime(2019, 1, 6), // WE + new DateTime(2019, 1, 9), // SA + new DateTime(2019, 1, 12), // TU + new DateTime(2019, 1, 13), // WE + new DateTime(2019, 1, 19), // TU + new DateTime(2019, 1, 20), // WE + new DateTime(2019, 1, 26), // TU + new DateTime(2019, 1, 27), // WE + new DateTime(2019, 2, 4), // MO + new DateTime(2019, 2, 5), // TU + new DateTime(2019, 2, 6) // WE + ))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/FastForwardedTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/FastForwardedTest.java new file mode 100644 index 0000000..e933301 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/FastForwardedTest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.iterable.EmptyIterable; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.rfc5545.confidence.Recur.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class FastForwardedTest +{ + @Test + void testFastForwardEmptySet() + { + assertThat( + new FastForwarded(DateTime.parse("20240225"), + new OfList(new EmptyIterable<>())), + is(emptyRecurrenceSet())); + } + + @Test + void testFastForwardBeyondLastInstance() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240225"), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + is(emptyRecurrenceSet())); + } + + @Test + void testFastForwardRule() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + + @Test + void testFastForwardRuleWithUnsyncedStart() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;COUNT=3"), DateTime.parse("20240215"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240223")))); + } + + + @Test + void testFastForwardRuleWithUnsyncedStartMultipleInstances() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;COUNT=5"), DateTime.parse("20240207"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } + + @Test + void testFastForwardList() + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new OfList( + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + + @Test + void testFastForwardMultiple() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215")), + new OfList( + DateTime.parse("20240213"), + DateTime.parse("20240214"), + DateTime.parse("20240220"), + DateTime.parse("20240221"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240218"), + DateTime.parse("20240219"), + DateTime.parse("20240220"), + DateTime.parse("20240221")))); + } + + + @Test + void testFastForwardWithExceptions() throws InvalidRecurrenceRuleException + { + assertThat( + new FastForwarded(DateTime.parse("20240218"), + new Difference( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=7"), DateTime.parse("20240215")), + new OfList( + DateTime.parse("20240217"), + DateTime.parse("20240220")))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240218"), + DateTime.parse("20240219"), + DateTime.parse("20240221")))); + } + + /** + * See Issue 61 + */ + @Test + void testGithubIssue61() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); + assertThat( + new FastForwarded( + new DateTime(DateTime.UTC, 2019, 1, 1, 22, 0, 0), + new Merged( + new OfRule(new RecurrenceRule("FREQ=HOURLY;INTERVAL=5"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"), start) + )), + allOf( + is(infinite()), + startsWith( + new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0)))); + } + + /** + * See Issue 85 + */ + @Test + void testGithubIssue85() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); + assertThat( + new FastForwarded(start, + new OfRule(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"), start)), + allOf( + is(infinite()), + startsWith( + new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0)))); + } + + + @Test + void testFastForwardIntoPast() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); + assertThat( + new FastForwarded(start.addDuration(new Duration(-1, 10)), + new OfRule(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"), start)), + allOf( + is(infinite()), + startsWith( + new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0)))); + } + + + @Test + void testFastForwardSkipping1stInstance() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); + assertThat( + new FastForwarded(start.addDuration(new Duration(1, 0, 1)), + new OfRule(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"), start)), + allOf( + is(infinite()), + startsWith( + new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 6, 0, 0, 0)))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/MergedTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/MergedTest.java new file mode 100644 index 0000000..e8c1c06 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/MergedTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.jems2.iterable.EmptyIterable; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.rfc5545.confidence.Recur.*; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class MergedTest +{ + @Test + void testMergeSingleEmpty() + { + assertThat(new Merged( + new OfList(new EmptyIterable<>())), + is(emptyRecurrenceSet())); + } + + @Test + void testMergeMultipleEmpty() + { + assertThat(new Merged( + new OfList(new EmptyIterable<>()), + new OfList(new EmptyIterable<>())), + is(emptyRecurrenceSet())); + } + + @Test + void testMergeSingleFinite() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + + @Test + void testMergeSingleFiniteWithEmpty() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215")), + new OfList(new EmptyIterable<>())), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + @Test + void testMergeEmptyWithSingleFinite() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfList(new EmptyIterable<>()), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + @Test + void testSingleInfinite() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215"))), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + + @Test + void testTwoFiniteDelegates() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215T180000")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215T120000"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215T120000"), + DateTime.parse("20240215T180000"), + DateTime.parse("20240216T120000"), + DateTime.parse("20240216T180000"), + DateTime.parse("20240217T120000"), + DateTime.parse("20240217T180000"), + DateTime.parse("20240218T120000"), + DateTime.parse("20240218T180000"), + DateTime.parse("20240219T120000"), + DateTime.parse("20240219T180000")))); + } + + + @Test + void testOneFiniteAndOneInfiniteDelegates() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215T180000")), + new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215T120000"))), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240215T120000"), + DateTime.parse("20240215T180000"), + DateTime.parse("20240216T120000"), + DateTime.parse("20240216T180000"), + DateTime.parse("20240217T120000"), + DateTime.parse("20240217T180000"), + DateTime.parse("20240218T120000"), + DateTime.parse("20240218T180000"), + DateTime.parse("20240219T120000"), + DateTime.parse("20240219T180000")))); + } + + + @Test + void testOneInfiniteAndOneFiniteDelegates() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215T180000")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215T120000"))), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240215T120000"), + DateTime.parse("20240215T180000"), + DateTime.parse("20240216T120000"), + DateTime.parse("20240216T180000"), + DateTime.parse("20240217T120000"), + DateTime.parse("20240217T180000"), + DateTime.parse("20240218T120000"), + DateTime.parse("20240218T180000"), + DateTime.parse("20240219T120000"), + DateTime.parse("20240219T180000")))); + } + + + @Test + void testTwoInfiniteDelegates() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215T180000")), + new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215T120000"))), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240215T120000"), + DateTime.parse("20240215T180000"), + DateTime.parse("20240216T120000"), + DateTime.parse("20240216T180000"), + DateTime.parse("20240217T120000"), + DateTime.parse("20240217T180000"), + DateTime.parse("20240218T120000"), + DateTime.parse("20240218T180000"), + DateTime.parse("20240219T120000"), + DateTime.parse("20240219T180000")))); + } + + @Test + void testMultipleFinite() throws InvalidRecurrenceRuleException + { + assertThat(new Merged( + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240216")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240214")), + new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215"))), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240214"), + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219"), + DateTime.parse("20240220")))); + } + + /** + * See Issue 61 + */ + @Test + void test_github_issue_61() throws InvalidRecurrenceRuleException + { + DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); + assertThat( + new Merged( + new OfRule(new RecurrenceRule("FREQ=HOURLY;INTERVAL=5"), start), + new OfRule(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"), start) + ), + allOf( + is(infinite()), + startsWith( + new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 1, 5, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 1, 10, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 1, 15, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 1, 20, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0), + new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0) + ))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/OfListTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfListTest.java new file mode 100644 index 0000000..ea85cb8 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfListTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.DateTime; +import org.junit.jupiter.api.Test; + +import static java.util.TimeZone.getTimeZone; +import static org.dmfs.rfc5545.confidence.Recur.emptyRecurrenceSet; +import static org.dmfs.rfc5545.confidence.Recur.infinite; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class OfListTest +{ + @Test + void testEmptyList() + { + assertThat(new OfList(new DateTime[0]), + is(emptyRecurrenceSet())); + } + + @Test + void testSingletonList() + { + assertThat(new OfList(DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501")), + allOf( + is(not(infinite())), + iterates(DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501")))); + } + + @Test + void testOrderedList() + { + assertThat(new OfList( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501")))); + } + + @Test + void testUnorderedList() + { + assertThat(new OfList( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501")))); + } + + + @Test + void testStringList() + { + assertThat(new OfList( + getTimeZone("Europe/Berlin"), "20240216T162501,20240216T142501,20240216T182501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501")))); + } + + + @Test + void testStringLists() + { + assertThat(new OfList( + getTimeZone("Europe/Berlin"), + "20240216T162501,20240216T142501,20240216T182501", + "20240216T202501,20240216T232501,20240216T222501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T202501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T222501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T232501")))); + } + + + @Test + void testStrings() + { + assertThat(new OfList( + getTimeZone("Europe/Berlin"), + "20240216T162501", + "20240216T142501", + "20240216T182501", + "20240216T202501", + "20240216T232501", + "20240216T222501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T142501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T162501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T182501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T202501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T222501"), + DateTime.parse(getTimeZone("Europe/Berlin"), "20240216T232501")))); + } + + + @Test + void testFloatingStringList() + { + assertThat(new OfList( + "20240216T162501,20240216T142501,20240216T182501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240216T142501"), + DateTime.parse("20240216T162501"), + DateTime.parse("20240216T182501")))); + } + + + @Test + void testFloatingStringLists() + { + assertThat(new OfList( + "20240216T162501,20240216T142501,20240216T182501", + "20240216T202501,20240216T232501,20240216T222501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240216T142501"), + DateTime.parse("20240216T162501"), + DateTime.parse("20240216T182501"), + DateTime.parse("20240216T202501"), + DateTime.parse("20240216T222501"), + DateTime.parse("20240216T232501")))); + } + + + @Test + void testFloatingStrings() + { + assertThat(new OfList( + "20240216T162501", + "20240216T142501", + "20240216T182501", + "20240216T202501", + "20240216T232501", + "20240216T222501"), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240216T142501"), + DateTime.parse("20240216T162501"), + DateTime.parse("20240216T182501"), + DateTime.parse("20240216T202501"), + DateTime.parse("20240216T222501"), + DateTime.parse("20240216T232501")))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirstTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirstTest.java new file mode 100644 index 0000000..886b104 --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleAndFirstTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.rfc5545.confidence.Recur.infinite; +import static org.dmfs.rfc5545.confidence.Recur.startsWith; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class OfRuleAndFirstTest +{ + @Test + void testRuleWithCountSyncStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;COUNT=3"), DateTime.parse("20240216")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240216"), + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } + + @Test + void testRuleWithCountUnsyncedStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;COUNT=3"), DateTime.parse("20240214")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240214"), + DateTime.parse("20240216"), + DateTime.parse("20240223")))); + } + + @Test + void testRuleWithUntilSyncStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;UNTIL=20240301"), DateTime.parse("20240216")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240216"), + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } + + + @Test + void testRuleWithUntilUnsyncedStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR;UNTIL=20240301"), DateTime.parse("20240214")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240214"), + DateTime.parse("20240216"), + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } + + @Test + void testInfiniteRuleSyncStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR"), DateTime.parse("20240216")), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240216"), + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } + + + @Test + void testInfiniteRuleUnsyncedStart() throws InvalidRecurrenceRuleException + { + assertThat(new OfRuleAndFirst(new RecurrenceRule("FREQ=DAILY;BYDAY=FR"), DateTime.parse("20240214")), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240214"), + DateTime.parse("20240216"), + DateTime.parse("20240223"), + DateTime.parse("20240301")))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleTest.java new file mode 100644 index 0000000..7e0515f --- /dev/null +++ b/src/test/java/org/dmfs/rfc5545/recurrenceset/OfRuleTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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.recurrenceset; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; +import org.dmfs.rfc5545.recur.RecurrenceRule; +import org.junit.jupiter.api.Test; + +import static org.dmfs.rfc5545.confidence.Recur.infinite; +import static org.dmfs.rfc5545.confidence.Recur.startsWith; +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.*; + +class OfRuleTest +{ + @Test + void testRuleWithCount() throws InvalidRecurrenceRuleException + { + assertThat(new OfRule(new RecurrenceRule("FREQ=DAILY;COUNT=5"), DateTime.parse("20240215")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + @Test + void testRuleWithUntil() throws InvalidRecurrenceRuleException + { + assertThat(new OfRule(new RecurrenceRule("FREQ=DAILY;UNTIL=20240219"), DateTime.parse("20240215")), + allOf( + is(not(infinite())), + iterates( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } + + + @Test + void testInfiniteRule() throws InvalidRecurrenceRuleException + { + assertThat(new OfRule(new RecurrenceRule("FREQ=DAILY"), DateTime.parse("20240215")), + allOf( + is(infinite()), + startsWith( + DateTime.parse("20240215"), + DateTime.parse("20240216"), + DateTime.parse("20240217"), + DateTime.parse("20240218"), + DateTime.parse("20240219")))); + } +} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapterTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapterTest.java deleted file mode 100644 index 7b063fd..0000000 --- a/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceRuleAdapterTest.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2017 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.recurrenceset; - -import org.dmfs.rfc5545.DateTime; -import org.dmfs.rfc5545.recur.RecurrenceRule; -import org.junit.jupiter.api.Test; - -import java.util.TimeZone; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - - -/** - * @author marten - */ -public class RecurrenceRuleAdapterTest -{ - @Test - public void testGetIteratorSyncedStartInfinite() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - } - - - @Test - public void testGetIteratorSyncedStartWithCount() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=3")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(false)); - assertThat(iterator.hasNext(), is(false)); - } - - - @Test - public void testGetIteratorSyncedStartWithUntil() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20170312T113012Z")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170210T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170310T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(false)); - assertThat(iterator.hasNext(), is(false)); - } - - - @Test - public void testGetIteratorUnsyncedStartInfinite() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;BYMONTHDAY=11")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - // note the unsynced start is not a result, it's added separately by `RecurrenceSet` - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - } - - - @Test - public void testGetIteratorUnsyncedStartWithCount() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=3;BYMONTHDAY=11")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - // note the unsynced start is not a result, it's added separately by `RecurrenceSet` - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(false)); - assertThat(iterator.hasNext(), is(false)); - } - - - @Test - public void testGetIteratorUnsyncedStartWithUntil() throws Exception - { - AbstractRecurrenceAdapter.InstanceIterator iterator = new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20170312T113012Z;BYMONTHDAY=11")) - .getIterator(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()); - - // note the unsynced start is not a result, it's added separately by `RecurrenceSet` - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170111T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170211T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.hasNext(), is(true)); - assertThat(iterator.next(), is(DateTime.parse("Europe/Berlin", "20170311T113012").getTimestamp())); - assertThat(iterator.hasNext(), is(false)); - assertThat(iterator.hasNext(), is(false)); - } - - - @Test - public void testIsInfinite() throws Exception - { - assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY")).isInfinite(), is(true)); - assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=10")).isInfinite(), is(false)); - assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20171212")).isInfinite(), is(false)); - } - - - @Test - public void testGetLastInstance() throws Exception - { - assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;COUNT=10")) - .getLastInstance(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()), - is(DateTime.parse("Europe/Berlin", "20171010T113012").getTimestamp())); - assertThat(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=MONTHLY;UNTIL=20171212T101010Z")) - .getLastInstance(TimeZone.getTimeZone("Europe/Berlin"), DateTime.parse("Europe/Berlin", "20170110T113012").getTimestamp()), - is(DateTime.parse("Europe/Berlin", "20171210T113012").getTimestamp())); - } -} \ No newline at end of file diff --git a/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIteratorTest.java b/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIteratorTest.java deleted file mode 100644 index 2425f91..0000000 --- a/src/test/java/org/dmfs/rfc5545/recurrenceset/RecurrenceSetIteratorTest.java +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright 2019 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.recurrenceset; - -import org.dmfs.jems2.iterator.BaseIterator; -import org.dmfs.rfc5545.DateTime; -import org.dmfs.rfc5545.Duration; -import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; -import org.dmfs.rfc5545.recur.RecurrenceRule; -import org.junit.jupiter.api.Test; - -import java.util.NoSuchElementException; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; - -import static java.util.Arrays.asList; -import static org.dmfs.jems2.hamcrest.matchers.generatable.GeneratableMatcher.startsWith; -import static org.dmfs.jems2.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - - -/** - * Unit test for {@link RecurrenceSetIterator}. - * - * @author Marten Gajda - */ -public class RecurrenceSetIteratorTest -{ - /** - * Test results if a single exception list has been provided. - */ - @Test - public void testExceptionsAllDay() - { - TimeZone testZone = TimeZone.getTimeZone("UTC"); - DateTime start = DateTime.parse("20180101"); - RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator( - asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())), - asList(new RecurrenceList("20180102,20180103", testZone).getIterator(testZone, start.getTimestamp()))); - - // note we call hasNext twice to ensure it's idempotent - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.addDuration(new Duration(1, 3, 0)).getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - } - - - /** - * Test results if multiple exception lists have been provided. - */ - @Test - public void testMultipleExceptionsAllDay() - { - TimeZone testZone = TimeZone.getTimeZone("UTC"); - DateTime start = DateTime.parse("20180101"); - RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator( - asList(new RecurrenceList("20180101,20180102,20180103,20180104", testZone).getIterator(testZone, start.getTimestamp())), - asList(new RecurrenceList("20180103", testZone).getIterator(testZone, start.getTimestamp()), - new RecurrenceList("20180102", testZone).getIterator(testZone, start.getTimestamp()))); - - // note we call hasNext twice to ensure it's idempotent - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.addDuration(new Duration(1, 3, 0)).getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - } - - - /** - * Test results if a single exception list has been provided. - */ - @Test - public void testExceptions() - { - TimeZone testZone = TimeZone.getTimeZone("UTC"); - DateTime start = DateTime.parse("20180101T120000"); - RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator( - asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone, - start.getTimestamp())), - asList(new RecurrenceList("20180102T120000,20180103T120000", testZone).getIterator(testZone, start.getTimestamp()))); - - // note we call hasNext twice to ensure it's idempotent - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.addDuration(new Duration(1, 3, 0)).getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - } - - - /** - * Test results if multiple exception lists have been provided. - */ - @Test - public void testMultipleExceptions() - { - TimeZone testZone = TimeZone.getTimeZone("UTC"); - DateTime start = DateTime.parse("20180101T120000"); - RecurrenceSetIterator recurrenceSetIterator = new RecurrenceSetIterator( - asList(new RecurrenceList("20180101T120000,20180102T120000,20180103T120000,20180104T120000", testZone).getIterator(testZone, - start.getTimestamp())), - asList(new RecurrenceList("20180103T120000", testZone).getIterator(testZone, start.getTimestamp()), - new RecurrenceList("20180102T120000", testZone).getIterator(testZone, start.getTimestamp()))); - - // note we call hasNext twice to ensure it's idempotent - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.hasNext(), is(true)); - assertThat(recurrenceSetIterator.next(), is(start.addDuration(new Duration(1, 3, 0)).getTimestamp())); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - assertThat(recurrenceSetIterator.hasNext(), is(false)); - } - - - /** - * See https://github.com/dmfs/lib-recur/issues/61 - */ - @Test - public void testMultipleRules() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); - - // Combine all Recurrence Rules into a RecurrenceSet - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=HOURLY;INTERVAL=5"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - assertThat(() -> it::next, startsWith( - new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 1, 5, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 1, 10, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 1, 15, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 1, 20, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp() - )); - } - - - @Test - public void testMultipleRulesWithSameValues() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(2019, 1, 1); - - // Combine all Recurrence Rules into a RecurrenceSet - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TU,WE"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,TH,FR"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,FR,SA"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TH"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=TH,FR"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - assertThat(() -> it::next, startsWith( - new DateTime(2019, 1, 2).getTimestamp(), // SA - new DateTime(2019, 1, 5).getTimestamp(), // TU - new DateTime(2019, 1, 6).getTimestamp(), // WE - new DateTime(2019, 1, 9).getTimestamp(), // SA - new DateTime(2019, 1, 12).getTimestamp(), // TU - new DateTime(2019, 1, 13).getTimestamp(), // WE - new DateTime(2019, 1, 16).getTimestamp(), // SA - new DateTime(2019, 1, 19).getTimestamp(), // TU - new DateTime(2019, 1, 20).getTimestamp(), // WE - new DateTime(2019, 1, 23).getTimestamp(), // SA - new DateTime(2019, 1, 26).getTimestamp(), // TU - new DateTime(2019, 1, 27).getTimestamp(), // WE - new DateTime(2019, 2, 2).getTimestamp(), // SA - new DateTime(2019, 2, 5).getTimestamp() // TU - )); - } - - - /** - * See https://github.com/dmfs/lib-recur/issues/93 - */ - @Test - public void testGithubIssue93() throws InvalidRecurrenceRuleException - { - DateTime start = DateTime.parse("20200414T160000Z"); - - // Combine all Recurrence Rules into a RecurrenceSet - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=WEEKLY;UNTIL=20200511T000000Z;BYDAY=TU"))); - ruleSet.addExceptions(new RecurrenceList("20200421T160000Z,20200505T160000Z", DateTime.UTC)); - - // Create an iterator using the RecurrenceSet - assertThat(() -> new RecurrenceAdapter(ruleSet.iterator(start.getTimeZone(), start.getTimestamp())), - iteratorOf( - DateTime.parse("20200414T160000Z").getTimestamp(), - DateTime.parse("20200428T160000Z").getTimestamp())); - } - - - @Test - public void testMultipleRulesWithSameValuesAndCount() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(2019, 1, 1); - - // Combine all Recurrence Rules into a RecurrenceSet - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TU,WE"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,TH,FR;COUNT=10"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=WE,FR,SA;COUNT=5"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO,TH;UNTIL=20190212"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=MO;COUNT=4"))); - ruleSet.addExceptions(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;BYDAY=TH,FR"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - assertThat(() -> it::next, startsWith( - new DateTime(2019, 1, 2).getTimestamp(), // SA - new DateTime(2019, 1, 5).getTimestamp(), // TU - new DateTime(2019, 1, 6).getTimestamp(), // WE - new DateTime(2019, 1, 9).getTimestamp(), // SA - new DateTime(2019, 1, 12).getTimestamp(), // TU - new DateTime(2019, 1, 13).getTimestamp(), // WE - //new DateTime(2019, 1, 16).getTimestamp(), // SA - new DateTime(2019, 1, 19).getTimestamp(), // TU - new DateTime(2019, 1, 20).getTimestamp(), // WE - //new DateTime(2019, 1, 23).getTimestamp(), // SA - new DateTime(2019, 1, 25).getTimestamp(), // MO - new DateTime(2019, 1, 26).getTimestamp(), // TU - new DateTime(2019, 1, 27).getTimestamp(), // WE - //new DateTime(2019, 2, 2).getTimestamp(), // SA - new DateTime(2019, 2, 4).getTimestamp(), // MO - new DateTime(2019, 2, 5).getTimestamp() // TU - )); - } - - - /** - * See https://github.com/dmfs/lib-recur/issues/61 - */ - @Test - public void testMultipleRulesWithFastForward() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); - - // Combine all Recurrence Rules into a RecurrenceSet - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=HOURLY;INTERVAL=5"))); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - // Fast forward to the time of calculation (1/1/2019 at 10pm). - it.fastForward(new DateTime(DateTime.UTC, 2019, 1, 1, 22, 0, 0).getTimestamp()); - - assertThat(() -> it::next, startsWith( - new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 1, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 6, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 11, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 16, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 21, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 2, 0, 0).getTimestamp() - )); - } - - - /** - * See https://github.com/dmfs/lib-recur/issues/85 - *

- * Fast forward to the start date (i.e. not fast forwarding at all) - */ - @Test - public void testFastForwardToStart() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); - - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - // "Fast forward" to start. - it.fastForward(start.getTimestamp()); - - assertThat(() -> it::next, startsWith( - new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp() - )); - } - - - @Test - public void testFastForwardToPast() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); - - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - // "Fast forward" to 100 days in the past. - it.fastForward(start.getTimestamp() - TimeUnit.DAYS.toMillis(100)); - - assertThat(() -> it::next, startsWith( - new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp() - )); - } - - - @Test - public void testFastForwardToNext() throws InvalidRecurrenceRuleException - { - DateTime start = new DateTime(DateTime.UTC, 2019, 1, 1, 0, 0, 0); - - RecurrenceSet ruleSet = new RecurrenceSet(); - ruleSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;INTERVAL=1"))); - - // Create an iterator using the RecurrenceSet - RecurrenceSetIterator it = ruleSet.iterator(start.getTimeZone(), start.getTimestamp()); - - // "Fast forward" to 1 millisecond after start (skipping the first instance only) - it.fastForward(start.getTimestamp() + 1); - - assertThat(() -> it::next, startsWith( - new DateTime(DateTime.UTC, 2019, 1, 2, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 3, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 4, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 5, 0, 0, 0).getTimestamp(), - new DateTime(DateTime.UTC, 2019, 1, 6, 0, 0, 0).getTimestamp() - )); - } - - - private final static class RecurrenceAdapter extends BaseIterator - { - - private final RecurrenceSetIterator mDelegate; - - - private RecurrenceAdapter(RecurrenceSetIterator delegate) - { - mDelegate = delegate; - } - - - @Override - public boolean hasNext() - { - return mDelegate.hasNext(); - } - - - @Override - public Long next() - { - if (!hasNext()) - { - throw new NoSuchElementException(); - } - return mDelegate.next(); - } - } -} \ No newline at end of file