Skip to content

Commit 4047773

Browse files
committed
Evaluation: Make 'MaxIncrementCount' configurable.
1 parent 79b5517 commit 4047773

19 files changed

+136
-79
lines changed

Ical.Net.Tests/RecurrenceTests.cs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using Ical.Net.Serialization;
1818
using Ical.Net.Serialization.DataTypes;
1919
using NUnit.Framework;
20+
using NUnit.Framework.Constraints;
2021

2122
namespace Ical.Net.Tests;
2223

@@ -1232,8 +1233,8 @@ public void WeekNoOrderingShouldNotMatter()
12321233
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"));
12331234
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"));
12341235

1235-
var recurringPeriods1 = rpe1.Evaluate(new CalDateTime(start), start, end).ToList();
1236-
var recurringPeriods2 = rpe2.Evaluate(new CalDateTime(start), start, end).ToList();
1236+
var recurringPeriods1 = rpe1.Evaluate(new CalDateTime(start), start, end, default).ToList();
1237+
var recurringPeriods2 = rpe2.Evaluate(new CalDateTime(start), start, end, default).ToList();
12371238

12381239
Assert.That(recurringPeriods2, Has.Count.EqualTo(recurringPeriods1.Count));
12391240
}
@@ -2627,7 +2628,7 @@ public void BugByWeekNoNotWorking()
26272628
var end = new CalDateTime(2019, 12, 31);
26282629
var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYWEEKNO=2"));
26292630

2630-
var recurringPeriods = rpe.Evaluate(start, start, end).ToList();
2631+
var recurringPeriods = rpe.Evaluate(start, start, end, default).ToList();
26312632

26322633
Assert.That(recurringPeriods, Has.Count.EqualTo(1));
26332634
Assert.That(recurringPeriods.First().StartTime, Is.EqualTo(new CalDateTime(2019, 1, 7)));
@@ -2643,7 +2644,7 @@ public void BugByMonthWhileFreqIsWeekly()
26432644
var end = new CalDateTime(2020, 12, 31);
26442645
var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1"));
26452646

2646-
var recurringPeriods = rpe.Evaluate(start, start, end).OrderBy(x => x).ToList();
2647+
var recurringPeriods = rpe.Evaluate(start, start, end, default).OrderBy(x => x).ToList();
26472648

26482649
Assert.That(recurringPeriods, Has.Count.EqualTo(4));
26492650
Assert.Multiple(() =>
@@ -2686,7 +2687,7 @@ public void BugByMonthWhileFreqIsMonthly()
26862687
var end = new CalDateTime(2020, 12, 31);
26872688
var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=MONTHLY;BYDAY=MO;BYMONTH=1"));
26882689

2689-
var recurringPeriods = rpe.Evaluate(start, start, end).OrderBy(x => x).ToList();
2690+
var recurringPeriods = rpe.Evaluate(start, start, end, default).OrderBy(x => x).ToList();
26902691

26912692
Assert.That(recurringPeriods, Has.Count.EqualTo(4));
26922693
Assert.Multiple(() =>
@@ -2710,7 +2711,7 @@ public void Bug3119920()
27102711
var serializer = new RecurrencePatternSerializer();
27112712
var rp = (RecurrencePattern)serializer.Deserialize(sr)!;
27122713
var rpe = new RecurrencePatternEvaluator(rp);
2713-
var recurringPeriods = rpe.Evaluate(start, start, rp.Until).ToList();
2714+
var recurringPeriods = rpe.Evaluate(start, start, rp.Until, default).ToList();
27142715

27152716
var period = recurringPeriods.ElementAt(recurringPeriods.Count - 1);
27162717

@@ -2932,7 +2933,7 @@ public void RecurrencePattern1()
29322933
var occurrences = evaluator.Evaluate(
29332934
startDate,
29342935
fromDate,
2935-
toDate)
2936+
toDate, default)
29362937
.OrderBy(o => o.StartTime)
29372938
.ToList();
29382939
Assert.That(occurrences, Has.Count.EqualTo(4));
@@ -2964,7 +2965,7 @@ public void RecurrencePattern2()
29642965
var occurrences = evaluator.Evaluate(
29652966
startDate,
29662967
fromDate,
2967-
toDate);
2968+
toDate, default);
29682969
Assert.That(occurrences.Count, Is.Not.EqualTo(0));
29692970
}
29702971

