Skip to content

Commit c7fd686

Browse files
authored
Refactor handling of FREQ in recurrence pattern (#789)
* Resolves #788 * Update xmldoc for RecurrencePattern.Interval * Add range check for the setter of RecurrencePattern.Frequency
1 parent 19cf410 commit c7fd686

File tree

6 files changed

+65
-39
lines changed

6 files changed

+65
-39
lines changed

Ical.Net.Tests/RecurrenceTests.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3010,20 +3010,18 @@ public void GetOccurrences1()
30103010
}
30113011

30123012
[Test, Category("Recurrence")]
3013-
public void Test1()
3013+
public void TryingToSetInvalidFrequency_ShouldThrow()
30143014
{
3015-
var cal = new Calendar();
3016-
var evt = cal.Create<CalendarEvent>();
3017-
evt.Summary = "Event summary";
3018-
evt.Start = new CalDateTime(DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc));
3019-
3020-
var recur = new RecurrencePattern();
3021-
evt.RecurrenceRules.Add(recur);
3022-
3023-
Assert.That(() =>
3015+
Assert.Multiple(() =>
30243016
{
3025-
_ = evt.GetOccurrences(CalDateTime.Today.AddDays(1)).TakeUntil(CalDateTime.Today.AddDays(2));
3026-
}, Throws.Exception, "An exception should be thrown when evaluating a recurrence with no specified FREQUENCY");
3017+
// Using the constructor
3018+
Assert.That(() => _ = new RecurrencePattern((FrequencyType) int.MaxValue, 1),
3019+
Throws.TypeOf<ArgumentOutOfRangeException>());
3020+
3021+
// Using the property
3022+
Assert.That(() => _ = new RecurrencePattern {Frequency = (FrequencyType) 9876543 },
3023+
Throws.TypeOf<ArgumentOutOfRangeException>());
3024+
});
30273025
}
30283026

30293027
[Test, Category("Recurrence")]
@@ -4070,11 +4068,11 @@ public void Recurrence_RRULE_Without_Freq_Should_Throw()
40704068
}
40714069

40724070
[Test]
4073-
public void Recurrence_RRULE_With_Freq_None_Should_Throw()
4071+
public void Recurrence_RRULE_With_Freq_Undefined_Should_Throw()
40744072
{
40754073
var serializer = new RecurrencePatternSerializer();
40764074

4077-
Assert.That(() => serializer.Deserialize(new StringReader("FREQ=NONE;INTERVAL=2;UNTIL=20250430T000000Z")), Throws.TypeOf<ArgumentOutOfRangeException>());
4075+
Assert.That(() => serializer.Deserialize(new StringReader("FREQ=UNDEFINED;INTERVAL=2;UNTIL=20250430T000000Z")), Throws.TypeOf<ArgumentOutOfRangeException>());
40784076
}
40794077

40804078
[Test]

Ical.Net/Constants.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ public enum FreeBusyStatus
176176

