From a759d3d50160b4e71171858b14fa982ae724cbc4 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Mon, 16 Jan 2017 16:44:32 -0500 Subject: [PATCH] GetOccurrences shouldn't cache the result of a previous computation, because rules and data governing recurrence sets are mutable after the event is created. #223 --- release-notes.md | 1 + v2/Ical.Net.nuspec | 2 +- v2/ical.NET.UnitTests/RecurrenceTests.cs | 43 ++++++++++++ v2/ical.NET/Evaluation/RecurringEvaluator.cs | 70 +++++++++----------- v3/ical.NET.UnitTests/RecurrenceTests.cs | 43 ++++++++++++ v3/ical.NET/Evaluation/RecurringEvaluator.cs | 70 +++++++++----------- 6 files changed, 154 insertions(+), 75 deletions(-) diff --git a/release-notes.md b/release-notes.md index 21cec95e8..def0ad303 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ A listing of what each [Nuget package](https://www.nuget.org/packages/Ical.Net) ### v2 +* 2.2.29: Calling `GetOccurrences()` on a recurrable component should recompute the recurrence set. Specifying `EXDATE` values that don't have a `TimeOfDay` component should "black out" that day from a recurring component's `StartTime`.[#223](https://github.com/rianjs/ical.net/issues/223) * 2.2.28: Working with `Resources` on `Event`s didn't allow you to do normal set operations: `Add`, `Remove`, `UnionWith`, `ExceptWith`, etc. [#189](https://github.com/rianjs/ical.net/issues/189) * 2.2.27: N/A -- unpublished, no downloads * 2.2.26: Unpublished due to data duplication bug diff --git a/v2/Ical.Net.nuspec b/v2/Ical.Net.nuspec index 861196a50..9a867b7c7 100644 --- a/v2/Ical.Net.nuspec +++ b/v2/Ical.Net.nuspec @@ -2,7 +2,7 @@ Ical.Net - 2.2.28 + 2.2.29 Ical.Net Rian Stockbower, Douglas Day, M. David Peterson Rian Stockbower diff --git a/v2/ical.NET.UnitTests/RecurrenceTests.cs b/v2/ical.NET.UnitTests/RecurrenceTests.cs index 49ed01638..57f7766bd 100644 --- a/v2/ical.NET.UnitTests/RecurrenceTests.cs +++ b/v2/ical.NET.UnitTests/RecurrenceTests.cs @@ -3087,5 +3087,48 @@ public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet() Assert.AreEqual(expectedSept3Start, orderedOccurrences[5].StartTime); Assert.AreEqual(expectedSept3End, orderedOccurrences[5].EndTime); } + + [Test] + public void AddExDateToEventAfterGetOccurrencesShouldRecomputeResult() + { + var searchStart = _now.AddDays(-1); + var searchEnd = _now.AddDays(7); + var e = GetEventWithRecurrenceRules(); + var occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 5); + + var exDate = _now.AddDays(1); + var period = new Period(new CalDateTime(exDate)); + var periodList = new PeriodList {period}; + e.ExceptionDates.Add(periodList); + occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 4); + + //Specifying just a date should "black out" that date + var excludeTwoDaysFromNow = _now.AddDays(2).Date; + period = new Period(new CalDateTime(excludeTwoDaysFromNow)); + periodList.Add(period); + occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 3); + } + + private static readonly DateTime _now = DateTime.Now; + private static readonly DateTime _later = _now.AddHours(1); + private static Event GetEventWithRecurrenceRules() + { + var dailyForFiveDays = new RecurrencePattern(FrequencyType.Daily, 1) + { + Count = 5, + }; + + var calendarEvent = new Event + { + Start = new CalDateTime(_now), + End = new CalDateTime(_later), + RecurrenceRules = new List { dailyForFiveDays }, + Resources = new HashSet(new[] {"Foo", "Bar", "Baz"}), + }; + return calendarEvent; + } } } diff --git a/v2/ical.NET/Evaluation/RecurringEvaluator.cs b/v2/ical.NET/Evaluation/RecurringEvaluator.cs index 3b0419f8c..746e9a9af 100644 --- a/v2/ical.NET/Evaluation/RecurringEvaluator.cs +++ b/v2/ical.NET/Evaluation/RecurringEvaluator.cs @@ -113,49 +113,45 @@ protected HashSet EvaluateExDate(IDateTime referenceDate, DateTime peri public override HashSet Evaluate(IDateTime referenceDate, DateTime periodStart, DateTime periodEnd, bool includeReferenceDateInResults) { - // Evaluate extra time periods, without re-evaluating ones that were already evaluated - if ((EvaluationStartBounds == DateTime.MaxValue && EvaluationEndBounds == DateTime.MinValue) || periodEnd.Equals(EvaluationStartBounds) || - periodStart.Equals(EvaluationEndBounds)) + Periods.Clear(); + + var rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); + if (includeReferenceDateInResults) + { + rruleOccurrences.UnionWith(new[] { new Period(referenceDate), }); + } + + var rdateOccurrences = EvaluateRDate(referenceDate, periodStart, periodEnd); + + var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd); + var exDateExclusions = EvaluateExDate(referenceDate, periodStart, periodEnd); + + //Exclusions trump inclusions + Periods.UnionWith(rruleOccurrences); + Periods.UnionWith(rdateOccurrences); + Periods.ExceptWith(exRuleExclusions); + Periods.ExceptWith(exDateExclusions); + + var dateOverlaps = FindDateOverlaps(exDateExclusions); + Periods.ExceptWith(dateOverlaps); + + if (EvaluationStartBounds == DateTime.MaxValue || EvaluationStartBounds > periodStart) { - //Exclusions take precedence over inclusions, so build the master set, then subtract the exclusions from it - var rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); - if (includeReferenceDateInResults) - { - rruleOccurrences.UnionWith(new [] {new Period(referenceDate), }); - } - - var rdateOccurrences = EvaluateRDate(referenceDate, periodStart, periodEnd); - - var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd); - var exDateExclusions = EvaluateExDate(referenceDate, periodStart, periodEnd); - - Periods.UnionWith(rruleOccurrences); - Periods.UnionWith(rdateOccurrences); - Periods.ExceptWith(exRuleExclusions); - Periods.ExceptWith(exDateExclusions); - - if (EvaluationStartBounds == DateTime.MaxValue || EvaluationStartBounds > periodStart) - { - EvaluationStartBounds = periodStart; - } - if (EvaluationEndBounds == DateTime.MinValue || EvaluationEndBounds < periodEnd) - { - EvaluationEndBounds = periodEnd; - } + EvaluationStartBounds = periodStart; } - else + if (EvaluationEndBounds == DateTime.MinValue || EvaluationEndBounds < periodEnd) { - if (EvaluationStartBounds != DateTime.MaxValue && periodStart < EvaluationStartBounds) - { - Evaluate(referenceDate, periodStart, EvaluationStartBounds, includeReferenceDateInResults); - } - if (EvaluationEndBounds != DateTime.MinValue && periodEnd > EvaluationEndBounds) - { - Evaluate(referenceDate, EvaluationEndBounds, periodEnd, includeReferenceDateInResults); - } + EvaluationEndBounds = periodEnd; } return Periods; } + + private HashSet FindDateOverlaps(HashSet dates) + { + var datesWithoutTimes = new HashSet(dates.Where(d => d.StartTime.Value.TimeOfDay == TimeSpan.Zero).Select(d => d.StartTime.Value)); + var overlaps = new HashSet(Periods.Where(p => datesWithoutTimes.Contains(p.StartTime.Value.Date))); + return overlaps; + } } } \ No newline at end of file diff --git a/v3/ical.NET.UnitTests/RecurrenceTests.cs b/v3/ical.NET.UnitTests/RecurrenceTests.cs index f0dda77f5..bec865f38 100644 --- a/v3/ical.NET.UnitTests/RecurrenceTests.cs +++ b/v3/ical.NET.UnitTests/RecurrenceTests.cs @@ -3085,5 +3085,48 @@ public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet() Assert.AreEqual(expectedSept3Start, orderedOccurrences[5].StartTime); Assert.AreEqual(expectedSept3End, orderedOccurrences[5].EndTime); } + + [Test] + public void AddExDateToEventAfterGetOccurrencesShouldRecomputeResult() + { + var searchStart = _now.AddDays(-1); + var searchEnd = _now.AddDays(7); + var e = GetEventWithRecurrenceRules(); + var occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 5); + + var exDate = _now.AddDays(1); + var period = new Period(new CalDateTime(exDate)); + var periodList = new PeriodList { period }; + e.ExceptionDates.Add(periodList); + occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 4); + + //Specifying just a date should "black out" that date + var excludeTwoDaysFromNow = _now.AddDays(2).Date; + period = new Period(new CalDateTime(excludeTwoDaysFromNow)); + periodList.Add(period); + occurrences = e.GetOccurrences(searchStart, searchEnd); + Assert.IsTrue(occurrences.Count == 3); + } + + private static readonly DateTime _now = DateTime.Now; + private static readonly DateTime _later = _now.AddHours(1); + private static CalendarEvent GetEventWithRecurrenceRules() + { + var dailyForFiveDays = new RecurrencePattern(FrequencyType.Daily, 1) + { + Count = 5, + }; + + var calendarEvent = new CalendarEvent + { + Start = new CalDateTime(_now), + End = new CalDateTime(_later), + RecurrenceRules = new List { dailyForFiveDays }, + Resources = new HashSet(new[] { "Foo", "Bar", "Baz" }), + }; + return calendarEvent; + } } } diff --git a/v3/ical.NET/Evaluation/RecurringEvaluator.cs b/v3/ical.NET/Evaluation/RecurringEvaluator.cs index 4a78afb5c..bb30af54f 100644 --- a/v3/ical.NET/Evaluation/RecurringEvaluator.cs +++ b/v3/ical.NET/Evaluation/RecurringEvaluator.cs @@ -113,49 +113,45 @@ protected HashSet EvaluateExDate(IDateTime referenceDate, DateTime perio public override HashSet Evaluate(IDateTime referenceDate, DateTime periodStart, DateTime periodEnd, bool includeReferenceDateInResults) { - // Evaluate extra time periods, without re-evaluating ones that were already evaluated - if ((EvaluationStartBounds == DateTime.MaxValue && EvaluationEndBounds == DateTime.MinValue) || periodEnd.Equals(EvaluationStartBounds) || - periodStart.Equals(EvaluationEndBounds)) + Periods.Clear(); + + var rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); + if (includeReferenceDateInResults) + { + rruleOccurrences.UnionWith(new[] { new Period(referenceDate), }); + } + + var rdateOccurrences = EvaluateRDate(referenceDate, periodStart, periodEnd); + + var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd); + var exDateExclusions = EvaluateExDate(referenceDate, periodStart, periodEnd); + + //Exclusions trump inclusions + Periods.UnionWith(rruleOccurrences); + Periods.UnionWith(rdateOccurrences); + Periods.ExceptWith(exRuleExclusions); + Periods.ExceptWith(exDateExclusions); + + var dateOverlaps = FindDateOverlaps(exDateExclusions); + Periods.ExceptWith(dateOverlaps); + + if (EvaluationStartBounds == DateTime.MaxValue || EvaluationStartBounds > periodStart) { - //Exclusions take precedence over inclusions, so build the master set, then subtract the exclusions from it - var rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); - if (includeReferenceDateInResults) - { - rruleOccurrences.UnionWith(new [] {new Period(referenceDate), }); - } - - var rdateOccurrences = EvaluateRDate(referenceDate, periodStart, periodEnd); - - var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd); - var exDateExclusions = EvaluateExDate(referenceDate, periodStart, periodEnd); - - Periods.UnionWith(rruleOccurrences); - Periods.UnionWith(rdateOccurrences); - Periods.ExceptWith(exRuleExclusions); - Periods.ExceptWith(exDateExclusions); - - if (EvaluationStartBounds == DateTime.MaxValue || EvaluationStartBounds > periodStart) - { - EvaluationStartBounds = periodStart; - } - if (EvaluationEndBounds == DateTime.MinValue || EvaluationEndBounds < periodEnd) - { - EvaluationEndBounds = periodEnd; - } + EvaluationStartBounds = periodStart; } - else + if (EvaluationEndBounds == DateTime.MinValue || EvaluationEndBounds < periodEnd) { - if (EvaluationStartBounds != DateTime.MaxValue && periodStart < EvaluationStartBounds) - { - Evaluate(referenceDate, periodStart, EvaluationStartBounds, includeReferenceDateInResults); - } - if (EvaluationEndBounds != DateTime.MinValue && periodEnd > EvaluationEndBounds) - { - Evaluate(referenceDate, EvaluationEndBounds, periodEnd, includeReferenceDateInResults); - } + EvaluationEndBounds = periodEnd; } return Periods; } + + private HashSet FindDateOverlaps(HashSet dates) + { + var datesWithoutTimes = new HashSet(dates.Where(d => d.StartTime.Value.TimeOfDay == TimeSpan.Zero).Select(d => d.StartTime.Value)); + var overlaps = new HashSet(Periods.Where(p => datesWithoutTimes.Contains(p.StartTime.Value.Date))); + return overlaps; + } } } \ No newline at end of file