@@ -3071,7 +3072,7 @@ public void Test4()
30713072
var periods = evaluator.Evaluate(
30723073
evtStart,
30733074
evtStart,
3074-
evtEnd)
3075+
evtEnd, default)
30753076
.OrderBy(p => p.StartTime)
30763077
.ToList();
30773078
Assert.That(periods, Has.Count.EqualTo(10));
@@ -3905,7 +3906,7 @@ public void TestDtStartTimezone(string? tzId)
39053906
var cal = Calendar.Load(icalText);
39063907
var evt = cal.Events.First();
39073908
var ev = new EventEvaluator(evt);
3908-
var occurrences = ev.Evaluate(evt.DtStart, evt.DtStart.ToTimeZone(tzId), evt.DtStart.AddMinutes(61).ToTimeZone(tzId));
3909+
var occurrences = ev.Evaluate(evt.DtStart, evt.DtStart.ToTimeZone(tzId), evt.DtStart.AddMinutes(61).ToTimeZone(tzId), default);
39093910
var occurrencesStartTimes = occurrences.Select(x => x.StartTime).Take(2).ToList();
39103911

39113912
var expectedStartTimes = new[]
@@ -3916,4 +3917,35 @@ public void TestDtStartTimezone(string? tzId)
39163917

39173918
Assert.That(expectedStartTimes.SequenceEqual(occurrencesStartTimes), Is.True);
39183919
}
3920+
3921+
[Test]
3922+
[TestCase(null, false)]
3923+
[TestCase(0, true)]
3924+
[TestCase(1000, true)]
3925+
[TestCase(1440, false)]
3926+
public void TestMaxIncrementCount(int? limit, bool expectException)
3927+
{
3928+
var ical = """
3929+
BEGIN:VCALENDAR
3930+
BEGIN:VEVENT
3931+
DTSTART:20250305T000000
3932+
RRULE:FREQ=MINUTELY;BYHOUR=0;COUNT=100
3933+
END:VEVENT
3934+
END:VCALENDAR
3935+
""";
3936+
3937+
var cal = Calendar.Load(ical);
3938+
3939+
var options = new EvaluationOptions
3940+
{
3941+
MaxIncrementCount = limit,
3942+
};
3943+
3944+
IResolveConstraint constraint =
3945+
expectException
3946+
? Throws.Exception.TypeOf<MaxIncrementsExceededEvaluationException>()
3947+
: Throws.Nothing;
3948+
3949+
Assert.That(() => cal.GetOccurrences(options: options).ToList(), constraint);
3950+
}
39193951
}

Ical.Net/Calendar.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Text;
1212
using Ical.Net.CalendarComponents;
1313
using Ical.Net.DataTypes;
14+
using Ical.Net.Evaluation;
1415
using Ical.Net.Proxies;
1516
using Ical.Net.Serialization;
1617
using Ical.Net.Utility;
@@ -190,16 +191,16 @@ public VTimeZone AddTimeZone(VTimeZone tz)
190191
/// <param name="startTime">The beginning date/time of the range.</param>
191192
/// <param name="endTime">The end date/time of the range.</param>
192193
/// <returns>A list of occurrences that fall between the date/time arguments provided.</returns>
193-
public virtual IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null)
194-
=> GetOccurrences<IRecurringComponent>(startTime, endTime);
194+
public virtual IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default)
195+
=> GetOccurrences<IRecurringComponent>(startTime, endTime, options);
195196