177177
public enum FrequencyType
178178
{
179-
None,
180179
Secondly,
181180
Minutely,
182181
Hourly,

Ical.Net/DataTypes/RecurrencePattern.cs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Collections.Generic;
88
using System.IO;
99
using System.Linq;
10-
using Ical.Net.Evaluation;
1110
using Ical.Net.Serialization.DataTypes;
1211
using Ical.Net.Utility;
1312

@@ -20,10 +19,30 @@ namespace Ical.Net.DataTypes;
2019
public class RecurrencePattern : EncodableDataType
2120
{
2221
private int? _interval;
22+
private FrequencyType _frequency;
2323
private CalDateTime? _until;
2424

25-
public FrequencyType Frequency { get; set; }
25+
/// <summary>
26+
/// Specifies the frequency <i>FREQ</i> of the recurrence.
27+
/// The default value is <see cref="FrequencyType.Yearly"/>.
28+
/// </summary>
29+
public FrequencyType Frequency
30+
{
31+
get => _frequency;
32+
set
33+
{
34+
if (!Enum.IsDefined(typeof(FrequencyType), value))
35+
{
36+
throw new ArgumentOutOfRangeException(nameof(Frequency), $"Invalid FrequencyType '{value}'.");
37+
}
38+
_frequency = value;
39+
}
40+
}
2641

42+
/// <summary>
43+
/// Specifies the end date of the recurrence (optional).
44+
/// This property <b>must be null</b> if the <see cref="Count"/> property is set.
45+
/// </summary>
2746
public CalDateTime? Until
2847
{
2948
get => _until;
@@ -37,13 +56,21 @@ public CalDateTime? Until
3756
}
3857
}
3958

59+
/// <summary>
60+
/// Specifies the number of occurrences of the recurrence (optional).
61+
/// This property <b>must be null</b> if the <see cref="Until"/> property is set.
62+
/// </summary>
4063
public int? Count { get; set; }
4164

65+
4266
/// <summary>
43-
/// Specifies how often the recurrence should repeat.
44-
/// - 1 = every
45-
/// - 2 = every second
46-
/// - 3 = every third
67+
/// The INTERVAL rule part contains a positive integer representing at
68+
/// which intervals the recurrence rule repeats. The default value is
69+
/// 1, meaning every second for a SECONDLY rule, every minute for a
70+
/// MINUTELY rule, every hour for an HOURLY rule, every day for a
71+
/// DAILY rule, every week for a WEEKLY rule, every month for a
72+
/// MONTHLY rule, and every year for a YEARLY rule. For example,
73+
/// within a DAILY rule, a value of 8 means every eight days.
4774
/// </summary>
4875
public int Interval
4976
{
@@ -88,14 +115,21 @@ public int Interval
88115

89116
public DayOfWeek FirstDayOfWeek { get; set; } = DayOfWeek.Monday;
90117

118+
/// <summary>
119+
/// Default constructor. Sets the <see cref="Frequency"/> to <see cref="FrequencyType.Yearly"/>
120+
/// and <see cref="Interval"/> to 1.
121+
/// </summary>
91122
public RecurrencePattern()
92-
{ }
123+
{
124+
Frequency = FrequencyType.Yearly;
125+
Interval = 1;
126+
}
93127

94128
public RecurrencePattern(FrequencyType frequency) : this(frequency, 1) { }
95129

96130
public RecurrencePattern(FrequencyType frequency, int interval) : this()
97131
{
98-
Frequency = frequency;
132+
Frequency = frequency; // for proper validation don't use the backing field
99133
Interval = interval;
100134
}
101135

Ical.Net/Evaluation/Evaluator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ protected void IncrementDate(ref CalDateTime dt, RecurrencePattern pattern, int
4343
case FrequencyType.Yearly:
4444
dt = old.AddDays(-old.DayOfYear + 1).AddYears(interval);
4545
break;
46-
// FIXME: use a more specific exception.
4746
default:
48-
throw new Exception("FrequencyType.NONE cannot be evaluated. Please specify a FrequencyType before evaluating the recurrence.");
47+
// Frequency should always be valid at this stage.
48+
System.Diagnostics.Debug.Fail($"'{pattern.Frequency}' as RecurrencePattern.Frequency is not implemented.");
49+
break;
4950
}
5051
}
5152
catch (ArgumentOutOfRangeException)

Ical.Net/Evaluation/RecurrencePatternEvaluator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ private static Period CreatePeriod(CalDateTime dateTime, CalDateTime referenceDa
759759
/// <returns></returns>
760760
public override IEnumerable<Period> Evaluate(CalDateTime referenceDate, CalDateTime? periodStart, EvaluationOptions? options)
761761
{
762-
if (Pattern.Frequency != FrequencyType.None && Pattern.Frequency < FrequencyType.Daily && !referenceDate.HasTime)
762+
if (Pattern.Frequency < FrequencyType.Daily && !referenceDate.HasTime)
763763
{
764764
// This case is not defined by RFC 5545. We handle it by evaluating the rule
765765
// as if referenceDate had a time (i.e. set to midnight).

Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,11 @@ private static void SerializeByValue(List<string> aggregate, IList<int> byValue,
112112

113113
// Push the recurrence pattern onto the serialization stack
114114
SerializationContext.Push(recur);
115-
var values = new List<string>()
115+
var values = new List<string>
116116
{
117-
$"FREQ={Enum.GetName(typeof(FrequencyType), recur.Frequency)?.ToUpper()}"
117+
$"FREQ={recur.Frequency.ToString().ToUpper()}"
118118
};
119119

120-
121120
//-- FROM RFC2445 --
122121
//The INTERVAL rule part contains a positive integer representing how
123122
//often the recurrence rule repeats. The default value is "1", meaning
@@ -243,26 +242,21 @@ private void DeserializePattern(string value, RecurrencePattern r, ISerializerFa
243242
ProcessKeyValuePair(keyValues[0].ToLower(), keyValues[1], r, factory);
244243
}
245244

246-
if (!freqPartExists || r.Frequency == FrequencyType.None)
245+
if (!freqPartExists)
247246
{
248247
throw new ArgumentOutOfRangeException(nameof(value),
249-
"The recurrence rule must specify a FREQ part that is not NONE.");
248+
"The recurrence rule must specify a valid FREQ part.");
250249
}
251250
CheckMutuallyExclusive("COUNT", "UNTIL", r.Count, r.Until);
252251
CheckRanges(r);
253252
}
254253

255254
private void ProcessKeyValuePair(string key, string value, RecurrencePattern r, ISerializerFactory factory)
256255
{
257-
if (SerializationContext == null)
258-
{
259-
throw new InvalidOperationException("SerializationContext is not set.");
260-
}
261-
262256
switch (key)
263257
{
264-
case "freq":
265-
r.Frequency = (FrequencyType) Enum.Parse(typeof(FrequencyType), value, true);
258+
case "freq" when Enum.TryParse(value, true, out FrequencyType freq):
259+
r.Frequency = freq;
266260
break;
267261

268262
case "until":
@@ -320,7 +314,7 @@ private void ProcessKeyValuePair(string key, string value, RecurrencePattern r,
320314

321315
default:
322316
throw new ArgumentOutOfRangeException(nameof(key),
323-
$"The recurrence rule part '{key}' is not supported.");
317+
$"The recurrence rule part '{key}' or its value {value} is not supported.");
324318
}
325319
}
326320

0 commit comments

Comments
 (0)