Skip to content

Commit 8ce5766

Browse files
authored
Tolerate all-day UNTIL with datetime start in lax mode, closes #109 (#117)
1 parent d302a9b commit 8ce5766

File tree

4 files changed

+156
-25
lines changed

4 files changed

+156
-25
lines changed

src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public enum RfcMode
5050
* Parses recurrence rules according to <a href="http://tools.ietf.org/html/rfc2445#section-4.3.10">RFC 2445</a>. Every error will cause an exception to
5151
* be thrown.
5252
*/
53-
RFC2445_STRICT,
53+
RFC2445_STRICT(false),
5454

5555
/**
5656
* Parses recurrence rules according to <a href="http://tools.ietf.org/html/rfc2445#section-4.3.10">RFC 2445</a> in a more tolerant way. The parser will
@@ -59,13 +59,13 @@ public enum RfcMode
5959
* differently than with {@link #RFC5545_LAX}. {@link #RFC5545_LAX} will just drop all invalid parts and evaluate the rule according to RFC 5545. This
6060
* mode will evaluate all rules. <p> Also this mode will output rules that comply with RFC 2445. </p>
6161
*/
62-
RFC2445_LAX,
62+
RFC2445_LAX(true),
6363

6464
/**
6565
* Parses recurrence rules according to <a href="http://tools.ietf.org/html/rfc5545#section-3.3.10">RFC 5545</a>. Every error will cause an exception to
6666
* be thrown.
6767
*/
68-
RFC5545_STRICT,
68+
RFC5545_STRICT(false),
6969

7070
/**
7171
* Parses recurrence rules according to <a href="http://tools.ietf.org/html/rfc5545#section-3.3.10">RFC 5545</a> in a more tolerant way. The parser will
@@ -74,7 +74,15 @@ public enum RfcMode
7474
* are evaluated differently than with {@link #RFC2445_LAX}. This mode will just drop all invalid parts and evaluate the rule according to RFC 5545.
7575
* {@link #RFC2445_LAX} will evaluate all rules. <p> Also this mode will output rules that comply with RFC 5545. </p>
7676
*/
77-
RFC5545_LAX;
77+
RFC5545_LAX(true);
78+
79+
final boolean mIsLax;
80+
81+
82+
RfcMode(boolean isLax)
83+
{
84+
mIsLax = isLax;
85+
}
7886
}
7987

8088

@@ -847,7 +855,9 @@ boolean expands(RecurrenceRule rule)
847855
@Override
848856
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
849857
{
850-
return new UntilLimiter(rule, previous, calendarMetrics, startTimeZone);
858+
return rule.mode.mIsLax && rule.getUntil() != null && rule.getUntil().isAllDay()
859+
? new UntilDateLimiter(rule, previous)
860+
: new UntilLimiter(rule, previous, startTimeZone);
851861
}
852862

853863

@@ -1151,6 +1161,11 @@ public String toString()
11511161
*/
11521162
private final static CalendarMetrics DEFAULT_CALENDAR_SCALE = new GregorianCalendarMetrics(Weekday.MO, 4);
11531163

1164+
/**
1165+
* {@link Part}s that don't provide expander or limiter.
1166+
*/
1167+
private final static Set<Part> NON_EXPANDABLE = EnumSet.of(Part.FREQ, Part.INTERVAL, Part.WKST, Part.RSCALE);
1168+
11541169
/**
11551170
* The default skip value if RSCALE is present but SKIP is not.
11561171
*/
@@ -2140,10 +2155,10 @@ public RecurrenceRuleIterator iterator(DateTime start)
21402155
DateTime until = getUntil();
21412156
if (until != null)
21422157
{
2143-
if (until.isAllDay() != start.isAllDay())
2158+
if (!mode.mIsLax && until.isAllDay() != start.isAllDay())
21442159
{
21452160
throw new IllegalArgumentException(
2146-
"using allday start times with non-allday until values (and vice versa) is not allowed");
2161+
"using allday start times with non-allday until values (and vice versa) is not allowed in strict modes");
21472162
}
21482163
if (until.isFloating() != start.isFloating())
21492164
{
@@ -2182,21 +2197,20 @@ public RecurrenceRuleIterator iterator(DateTime start)
21822197
}
21832198
}
21842199