196197
/// <inheritdoc cref="GetOccurrences(CalDateTime, CalDateTime)"/>
197-
public virtual IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime)
198-
=> GetOccurrences<IRecurringComponent>(startTime?.AsCalDateTime(), endTime?.AsCalDateTime());
198+
public virtual IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default)
199+
=> GetOccurrences<IRecurringComponent>(startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options);
199200

200201
/// <inheritdoc cref="GetOccurrences(CalDateTime, CalDateTime)"/>
201-
public virtual IEnumerable<Occurrence> GetOccurrences<T>(DateTime? startTime, DateTime? endTime) where T : IRecurringComponent
202-
=> GetOccurrences<T>(startTime?.AsCalDateTime(), endTime?.AsCalDateTime());
202+
public virtual IEnumerable<Occurrence> GetOccurrences<T>(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) where T : IRecurringComponent
203+
=> GetOccurrences<T>(startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options);
203204

204205
/// <summary>
205206
/// Returns all occurrences of components of type T that start within the date range provided.
@@ -208,7 +209,7 @@ public virtual IEnumerable<Occurrence> GetOccurrences<T>(DateTime? startTime, Da
208209
/// </summary>
209210
/// <param name="startTime">The starting date range</param>
210211
/// <param name="endTime">The ending date range</param>
211-
public virtual IEnumerable<Occurrence> GetOccurrences<T>(CalDateTime startTime = null, CalDateTime endTime = null) where T : IRecurringComponent
212+
public virtual IEnumerable<Occurrence> GetOccurrences<T>(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) where T : IRecurringComponent
212213
{
213214
// These are the UID/RECURRENCE-ID combinations that replace other occurrences.
214215
var recurrenceIdsAndUids = this.Children.OfType<IRecurrable>()
@@ -219,7 +220,7 @@ public virtual IEnumerable<Occurrence> GetOccurrences<T>(CalDateTime startTime =
219220

220221
var occurrences = RecurringItems
221222
.OfType<T>()
222-
.Select(recurrable => recurrable.GetOccurrences(startTime, endTime))
223+
.Select(recurrable => recurrable.GetOccurrences(startTime, endTime, options))
223224

224225
// Enumerate the list of occurrences (not the occurrences themselves) now to ensure
225226
// the initialization code is run, including validation and error handling.

Ical.Net/CalendarCollection.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
using System.Text;
1111
using Ical.Net.CalendarComponents;
1212
using Ical.Net.DataTypes;
13+
using Ical.Net.Evaluation;
1314
using Ical.Net.Serialization;
1415
using Ical.Net.Utility;
16+
using static NodaTime.TimeZones.ZoneEqualityComparer;
1517

1618
namespace Ical.Net;
1719

@@ -55,17 +57,17 @@ private IEnumerable<Occurrence> GetOccurrences(Func<Calendar, IEnumerable<Occurr
5557
// being ordered to avoid full enumeration.
5658
.OrderedMergeMany();
5759

58-
public IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null)
59-
=> GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime));
60+
public IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default)
61+
=> GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options));
6062

61-
public IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime)
62-
=> GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime));
63+
public IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default)
64+
=> GetOccurrences(iCal => iCal.GetOccurrences(startTime, endTime, options));
6365

64-
public IEnumerable<Occurrence> GetOccurrences<T>(CalDateTime startTime = null, CalDateTime endTime = null) where T : IRecurringComponent
65-
=> GetOccurrences(iCal => iCal.GetOccurrences<T>(startTime, endTime));
66+
public IEnumerable<Occurrence> GetOccurrences<T>(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default) where T : IRecurringComponent
67+
=> GetOccurrences(iCal => iCal.GetOccurrences<T>(startTime, endTime, options));
6668

67-
public IEnumerable<Occurrence> GetOccurrences<T>(DateTime? startTime, DateTime? endTime) where T : IRecurringComponent
68-
=> GetOccurrences(iCal => iCal.GetOccurrences<T>(startTime, endTime));
69+
public IEnumerable<Occurrence> GetOccurrences<T>(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default) where T : IRecurringComponent
70+
=> GetOccurrences(iCal => iCal.GetOccurrences<T>(startTime, endTime, options));
6971

