Skip to content

Commit

Permalink
Tolerate all-day UNTIL with datetime start in lax mode, closes #109 (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dmfs authored Mar 5, 2023
1 parent d302a9b commit 8ce5766
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 25 deletions.
50 changes: 32 additions & 18 deletions src/main/java/org/dmfs/rfc5545/recur/RecurrenceRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public enum RfcMode
* 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
* be thrown.
*/
RFC2445_STRICT,
RFC2445_STRICT(false),

/**
* 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
Expand All @@ -59,13 +59,13 @@ public enum RfcMode
* differently than with {@link #RFC5545_LAX}. {@link #RFC5545_LAX} will just drop all invalid parts and evaluate the rule according to RFC 5545. This
* mode will evaluate all rules. <p> Also this mode will output rules that comply with RFC 2445. </p>
*/
RFC2445_LAX,
RFC2445_LAX(true),

/**
* 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
* be thrown.
*/
RFC5545_STRICT,
RFC5545_STRICT(false),

/**
* 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
Expand All @@ -74,7 +74,15 @@ public enum RfcMode
* are evaluated differently than with {@link #RFC2445_LAX}. This mode will just drop all invalid parts and evaluate the rule according to RFC 5545.
* {@link #RFC2445_LAX} will evaluate all rules. <p> Also this mode will output rules that comply with RFC 5545. </p>
*/
RFC5545_LAX;
RFC5545_LAX(true);

final boolean mIsLax;


RfcMode(boolean isLax)
{
mIsLax = isLax;
}
}


Expand Down Expand Up @@ -847,7 +855,9 @@ boolean expands(RecurrenceRule rule)
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
return new UntilLimiter(rule, previous, calendarMetrics, startTimeZone);
return rule.mode.mIsLax && rule.getUntil() != null && rule.getUntil().isAllDay()
? new UntilDateLimiter(rule, previous)
: new UntilLimiter(rule, previous, startTimeZone);
}


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

/**
* {@link Part}s that don't provide expander or limiter.
*/
private final static Set<Part> NON_EXPANDABLE = EnumSet.of(Part.FREQ, Part.INTERVAL, Part.WKST, Part.RSCALE);

/**
* The default skip value if RSCALE is present but SKIP is not.
*/
Expand Down Expand Up @@ -2140,10 +2155,10 @@ public RecurrenceRuleIterator iterator(DateTime start)
DateTime until = getUntil();
if (until != null)
{
if (until.isAllDay() != start.isAllDay())
if (!mode.mIsLax && until.isAllDay() != start.isAllDay())
{
throw new IllegalArgumentException(
"using allday start times with non-allday until values (and vice versa) is not allowed");
"using allday start times with non-allday until values (and vice versa) is not allowed in strict modes");
}
if (until.isFloating() != start.isFloating())
{
Expand Down Expand Up @@ -2182,21 +2197,20 @@ public RecurrenceRuleIterator iterator(DateTime start)
}
}

parts.removeAll(NON_EXPANDABLE);

for (Part p : parts)
{
// add a filter for each rule part
if (p != Part.FREQ && p != Part.INTERVAL && p != Part.WKST && p != Part.RSCALE)
if (p.expands(this))
{
if (p.expands(this))
{
// if a part returns null for the expander just skip it
RuleIterator newIterator = p.getExpander(this, iterator, rScaleCalendarMetrics, startInstance, startTimeZone);
iterator = newIterator == null ? iterator : newIterator;
}
else
{
((ByExpander) iterator).addFilter(p.getFilter(this, rScaleCalendarMetrics));
}
// if a part returns null for the expander just skip it
RuleIterator newIterator = p.getExpander(this, iterator, rScaleCalendarMetrics, startInstance, startTimeZone);
iterator = newIterator == null ? iterator : newIterator;
}
else
{
((ByExpander) iterator).addFilter(p.getFilter(this, rScaleCalendarMetrics));
}
}
return new RecurrenceRuleIterator(iterator, start, rScaleCalendarMetrics);
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/org/dmfs/rfc5545/recur/UntilDateLimiter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2013 Marten Gajda <[email protected]>
*
* 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.recur;

import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.Instance;


