diff --git a/Ical.Net.Tests/GetOccurrenceTests.cs b/Ical.Net.Tests/GetOccurrenceTests.cs index f3ef1fda7..5ad561afa 100644 --- a/Ical.Net.Tests/GetOccurrenceTests.cs +++ b/Ical.Net.Tests/GetOccurrenceTests.cs @@ -82,8 +82,7 @@ public void SkippedOccurrenceOnWeeklyPattern() var occurrences = RecurrenceUtil.GetOccurrences( recurrable: vEvent, periodStart: intervalStart, - periodEnd: intervalEnd, - includeReferenceDateInResults: false); + periodEnd: intervalEnd); var occurrenceSet = new HashSet(occurrences.Select(o => o.Period.StartTime)); Assert.That(occurrenceSet, Has.Count.EqualTo(evaluationsCount)); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 5998026f2..d0d5e212c 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -17,6 +17,7 @@ using Ical.Net.Serialization; using Ical.Net.Serialization.DataTypes; using NUnit.Framework; +using NUnit.Framework.Constraints; namespace Ical.Net.Tests; @@ -1232,8 +1233,8 @@ public void WeekNoOrderingShouldNotMatter() var rpe1 = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=YEARLY;WKST=MO;BYDAY=MO;BYWEEKNO=1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53")); var rpe2 = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=YEARLY;WKST=MO;BYDAY=MO;BYWEEKNO=53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1")); - var recurringPeriods1 = rpe1.Evaluate(new CalDateTime(start), start, end, false).ToList(); - var recurringPeriods2 = rpe2.Evaluate(new CalDateTime(start), start, end, false).ToList(); + var recurringPeriods1 = rpe1.Evaluate(new CalDateTime(start), start, end, default).ToList(); + var recurringPeriods2 = rpe2.Evaluate(new CalDateTime(start), start, end, default).ToList(); Assert.That(recurringPeriods2, Has.Count.EqualTo(recurringPeriods1.Count)); } @@ -2627,7 +2628,7 @@ public void BugByWeekNoNotWorking() var end = new CalDateTime(2019, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYWEEKNO=2")); - var recurringPeriods = rpe.Evaluate(start, start, end, false).ToList(); + var recurringPeriods = rpe.Evaluate(start, start, end, default).ToList(); Assert.That(recurringPeriods, Has.Count.EqualTo(1)); Assert.That(recurringPeriods.First().StartTime, Is.EqualTo(new CalDateTime(2019, 1, 7))); @@ -2643,7 +2644,7 @@ public void BugByMonthWhileFreqIsWeekly() var end = new CalDateTime(2020, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1")); - var recurringPeriods = rpe.Evaluate(start, start, end, false).OrderBy(x => x).ToList(); + var recurringPeriods = rpe.Evaluate(start, start, end, default).OrderBy(x => x).ToList(); Assert.That(recurringPeriods, Has.Count.EqualTo(4)); Assert.Multiple(() => @@ -2686,7 +2687,7 @@ public void BugByMonthWhileFreqIsMonthly() var end = new CalDateTime(2020, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=MONTHLY;BYDAY=MO;BYMONTH=1")); - var recurringPeriods = rpe.Evaluate(start, start, end, false).OrderBy(x => x).ToList(); + var recurringPeriods = rpe.Evaluate(start, start, end, default).OrderBy(x => x).ToList(); Assert.That(recurringPeriods, Has.Count.EqualTo(4)); Assert.Multiple(() => @@ -2710,7 +2711,7 @@ public void Bug3119920() var serializer = new RecurrencePatternSerializer(); var rp = (RecurrencePattern)serializer.Deserialize(sr)!; var rpe = new RecurrencePatternEvaluator(rp); - var recurringPeriods = rpe.Evaluate(start, start, rp.Until, false).ToList(); + var recurringPeriods = rpe.Evaluate(start, start, rp.Until, default).ToList(); var period = recurringPeriods.ElementAt(recurringPeriods.Count - 1); @@ -2932,8 +2933,7 @@ public void RecurrencePattern1() var occurrences = evaluator.Evaluate( startDate, fromDate, - toDate, - false) + toDate, default) .OrderBy(o => o.StartTime) .ToList(); Assert.That(occurrences, Has.Count.EqualTo(4)); @@ -2965,8 +2965,7 @@ public void RecurrencePattern2() var occurrences = evaluator.Evaluate( startDate, fromDate, - toDate, - false); + toDate, default); Assert.That(occurrences.Count, Is.Not.EqualTo(0)); } @@ -3073,8 +3072,7 @@ public void Test4() var periods = evaluator.Evaluate( evtStart, evtStart, - evtEnd, - false) + evtEnd, default) .OrderBy(p => p.StartTime) .ToList(); Assert.That(periods, Has.Count.EqualTo(10)); @@ -3908,7 +3906,7 @@ public void TestDtStartTimezone(string? tzId) var cal = Calendar.Load(icalText); var evt = cal.Events.First(); var ev = new EventEvaluator(evt); - var occurrences = ev.Evaluate(evt.DtStart, evt.DtStart.ToTimeZone(tzId), evt.DtStart.AddMinutes(61).ToTimeZone(tzId), false); + var occurrences = ev.Evaluate(evt.DtStart, evt.DtStart.ToTimeZone(tzId), evt.DtStart.AddMinutes(61).ToTimeZone(tzId), default); var occurrencesStartTimes = occurrences.Select(x => x.StartTime).Take(2).ToList(); var expectedStartTimes = new[] @@ -3919,4 +3917,35 @@ public void TestDtStartTimezone(string? tzId) Assert.That(expectedStartTimes.SequenceEqual(occurrencesStartTimes), Is.True); } + + [Test] + [TestCase(null, false)] + [TestCase(0, true)] + [TestCase(1000, true)] + [TestCase(1440, false)] + public void TestMaxIncrementCount(int? limit, bool expectException) + { + var ical = """ + BEGIN:VCALENDAR + BEGIN:VEVENT + DTSTART:20250305T000000 + RRULE:FREQ=MINUTELY;BYHOUR=0;COUNT=100 + END:VEVENT + END:VCALENDAR + """; + + var cal = Calendar.Load(ical); + + var options = new EvaluationOptions + { + MaxUnmatchedIncrementsLimit = limit, + }; + + IResolveConstraint constraint = + expectException + ? Throws.Exception.TypeOf() + : Throws.Nothing; + + Assert.That(() => cal.GetOccurrences(options: options).ToList(), constraint); + } } diff --git a/Ical.Net/Calendar.cs b/Ical.Net/Calendar.cs index 77222ecec..30fd3565c 100644 --- a/Ical.Net/Calendar.cs +++ b/Ical.Net/Calendar.cs @@ -11,6 +11,7 @@ using System.Text; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; +using Ical.Net.Evaluation; using Ical.Net.Proxies; using Ical.Net.Serialization; using Ical.Net.Utility; @@ -190,16 +191,16 @@ public VTimeZone AddTimeZone(VTimeZone tz) /// The beginning date/time of the range. /// The end date/time of the range. /// A list of occurrences that fall between the date/time arguments provided. - public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) - => GetOccurrences(startTime, endTime); + public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) + => GetOccurrences(startTime, endTime, options); /// - public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) - => GetOccurrences(startTime?.AsCalDateTime(), endTime?.AsCalDateTime()); + public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) + => GetOccurrences(startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options); /// - public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) where T : IRecurringComponent - => GetOccurrences(startTime?.AsCalDateTime(), endTime?.AsCalDateTime()); + public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) where T : IRecurringComponent + => GetOccurrences(startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options); /// /// Returns all occurrences of components of type T that start within the date range provided. @@ -208,7 +209,7 @@ public virtual IEnumerable GetOccurrences(DateTime? startTime, Da /// /// The starting date range /// The ending date range - public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) where T : IRecurringComponent + public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) where T : IRecurringComponent { // These are the UID/RECURRENCE-ID combinations that replace other occurrences. var recurrenceIdsAndUids = this.Children.OfType() @@ -219,7 +220,7 @@ public virtual IEnumerable GetOccurrences(CalDateTime startTime = var occurrences = RecurringItems .OfType() - .Select(recurrable => recurrable.GetOccurrences(startTime, endTime)) + .Select(recurrable => recurrable.GetOccurrences(startTime, endTime, options)) // Enumerate the list of occurrences (not the occurrences themselves) now to ensure // the initialization code is run, including validation and error handling. diff --git a/Ical.Net/CalendarCollection.cs b/Ical.Net/CalendarCollection.cs index 1d24c3087..3a75badb6 100644 --- a/Ical.Net/CalendarCollection.cs +++ b/Ical.Net/CalendarCollection.cs @@ -10,8 +10,10 @@ using System.Text; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; +using Ical.Net.Evaluation; using Ical.Net.Serialization; using Ical.Net.Utility; +using static NodaTime.TimeZones.ZoneEqualityComparer; namespace Ical.Net; @@ -55,17 +57,17 @@ private IEnumerable GetOccurrences(Func GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) - => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime)); + public IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) + => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options)); - public IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) - => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime)); + public IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) + => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options)); - public IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) where T : IRecurringComponent - => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime)); + public IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) where T : IRecurringComponent + => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options)); - public IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) where T : IRecurringComponent - => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime)); + public IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) where T : IRecurringComponent + => GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options)); private FreeBusy CombineFreeBusy(FreeBusy main, FreeBusy current) { diff --git a/Ical.Net/CalendarComponents/Alarm.cs b/Ical.Net/CalendarComponents/Alarm.cs index 166736313..d13801fbb 100644 --- a/Ical.Net/CalendarComponents/Alarm.cs +++ b/Ical.Net/CalendarComponents/Alarm.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using Ical.Net.DataTypes; +using Ical.Net.Evaluation; namespace Ical.Net.CalendarComponents; @@ -73,7 +74,7 @@ public Alarm() /// Gets a list of alarm occurrences for the given recurring component, /// that occur between and . /// - public virtual IList GetOccurrences(IRecurringComponent rc, CalDateTime? fromDate, CalDateTime? toDate) + public virtual IList GetOccurrences(IRecurringComponent rc, CalDateTime? fromDate, CalDateTime? toDate, EvaluationOptions options) { if (Trigger == null) { @@ -94,7 +95,7 @@ public virtual IList GetOccurrences(IRecurringComponent rc, Cal } Duration? duration = null; - foreach (var o in rc.GetOccurrences(fromDate, toDate)) + foreach (var o in rc.GetOccurrences(fromDate, toDate, options)) { var dt = o.Period.StartTime; if (string.Equals(Trigger.Related, TriggerRelation.End, TriggerRelation.Comparison)) @@ -143,7 +144,7 @@ public virtual IList GetOccurrences(IRecurringComponent rc, Cal /// The earliest date/time to poll trigered alarms for. /// /// A list of objects, each containing a triggered alarm. - public virtual IList Poll(CalDateTime start, CalDateTime end) + public virtual IList Poll(CalDateTime start, CalDateTime end, EvaluationOptions options = default) { var results = new List(); @@ -154,7 +155,7 @@ public virtual IList Poll(CalDateTime start, CalDateTime end) return results; } - results.AddRange(GetOccurrences(rc, start, end)); + results.AddRange(GetOccurrences(rc, start, end, options)); return results; } diff --git a/Ical.Net/CalendarComponents/CalendarEvent.cs b/Ical.Net/CalendarComponents/CalendarEvent.cs index f3c51cba2..bd8ff372b 100644 --- a/Ical.Net/CalendarComponents/CalendarEvent.cs +++ b/Ical.Net/CalendarComponents/CalendarEvent.cs @@ -251,9 +251,6 @@ private void Initialize() /// True if the event has not been cancelled, False otherwise. public virtual bool IsActive => !string.Equals(Status, EventStatus.Cancelled, EventStatus.Comparison); - /// - protected override bool EvaluationIncludesReferenceDate => true; - /// protected override void OnDeserializing(StreamingContext context) { @@ -280,7 +277,6 @@ protected bool Equals(CalendarEvent? other) && string.Equals(Status, other.Status, StringComparison.Ordinal) && IsActive == other.IsActive && string.Equals(Transparency, other.Transparency, TransparencyType.Comparison) - && EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate && Attachments.SequenceEqual(other.Attachments) && CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules) && CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules); diff --git a/Ical.Net/CalendarComponents/FreeBusy.cs b/Ical.Net/CalendarComponents/FreeBusy.cs index 8eeead7f2..5566986ae 100644 --- a/Ical.Net/CalendarComponents/FreeBusy.cs +++ b/Ical.Net/CalendarComponents/FreeBusy.cs @@ -7,20 +7,21 @@ using System.Collections.Generic; using System.Linq; using Ical.Net.DataTypes; +using Ical.Net.Evaluation; using Ical.Net.Utility; namespace Ical.Net.CalendarComponents; public class FreeBusy : UniqueComponent, IMergeable { - public static FreeBusy Create(ICalendarObject obj, FreeBusy freeBusyRequest) + public static FreeBusy Create(ICalendarObject obj, FreeBusy freeBusyRequest, EvaluationOptions options = default) { if (!(obj is IGetOccurrencesTyped)) { return null; } var getOccurrences = (IGetOccurrencesTyped) obj; - var occurrences = getOccurrences.GetOccurrences(freeBusyRequest.Start, freeBusyRequest.End); + var occurrences = getOccurrences.GetOccurrences(freeBusyRequest.Start, freeBusyRequest.End, options); var contacts = new List(); var isFilteredByAttendees = false; @@ -196,4 +197,4 @@ public virtual void MergeWith(IMergeable obj) Entries.AddRange(fb.Entries.Where(entry => !Entries.Contains(entry))); } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/Journal.cs b/Ical.Net/CalendarComponents/Journal.cs index 0cedc9d4a..ba7240929 100644 --- a/Ical.Net/CalendarComponents/Journal.cs +++ b/Ical.Net/CalendarComponents/Journal.cs @@ -27,8 +27,6 @@ public Journal() Name = JournalStatus.Name; } - protected override bool EvaluationIncludesReferenceDate => true; - protected override void OnDeserializing(StreamingContext context) { base.OnDeserializing(context); @@ -49,4 +47,4 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ base.GetHashCode(); return hashCode; } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/RecurringComponent.cs b/Ical.Net/CalendarComponents/RecurringComponent.cs index 60cb933f3..c9dbc38a3 100644 --- a/Ical.Net/CalendarComponents/RecurringComponent.cs +++ b/Ical.Net/CalendarComponents/RecurringComponent.cs @@ -28,8 +28,6 @@ public class RecurringComponent : UniqueComponent, IRecurringComponent public static IEnumerable SortByDate(IEnumerable list) => list.OrderBy(d => d); - protected virtual bool EvaluationIncludesReferenceDate => false; - public virtual IList Attachments { get => Properties.GetMany("ATTACH"); @@ -187,11 +185,11 @@ protected override void OnDeserializing(StreamingContext context) Initialize(); } - public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) - => RecurrenceUtil.GetOccurrences(this, startTime, endTime, EvaluationIncludesReferenceDate); + public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) + => RecurrenceUtil.GetOccurrences(this, startTime, endTime, options); - public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) - => RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), EvaluationIncludesReferenceDate); + public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) + => RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options); public virtual IList PollAlarms() => PollAlarms(null, null); diff --git a/Ical.Net/CalendarComponents/Todo.cs b/Ical.Net/CalendarComponents/Todo.cs index 78d447516..df52bd65d 100644 --- a/Ical.Net/CalendarComponents/Todo.cs +++ b/Ical.Net/CalendarComponents/Todo.cs @@ -146,7 +146,7 @@ public virtual bool IsCompleted(CalDateTime currDt) } // Evaluate to the previous occurrence. - var periods = _mEvaluator.EvaluateToPreviousOccurrence(Completed, currDt); + var periods = _mEvaluator.EvaluateToPreviousOccurrence(Completed, currDt, options: default); return periods.All(p => !p.StartTime.GreaterThan(Completed) || !currDt.GreaterThanOrEqual(p.StartTime)); } @@ -169,8 +169,6 @@ public virtual bool IsActive(CalDateTime currDt) /// True if the todo was cancelled, False otherwise. public virtual bool IsCancelled => string.Equals(Status, TodoStatus.Cancelled, TodoStatus.Comparison); - protected override bool EvaluationIncludesReferenceDate => true; - protected override void OnDeserializing(StreamingContext context) { //ToDo: a necessary evil, for now diff --git a/Ical.Net/DataTypes/PeriodList.cs b/Ical.Net/DataTypes/PeriodList.cs index b66fdf9f9..f43729691 100644 --- a/Ical.Net/DataTypes/PeriodList.cs +++ b/Ical.Net/DataTypes/PeriodList.cs @@ -34,11 +34,8 @@ internal class PeriodList : EncodableDataType, IList /// protected IList Periods { get; } = new List(); - // Also needed for the serialization factory public PeriodList() - { - SetService(new PeriodListEvaluator(this)); - } + { } /// /// Creates a new instance of the class from the . @@ -52,8 +49,6 @@ private PeriodList(StringReader value) { CopyFrom(deserialized); } - - SetService(new PeriodListEvaluator(this)); } /// diff --git a/Ical.Net/Evaluation/EvaluationException.cs b/Ical.Net/Evaluation/EvaluationException.cs new file mode 100644 index 000000000..5d592806a --- /dev/null +++ b/Ical.Net/Evaluation/EvaluationException.cs @@ -0,0 +1,14 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; + +namespace Ical.Net.Evaluation; + +/// +/// Represents an exception that may occur during calendar evaluation. +/// +public class EvaluationException : Exception +{ } diff --git a/Ical.Net/Evaluation/EvaluationLimitExceededException.cs b/Ical.Net/Evaluation/EvaluationLimitExceededException.cs new file mode 100644 index 000000000..f86fb1fb9 --- /dev/null +++ b/Ical.Net/Evaluation/EvaluationLimitExceededException.cs @@ -0,0 +1,14 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace Ical.Net.Evaluation; + +/// +/// Represents an exception will be raised during calendar evaluation when the maximum number of +/// increments is exceeded. +/// +/// +public class EvaluationLimitExceededException : EvaluationException +{ } diff --git a/Ical.Net/Evaluation/EvaluationOptions.cs b/Ical.Net/Evaluation/EvaluationOptions.cs new file mode 100644 index 000000000..da8bf5bb0 --- /dev/null +++ b/Ical.Net/Evaluation/EvaluationOptions.cs @@ -0,0 +1,20 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace Ical.Net.Evaluation; +public class EvaluationOptions +{ + /// + /// The maximum number of increments to evaluate without finding a recurrence before + /// evaluation is stopped exceptionally. If null, the evaluation will continue indefinitely. + /// + /// + /// This option only applies to the evaluation of RecurrencePatterns. + /// + /// If the specified number of increments is exceeded without finding a recurrence, an + /// exception of type will be thrown. + /// + public int? MaxUnmatchedIncrementsLimit { get; set; } +} diff --git a/Ical.Net/Evaluation/Evaluator.cs b/Ical.Net/Evaluation/Evaluator.cs index e07f04f9c..b3323fb93 100644 --- a/Ical.Net/Evaluation/Evaluator.cs +++ b/Ical.Net/Evaluation/Evaluator.cs @@ -56,5 +56,5 @@ protected void IncrementDate(ref CalDateTime dt, RecurrencePattern pattern, int public System.Globalization.Calendar Calendar { get; private set; } - public abstract IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults); + public abstract IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options); } diff --git a/Ical.Net/Evaluation/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index 040497225..a41b9cadb 100644 --- a/Ical.Net/Evaluation/EventEvaluator.cs +++ b/Ical.Net/Evaluation/EventEvaluator.cs @@ -40,12 +40,12 @@ public EventEvaluator(CalendarEvent evt) : base(evt) { } /// /// The beginning date of the range to evaluate. /// The end date of the range to evaluate. - /// + /// /// - public override IEnumerable Evaluate(CalDateTime referenceTime, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + public override IEnumerable Evaluate(CalDateTime referenceTime, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { // Evaluate recurrences normally - var periods = base.Evaluate(referenceTime, periodStart, periodEnd, includeReferenceDateInResults) + var periods = base.Evaluate(referenceTime, periodStart, periodEnd, options) .Select(WithEndTime); return periods; diff --git a/Ical.Net/Evaluation/IEvaluator.cs b/Ical.Net/Evaluation/IEvaluator.cs index 960ab5ac1..ea8eb09ff 100644 --- a/Ical.Net/Evaluation/IEvaluator.cs +++ b/Ical.Net/Evaluation/IEvaluator.cs @@ -37,10 +37,10 @@ public interface IEvaluator /// /// /// - /// + /// /// /// A sequence of objects for /// each date/time when this item occurs/recurs. /// - IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults); + IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options); } diff --git a/Ical.Net/Evaluation/PeriodListEvaluator.cs b/Ical.Net/Evaluation/PeriodListEvaluator.cs deleted file mode 100644 index edc2f1138..000000000 --- a/Ical.Net/Evaluation/PeriodListEvaluator.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright ical.net project maintainers and contributors. -// Licensed under the MIT license. -// - -#nullable enable -using System; -using System.Collections.Generic; -using Ical.Net.DataTypes; - -namespace Ical.Net.Evaluation; - -internal class PeriodListEvaluator : Evaluator -{ - private readonly PeriodList _mPeriodList; - - public PeriodListEvaluator(PeriodList rdt) - { - _mPeriodList = rdt; - } - - public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) - { - var periods = new SortedSet(); - - if (includeReferenceDateInResults) - { - var p = new Period(referenceDate); - periods.Add(p); - } - - if ((periodStart is not null) && (periodEnd is not null) && periodEnd.LessThan(periodStart)) - { - return periods; - } - - periods.UnionWith(_mPeriodList); - return periods; - } -} diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index 215993912..442a90546 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -13,8 +13,6 @@ namespace Ical.Net.Evaluation; public class RecurrencePatternEvaluator : Evaluator { - private const int MaxIncrementCount = 1000; - protected RecurrencePattern Pattern { get; set; } public RecurrencePatternEvaluator(RecurrencePattern pattern) @@ -103,7 +101,7 @@ private RecurrencePattern ProcessRecurrencePattern(CalDateTime referenceDate) /// the start dates returned should all be at 9:00AM, and not 12:19PM. /// private IEnumerable GetDates(CalDateTime seed, CalDateTime? periodStart, CalDateTime? periodEnd, int maxCount, RecurrencePattern pattern, - bool includeReferenceDateInResults) + EvaluationOptions options) { // In the first step, we work with DateTime values, so we need to convert the CalDateTime to DateTime var originalDate = seed; @@ -138,10 +136,10 @@ private IEnumerable GetDates(CalDateTime seed, CalDateTime? periodS // Do the enumeration in a separate method, as it is a generator method that is // only executed after enumeration started. In order to do most validation upfront, // do as many steps outside the generator as possible. - return EnumerateDates(originalDate, seedCopy, periodStartDt, periodEndDt, maxCount, pattern); + return EnumerateDates(originalDate, seedCopy, periodStartDt, periodEndDt, maxCount, pattern, options); } - private IEnumerable EnumerateDates(CalDateTime originalDate, CalDateTime intervalRefTime, CalDateTime? periodStart, CalDateTime? periodEnd, int maxCount, RecurrencePattern pattern) + private IEnumerable EnumerateDates(CalDateTime originalDate, CalDateTime intervalRefTime, CalDateTime? periodStart, CalDateTime? periodEnd, int maxCount, RecurrencePattern pattern, EvaluationOptions options) { var expandBehavior = RecurrenceUtil.GetExpandBehaviorList(pattern); @@ -206,10 +204,8 @@ private IEnumerable EnumerateDates(CalDateTime originalDate, CalDat else { noCandidateIncrementCount++; - if (noCandidateIncrementCount > MaxIncrementCount) - { - break; - } + if (noCandidateIncrementCount > options?.MaxUnmatchedIncrementsLimit) + throw new EvaluationLimitExceededException(); } IncrementDate(ref intervalRefTime, pattern, pattern.Interval); @@ -880,9 +876,9 @@ private static Period CreatePeriod(CalDateTime dateTime, CalDateTime referenceDa /// The reference date, i.e. DTSTART. /// Start (incl.) of the period occurrences are generated for. /// End (excl.) of the period occurrences are generated for. - /// Whether the referenceDate itself should be returned. Ignored as the reference data MUST equal the first occurrence of an RRULE. + /// /// - public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { if (Pattern.Frequency != FrequencyType.None && Pattern.Frequency < FrequencyType.Daily && !referenceDate.HasTime) { @@ -894,7 +890,7 @@ public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateT // Create a recurrence pattern suitable for use during evaluation. var pattern = ProcessRecurrencePattern(referenceDate); - var periodQuery = GetDates(referenceDate, periodStart, periodEnd, -1, pattern, includeReferenceDateInResults) + var periodQuery = GetDates(referenceDate, periodStart, periodEnd, -1, pattern, options) .Select(dt => CreatePeriod(dt, referenceDate)); if (pattern.Until is not null) diff --git a/Ical.Net/Evaluation/RecurrenceUtil.cs b/Ical.Net/Evaluation/RecurrenceUtil.cs index b3fe0546c..15e886f89 100644 --- a/Ical.Net/Evaluation/RecurrenceUtil.cs +++ b/Ical.Net/Evaluation/RecurrenceUtil.cs @@ -14,10 +14,10 @@ namespace Ical.Net.Evaluation; internal class RecurrenceUtil { - public static IEnumerable GetOccurrences(IRecurrable recurrable, CalDateTime dt, bool includeReferenceDateInResults) => GetOccurrences(recurrable, - new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1)), includeReferenceDateInResults); + public static IEnumerable GetOccurrences(IRecurrable recurrable, CalDateTime dt, EvaluationOptions options = default) => GetOccurrences(recurrable, + new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1)), options); - public static IEnumerable GetOccurrences(IRecurrable recurrable, CalDateTime periodStart, CalDateTime periodEnd, bool includeReferenceDateInResults) + public static IEnumerable GetOccurrences(IRecurrable recurrable, CalDateTime periodStart, CalDateTime periodEnd, EvaluationOptions options = default) { var evaluator = recurrable.GetService(typeof(IEvaluator)) as IEvaluator; if (evaluator == null || recurrable.Start == null) @@ -36,8 +36,7 @@ public static IEnumerable GetOccurrences(IRecurrable recurrable, Cal if (periodEnd != null) periodEnd = new CalDateTime(periodEnd.Date, periodEnd.Time, start.TzId); - var periods = evaluator.Evaluate(start, periodStart, periodEnd, - includeReferenceDateInResults); + var periods = evaluator.Evaluate(start, periodStart, periodEnd, options); var occurrences = from p in periods diff --git a/Ical.Net/Evaluation/RecurringEvaluator.cs b/Ical.Net/Evaluation/RecurringEvaluator.cs index 1f9c66225..1a43e61f2 100644 --- a/Ical.Net/Evaluation/RecurringEvaluator.cs +++ b/Ical.Net/Evaluation/RecurringEvaluator.cs @@ -29,7 +29,7 @@ public RecurringEvaluator(IRecurrable obj) /// The beginning date of the range to evaluate. /// The end date of the range to evaluate. /// - protected IEnumerable EvaluateRRule(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + protected IEnumerable EvaluateRRule(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { if (Recurrable.RecurrenceRules == null || !Recurrable.RecurrenceRules.Any()) return []; @@ -41,20 +41,13 @@ protected IEnumerable EvaluateRRule(CalDateTime referenceDate, CalDateTi { return Enumerable.Empty(); } - return ruleEvaluator.Evaluate(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); + return ruleEvaluator.Evaluate(referenceDate, periodStart, periodEnd, options); }) // Enumerate the outer sequence (not the inner sequences of periods themselves) now to ensure // the initialization code is run, including validation and error handling. // This way we receive validation errors early, not only when enumeration starts. .ToList(); //NOSONAR - deliberately enumerate here - - //Only add referenceDate if there are no RecurrenceRules defined - if (includeReferenceDateInResults && (Recurrable.RecurrenceRules == null || !Recurrable.RecurrenceRules.Any())) - { - periodsQueries.Add([new Period(referenceDate)]); - } - return periodsQueries.OrderedMergeMany(); } @@ -74,7 +67,7 @@ protected IEnumerable EvaluateRDate(CalDateTime referenceDate, CalDateTi /// /// The beginning date of the range to evaluate. /// The end date of the range to evaluate. - protected IEnumerable EvaluateExRule(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd) + protected IEnumerable EvaluateExRule(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { if (Recurrable.ExceptionRules == null || !Recurrable.ExceptionRules.Any()) return []; @@ -86,7 +79,7 @@ protected IEnumerable EvaluateExRule(CalDateTime referenceDate, CalDateT { return Enumerable.Empty(); } - return exRuleEvaluator.Evaluate(referenceDate, periodStart, periodEnd, false); + return exRuleEvaluator.Evaluate(referenceDate, periodStart, periodEnd, options); }) // Enumerate the outer sequence (not the inner sequences of periods themselves) now to ensure // the initialization code is run, including validation and error handling. @@ -109,18 +102,21 @@ protected IEnumerable EvaluateExDate(CalDateTime referenceDate, CalDateT return exDates; } - public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { - var rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, includeReferenceDateInResults); - //Only add referenceDate if there are no RecurrenceRules defined - if (includeReferenceDateInResults && (Recurrable.RecurrenceRules == null || !Recurrable.RecurrenceRules.Any())) - { - rruleOccurrences = rruleOccurrences.Append(new Period(referenceDate)); - } + IEnumerable rruleOccurrences; + + // Only add referenceDate if there are no RecurrenceRules defined. This is in line + // with RFC 5545 which requires DTSTART to match any RRULE. If it doesn't, the behaviour + // is undefined. It seems to be good practice not to return the referenceDate in this case. + if ((Recurrable.RecurrenceRules == null) || !Recurrable.RecurrenceRules.Any()) + rruleOccurrences = [new Period(referenceDate)]; + else + rruleOccurrences = EvaluateRRule(referenceDate, periodStart, periodEnd, options); var rdateOccurrences = EvaluateRDate(referenceDate, periodStart, periodEnd); - var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd); + var exRuleExclusions = EvaluateExRule(referenceDate, periodStart, periodEnd, options); var exDateExclusions = EvaluateExDate(referenceDate, periodStart, periodEnd); var periods = diff --git a/Ical.Net/Evaluation/TimeZoneInfoEvaluator.cs b/Ical.Net/Evaluation/TimeZoneInfoEvaluator.cs index 862e046df..316cbf23c 100644 --- a/Ical.Net/Evaluation/TimeZoneInfoEvaluator.cs +++ b/Ical.Net/Evaluation/TimeZoneInfoEvaluator.cs @@ -21,7 +21,7 @@ protected VTimeZoneInfo TimeZoneInfo public TimeZoneInfoEvaluator(IRecurrable tzi) : base(tzi) { } - public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { // Time zones must include an effective start date/time // and must provide an evaluator. @@ -29,7 +29,7 @@ public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateT return []; // Always include the reference date in the results - var periods = base.Evaluate(referenceDate, periodStart, periodEnd, true); + var periods = base.Evaluate(referenceDate, periodStart, periodEnd, options); return periods; } } diff --git a/Ical.Net/Evaluation/TodoEvaluator.cs b/Ical.Net/Evaluation/TodoEvaluator.cs index a1c014cea..66cc70f1e 100644 --- a/Ical.Net/Evaluation/TodoEvaluator.cs +++ b/Ical.Net/Evaluation/TodoEvaluator.cs @@ -19,7 +19,7 @@ public class TodoEvaluator : RecurringEvaluator public TodoEvaluator(Todo todo) : base(todo) { } - internal IEnumerable EvaluateToPreviousOccurrence(CalDateTime completedDate, CalDateTime currDt) + internal IEnumerable EvaluateToPreviousOccurrence(CalDateTime completedDate, CalDateTime currDt, EvaluationOptions options) { var beginningDate = completedDate.Copy(); @@ -44,7 +44,7 @@ internal IEnumerable EvaluateToPreviousOccurrence(CalDateTime completedD DetermineStartingRecurrence(Todo.ExceptionDates.GetAllDates(), ref beginningDate); - return Evaluate(Todo.Start, beginningDate, currDt.AddSeconds(1), true); + return Evaluate(Todo.Start, beginningDate, currDt.AddSeconds(1), options); } private static void DetermineStartingRecurrence(IEnumerable rdate, ref CalDateTime referenceDateTime) @@ -77,13 +77,13 @@ private void DetermineStartingRecurrence(RecurrencePattern recur, ref CalDateTim } } - public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, bool includeReferenceDateInResults) + public override IEnumerable Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options) { // TODO items can only recur if a start date is specified if (Todo.Start == null) return []; - return base.Evaluate(referenceDate, periodStart, periodEnd, includeReferenceDateInResults) + return base.Evaluate(referenceDate, periodStart, periodEnd, options) .Select(p => p); } } diff --git a/Ical.Net/IGetOccurrences.cs b/Ical.Net/IGetOccurrences.cs index ddc4f04a1..ce3c19a98 100644 --- a/Ical.Net/IGetOccurrences.cs +++ b/Ical.Net/IGetOccurrences.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; +using Ical.Net.Evaluation; namespace Ical.Net; @@ -19,9 +20,9 @@ public interface IGetOccurrences /// The starting date range /// The ending date range /// An IEnumerable that calculates and returns Periods representing the occurrences of this object in ascending order. - IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null); + IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default); - IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime); + IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default); } public interface IGetOccurrencesTyped : IGetOccurrences @@ -34,7 +35,7 @@ public interface IGetOccurrencesTyped : IGetOccurrences /// The starting date range. If set to null, occurrences are returned from the beginning. /// The ending date range. If set to null, occurrences are returned until the end. /// An IEnumerable that calculates and returns Periods representing the occurrences of this object in ascending order. - IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) where T : IRecurringComponent; + IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) where T : IRecurringComponent; - IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) where T : IRecurringComponent; + IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) where T : IRecurringComponent; } diff --git a/Ical.Net/VTimeZoneInfo.cs b/Ical.Net/VTimeZoneInfo.cs index 27150388f..4d6f0e5db 100644 --- a/Ical.Net/VTimeZoneInfo.cs +++ b/Ical.Net/VTimeZoneInfo.cs @@ -179,9 +179,9 @@ public virtual CalDateTime RecurrenceId set => Properties.Set("RECURRENCE-ID", value); } - public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null) - => RecurrenceUtil.GetOccurrences(this, startTime, endTime, true); + public virtual IEnumerable GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) + => RecurrenceUtil.GetOccurrences(this, startTime, endTime, options); - public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime) - => RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), true); + public virtual IEnumerable GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) + => RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options); }