7072
private FreeBusy CombineFreeBusy(FreeBusy main, FreeBusy current)
7173
{

Ical.Net/CalendarComponents/Alarm.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System;
88
using System.Collections.Generic;
99
using Ical.Net.DataTypes;
10+
using Ical.Net.Evaluation;
1011

1112
namespace Ical.Net.CalendarComponents;
1213

@@ -73,7 +74,7 @@ public Alarm()
7374
/// Gets a list of alarm occurrences for the given recurring component, <paramref name="rc"/>
7475
/// that occur between <paramref name="fromDate"/> and <paramref name="toDate"/>.
7576
/// </summary>
76-
public virtual IList<AlarmOccurrence> GetOccurrences(IRecurringComponent rc, CalDateTime? fromDate, CalDateTime? toDate)
77+
public virtual IList<AlarmOccurrence> GetOccurrences(IRecurringComponent rc, CalDateTime? fromDate, CalDateTime? toDate, EvaluationOptions options)
7778
{
7879
if (Trigger == null)
7980
{
@@ -94,7 +95,7 @@ public virtual IList<AlarmOccurrence> GetOccurrences(IRecurringComponent rc, Cal
9495
}
9596

9697
Duration? duration = null;
97-
foreach (var o in rc.GetOccurrences(fromDate, toDate))
98+
foreach (var o in rc.GetOccurrences(fromDate, toDate, options))
9899
{
99100
var dt = o.Period.StartTime;
100101
if (string.Equals(Trigger.Related, TriggerRelation.End, TriggerRelation.Comparison))
@@ -143,7 +144,7 @@ public virtual IList<AlarmOccurrence> GetOccurrences(IRecurringComponent rc, Cal
143144
/// <param name="start">The earliest date/time to poll trigered alarms for.</param>
144145
/// <param name="end"></param>
145146
/// <returns>A list of <see cref="AlarmOccurrence"/> objects, each containing a triggered alarm.</returns>
146-
public virtual IList<AlarmOccurrence> Poll(CalDateTime start, CalDateTime end)
147+
public virtual IList<AlarmOccurrence> Poll(CalDateTime start, CalDateTime end, EvaluationOptions options = default)
147148
{
148149
var results = new List<AlarmOccurrence>();
149150

@@ -154,7 +155,7 @@ public virtual IList<AlarmOccurrence> Poll(CalDateTime start, CalDateTime end)
154155
return results;
155156
}
156157

157-
results.AddRange(GetOccurrences(rc, start, end));
158+
results.AddRange(GetOccurrences(rc, start, end, options));
158159
return results;
159160
}
160161

Ical.Net/CalendarComponents/FreeBusy.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@
77
using System.Collections.Generic;
88
using System.Linq;
99
using Ical.Net.DataTypes;
10+
using Ical.Net.Evaluation;
1011
using Ical.Net.Utility;
1112

1213
namespace Ical.Net.CalendarComponents;
1314

1415
public class FreeBusy : UniqueComponent, IMergeable
1516
{
16-
public static FreeBusy Create(ICalendarObject obj, FreeBusy freeBusyRequest)
17+
public static FreeBusy Create(ICalendarObject obj, FreeBusy freeBusyRequest, EvaluationOptions options = default)
1718
{
1819
if (!(obj is IGetOccurrencesTyped))
1920
{
2021
return null;
2122
}
2223
var getOccurrences = (IGetOccurrencesTyped) obj;
23-
var occurrences = getOccurrences.GetOccurrences<CalendarEvent>(freeBusyRequest.Start, freeBusyRequest.End);
24+
var occurrences = getOccurrences.GetOccurrences<CalendarEvent>(freeBusyRequest.Start, freeBusyRequest.End, options);
2425
var contacts = new List<string>();
2526
var isFilteredByAttendees = false;
2627

@@ -196,4 +197,4 @@ public virtual void MergeWith(IMergeable obj)
196197

197198
Entries.AddRange(fb.Entries.Where(entry => !Entries.Contains(entry)));
198199
}
199-
}
200+
}

Ical.Net/CalendarComponents/RecurringComponent.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ protected override void OnDeserializing(StreamingContext context)
185185
Initialize();
186186
}
187187

188-
public virtual IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null)
189-
=> RecurrenceUtil.GetOccurrences(this, startTime, endTime);
188+
public virtual IEnumerable<Occurrence> GetOccurrences(CalDateTime startTime = null, CalDateTime endTime = null, EvaluationOptions options = default)
189+
=> RecurrenceUtil.GetOccurrences(this, startTime, endTime, options);
190190

191-
public virtual IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime)
192-
=> RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime());
191+
public virtual IEnumerable<Occurrence> GetOccurrences(DateTime? startTime, DateTime? endTime, EvaluationOptions options = default)
192+
=> RecurrenceUtil.GetOccurrences(this, startTime?.AsCalDateTime(), endTime?.AsCalDateTime(), options);
193193