/**
* A {@link Limiter} that filters all instances after a certain all-day date (the one specified in the UNTIL part).
*/
final class UntilDateLimiter extends Limiter
{
/**
* The latest allowed instance start date.
*/
private final long mUntil;


/**
* Create a new limiter for an all-day UNTIL part.
*/
public UntilDateLimiter(RecurrenceRule rule, RuleIterator previous)
{
super(previous);
DateTime until = rule.getUntil();
if (!until.isAllDay())
{
throw new RuntimeException("Illegal use of UntilDateLimiter with non-allday date " + until);
}
mUntil = until.getInstance();
}


@Override
boolean stop(long instance)
{
return mUntil < Instance.setSecond(Instance.setMinute(Instance.setHour(Instance.maskWeekday(instance), 0), 0), 0);
}
}
9 changes: 4 additions & 5 deletions src/main/java/org/dmfs/rfc5545/recur/UntilLimiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@
* 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.calendarmetrics.CalendarMetrics;

import java.util.TimeZone;

Expand All @@ -41,11 +40,11 @@ final class UntilLimiter extends Limiter
* Create a new limiter for the UNTIL part.
*
* @param rule
* The {@link RecurrenceRule} to filter.
* The {@link RecurrenceRule} to filter.
* @param previous
* The previous filter instance.
* The previous filter instance.
*/
public UntilLimiter(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, TimeZone startTimezone)
public UntilLimiter(RecurrenceRule rule, RuleIterator previous, TimeZone startTimezone)
{
super(previous);
DateTime until = rule.getUntil();
Expand Down
67 changes: 65 additions & 2 deletions src/test/java/org/dmfs/rfc5545/recur/RecurrenceRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@
import org.dmfs.rfc5545.Weekday;
import org.junit.jupiter.api.Test;

import static org.dmfs.jems2.hamcrest.matchers.LambdaMatcher.having;
import static org.dmfs.jems2.hamcrest.matchers.fragile.BrokenFragileMatcher.throwing;
import static org.dmfs.jems2.hamcrest.matchers.single.SingleMatcher.hasValue;
import static org.dmfs.rfc5545.Weekday.*;
import static org.dmfs.rfc5545.hamcrest.GeneratorMatcher.generates;
import static org.dmfs.rfc5545.hamcrest.RecurrenceRuleMatcher.*;
import static org.dmfs.rfc5545.hamcrest.datetime.BeforeMatcher.before;
import static org.dmfs.rfc5545.hamcrest.datetime.DayOfMonthMatcher.onDayOfMonth;
import static org.dmfs.rfc5545.hamcrest.datetime.MonthMatcher.inMonth;
import static org.dmfs.rfc5545.hamcrest.datetime.WeekDayMatcher.onWeekDay;
import static org.dmfs.rfc5545.hamcrest.datetime.YearMatcher.inYear;
import static org.dmfs.rfc5545.recur.RecurrenceRule.RfcMode.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.*;


/**
Expand Down Expand Up @@ -94,4 +97,64 @@ public void test() throws InvalidRecurrenceRuleException
System.out.println(rule.getByPart(RecurrenceRule.Part.BYMONTH));
System.out.println(rule.toString());
}


/**
* see https://github.com/dmfs/lib-recur/issues/109
*/
@Test
void testAllDayUntilAndDateTimeStart() throws InvalidRecurrenceRuleException
{
assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC5545_LAX),
allOf(validRule(DateTime.parse("20230301T000000"),
walking(),
results(5)),
generates("20230301T000000",
"20230301T120000",
"20230302T120000",
"20230303T120000",
"20230304T120000",
"20230305T120000")));

assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC2445_LAX),
allOf(validRule(DateTime.parse("20230301T000000"),
walking(),
results(5)),
generates("20230301T000000",
"20230301T120000",
"20230302T120000",
"20230303T120000",
"20230304T120000",
"20230305T120000")));

assertThat(new RecurrenceRule("FREQ=DAILY;UNTIL=20230305", RFC5545_LAX),
allOf(validRule(DateTime.parse("20230301T000000"),
walking(),
results(5)),
generates("20230301T000000",
"20230301T000000",
"20230302T000000",
"20230303T000000",
"20230304T000000",
"20230305T000000")));

assertThat(new RecurrenceRule("FREQ=DAILY;UNTIL=20230305", RFC2445_LAX),
allOf(validRule(DateTime.parse("20230301T000000"),
walking(),
results(5)),
generates("20230301T000000",
"20230301T000000",
"20230302T000000",
"20230303T000000",
"20230304T000000",
"20230305T000000")));

assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC5545_STRICT),
is(having(
r -> () -> r.iterator(DateTime.parse("20230301T000000")), is(throwing(IllegalArgumentException.class)))));

assertThat(new RecurrenceRule("FREQ=DAILY;BYHOUR=12;UNTIL=20230305", RFC2445_STRICT),
is(having(
r -> () -> r.iterator(DateTime.parse("20230301T000000")), is(throwing(IllegalArgumentException.class)))));
}
}

0 comments on commit 8ce5766

Please sign in to comment.