2200+
parts.removeAll(NON_EXPANDABLE);
2201+
21852202
for (Part p : parts)
21862203
{
21872204
// add a filter for each rule part
2188-
if (p != Part.FREQ && p != Part.INTERVAL && p != Part.WKST && p != Part.RSCALE)
2205+
if (p.expands(this))
21892206
{
2190-
if (p.expands(this))
2191-
{
2192-
// if a part returns null for the expander just skip it
2193-
RuleIterator newIterator = p.getExpander(this, iterator, rScaleCalendarMetrics, startInstance, startTimeZone);
2194-
iterator = newIterator == null ? iterator : newIterator;
2195-
}
2196-
else
2197-
{
2198-
((ByExpander) iterator).addFilter(p.getFilter(this, rScaleCalendarMetrics));
2199-
}
2207+
// if a part returns null for the expander just skip it
2208+
RuleIterator newIterator = p.getExpander(this, iterator, rScaleCalendarMetrics, startInstance, startTimeZone);
2209+
iterator = newIterator == null ? iterator : newIterator;
2210+
}
2211+
else
2212+
{
2213+
((ByExpander) iterator).addFilter(p.getFilter(this, rScaleCalendarMetrics));
22002214
}
22012215
}
22022216
return new RecurrenceRuleIterator(iterator, start, rScaleCalendarMetrics);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (C) 2013 Marten Gajda <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.dmfs.rfc5545.recur;
19+
20+
import org.dmfs.rfc5545.DateTime;
21+
import org.dmfs.rfc5545.Instance;
22+
23+
24+
/**
25+
* A {@link Limiter} that filters all instances after a certain all-day date (the one specified in the UNTIL part).
26+
*/
27+
final class UntilDateLimiter extends Limiter
28+
{
29+
/**
30+
* The latest allowed instance start date.
31+
*/
32+
private final long mUntil;
33+
34+
35+
/**
36+
* Create a new limiter for an all-day UNTIL part.
37+
*/
38+
public UntilDateLimiter(RecurrenceRule rule, RuleIterator previous)
39+
{
40+
super(previous);
41+
DateTime until = rule.getUntil();
42+
if (!until.isAllDay())
43+
{
44+
throw new RuntimeException("Illegal use of UntilDateLimiter with non-allday date " + until);
45+
}
46+
mUntil = until.getInstance();
47+
}
48+
49+
50+
@Override
51+
boolean stop(long instance)
52+
{
53+
return mUntil < Instance.setSecond(Instance.setMinute(Instance.setHour(Instance.maskWeekday(instance), 0), 0), 0);
54+
}
55+
}

src/main/java/org/dmfs/rfc5545/recur/UntilLimiter.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
15-
*
15+
*
1616
*/
1717

1818
package org.dmfs.rfc5545.recur;
1919

2020
import org.dmfs.rfc5545.DateTime;
2121
import org.dmfs.rfc5545.Instance;
22-
import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics;
2322

2423
import java.util.TimeZone;
2524