194194
public virtual IList<AlarmOccurrence> PollAlarms() => PollAlarms(null, null);
195195

Ical.Net/CalendarComponents/Todo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public virtual bool IsCompleted(CalDateTime currDt)
146146
}
147147

148148
// Evaluate to the previous occurrence.
149-
var periods = _mEvaluator.EvaluateToPreviousOccurrence(Completed, currDt);
149+
var periods = _mEvaluator.EvaluateToPreviousOccurrence(Completed, currDt, options: default);
150150

151151
return periods.All(p => !p.StartTime.GreaterThan(Completed) || !currDt.GreaterThanOrEqual(p.StartTime));
152152
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Copyright ical.net project maintainers and contributors.
3+
// Licensed under the MIT license.
4+
//
5+
6+
namespace Ical.Net.Evaluation;
7+
public class EvaluationOptions
8+
{
9+
/// <summary>
10+
/// The maximum number of increments to evaluate without finding a recurrence before
11+
/// evaluation is stopped exceptionally. If null, the evaluation will continue indefinitely.
12+
/// </summary>
13+
/// <remarks>
14+
/// This option only applies to the evaluation of RecurrencePatterns.
15+
///
16+
/// If the specified number of increments is exceeded without finding a recurrence, an
17+
/// exception of type <see cref="Ical.Net.Evaluation.MaxIncrementsExceededEvaluationException"/> will be thrown.
18+
/// </remarks>
19+
public int? MaxIncrementCount { get; set; }
20+
}

Ical.Net/Evaluation/Evaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ protected void IncrementDate(ref CalDateTime dt, RecurrencePattern pattern, int
5656

5757
public System.Globalization.Calendar Calendar { get; private set; }
5858

59-
public abstract IEnumerable<Period> Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd);
59+
public abstract IEnumerable<Period> Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options);
6060
}

Ical.Net/Evaluation/EventEvaluator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ public EventEvaluator(CalendarEvent evt) : base(evt) { }
4040
/// <param name="referenceTime"></param>
4141
/// <param name="periodStart">The beginning date of the range to evaluate.</param>
4242
/// <param name="periodEnd">The end date of the range to evaluate.</param>
43-
/// <param name="includeReferenceDateInResults"></param>
43+
/// <param name="options"></param>
4444
/// <returns></returns>
45-
public override IEnumerable<Period> Evaluate(CalDateTime referenceTime, CalDateTime? periodStart, CalDateTime? periodEnd)
45+
public override IEnumerable<Period> Evaluate(CalDateTime referenceTime, CalDateTime? periodStart, CalDateTime? periodEnd, EvaluationOptions options)
4646
{
4747
// Evaluate recurrences normally
48-
var periods = base.Evaluate(referenceTime, periodStart, periodEnd)
48+
var periods = base.Evaluate(referenceTime, periodStart, periodEnd, options)
4949
.Select(WithEndTime);
5050

5151
return periods;

0 commit comments

Comments
 (0)