diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 03998218..fb163a0b 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -37,26 +37,30 @@ int eventIndex .OrderBy(o => o.Period.StartTime) .ToList(); - Assert.That( - occurrences, -Has.Count.EqualTo(dateTimes.Length), - "There should be exactly " + dateTimes.Length + " occurrences; there were " + occurrences.Count); - - if (evt.RecurrenceRules.Count > 0) + Assert.Multiple(() => { - Assert.That(evt.RecurrenceRules, Has.Count.EqualTo(1)); - } + Assert.That( + occurrences, + Has.Count.GreaterThanOrEqualTo(dateTimes.Length), + "There should have " + dateTimes.Length + " or more occurrences; there were " + occurrences.Count); - for (var i = 0; i < dateTimes.Length; i++) - { - // Associate each incoming date/time with the calendar. - dateTimes[i].AssociatedObject = cal; + if (evt.RecurrenceRules.Count > 0) + { + Assert.That(evt.RecurrenceRules, Has.Count.EqualTo(1)); + } + + for (var i = 0; i < dateTimes.Length; i++) + { + // Associate each incoming date/time with the calendar. + dateTimes[i].AssociatedObject = cal; - var dt = dateTimes[i]; - Assert.That(occurrences[i].Period.StartTime, Is.EqualTo(dt), "Event should occur on " + dt); - if (timeZones != null) - Assert.That(dt.TimeZoneName, Is.EqualTo(timeZones[i]), "Event " + dt + " should occur in the " + timeZones[i] + " timezone"); - } + var dt = dateTimes[i]; + Assert.That(occurrences[i].Period.StartTime, Is.EqualTo(dt), "Event should occur on " + dt); + if (timeZones != null) + Assert.That(dt.TimeZoneName, Is.EqualTo(timeZones[i]), + "Event " + dt + " should occur in the " + timeZones[i] + " timezone"); + } + }); } private void EventOccurrenceTest( @@ -1868,27 +1872,26 @@ public void Bug1741093() } /// - /// Ensures that, by default, SECONDLY recurrence rules are not allowed. + /// Too many SECONDLY recurrences should time out. /// [Test, Category("Recurrence")] - public void Secondly1() + public void Secondly_HighNumberOfOccurrences_ShouldTimeout() { + RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout Assert.That(() => { var iCal = Calendar.Load(IcsFiles.Secondly1); _ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid)); - }, Throws.Exception.TypeOf(), "Evaluation engine should have failed."); + }, Throws.Exception.TypeOf(), "Too many occurrences should timeout."); } /// - /// Ensures that the proper behavior occurs when the evaluation - /// mode is set to adjust automatically for SECONDLY evaluation + /// At least a few SECONDLY occurrences are generated without TimeoutException. /// [Test, Category("Recurrence")] - public void Secondly1_1() + public void Secondly_LowNumberOfOccurrences_ShouldSucceed() { var iCal = Calendar.Load(IcsFiles.Secondly1); - iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically; EventOccurrenceTest( iCal, @@ -1897,47 +1900,45 @@ public void Secondly1_1() new[] { new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 1, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 2, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 3, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 4, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 5, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 6, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 7, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 8, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 9, 0, _tzid), - new CalDateTime(2007, 6, 21, 8, 10, 0, _tzid) + new CalDateTime(2007, 6, 21, 8, 0, 1, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 2, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 3, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 4, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 5, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 6, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 7, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 8, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 9, _tzid), + new CalDateTime(2007, 6, 21, 8, 0, 10, _tzid) }, null ); } /// - /// Ensures that if configured, MINUTELY recurrence rules are not allowed. + /// Too many MINUTELY occurrences time out. /// [Test, Category("Recurrence")] - public void Minutely1() + public void Minutely_HighNumberOfOccurrences_ShouldTimeout() { + RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout Assert.That(() => { var iCal = Calendar.Load(IcsFiles.Minutely1); - iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictMinutely; - var occurrences = iCal.GetOccurrences( + var evt = iCal.Events.First(); + var occurrences = evt.GetOccurrences( new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid)); - }, Throws.Exception.TypeOf()); + }, Throws.Exception.TypeOf(), "Too many occurrences should timeout."); } /// - /// Ensures that the proper behavior occurs when the evaluation - /// mode is set to adjust automatically for MINUTELY evaluation + /// At least a few MINUTELY occurrences are generated without TimeoutException. /// [Test, Category("Recurrence")] - public void Minutely1_1() + public void Minutely_LowNumberOfOccurrences_ShouldSucceed() { var iCal = Calendar.Load(IcsFiles.Minutely1); - iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictMinutely; - iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically; EventOccurrenceTest( iCal, @@ -1946,40 +1947,38 @@ public void Minutely1_1() new[] { new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 21, 9, 0, 0, _tzid), - new CalDateTime(2007, 6, 21, 10, 0, 0, _tzid), - new CalDateTime(2007, 6, 21, 11, 0, 0, _tzid), - new CalDateTime(2007, 6, 21, 12, 0, 0, _tzid) + new CalDateTime(2007, 6, 21, 8, 1, 0, _tzid), + new CalDateTime(2007, 6, 21, 8, 2, 0, _tzid), + new CalDateTime(2007, 6, 21, 8, 3, 0, _tzid), + new CalDateTime(2007, 6, 21, 8, 4, 0, _tzid) }, null ); } /// - /// Ensures that if configured, HOURLY recurrence rules are not allowed. + /// Too many HOURLY occurrences time out. /// - [Test, Category("Recurrence")/*, ExpectedException(typeof(EvaluationEngineException))*/] - public void Hourly1() + [Test, Category("Recurrence")] + public void Hourly_HighNumberOfOccurrences_ShouldTimeout() { + RecurrenceUtil.DefaultTimeout = 50; // reduce below default to ensure timeout Assert.That(() => - { + { var iCal = Calendar.Load(IcsFiles.Hourly1); - iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictHourly; - _ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), new CalDateTime(2007, 7, 21, 8, 0, 0, _tzid)); - - }, Throws.Exception.TypeOf()); + _ = iCal.GetOccurrences(new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), + new CalDateTime(2013, 7, 21, 8, 0, 0, _tzid)); + }, + Throws.Exception.TypeOf(), "Too many occurrences should timeout."); } /// - /// Ensures that the proper behavior occurs when the evaluation - /// mode is set to adjust automatically for HOURLY evaluation + /// At least a few HOURLY occurrences are generated without TimeoutException. /// [Test, Category("Recurrence")] - public void Hourly1_1() + public void Hourly_LowNumberOfOccurrences_ShouldSucceed() { var iCal = Calendar.Load(IcsFiles.Hourly1); - iCal.RecurrenceRestriction = RecurrenceRestrictionType.RestrictHourly; - iCal.RecurrenceEvaluationMode = RecurrenceEvaluationModeType.AdjustAutomatically; EventOccurrenceTest( iCal, @@ -1988,10 +1987,10 @@ public void Hourly1_1() new[] { new CalDateTime(2007, 6, 21, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 22, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 23, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 24, 8, 0, 0, _tzid), - new CalDateTime(2007, 6, 25, 8, 0, 0, _tzid) + new CalDateTime(2007, 6, 21, 9, 0, 0, _tzid), + new CalDateTime(2007, 6, 21, 10, 0, 0, _tzid), + new CalDateTime(2007, 6, 21, 11, 0, 0, _tzid), + new CalDateTime(2007, 6, 21, 12, 0, 0, _tzid) }, null ); @@ -2038,7 +2037,7 @@ public void YearlyInterval1() } /// - /// Ensures that "off-day" calcuation works correctly + /// Ensures that "off-day" calculation works correctly /// [Test, Category("Recurrence")] public void DailyInterval1() @@ -2841,10 +2840,7 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) // This case (DTSTART of type DATE and FREQ=MINUTELY) is undefined in RFC 5545. // ical.net handles the case by pretending DTSTART has the time set to midnight. - evt.RecurrenceRules.Add(new RecurrencePattern($"FREQ={freq};INTERVAL=10;COUNT=5") - { - RestrictionType = RecurrenceRestrictionType.NoRestriction, - }); + evt.RecurrenceRules.Add(new RecurrencePattern($"FREQ={freq};INTERVAL=10;COUNT=5")); var occurrences = evt.GetOccurrences(CalDateTime.Today.AddDays(-1), CalDateTime.Today.AddDays(100)) .OrderBy(x => x) @@ -2868,8 +2864,7 @@ public void RecurrencePattern1() { // NOTE: evaluators are not generally meant to be used directly like this. // However, this does make a good test to ensure they behave as they should. - RecurrencePattern pattern = new RecurrencePattern("FREQ=SECONDLY;INTERVAL=10"); - pattern.RestrictionType = RecurrenceRestrictionType.NoRestriction; + var pattern = new RecurrencePattern("FREQ=SECONDLY;INTERVAL=10"); var us = new CultureInfo("en-US"); @@ -3177,10 +3172,14 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() Assert.That(occurrences.Last().StartTime.Equals(lastExpected), Is.True); } - [Test, Ignore("Turn on in v3")] + /// + /// Evaluate relevancy and validity of the request. + /// Find a solution for issue #120 or close forever + /// + [Test, Ignore("Turn on in v3", Until = "2024-12-31")] public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet() { - //https://github.com/rianjs/ical.net/issues/120 + //https://github.com/rianjs/ical.net/issues/120 dated Sep 5, 2016 const string ical = @"BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN diff --git a/Ical.Net/Calendar.cs b/Ical.Net/Calendar.cs index 47248aa6..cc38dafe 100644 --- a/Ical.Net/Calendar.cs +++ b/Ical.Net/Calendar.cs @@ -26,7 +26,7 @@ public static Calendar Load(Stream s) => CalendarCollection.Load(new StreamReader(s, Encoding.UTF8)).SingleOrDefault(); public static Calendar Load(TextReader tr) - => CalendarCollection.Load(tr).OfType().SingleOrDefault(); + => CalendarCollection.Load(tr)?.SingleOrDefault(); public static IList Load(Stream s, Encoding e) => Load(new StreamReader(s, e)); @@ -118,7 +118,7 @@ public override int GetHashCode() public virtual IEnumerable RecurringItems => Children.OfType(); /// - /// A collection of components in the iCalendar. + /// A collection of components in the iCalendar. /// public virtual IUniqueComponentList Events => _mEvents; @@ -166,12 +166,14 @@ public virtual string Method set => Properties.Set("METHOD", value); } + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public virtual RecurrenceRestrictionType RecurrenceRestriction { get => Properties.Get("X-DDAY-ICAL-RECURRENCE-RESTRICTION"); set => Properties.Set("X-DDAY-ICAL-RECURRENCE-RESTRICTION", value); } + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public virtual RecurrenceEvaluationModeType RecurrenceEvaluationMode { get => Properties.Get("X-DDAY-ICAL-RECURRENCE-EVALUATION-MODE"); @@ -190,35 +192,7 @@ public VTimeZone AddTimeZone(VTimeZone tz) return tz; } - /// - /// Evaluates component recurrences for the given range of time. - /// - /// For example, if you are displaying a month-view for January 2007, - /// you would want to evaluate recurrences for Jan. 1, 2007 to Jan. 31, 2007 - /// to display relevant information for those dates. - /// - /// - /// The beginning date/time of the range to test. - /// The end date/time of the range to test. - [Obsolete("This method is no longer supported. Use GetOccurrences() instead.")] - public void Evaluate(IDateTime fromDate, IDateTime toDate) - { - throw new NotSupportedException("Evaluate() is no longer supported as a public method. Use GetOccurrences() instead."); - } - - /// - /// Evaluates component recurrences for the given range of time, for - /// the type of recurring component specified. - /// - /// The type of component to be evaluated for recurrences. - /// The beginning date/time of the range to test. - /// The end date/time of the range to test. - [Obsolete("This method is no longer supported. Use GetOccurrences() instead.")] - public void Evaluate(IDateTime fromDate, IDateTime toDate) - { - throw new NotSupportedException("Evaluate() is no longer supported as a public method. Use GetOccurrences() instead."); - } - + /// /// Clears recurrence evaluations for recurring components. /// @@ -273,6 +247,9 @@ public virtual HashSet GetOccurrences(IDateTime dt) where T : IRe public virtual HashSet GetOccurrences(DateTime dt) where T : IRecurringComponent => GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddTicks(-1))); + public virtual HashSet GetOccurrences(DateTime startTime, DateTime endTime) where T : IRecurringComponent + => GetOccurrences(new CalDateTime(startTime), new CalDateTime(endTime)); + /// /// Returns all occurrences of components of type T that start within the date range provided. /// All components occurring between and @@ -282,24 +259,21 @@ public virtual HashSet GetOccurrences(DateTime dt) where T : IRec /// The ending date range public virtual HashSet GetOccurrences(IDateTime startTime, IDateTime endTime) where T : IRecurringComponent { - var occurrences = new HashSet(RecurringItems - .OfType() - .SelectMany(recurrable => recurrable.GetOccurrences(startTime, endTime))); - - var removeOccurrencesQuery = occurrences - .Where(o => o.Source is UniqueComponent) - .GroupBy(o => ((UniqueComponent)o.Source).Uid) - .SelectMany(group => group - .Where(o => o.Source.RecurrenceId != null) - .SelectMany(occurrence => group. - Where(o => o.Source.RecurrenceId == null && occurrence.Source.RecurrenceId.Date.Equals(o.Period.StartTime.Date)))); - - occurrences.ExceptWith(removeOccurrencesQuery); - return occurrences; - } - - public virtual HashSet GetOccurrences(DateTime startTime, DateTime endTime) where T : IRecurringComponent - => GetOccurrences(new CalDateTime(startTime), new CalDateTime(endTime)); + var occurrences = new HashSet(RecurringItems + .OfType() + .SelectMany(recurrable => recurrable.GetOccurrences(startTime, endTime))); + + var removeOccurrencesQuery = occurrences + .Where(o => o.Source is UniqueComponent) + .GroupBy(o => ((UniqueComponent)o.Source).Uid) + .SelectMany(group => group + .Where(o => o.Source.RecurrenceId != null) + .SelectMany(occurrence => group. + Where(o => o.Source.RecurrenceId == null && occurrence.Source.RecurrenceId.Date.Equals(o.Period.StartTime.Date)))); + + occurrences.ExceptWith(removeOccurrencesQuery); + return occurrences; + } /// /// Creates a typed object that is a direct child of the iCalendar itself. Generally, diff --git a/Ical.Net/Constants.cs b/Ical.Net/Constants.cs index 062b27e8..0485f61e 100644 --- a/Ical.Net/Constants.cs +++ b/Ical.Net/Constants.cs @@ -219,6 +219,7 @@ public enum FrequencyOccurrence Fifth = 5 } + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public enum RecurrenceRestrictionType { /// @@ -247,6 +248,7 @@ public enum RecurrenceRestrictionType RestrictHourly } + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public enum RecurrenceEvaluationModeType { /// diff --git a/Ical.Net/DataTypes/RecurrencePattern.cs b/Ical.Net/DataTypes/RecurrencePattern.cs index bde5ebff..24d09305 100644 --- a/Ical.Net/DataTypes/RecurrencePattern.cs +++ b/Ical.Net/DataTypes/RecurrencePattern.cs @@ -84,6 +84,11 @@ public int Interval public DayOfWeek FirstDayOfWeek { get; set; } = DayOfWeek.Monday; + /// + /// The type of restriction to apply to the evaluation of this recurrence pattern. + /// Returns if not set. + /// + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public RecurrenceRestrictionType RestrictionType { get @@ -93,11 +98,12 @@ public RecurrenceRestrictionType RestrictionType { return _restrictionType.Value; } - return Calendar?.RecurrenceRestriction ?? RecurrenceRestrictionType.Default; + return RecurrenceRestrictionType.NoRestriction; } set => _restrictionType = value; } + [Obsolete("Usage may cause undesired results or exceptions. Will be removed.", false)] public RecurrenceEvaluationModeType EvaluationMode { get diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index f91a644f..43cbffb3 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -48,9 +48,6 @@ namespace Ical.Net.Evaluation /// public class RecurrencePatternEvaluator : Evaluator { - // FIXME: in ical4j this is configurable. - private const int _maxIncrementCount = 1000; - protected RecurrencePattern Pattern { get; set; } public RecurrencePatternEvaluator(RecurrencePattern pattern) @@ -232,14 +229,13 @@ private void EnforceEvaluationRestrictions(RecurrencePattern pattern) } } - /** - * Returns a list of start dates in the specified period represented by this recur. This method includes a base date - * argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject - * default values to return a set of dates in the correct format. For example, if the search start date (start) is - * Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at - * 9:00AM, and not 12:19PM. - */ - + /// + /// Returns a list of start dates in the specified period represented by this recurrence pattern. + /// This method includes a base date argument, which indicates the start of the first occurrence of this recurrence. + /// The base date is used to inject default values to return a set of dates in the correct format. + /// For example, if the search start date (start) is Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, + /// the start dates returned should all be at 9:00AM, and not 12:19PM. + /// private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTime periodEnd, int maxCount, RecurrencePattern pattern, bool includeReferenceDateInResults) { @@ -261,7 +257,6 @@ private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTim var expandBehavior = RecurrenceUtil.GetExpandBehaviorList(pattern); - var noCandidateIncrementCount = 0; var candidate = DateTime.MinValue; while (maxCount < 0 || dates.Count < maxCount) { @@ -289,8 +284,6 @@ private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTim var candidates = GetCandidates(seedCopy, pattern, expandBehavior); if (candidates.Count > 0) { - noCandidateIncrementCount = 0; - foreach (var t in candidates.OrderBy(c => c).Where(t => t >= originalDate)) { candidate = t; @@ -316,14 +309,6 @@ private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTim } } } - else - { - noCandidateIncrementCount++; - if (_maxIncrementCount > 0 && noCandidateIncrementCount > _maxIncrementCount) - { - break; - } - } IncrementDate(ref seedCopy, pattern, pattern.Interval); } @@ -331,13 +316,13 @@ private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTim return dates; } - /** - * Returns a list of possible dates generated from the applicable BY* rules, using the specified date as a seed. - * @param date the seed date - * @param value the type of date list to return - * @return a DateList - */ - + /// + /// Returns a list of possible dates generated from the applicable BY* rules, using the specified date as a seed. + /// + /// The seed date. + /// + /// + /// A list of possible dates. private List GetCandidates(DateTime date, RecurrencePattern pattern, bool?[] expandBehaviors) { var dates = new List { date }; @@ -353,11 +338,12 @@ private List GetCandidates(DateTime date, RecurrencePattern pattern, b return dates; } - /** - * Applies BYSETPOS rules to dates. Valid positions are from 1 to the size of the date list. Invalid - * positions are ignored. - * @param dates - */ + /// + /// Applies BYSETPOS rules to . Valid positions are from 1 to the size of the date list. Invalid + /// positions are ignored. + /// + /// The list of dates to which the BYSETPOS rules will be applied. + /// private List ApplySetPosRules(List dates, RecurrencePattern pattern) { // return if no SETPOS rules specified.. @@ -379,12 +365,14 @@ private List ApplySetPosRules(List dates, RecurrencePattern return setPosDates; } - /** - * Applies BYMONTH rules specified in this Recur instance to the specified date list. If no BYMONTH rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ + /// + /// Applies BYMONTH rules specified in this Recur instance to the specified date list. + /// If no BYMONTH rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYMONTH rules will be applied. + /// + /// + /// The modified list of dates after applying the BYMONTH rules. private List GetMonthVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByMonth.Count == 0) @@ -406,12 +394,12 @@ private List GetMonthVariants(List dates, RecurrencePattern return dateSet.ToList(); } - /** - * Applies BYWEEKNO rules specified in this Recur instance to the specified date list. If no BYWEEKNO rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ + /// + /// Applies BYWEEKNO rules specified in this Recur instance to the specified date list. + /// If no BYWEEKNO rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYWEEKNO rules will be applied. + /// The modified list of dates after applying the BYWEEKNO rules. private List GetWeekNoVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByWeekNo.Count == 0) @@ -462,13 +450,12 @@ private List GetWeekNoVariants(List dates, RecurrencePattern return weekNoDates; } - /** - * Applies BYYEARDAY rules specified in this Recur instance to the specified date list. If no BYYEARDAY rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYYEARDAY rules specified in this Recur instance to the specified date list. + /// If no BYYEARDAY rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYYEARDAY rules will be applied. + /// The modified list of dates after applying the BYYEARDAY rules. private List GetYearDayVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByYearDay.Count == 0) @@ -516,13 +503,12 @@ private List GetYearDayVariants(List dates, RecurrencePatter return dates; } - /** - * Applies BYMONTHDAY rules specified in this Recur instance to the specified date list. If no BYMONTHDAY rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYMONTHDAY rules specified in this Recur instance to the specified date list. + /// If no BYMONTHDAY rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYMONTHDAY rules will be applied. + /// The modified list of dates after applying the BYMONTHDAY rules. private List GetMonthDayVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByMonthDay.Count == 0) @@ -571,20 +557,19 @@ select monthDay > 0 } } - Next: + Next: dates.RemoveAt(i); } return dates; } - /** - * Applies BYDAY rules specified in this Recur instance to the specified date list. If no BYDAY rules are specified - * the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYDAY rules specified in this Recur instance to the specified date list. + /// If no BYDAY rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which BYDAY rules will be applied. + /// The modified list of dates after applying BYDAY rules, or the original list if no BYDAY rules are specified. private List GetDayVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByDay.Count == 0) @@ -632,14 +617,13 @@ private List GetDayVariants(List dates, RecurrencePattern pa return dates; } - /** - * Returns a list of applicable dates corresponding to the specified week day in accordance with the frequency - * specified by this recurrence rule. - * @param date - * @param weekDay - * @return - */ - + /// + /// Returns a list of applicable dates corresponding to the specified week day in accordance with the frequency + /// specified by this recurrence rule. + /// + /// The date to start the evaluation from. + /// The week day to evaluate. + /// A list of applicable dates. private List GetAbsWeekDays(DateTime date, WeekDay weekDay, RecurrencePattern pattern) { var days = new List(); @@ -723,15 +707,13 @@ private List GetAbsWeekDays(DateTime date, WeekDay weekDay, Recurrence return GetOffsetDates(days, weekDay.Offset); } - /** - * Returns a single-element sublist containing the element of list at offset. Valid - * offsets are from 1 to the size of the list. If an invalid offset is supplied, all elements from list - * are added to sublist. - * @param list - * @param offset - * @param sublist - */ - + /// + /// Returns a single-element sublist containing the element of at . + /// Valid offsets are from 1 to the size of the list. If an invalid offset is supplied, all elements from + /// are added to result. + /// + /// The list from which to extract the element. + /// The position of the element to extract. private List GetOffsetDates(List dates, int offset) { if (offset == int.MinValue) @@ -752,13 +734,14 @@ private List GetOffsetDates(List dates, int offset) return offsetDates; } - /** - * Applies BYHOUR rules specified in this Recur instance to the specified date list. If no BYHOUR rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYHOUR rules specified in this Recur instance to the specified date list. + /// If no BYHOUR rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYHOUR rules will be applied. + /// + /// + /// The modified list of dates after applying the BYHOUR rules. private List GetHourVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByHour.Count == 0) @@ -802,13 +785,14 @@ private List GetHourVariants(List dates, RecurrencePattern p return dates; } - /** - * Applies BYMINUTE rules specified in this Recur instance to the specified date list. If no BYMINUTE rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYMINUTE rules specified in this Recur instance to the specified date list. + /// If no BYMINUTE rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYMINUTE rules will be applied. + /// + /// + /// The modified list of dates after applying the BYMINUTE rules. private List GetMinuteVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByMinute.Count == 0) @@ -852,13 +836,14 @@ private List GetMinuteVariants(List dates, RecurrencePattern return dates; } - /** - * Applies BYSECOND rules specified in this Recur instance to the specified date list. If no BYSECOND rules are - * specified the date list is returned unmodified. - * @param dates - * @return - */ - + /// + /// Applies BYSECOND rules specified in this Recur instance to the specified date list. + /// If no BYSECOND rules are specified, the date list is returned unmodified. + /// + /// The list of dates to which the BYSECOND rules will be applied. + /// + /// + /// The modified list of dates after applying the BYSECOND rules. private List GetSecondVariants(List dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.BySecond.Count == 0) @@ -927,7 +912,7 @@ private Period CreatePeriod(DateTime dt, IDateTime referenceDate) /// public override HashSet Evaluate(IDateTime referenceDate, DateTime periodStart, DateTime periodEnd, bool includeReferenceDateInResults) { - if ((this.Pattern.Frequency != FrequencyType.None) && (this.Pattern.Frequency < FrequencyType.Daily) && !referenceDate.HasTime) + if (Pattern.Frequency != FrequencyType.None && Pattern.Frequency < FrequencyType.Daily && !referenceDate.HasTime) { // This case is not defined by RFC 5545. We handle it by evaluating the rule // as if referenceDate had a time (i.e. set to midnight). diff --git a/Ical.Net/Evaluation/RecurrenceUtil.cs b/Ical.Net/Evaluation/RecurrenceUtil.cs index 7708e893..19ca3efd 100644 --- a/Ical.Net/Evaluation/RecurrenceUtil.cs +++ b/Ical.Net/Evaluation/RecurrenceUtil.cs @@ -1,13 +1,20 @@ using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Utility; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Ical.Net.Evaluation { internal class RecurrenceUtil { + // for v5 the timeout should be configurable (results in a breaking change) + // 500ms should be a good default value to cover most cases + internal static int DefaultTimeout = 500; // milliseconds + public static void ClearEvaluation(IRecurrable recurrable) { var evaluator = recurrable.GetService(typeof(IEvaluator)) as IEvaluator; @@ -15,9 +22,10 @@ public static void ClearEvaluation(IRecurrable recurrable) } public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) => GetOccurrences(recurrable, - new CalDateTime(dt.AsSystemLocal.Date), new CalDateTime(dt.AsSystemLocal.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); + new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); - public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime periodStart, IDateTime periodEnd, bool includeReferenceDateInResults) + public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime periodStart, + IDateTime periodEnd, bool includeReferenceDateInResults) { var evaluator = recurrable.GetService(typeof(IEvaluator)) as IEvaluator; if (evaluator == null || recurrable.Start == null) @@ -35,19 +43,41 @@ public static HashSet GetOccurrences(IRecurrable recurrable, IDateTi periodStart.TzId = start.TzId; periodEnd.TzId = start.TzId; - var periods = evaluator.Evaluate(start, DateUtil.GetSimpleDateTimeData(periodStart), DateUtil.GetSimpleDateTimeData(periodEnd), - includeReferenceDateInResults); + using var cancellationTokenSource = new CancellationTokenSource(); + var task = Task.Run(() => + { + var periods = evaluator.Evaluate(start, DateUtil.GetSimpleDateTimeData(periodStart), + DateUtil.GetSimpleDateTimeData(periodEnd), + includeReferenceDateInResults); + + var otherOccurrences = from p in periods + let endTime = p.EndTime ?? p.StartTime + where + (endTime.GreaterThan(periodStart) && p.StartTime.LessThan(periodEnd) || + (periodStart.Equals(periodEnd) && p.StartTime.LessThanOrEqual(periodStart) && + endTime.GreaterThan(periodEnd))) || //A period that starts at the same time it ends + (p.StartTime.Equals(endTime) && + periodStart.Equals(p.StartTime)) //An event that starts at the same time it ends + select new Occurrence(recurrable, p); + + var occurrences = new HashSet(otherOccurrences); + return occurrences; + }, cancellationTokenSource.Token); - var otherOccurrences = from p in periods - let endTime = p.EndTime ?? p.StartTime - where - (endTime.GreaterThan(periodStart) && p.StartTime.LessThan(periodEnd) || - (periodStart.Equals(periodEnd) && p.StartTime.LessThanOrEqual(periodStart) && endTime.GreaterThan(periodEnd))) || //A period that starts at the same time it ends - (p.StartTime.Equals(endTime) && periodStart.Equals(p.StartTime)) //An event that starts at the same time it ends - select new Occurrence(recurrable, p); + if (task.Wait(TimeSpan.FromMilliseconds(DefaultTimeout))) + { + if (task.IsFaulted && task.Exception != null) + { + // maintain original exception details + throw task.Exception; + } + + return task.Result; + } - var occurrences = new HashSet(otherOccurrences); - return occurrences; + cancellationTokenSource.Cancel(); + // maintain any exception inside the task before timeout + throw new TimeoutException("Getting recurrences has timed out.", task.Exception); } public static bool?[] GetExpandBehaviorList(RecurrencePattern p) diff --git a/Ical.Net/Evaluation/RecurringEvaluator.cs b/Ical.Net/Evaluation/RecurringEvaluator.cs index c435b4a6..1460f562 100644 --- a/Ical.Net/Evaluation/RecurringEvaluator.cs +++ b/Ical.Net/Evaluation/RecurringEvaluator.cs @@ -29,7 +29,7 @@ public RecurringEvaluator(IRecurrable obj) } /// - /// Evaulates the RRule component, and adds each specified Period to the Periods collection. + /// Evaluates the RRule component, and adds each specified Period to the Periods collection. /// /// /// The beginning date of the range to evaluate. @@ -62,7 +62,7 @@ protected HashSet EvaluateRRule(IDateTime referenceDate, DateTime period return periods; } - /// Evalates the RDate component, and adds each specified DateTime or Period to the Periods collection. + /// Evaluates the RDate component, and adds each specified DateTime or Period to the Periods collection. protected HashSet EvaluateRDate(IDateTime referenceDate, DateTime periodStart, DateTime periodEnd) { if (Recurrable.RecurrenceDates == null || !Recurrable.RecurrenceDates.Any()) @@ -75,7 +75,7 @@ protected HashSet EvaluateRDate(IDateTime referenceDate, DateTime period } /// - /// Evaulates the ExRule component, and excludes each specified DateTime from the Periods collection. + /// Evaluates the ExRule component, and excludes each specified DateTime from the Periods collection. /// /// /// The beginning date of the range to evaluate. @@ -102,7 +102,7 @@ protected HashSet EvaluateExRule(IDateTime referenceDate, DateTime perio } /// - /// Evalates the ExDate component, and excludes each specified DateTime or Period from the Periods collection. + /// Evaluates the ExDate component, and excludes each specified DateTime or Period from the Periods collection. /// /// /// The beginning date of the range to evaluate.