@@ -41,11 +40,11 @@ final class UntilLimiter extends Limiter
4140
* Create a new limiter for the UNTIL part.
4241
*
4342
* @param rule
44-
* The {@link RecurrenceRule} to filter.
43+
* The {@link RecurrenceRule} to filter.
4544
* @param previous
46-
* The previous filter instance.
45+
* The previous filter instance.
4746
*/
48-
public UntilLimiter(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, TimeZone startTimezone)
47+
public UntilLimiter(RecurrenceRule rule, RuleIterator previous, TimeZone startTimezone)
4948
{
5049
super(previous);
5150
DateTime until = rule.getUntil();

src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleTest.java

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@
2121
import org.dmfs.rfc5545.Weekday;
2222
import org.junit.jupiter.api.Test;
2323

24+
import static org.dmfs.jems2.hamcrest.matchers.LambdaMatcher.having;
25+
import static org.dmfs.jems2.hamcrest.matchers.fragile.BrokenFragileMatcher.throwing;
2426
import static org.dmfs.jems2.hamcrest.matchers.single.SingleMatcher.hasValue;
2527
import static org.dmfs.rfc5545.Weekday.*;
28+
import static org.dmfs.rfc5545.hamcrest.GeneratorMatcher.generates;
2629
import static org.dmfs.rfc5545.hamcrest.RecurrenceRuleMatcher.*;
2730
import static org.dmfs.rfc5545.hamcrest.datetime.BeforeMatcher.before;
2831
import static org.dmfs.rfc5545.hamcrest.datetime.DayOfMonthMatcher.onDayOfMonth;
2932
import static org.dmfs.rfc5545.hamcrest.datetime.MonthMatcher.inMonth;
3033
import static org.dmfs.rfc5545.hamcrest.datetime.WeekDayMatcher.onWeekDay;
3134
import static org.dmfs.rfc5545.hamcrest.datetime.YearMatcher.inYear;
35+
import static org.dmfs.rfc5545.recur.RecurrenceRule.RfcMode.*;
3236
import static org.hamcrest.MatcherAssert.assertThat;
33-
import static org.hamcrest.Matchers.hasToString;
34-
import static org.hamcrest.Matchers.is;
37+
import static org.hamcrest.Matchers.*;
3538

3639

3740
/**
@@ -94,4 +97,64 @@ public void test() throws InvalidRecurrenceRuleException
9497
System.out.println(rule.getByPart(RecurrenceRule.Part.BYMONTH));
9598
System.out.println(rule.toString());
9699
}
100+
101+
102+
/**
103+
* see https://github.com/dmfs/lib-recur/issues/109
104+
*/
105+
@Test
106+
void testAllDayUntilAndDateTimeStart() throws InvalidRecurrenceRuleException
107+
{
108+
assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC5545_LAX),
109+
allOf(validRule(DateTime.parse("20230301T000000"),
110+
walking(),
111+
results(5)),
112+
generates("20230301T000000",
113+
"20230301T120000",
114+
"20230302T120000",
115+
"20230303T120000",
116+
"20230304T120000",
117+
"20230305T120000")));
118+
119+
assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC2445_LAX),
120+
allOf(validRule(DateTime.parse("20230301T000000"),
121+
walking(),
122+
results(5)),
123+
generates("20230301T000000",
124+
"20230301T120000",
125+
"20230302T120000",
126+
"20230303T120000",
127+
"20230304T120000",
128+
"20230305T120000")));
129+
130+
assertThat(new RecurrenceRule("FREQ=DAILY;UNTIL=20230305", RFC5545_LAX),
131+
allOf(validRule(DateTime.parse("20230301T000000"),
132+
walking(),
133+
results(5)),
134+
generates("20230301T000000",
135+
"20230301T000000",
136+
"20230302T000000",
137+
"20230303T000000",
138+
"20230304T000000",
139+
"20230305T000000")));
140+
141+
assertThat(new RecurrenceRule("FREQ=DAILY;UNTIL=20230305", RFC2445_LAX),
142+
allOf(validRule(DateTime.parse("20230301T000000"),
143+
walking(),
144+
results(5)),
145+
generates("20230301T000000",
146+
"20230301T000000",
147+
"20230302T000000",
148+
"20230303T000000",
149+
"20230304T000000",
150+
"20230305T000000")));
151+
152+
assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC5545_STRICT),
153+
is(having(
154+
r -> () -> r.iterator(DateTime.parse("20230301T000000")), is(throwing(IllegalArgumentException.class)))));
155+
156+
assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC2445_STRICT),
157+
is(having(
158+
r -> () -> r.iterator(DateTime.parse("20230301T000000")), is(throwing(IllegalArgumentException.class)))));
159+
}
97160
}

0 commit comments

Comments
 (0)