diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index a17ab58ce..695e855d1 100644 --- a/Ical.Net.Benchmarks/OccurencePerfTests.cs +++ b/Ical.Net.Benchmarks/OccurencePerfTests.cs @@ -7,7 +7,6 @@ using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using System; -using System.Collections.Generic; using System.Linq; namespace Ical.Net.Benchmarks; @@ -66,10 +65,7 @@ private static Calendar GenerateCalendarWithRecurrences() { Start = new CalDateTime(2025, 3, 1), End = null, - RecurrenceRules = new List - { - new RecurrencePattern(FrequencyType.Daily, 1) { Count = 1000 } - } + RecurrenceRule = new(FrequencyType.Daily, 1) { Count = 1000 } }; calendar.Events.Add(dailyEvent); return calendar; @@ -96,7 +92,7 @@ private static Calendar GetFourCalendarEventsWithUntilRule() { Start = startTime.AddMinutes(5).ToTimeZone(tzid), End = startTime.AddMinutes(10).ToTimeZone(tzid), - RecurrenceRules = new List { rrule }, + RecurrenceRule = rrule, }; startTime = startTime.Add(Duration.FromTimeSpanExact(interval)); return e; @@ -160,7 +156,7 @@ private static Calendar GetFourCalendarEventsWithCountRule() { Start = new CalDateTime(startTime.AddMinutes(5), tzid), End = new CalDateTime(startTime.AddMinutes(10), tzid), - RecurrenceRules = new List { rrule }, + RecurrenceRule = rrule, }; startTime = startTime.Add(interval); return e; diff --git a/Ical.Net.Benchmarks/SerializationPerfTests.cs b/Ical.Net.Benchmarks/SerializationPerfTests.cs index 34d9c98b3..094046e4b 100644 --- a/Ical.Net.Benchmarks/SerializationPerfTests.cs +++ b/Ical.Net.Benchmarks/SerializationPerfTests.cs @@ -8,7 +8,6 @@ using Ical.Net.DataTypes; using Ical.Net.Serialization; using System; -using System.Collections.Generic; using System.Linq; namespace Ical.Net.Benchmarks; @@ -83,12 +82,9 @@ private static Calendar CreateSimpleCalendar() { Start = new CalDateTime(DateTime.Now, timeZoneId), End = new CalDateTime(DateTime.Now + TimeSpan.FromHours(1), timeZoneId), - RecurrenceRules = new List + RecurrenceRule = new(FrequencyType.Daily, 1) { - new RecurrencePattern(FrequencyType.Daily, 1) - { - Count = 100, - } + Count = 100, } }; diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 2ae019098..f69f640d2 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -32,7 +32,7 @@ private static CalendarEvent GetEventWithRecurrenceRules(string tzId) { Start = new CalDateTime(_now, tzId), End = new CalDateTime(_later, tzId), - RecurrenceRules = new List { dailyForFiveDays }, + RecurrenceRule = dailyForFiveDays, Resources = new List(new[] { "Foo", "Bar", "Baz" }), }; return calendarEvent; diff --git a/Ical.Net.Tests/DocumentationExamples.cs b/Ical.Net.Tests/DocumentationExamples.cs index cb2e350dc..62c9bd04f 100644 --- a/Ical.Net.Tests/DocumentationExamples.cs +++ b/Ical.Net.Tests/DocumentationExamples.cs @@ -32,7 +32,7 @@ public void Daily_Test() Until = new CalDateTime("20160731T235959") }; - vEvent.RecurrenceRules = new List { recurrenceRule }; + vEvent.RecurrenceRule = recurrenceRule; var calendar = new Calendar(); calendar.Events.Add(vEvent); @@ -53,14 +53,13 @@ public void EveryOtherTuesdayUntilTheEndOfTheYear_Test() { DtStart = new CalDateTime(DateTime.Parse("2016-07-05T07:00", CultureInfo.InvariantCulture)), DtEnd = new CalDateTime(DateTime.Parse("2016-07-05T08:00",CultureInfo.InvariantCulture)), - }; - // Recurring every other Tuesday until Dec 31 - var rrule = new RecurrencePattern(FrequencyType.Weekly, 2) - { - Until = new CalDateTime("20161231T115959") + // Recurring every other Tuesday until Dec 31 + RecurrenceRule = new(FrequencyType.Weekly, 2) + { + Until = new CalDateTime("20161231T115959") + } }; - vEvent.RecurrenceRules = new List { rrule }; // Count every other Tuesday between July 1 and Dec 31. // The first Tuesday is July 5. There should be 13 in total @@ -80,17 +79,16 @@ public void FourthThursdayOfNovember_Tests() { DtStart = new CalDateTime(DateTime.Parse("2000-11-23T07:00", CultureInfo.InvariantCulture)), DtEnd = new CalDateTime(DateTime.Parse("2000-11-23T19:00", CultureInfo.InvariantCulture)), - }; - // Recurring every other Tuesday until Dec 31 - var rrule = new RecurrencePattern(FrequencyType.Yearly, 1) - { - Frequency = FrequencyType.Yearly, - Interval = 1, - ByMonth = new List { 11 }, - ByDay = new List { new WeekDay { DayOfWeek = DayOfWeek.Thursday, Offset = 4 } }, + // Recurring every other Tuesday until Dec 31 + RecurrenceRule = new(FrequencyType.Yearly, 1) + { + Frequency = FrequencyType.Yearly, + Interval = 1, + ByMonth = [11], + ByDay = [new WeekDay { DayOfWeek = DayOfWeek.Thursday, Offset = 4 }], + } }; - vEvent.RecurrenceRules = new List { rrule }; var searchStart = new CalDateTime(2000, 01, 01); var searchEnd = new CalDateTime(2017, 01, 01); @@ -111,7 +109,7 @@ public void DailyExceptSunday_Test() { DtStart = new CalDateTime(DateTime.Parse("2016-01-01T07:00", CultureInfo.InvariantCulture)), DtEnd = new CalDateTime(DateTime.Parse("2016-12-31T08:00", CultureInfo.InvariantCulture)), - RecurrenceRules = new List { new RecurrencePattern(FrequencyType.Daily, 1) }, + RecurrenceRule = new(FrequencyType.Daily, 1), }; //Define the exceptions: Sunday diff --git a/Ical.Net.Tests/MatchTimeZoneTests.cs b/Ical.Net.Tests/MatchTimeZoneTests.cs index a3dc9a62a..46dc215b2 100644 --- a/Ical.Net.Tests/MatchTimeZoneTests.cs +++ b/Ical.Net.Tests/MatchTimeZoneTests.cs @@ -35,7 +35,7 @@ public void MatchTimeZone_LocalTimeUsaWithTimeZone() var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = new CalDateTime(2023, 11, 05, 13, 00, 00, CalDateTime.UtcTzId); var occurrences = evt.GetOccurrences(new CalDateTime(2023, 11, 01)).TakeWhileBefore(new CalDateTime(2023, 11, 06)); @@ -81,7 +81,7 @@ public void MatchTimeZone_LocalTimeAustraliaWithTimeZone(string inputUntil, int var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = DateTime.ParseExact(inputUntil, "yyyyMMddTHHmmssZ", System.Globalization.CultureInfo.InvariantCulture, @@ -130,7 +130,7 @@ public void MatchTimeZone_UTCTime() var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = new CalDateTime(2023, 11, 05, 09, 00, 00, CalDateTime.UtcTzId); var occurrences = evt.GetOccurrences(new CalDateTime(2023, 11, 01)).TakeWhileBefore(new CalDateTime(2023, 11, 06)); @@ -163,7 +163,7 @@ public void MatchTimeZone_FloatingTime() var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = new CalDateTime(2023, 11, 05, 09, 00, 00, null); var occurrences = evt.GetOccurrences(new CalDateTime(2023, 11, 01)).TakeWhileBefore(new CalDateTime(2023, 11, 06)); @@ -197,7 +197,7 @@ public void MatchTimeZone_LocalTimeNoTimeZone() var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = new CalDateTime(2023, 11, 05, 09, 00, 00, null); var occurrences = evt.GetOccurrences(new CalDateTime(2023, 11, 01)).TakeWhileBefore(new CalDateTime(2023, 11, 06)); @@ -230,7 +230,7 @@ public void MatchTimeZone_DateOnly() var calendar = Calendar.Load(ical); var evt = calendar.Events.First(); - var until = evt.RecurrenceRules.First().Until; + var until = evt.RecurrenceRule.Until; var expectedUntil = new CalDateTime(2023, 11, 05); var occurrences = evt.GetOccurrences(new CalDateTime(2023, 11, 01)).TakeWhileBefore(new CalDateTime(2023, 11, 06)); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 154eecd9d..37dee2ec7 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -2662,17 +2662,14 @@ public void ReccurencePattern_MaxDate_StopsOnCount() var evt = new CalendarEvent { Start = new CalDateTime(2018, 1, 1, 12, 0, 0), - Duration = Duration.FromHours(1) - }; - - var pattern = new RecurrencePattern - { - Frequency = FrequencyType.Daily, - Count = 10 + Duration = Duration.FromHours(1), + RecurrenceRule = new() + { + Frequency = FrequencyType.Daily, + Count = 10 + } }; - evt.RecurrenceRules.Add(pattern); - var occurrences = evt.GetOccurrences(new CalDateTime(2018, 1, 1)) .TakeWhileBefore(new CalDateTime(DateTime.MaxValue, false)).ToList(); Assert.That(occurrences, Has.Count.EqualTo(10), "There should be 10 occurrences of this event."); @@ -2709,7 +2706,7 @@ public void Bug3119920() { using var sr = new StringReader("FREQ=WEEKLY;UNTIL=20251126T120000;INTERVAL=1;BYDAY=MO"); var start = new CalDateTime(2010, 11, 27, 9, 0, 0); - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); var rp = (RecurrencePattern) serializer.Deserialize(sr)!; var rpe = new RecurrencePatternEvaluator(rp); var recurringPeriods = rpe.Evaluate(start, start, null) @@ -2731,19 +2728,16 @@ public void Bug3178652() { Start = new CalDateTime(2011, 1, 29, 11, 0, 0), Duration = Duration.FromMinutes(90), - Summary = "29th February Test" - }; - - var pattern = new RecurrencePattern - { - Frequency = FrequencyType.Monthly, - Until = new CalDateTime(2011, 12, 25, 0, 0, 0, CalDateTime.UtcTzId), - FirstDayOfWeek = DayOfWeek.Sunday, - ByMonthDay = new List(new[] { 29 }) + Summary = "29th February Test", + RecurrenceRule = new() + { + Frequency = FrequencyType.Monthly, + Until = new CalDateTime(2011, 12, 25, 0, 0, 0, CalDateTime.UtcTzId), + FirstDayOfWeek = DayOfWeek.Sunday, + ByMonthDay = [29] + } }; - evt.RecurrenceRules.Add(pattern); - var occurrences = evt.GetOccurrences(new CalDateTime(2011, 1, 1)).TakeWhileBefore(new CalDateTime(2012, 1, 1)) .ToList(); Assert.That(occurrences, Has.Count.EqualTo(10), @@ -2758,7 +2752,7 @@ public void Bug3178652() public void Bug3292737() { using var sr = new StringReader("FREQ=WEEKLY;UNTIL=20251126"); - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); var rp = (RecurrencePattern) serializer.Deserialize(sr)!; Assert.That(rp, Is.Not.Null); @@ -2772,19 +2766,17 @@ public void Bug3292737() [Test, Category("Recurrence")] public void Issue432() { - var rrule = new RecurrencePattern - { - Frequency = FrequencyType.Daily, - Until = CalDateTime.Today.AddMonths(4) - }; var vEvent = new CalendarEvent { Start = new CalDateTime(DateTime.Parse("2019-01-04T08:00Z", CultureInfo.InvariantCulture).ToUniversalTime()), + RecurrenceRule = new() + { + Frequency = FrequencyType.Daily, + Until = CalDateTime.Today.AddMonths(4) + } }; - vEvent.RecurrenceRules.Add(rrule); - //Testing on both the first day and the next, results used to be different for (var i = 0; i <= 1; i++) { @@ -2905,7 +2897,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")); + evt.RecurrenceRule = new($"FREQ={freq};INTERVAL=10;COUNT=5"); var occurrences = evt.GetOccurrences(evt.Start.AddDays(-1)).TakeWhileBefore(evt.Start.AddDays(100)) .ToList(); @@ -2987,7 +2979,7 @@ public void GetOccurrences1() var evt = cal.Create(); evt.Start = new CalDateTime(2009, 11, 18, 5, 0, 0); evt.End = new CalDateTime(2009, 11, 18, 5, 10, 0); - evt.RecurrenceRules.Add(new RecurrencePattern(FrequencyType.Daily)); + evt.RecurrenceRule = new(FrequencyType.Daily); evt.Summary = "xxxxxxxxxxxxx"; var previousDateAndTime = new CalDateTime(2009, 11, 17, 0, 15, 0); @@ -3009,8 +3001,8 @@ public void GetOccurrences1() Assert.That(occurrences, Has.Count.EqualTo(3)); // Add ByHour "9" and "12" - evt.RecurrenceRules[0].ByHour.Add(9); - evt.RecurrenceRules[0].ByHour.Add(12); + evt.RecurrenceRule.ByHour.Add(9); + evt.RecurrenceRule.ByHour.Add(12); occurrences = evt.GetOccurrences(previousDateAndTime).TakeWhileBefore(end).ToList(); Assert.That(occurrences, Has.Count.EqualTo(10)); @@ -3054,9 +3046,9 @@ public void Test2() recur.ByDay.Add(new WeekDay(DayOfWeek.Monday)); recur.ByDay.Add(new WeekDay(DayOfWeek.Wednesday)); recur.ByDay.Add(new WeekDay(DayOfWeek.Friday)); - evt.RecurrenceRules.Add(recur); + evt.RecurrenceRule = recur; - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); Assert.That( string.Compare(serializer.SerializeToString(recur), "FREQ=DAILY;COUNT=3;BYDAY=MO,WE,FR", StringComparison.Ordinal) == 0, @@ -3245,7 +3237,7 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() { DtStart = new CalDateTime(start, "UTC"), DtEnd = new CalDateTime(end, "UTC"), - RecurrenceRules = new List { rrule }, + RecurrenceRule = rrule, Summary = "This is an event", Uid = "abab717c-1786-4efc-87dd-6859c2b48eb6", }; @@ -3400,7 +3392,7 @@ private static CalendarEvent GetEventWithRecurrenceRules() { Start = new CalDateTime(_now), End = new CalDateTime(_later), - RecurrenceRules = new List { dailyForFiveDays }, + RecurrenceRule = dailyForFiveDays, Resources = new List(new[] { "Foo", "Bar", "Baz" }), }; return calendarEvent; @@ -3416,7 +3408,7 @@ public void ExDatesShouldGetMergedInOutput() { DtStart = new CalDateTime(start), DtEnd = new CalDateTime(end), - RecurrenceRules = new List { rrule } + RecurrenceRule = rrule }; var firstExclusion = new CalDateTime(start.AddDays(4)); @@ -3442,7 +3434,7 @@ public void ExDateTimeZone_Tests() { DtStart = new CalDateTime(_now.Date, _now.Time, tzid), DtEnd = new CalDateTime(_later.Date, _later.Time, tzid), - RecurrenceRules = new List { rrule }, + RecurrenceRule = rrule, }; e.ExceptionDates.Add(new CalDateTime(_now.Date, _now.Time, tzid).AddDays(1)); @@ -3480,18 +3472,13 @@ public void OneDayRange() [Test, Category("Recurrence")] public void SpecificMinute() { - var rrule = new RecurrencePattern - { - Frequency = FrequencyType.Daily - }; var vEvent = new CalendarEvent { Start = new CalDateTime(DateTime.Parse("2009-01-01 09:00:00", CultureInfo.InvariantCulture)), - End = new CalDateTime(DateTime.Parse("2009-01-01 17:00:00", CultureInfo.InvariantCulture)) + End = new CalDateTime(DateTime.Parse("2009-01-01 17:00:00", CultureInfo.InvariantCulture)), + RecurrenceRule = new(FrequencyType.Daily) }; - vEvent.RecurrenceRules.Add(rrule); - // Exactly on start time var testingTime = new CalDateTime(2019, 6, 7, 9, 0, 0); @@ -3808,7 +3795,7 @@ Type LoadType(string name) => return; } - evt.RecurrenceRules.Add(GetPattern()); + evt.RecurrenceRule = GetPattern(); IEnumerable GetOccurrences() => evt.GetOccurrences(testCase.StartAt ?? null); @@ -3845,7 +3832,7 @@ public void ShouldCreateARecurringYearlyEvent() { Start = new CalDateTime(2024, 04, 15), End = new CalDateTime(2024, 04, 15), - RecurrenceRules = new List { new RecurrencePattern(FrequencyType.Yearly, 1) }, + RecurrenceRule = new(FrequencyType.Yearly, 1), }; var calendar = new Calendar(); @@ -3857,8 +3844,7 @@ public void ShouldCreateARecurringYearlyEvent() springAdminEvent.Start = new CalDateTime(2024, 04, 16); springAdminEvent.End = new CalDateTime(2024, 04, 16); - springAdminEvent.RecurrenceRules = new List - { new RecurrencePattern(FrequencyType.Yearly, 1) }; + springAdminEvent.RecurrenceRule = new(FrequencyType.Yearly, 1); searchStart = new CalDateTime(2024, 04, 16); searchEnd = new CalDateTime(2050, 05, 31); @@ -4005,7 +3991,7 @@ public void TestMaxIncrementCount(int? limit, string ical, bool expectException) [TestCase("INTERVAL=2;UNTIL=20250430T000000Z;FREQ=DAILY")] public void Recurrence_RRULE_Properties_ShouldBeDeserialized_In_Any_Order(string rRule) { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); var recurrencePattern = serializer.Deserialize(new StringReader(rRule)) as RecurrencePattern; Assert.Multiple(() => @@ -4022,7 +4008,7 @@ public void Recurrence_RRULE_Properties_ShouldBeDeserialized_In_Any_Order(string [Test] public void Recurrence_RRULE_Without_Freq_Should_Throw() { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); Assert.That(() => serializer.Deserialize(new StringReader("INTERVAL=2;UNTIL=20250430T000000Z")), Throws.TypeOf()); @@ -4031,7 +4017,7 @@ public void Recurrence_RRULE_Without_Freq_Should_Throw() [Test] public void Recurrence_RRULE_With_Freq_Undefined_Should_Throw() { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); Assert.That(() => serializer.Deserialize(new StringReader("FREQ=UNDEFINED;INTERVAL=2;UNTIL=20250430T000000Z")), Throws.TypeOf()); @@ -4040,7 +4026,7 @@ public void Recurrence_RRULE_With_Freq_Undefined_Should_Throw() [Test] public void Recurrence_RRULE_With_Unsupported_Part_Should_Throw() { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); Assert.That(() => serializer.Deserialize(new StringReader("FREQ=DAILY;INTERVAL=2;FAILING=0")), Throws.TypeOf()); @@ -4049,7 +4035,7 @@ public void Recurrence_RRULE_With_Unsupported_Part_Should_Throw() [Test] public void Preceding_Appended_and_duplicate_Semicolons_Should_Be_Ignored() { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); var recurrencePattern = serializer.Deserialize(new StringReader(";FREQ=DAILY;INTERVAL=2;UNTIL=20250430T000000Z")) as @@ -4067,7 +4053,7 @@ public void Preceding_Appended_and_duplicate_Semicolons_Should_Be_Ignored() [Test] public void Disallowed_Recurrence_RangeChecks_Should_Throw() { - var serializer = new RecurrencePatternSerializer(); + var serializer = new RecurrenceRuleSerializer(); Assert.Multiple(() => { Assert.That(() => serializer.CheckMutuallyExclusive("a", "b", 1, CalDateTime.Now), @@ -4123,7 +4109,7 @@ public void GetOccurrences_ShouldNotIgnoreExDatesForToday(string periodStart, st { Start = new CalDateTime("20250101T100000"), End = new CalDateTime("20250101T200000"), - RecurrenceRules = [new RecurrencePattern("FREQ=DAILY")], + RecurrenceRule = new("FREQ=DAILY"), }; cal.ExceptionDates.Add(new CalDateTime("20250102")); @@ -4149,7 +4135,7 @@ public void GetOccurrences_WithMixedKindExDates_ShouldProperlyConsiderAll() { Start = new CalDateTime("20250702T100000"), End = new CalDateTime("20250702T200000"), - RecurrenceRules = [new RecurrencePattern("FREQ=DAILY")], + RecurrenceRule = new("FREQ=DAILY"), }; @@ -4282,7 +4268,7 @@ public void GetOccurrences_WithPeriodStart_ShouldConsiderTzCorrectly(string dtSt string periodStartStr, string? periodStartTzId) { var evt = new CalendarEvent(); - evt.RecurrenceRules.Add(new RecurrencePattern(FrequencyType.Yearly, 1) { Count = 3 }); + evt.RecurrenceRule = new(FrequencyType.Yearly, 1) { Count = 3 }; evt.Start = new CalDateTime(dtStartStr, dtStartTzId); var cal = new Calendar(); @@ -4455,14 +4441,12 @@ public void SkippedOccurrenceOnWeeklyPattern() // Test moved from former GetOccu { DtStart = eventStart, DtEnd = eventEnd, + RecurrenceRule = new(FrequencyType.Weekly) + { + ByDay = [new WeekDay(DayOfWeek.Friday)] + } }; - var pattern = new RecurrencePattern - { - Frequency = FrequencyType.Weekly, - ByDay = [new WeekDay(DayOfWeek.Friday)] - }; - vEvent.RecurrenceRules.Add(pattern); var calendar = new Calendar(); calendar.Events.Add(vEvent); diff --git a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs index 940864590..2fe01724f 100644 --- a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs +++ b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs @@ -290,17 +290,11 @@ public void CheckCalendarZone() Summary = "Unavailable Before Work", DtStart = new CalDateTime(blockDate, zone), //.ToTimeZone(zone), DtEnd = new CalDateTime(blockDate.Add(new TimeSpan(8, 0, 0)), zone), //.ToTimeZone(zone), - RecurrenceRules = new List + RecurrenceRule = new() { - new RecurrencePattern - { - Frequency = Ical.Net.FrequencyType.Weekly, - Interval = 1, - ByDay = new List - { - new WeekDay(DayOfWeek.Monday) - } - } + Frequency = FrequencyType.Weekly, + Interval = 1, + ByDay = [new WeekDay(DayOfWeek.Monday)] } }); @@ -319,17 +313,17 @@ public void Daylight_Savings_Changes_567() var calStart = new CalDateTime(DateTimeOffset.Parse("2023-01-14T19:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); var calFinish = new CalDateTime(DateTimeOffset.Parse("2023-03-14T18:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); var tz = "Pacific Standard Time"; - var pattern = new RecurrencePattern( - "FREQ=WEEKLY;BYDAY=SU,MO,TU,WE"); //Adjust the date to today so that the times remain constant + + //Adjust the date to today so that the times remain constant var localTzStartAdjust = (DateTime.Now.Add(TimeSpan.FromHours(2).Add(TimeSpan.FromMinutes(35)))); var localTzFinishAdjust = DateTime.Now.Add(TimeSpan.FromHours(17)); - var ev = new Ical.Net.CalendarComponents.CalendarEvent + var ev = new CalendarEvent { Class = "PUBLIC", Start = new CalDateTime(localTzStartAdjust, tz), End = new CalDateTime(localTzFinishAdjust, tz), Sequence = 0, - RecurrenceRules = new List { pattern } + RecurrenceRule = new("FREQ=WEEKLY;BYDAY=SU,MO,TU,WE") }; var col = ev.GetOccurrences(calStart).TakeWhileBefore(calFinish); @@ -358,7 +352,7 @@ private static void CheckDates(DateTime startDate, DateTime endDate, CalDateTime { DtStart = new CalDateTime(startDate), DtEnd = new CalDateTime(endDate), - RecurrenceRules = new List { rule } + RecurrenceRule = rule }; var occurrences = calendarEvent.GetOccurrences(new CalDateTime(startDate)).TakeWhileBefore(new CalDateTime(endDate)); @@ -487,7 +481,7 @@ public void Except_Tuesday_Thursday_Saturday_Sunday() Location = "Building 101",//location DtStart = new CalDateTime(DateTime.Parse("2017-06-01T08:00", CultureInfo.InvariantCulture)), DtEnd = new CalDateTime(DateTime.Parse("2017-06-01T09:30", CultureInfo.InvariantCulture)), - RecurrenceRules = new List { new RecurrencePattern(FrequencyType.Daily, 1) }, + RecurrenceRule = new(FrequencyType.Daily, 1), }; //Define the exceptions: Sunday diff --git a/Ical.Net.Tests/RecurrenceWithExDateTests.cs b/Ical.Net.Tests/RecurrenceWithExDateTests.cs index 2df3170d2..ed7843666 100644 --- a/Ical.Net.Tests/RecurrenceWithExDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithExDateTests.cs @@ -34,20 +34,18 @@ public void ShouldNotOccurOnLocalExceptionDate(bool useExDateWithTime) ? new CalDateTime(2024, 10, 19, 21, 0, 0, timeZoneId) : new CalDateTime(2024, 10, 19); - var recurrencePattern = new RecurrencePattern(FrequencyType.Hourly) - { - Count = 2, - Interval = 3 - }; - var recurringEvent = new CalendarEvent { Summary = "My Recurring Event", Uid = id.ToString(), Start = start, - End = end + End = end, + RecurrenceRule = new(FrequencyType.Hourly, 3) + { + Count = 2 + } }; - recurringEvent.RecurrenceRules.Add(recurrencePattern); + recurringEvent.ExceptionDates.Add(exceptionDate); var calendar = new Calendar(); diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index 1c1011018..c1b81358d 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -471,16 +471,15 @@ public void TestStandardDaylightTimeZoneInfoDeserialization() [Test] public void TestRRuleUntilSerialization() { - var rrule = new RecurrencePattern(FrequencyType.Daily) - { - Until = new CalDateTime(_nowTime.AddDays(7)), - }; const string someTz = "Europe/Volgograd"; var e = new CalendarEvent { Start = _nowTime.ToTimeZone(someTz), End = _nowTime.AddHours(1).ToTimeZone(someTz), - RecurrenceRules = new List { rrule }, + RecurrenceRule = new(FrequencyType.Daily) + { + Until = new CalDateTime(_nowTime.AddDays(7)), + }, }; var c = new Calendar { diff --git a/Ical.Net.Tests/TodoTest.cs b/Ical.Net.Tests/TodoTest.cs index 608bde0f3..63aea21df 100644 --- a/Ical.Net.Tests/TodoTest.cs +++ b/Ical.Net.Tests/TodoTest.cs @@ -228,7 +228,7 @@ public void Todo_WithFutureStart_AndNoDuration_ShouldSucceed() var todo = new Todo { Start = today, - RecurrenceRules = [new RecurrencePattern("FREQ=DAILY")] + RecurrenceRule = new("FREQ=DAILY") }; // periodStart is in the future, so filtering the first occurrence will also require @@ -248,7 +248,7 @@ public void Todo_RecurrenceWithNoEnd_IsCompletedUntilNextOccurrence() var todo = new Todo { Start = start, - RecurrenceRules = [new("FREQ=DAILY;BYDAY=TH")], + RecurrenceRule = new("FREQ=DAILY;BYDAY=TH"), }; todo.Status = "COMPLETED"; diff --git a/Ical.Net.Tests/VTimeZoneTest.cs b/Ical.Net.Tests/VTimeZoneTest.cs index d773ee570..862e8155d 100644 --- a/Ical.Net.Tests/VTimeZoneTest.cs +++ b/Ical.Net.Tests/VTimeZoneTest.cs @@ -4,7 +4,6 @@ // using System; -using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Ical.Net.CalendarComponents; @@ -318,7 +317,7 @@ private static Calendar CreateTestCalendar(string tzId, DateTime? earliestTime = Description = "Test Recurring Event", Start = new CalDateTime(DateTime.Now, tzId), End = new CalDateTime(DateTime.Now.AddHours(1), tzId), - RecurrenceRules = new List { new RecurrencePattern(FrequencyType.Daily) } + RecurrenceRule = new(FrequencyType.Daily) }; iCal.Events.Add(calEvent); @@ -327,7 +326,7 @@ private static Calendar CreateTestCalendar(string tzId, DateTime? earliestTime = Description = "Test Recurring Event 2", Start = new CalDateTime(DateTime.Now.AddHours(2), tzId), End = new CalDateTime(DateTime.Now.AddHours(3), tzId), - RecurrenceRules = new List { new RecurrencePattern(FrequencyType.Daily) } + RecurrenceRule = new(FrequencyType.Daily) }; iCal.Events.Add(calEvent2); return iCal; diff --git a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs index 12af965dc..832ebdff8 100644 --- a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs +++ b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs @@ -68,7 +68,7 @@ public void Introduction() { DtStart = new CalDateTime(2025, 07, 10), // Add the rule to the event. - RecurrenceRules = [recurrence] + RecurrenceRule = recurrence }; // Get all occurrences of the series. @@ -123,7 +123,7 @@ public void DailyIntervalCount() { DtStart = start, DtEnd = start.AddHours(1), - RecurrenceRules = [recurrence] + RecurrenceRule = recurrence }; // Add CalendarEvent to Calendar @@ -196,7 +196,7 @@ public void YearlyByMonthDayUntil() { DtStart = start, DtEnd = start.AddHours(1), - RecurrenceRules = [recurrence] + RecurrenceRule = recurrence }; // Add CalendarEvent to Calendar @@ -276,7 +276,7 @@ public void MonthlyByDayCountRDate() { DtStart = start, DtEnd = start.AddHours(4), - RecurrenceRules = [recurrence], + RecurrenceRule = recurrence, }; // Add additional an occurrence to the series. calendarEvent.RecurrenceDates @@ -354,7 +354,7 @@ public void HourlyUntilExDate() { DtStart = start, DtEnd = start.AddMinutes(15), - RecurrenceRules = [recurrence], + RecurrenceRule = recurrence, }; // Add the exception date to the series. calendarEvent.ExceptionDates @@ -438,7 +438,7 @@ public void DailyIntervalCountMoved() Summary = "Walking", DtStart = start, DtEnd = start.AddHours(1), - RecurrenceRules = [recurrence], + RecurrenceRule = recurrence, Sequence = 0 // default value }; @@ -541,7 +541,7 @@ public void RecurrenceWithTimeZoneChanges() { DtStart = start, DtEnd = start.AddHours(1), - RecurrenceRules = [recurrence] + RecurrenceRule = recurrence }; // Add CalendarEvent to Calendar @@ -607,7 +607,7 @@ public void GetFirstOccurrenceOfAllCalendarEvents() Summary = "Daily event", Start = start, End = start.AddHours(1), - RecurrenceRules = [new RecurrencePattern(FrequencyType.Daily, interval: 1)] + RecurrenceRule = new RecurrencePattern(FrequencyType.Daily, interval: 1) }); // Simple event in far future diff --git a/Ical.Net/CalendarComponents/IRecurrable.cs b/Ical.Net/CalendarComponents/IRecurrable.cs index 33cf68967..b2f1cdd8a 100644 --- a/Ical.Net/CalendarComponents/IRecurrable.cs +++ b/Ical.Net/CalendarComponents/IRecurrable.cs @@ -23,7 +23,12 @@ public interface IRecurrable : IGetOccurrences IList ExceptionRules { get; set; } RecurrenceDates RecurrenceDates { get; } + + [Obsolete("Use RecurrenceRule instead. Support for multiple recurrence rules will be removed in a future version.")] IList RecurrenceRules { get; set; } + + RecurrenceRule? RecurrenceRule { get; set; } + CalDateTime? RecurrenceId { get; set; } IEvaluator? Evaluator { get; } } diff --git a/Ical.Net/CalendarComponents/RecurringComponent.cs b/Ical.Net/CalendarComponents/RecurringComponent.cs index 982a423a9..4b719e237 100644 --- a/Ical.Net/CalendarComponents/RecurringComponent.cs +++ b/Ical.Net/CalendarComponents/RecurringComponent.cs @@ -107,12 +107,19 @@ internal virtual IList RecurrenceDatesPeriodLists public virtual RecurrenceDates RecurrenceDates { get; internal set; } = null!; + [Obsolete("Use RecurrenceRule instead. Support for multiple recurrence rules will be removed in a future version.")] public virtual IList RecurrenceRules { get => Properties.GetMany("RRULE"); set => Properties.Set("RRULE", value); } + public virtual RecurrenceRule? RecurrenceRule + { + get => RecurrenceRules?.FirstOrDefault(); + set => RecurrenceRules = value is null ? [] : [new(value)]; + } + /// /// Gets or sets the recurrence identifier for a specific instance of a recurring event. /// diff --git a/Ical.Net/DataTypes/CalendarDataType.cs b/Ical.Net/DataTypes/CalendarDataType.cs index d01d41119..46189534b 100644 --- a/Ical.Net/DataTypes/CalendarDataType.cs +++ b/Ical.Net/DataTypes/CalendarDataType.cs @@ -144,6 +144,13 @@ public virtual void CopyFrom(ICopyable obj) _proxy.SetProxiedObject(dt.Parameters); } + protected internal void CopyDataType(CalendarDataType other) + { + _proxy = other._proxy; + _parameters = other._parameters; + _associatedObject = other._associatedObject; + } + /// /// Creates a deep copy of the object. /// diff --git a/Ical.Net/DataTypes/RecurrencePattern.cs b/Ical.Net/DataTypes/RecurrencePattern.cs index 72effb7b6..2922816d3 100644 --- a/Ical.Net/DataTypes/RecurrencePattern.cs +++ b/Ical.Net/DataTypes/RecurrencePattern.cs @@ -3,177 +3,49 @@ // Licensed under the MIT license. // -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Ical.Net.Serialization.DataTypes; -using Ical.Net.Utility; - namespace Ical.Net.DataTypes; /// /// An iCalendar representation of the RRULE property. /// https://tools.ietf.org/html/rfc5545#section-3.3.10 /// -public class RecurrencePattern : EncodableDataType +public class RecurrencePattern : RecurrenceRule { - private int? _interval; - private FrequencyType _frequency; - private CalDateTime? _until; - - /// - /// Specifies the frequency FREQ of the recurrence. - /// The default value is . - /// - public FrequencyType Frequency - { - get => _frequency; - set - { - if (!Enum.IsDefined(typeof(FrequencyType), value)) - { - throw new ArgumentOutOfRangeException(nameof(Frequency), $"Invalid FrequencyType '{value}'."); - } - _frequency = value; - } - } - - /// - /// Specifies the end date of the recurrence (optional). - /// This property must be null if the property is set. - /// - public CalDateTime? Until - { - get => _until; - set - { - if (value != null && value.TzId != null && value.TzId != CalDateTime.UtcTzId) - throw new ArgumentOutOfRangeException(nameof(Until), - $"{nameof(Until)} must be either NULL, or its TzId must be UTC or NULL"); - - _until = value; - } - } - - /// - /// Specifies the number of occurrences of the recurrence (optional). - /// This property must be null if the property is set. - /// - public int? Count { get; set; } - - - /// - /// The INTERVAL rule part contains a positive integer representing at - /// which intervals the recurrence rule repeats. The default value is - /// 1, meaning every second for a SECONDLY rule, every minute for a - /// MINUTELY rule, every hour for an HOURLY rule, every day for a - /// DAILY rule, every week for a WEEKLY rule, every month for a - /// MONTHLY rule, and every year for a YEARLY rule. For example, - /// within a DAILY rule, a value of 8 means every eight days. - /// - public int Interval - { - get => _interval ?? 1; - set => _interval = value; - } - - public List BySecond { get; set; } = new List(); - - /// The ordinal minutes of the hour associated with this recurrence pattern. Valid values are 0-59. - public List ByMinute { get; set; } = new List(); - - public List ByHour { get; set; } = new List(); - - public List ByDay { get; set; } = new List(); - - /// The ordinal days of the month associated with this recurrence pattern. Valid values are 1-31. - public List ByMonthDay { get; set; } = new List(); - - /// - /// The ordinal days of the year associated with this recurrence pattern. Something recurring on the first day of the year would be a list containing - /// 1, and would also be New Year's Day. - /// - public List ByYearDay { get; set; } = new List(); - - /// - /// The ordinal week of the year. Valid values are -53 to +53. Negative values count backwards from the end of the specified year. - /// A week is defined by ISO.8601.2004 - /// - public List ByWeekNo { get; set; } = new List(); - - /// - /// List of months in the year associated with this rule. Valid values are 1 through 12. - /// - public List ByMonth { get; set; } = new List(); - - /// - /// Specify the n-th occurrence within the set of occurrences specified by the RRULE. - /// It is typically used in conjunction with other rule parts like BYDAY, BYMONTHDAY, etc. - /// - public List BySetPosition { get; set; } = new List(); - - public DayOfWeek FirstDayOfWeek { get; set; } = DayOfWeek.Monday; - - /// - /// Default constructor. Sets the to - /// and to 1. - /// public RecurrencePattern() { - Frequency = FrequencyType.Yearly; - Interval = 1; } - public RecurrencePattern(FrequencyType frequency) : this(frequency, 1) { } - - public RecurrencePattern(FrequencyType frequency, int interval) : this() + public RecurrencePattern(FrequencyType frequency) : base(frequency) { - Frequency = frequency; // for proper validation don't use the backing field - Interval = interval; } - public RecurrencePattern(string value) : this() + public RecurrencePattern(string value) : base(value) { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - var serializer = new RecurrencePatternSerializer(); - if (serializer.Deserialize(new StringReader(value)) is ICopyable deserialized) - CopyFrom(deserialized); } - public override string? ToString() + public RecurrencePattern(FrequencyType frequency, int interval) : base(frequency, interval) { - var serializer = new RecurrencePatternSerializer(); - return serializer.SerializeToString(this); } - /// - public override void CopyFrom(ICopyable obj) + /// + /// Shallow copy properties to wrap RecurrenceRule. + /// + internal RecurrencePattern(RecurrenceRule r) { - base.CopyFrom(obj); - if (obj is not RecurrencePattern r) - { - return; - } - + CopyDataType(r); Frequency = r.Frequency; Until = r.Until; Count = r.Count; Interval = r.Interval; - BySecond = new List(r.BySecond); - ByMinute = new List(r.ByMinute); - ByHour = new List(r.ByHour); - ByDay = new List(r.ByDay); - ByMonthDay = new List(r.ByMonthDay); - ByYearDay = new List(r.ByYearDay); - ByWeekNo = new List(r.ByWeekNo); - ByMonth = new List(r.ByMonth); - BySetPosition = new List(r.BySetPosition); + BySecond = r.BySecond; + ByMinute = r.ByMinute; + ByHour = r.ByHour; + ByDay = r.ByDay; + ByMonthDay = r.ByMonthDay; + ByYearDay = r.ByYearDay; + ByWeekNo = r.ByWeekNo; + ByMonth = r.ByMonth; + BySetPosition = r.BySetPosition; FirstDayOfWeek = r.FirstDayOfWeek; } - - private static bool CollectionEquals(IEnumerable c1, IEnumerable c2) => c1.SequenceEqual(c2); } diff --git a/Ical.Net/DataTypes/RecurrenceRule.cs b/Ical.Net/DataTypes/RecurrenceRule.cs new file mode 100644 index 000000000..ee1a66575 --- /dev/null +++ b/Ical.Net/DataTypes/RecurrenceRule.cs @@ -0,0 +1,177 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Ical.Net.Serialization.DataTypes; +using Ical.Net.Utility; + +namespace Ical.Net.DataTypes; + +/// +/// An iCalendar representation of the RRULE property. +/// https://tools.ietf.org/html/rfc5545#section-3.3.10 +/// +public class RecurrenceRule : EncodableDataType +{ + private int? _interval; + private FrequencyType _frequency; + private CalDateTime? _until; + + /// + /// Specifies the frequency FREQ of the recurrence. + /// The default value is . + /// + public FrequencyType Frequency + { + get => _frequency; + set + { + if (!Enum.IsDefined(typeof(FrequencyType), value)) + { + throw new ArgumentOutOfRangeException(nameof(Frequency), $"Invalid FrequencyType '{value}'."); + } + _frequency = value; + } + } + + /// + /// Specifies the end date of the recurrence (optional). + /// This property must be null if the property is set. + /// + public CalDateTime? Until + { + get => _until; + set + { + if (value != null && value.TzId != null && value.TzId != CalDateTime.UtcTzId) + throw new ArgumentOutOfRangeException(nameof(Until), + $"{nameof(Until)} must be either NULL, or its TzId must be UTC or NULL"); + + _until = value; + } + } + + /// + /// Specifies the number of occurrences of the recurrence (optional). + /// This property must be null if the property is set. + /// + public int? Count { get; set; } + + + /// + /// The INTERVAL rule part contains a positive integer representing at + /// which intervals the recurrence rule repeats. The default value is + /// 1, meaning every second for a SECONDLY rule, every minute for a + /// MINUTELY rule, every hour for an HOURLY rule, every day for a + /// DAILY rule, every week for a WEEKLY rule, every month for a + /// MONTHLY rule, and every year for a YEARLY rule. For example, + /// within a DAILY rule, a value of 8 means every eight days. + /// + public int Interval + { + get => _interval ?? 1; + set => _interval = value; + } + + public List BySecond { get; set; } = new List(); + + /// The ordinal minutes of the hour associated with this recurrence pattern. Valid values are 0-59. + public List ByMinute { get; set; } = new List(); + + public List ByHour { get; set; } = new List(); + + public List ByDay { get; set; } = new List(); + + /// The ordinal days of the month associated with this recurrence pattern. Valid values are 1-31. + public List ByMonthDay { get; set; } = new List(); + + /// + /// The ordinal days of the year associated with this recurrence pattern. Something recurring on the first day of the year would be a list containing + /// 1, and would also be New Year's Day. + /// + public List ByYearDay { get; set; } = new List(); + + /// + /// The ordinal week of the year. Valid values are -53 to +53. Negative values count backwards from the end of the specified year. + /// A week is defined by ISO.8601.2004 + /// + public List ByWeekNo { get; set; } = new List(); + + /// + /// List of months in the year associated with this rule. Valid values are 1 through 12. + /// + public List ByMonth { get; set; } = new List(); + + /// + /// Specify the n-th occurrence within the set of occurrences specified by the RRULE. + /// It is typically used in conjunction with other rule parts like BYDAY, BYMONTHDAY, etc. + /// + public List BySetPosition { get; set; } = new List(); + + public DayOfWeek FirstDayOfWeek { get; set; } = DayOfWeek.Monday; + + /// + /// Default constructor. Sets the to + /// and to 1. + /// + public RecurrenceRule() + { + Frequency = FrequencyType.Yearly; + Interval = 1; + } + + public RecurrenceRule(FrequencyType frequency) : this(frequency, 1) { } + + public RecurrenceRule(FrequencyType frequency, int interval) : this() + { + Frequency = frequency; // for proper validation don't use the backing field + Interval = interval; + } + + public RecurrenceRule(string value) : this() + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + var serializer = new RecurrenceRuleSerializer(); + if (serializer.Deserialize(new StringReader(value)) is ICopyable deserialized) + CopyFrom(deserialized); + } + + public override string? ToString() + { + var serializer = new RecurrenceRuleSerializer(); + return serializer.SerializeToString(this); + } + + /// + public override void CopyFrom(ICopyable obj) + { + base.CopyFrom(obj); + if (obj is not RecurrenceRule r) + { + return; + } + + Frequency = r.Frequency; + Until = r.Until; + Count = r.Count; + Interval = r.Interval; + BySecond = new List(r.BySecond); + ByMinute = new List(r.ByMinute); + ByHour = new List(r.ByHour); + ByDay = new List(r.ByDay); + ByMonthDay = new List(r.ByMonthDay); + ByYearDay = new List(r.ByYearDay); + ByWeekNo = new List(r.ByWeekNo); + ByMonth = new List(r.ByMonth); + BySetPosition = new List(r.BySetPosition); + FirstDayOfWeek = r.FirstDayOfWeek; + } +} diff --git a/Ical.Net/Evaluation/Evaluator.cs b/Ical.Net/Evaluation/Evaluator.cs index c9267df55..d34c41cb6 100644 --- a/Ical.Net/Evaluation/Evaluator.cs +++ b/Ical.Net/Evaluation/Evaluator.cs @@ -12,6 +12,13 @@ namespace Ical.Net.Evaluation; public abstract class Evaluator : IEvaluator { + + protected void IncrementDate(ref CalDateTime dt, RecurrenceRule rule, int interval) + { + IncrementDate(ref dt, new RecurrencePattern(rule), interval); + } + + [Obsolete("Use overload with RecurrenceRule instead.")] protected void IncrementDate(ref CalDateTime dt, RecurrencePattern pattern, int interval) { if (interval == 0) diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index 7decd808d..b131f28b8 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -20,8 +20,21 @@ public class RecurrencePatternEvaluator : Evaluator /// private static System.Globalization.Calendar Calendar { get; } = System.Globalization.CultureInfo.InvariantCulture.Calendar; + [Obsolete("Use Rule instead.")] protected RecurrencePattern Pattern { get; set; } + protected RecurrenceRule Rule + { + get => Pattern; + set => Pattern = new(value); + } + + public RecurrencePatternEvaluator(RecurrenceRule rule) + { + Pattern = new(rule); + } + + [Obsolete("Use constructor with RecurrenceRule instead.")] public RecurrencePatternEvaluator(RecurrencePattern pattern) { Pattern = pattern; diff --git a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs index 80c3af318..6c306eeb4 100644 --- a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs @@ -4,341 +4,13 @@ // using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Ical.Net.DataTypes; namespace Ical.Net.Serialization.DataTypes; -public class RecurrencePatternSerializer : EncodableDataTypeSerializer +[Obsolete("Use RecurrenceRuleSerializer instead.")] +public class RecurrencePatternSerializer : RecurrenceRuleSerializer { public RecurrencePatternSerializer() { } public RecurrencePatternSerializer(SerializationContext ctx) : base(ctx) { } - - public static DayOfWeek GetDayOfWeek(string value) - { - return value.ToUpper() switch - { - "SU" => DayOfWeek.Sunday, - "MO" => DayOfWeek.Monday, - "TU" => DayOfWeek.Tuesday, - "WE" => DayOfWeek.Wednesday, - "TH" => DayOfWeek.Thursday, - "FR" => DayOfWeek.Friday, - "SA" => DayOfWeek.Saturday, - _ => throw new ArgumentOutOfRangeException(nameof(value), - $"{value} is not a valid iCal day-of-week indicator.") - }; - } - - protected static void AddInt32Values(IList list, string value) - { - var values = value.Split(','); - foreach (var v in values) - { - list.Add(Convert.ToInt32(v, CultureInfo.InvariantCulture)); - } - } - - public virtual void CheckRange(string name, IList values, int min, int max) - { - var allowZero = (min == 0 || max == 0); - foreach (var value in values) - { - CheckRange(name, value, min, max, allowZero); - } - } - - public virtual void CheckRange(string name, int value, int min, int max) - { - var allowZero = min == 0 || max == 0; - CheckRange(name, value, min, max, allowZero); - } - - public virtual void CheckRange(string name, int? value, int min, int max) - { - var allowZero = min == 0 || max == 0; - CheckRange(name, value, min, max, allowZero); - } - - public virtual void CheckRange(string name, int value, int min, int max, bool allowZero) - { - if ((value < min || value > max || (!allowZero && value == 0))) - { - throw new ArgumentOutOfRangeException(nameof(name), - $"{name} value {value} is out of range. Valid values are between {min} and {max}{(allowZero ? "" : ", excluding zero (0)")}."); - } - } - - public virtual void CheckRange(string name, int? value, int min, int max, bool allowZero) - { - if (value != null && (value < min || value > max || (!allowZero && value == 0))) - { - throw new ArgumentOutOfRangeException(nameof(name), - $"{name} value {value} is out of range. Valid values are between {min} and {max}{(allowZero ? "" : ", excluding zero (0)")}."); - } - } - - public virtual void CheckMutuallyExclusive(string name1, string name2, int? obj1, CalDateTime? obj2) - { - if ((obj1 == null) || (obj2 == null)) - { - return; - } - - throw new ArgumentOutOfRangeException(nameof(name1), - $"Both {name1} and {name2} cannot be supplied together; they are mutually exclusive."); - } - - private static void SerializeByValue(List aggregate, IList byValue, string name) - { - if (byValue.Any()) - { - aggregate.Add($"{name}={string.Join(",", byValue.Select(i => i.ToString(CultureInfo.InvariantCulture)))}"); - } - } - - public override Type TargetType => typeof(RecurrencePattern); - - public override string? SerializeToString(object? obj) - { - var factory = GetService(); - if (obj is not RecurrencePattern recur) - { - return null; - } - - // Push the recurrence pattern onto the serialization stack - SerializationContext.Push(recur); - var values = new List - { - $"FREQ={recur.Frequency.ToString().ToUpper()}" - }; - - //-- FROM RFC2445 -- - //The INTERVAL rule part contains a positive integer representing how - //often the recurrence rule repeats. The default value is "1", meaning - //every second for a SECONDLY rule, or every minute for a MINUTELY - //rule, every hour for an HOURLY rule, every day for a DAILY rule, - //every week for a WEEKLY rule, every month for a MONTHLY rule and - //every year for a YEARLY rule. - var interval = recur.Interval; - if (interval != 1) - { - values.Add($"INTERVAL={interval}"); - } - - if (recur.Until is not null - && factory.Build(typeof(CalDateTime), SerializationContext) is IStringSerializer serializer1) - { - values.Add($"UNTIL={serializer1.SerializeToString(recur.Until)}"); - } - - if (recur.FirstDayOfWeek != DayOfWeek.Monday) - { - values.Add($"WKST={Enum.GetName(typeof(DayOfWeek), recur.FirstDayOfWeek)?.ToUpper().Substring(0, 2)}"); - } - - if (recur.Count.HasValue) - { - values.Add($"COUNT={recur.Count}"); - } - - if (recur.ByDay.Count > 0) - { - var bydayValues = new List(recur.ByDay.Count); - - if (factory.Build(typeof(WeekDay), SerializationContext) is IStringSerializer serializer) - { - bydayValues.AddRange(recur.ByDay - .Select(byday => serializer.SerializeToString(byday)) - .Where(serialized => serialized != null) - // tell the compiler that the filtered values are not null after the Where clause - .Select(serialized => serialized!)); - } - - values.Add($"BYDAY={string.Join(",", bydayValues)}"); - } - - SerializeByValue(values, recur.ByHour, "BYHOUR"); - SerializeByValue(values, recur.ByMinute, "BYMINUTE"); - SerializeByValue(values, recur.ByMonth, "BYMONTH"); - SerializeByValue(values, recur.ByMonthDay, "BYMONTHDAY"); - SerializeByValue(values, recur.BySecond, "BYSECOND"); - SerializeByValue(values, recur.BySetPosition, "BYSETPOS"); - SerializeByValue(values, recur.ByWeekNo, "BYWEEKNO"); - SerializeByValue(values, recur.ByYearDay, "BYYEARDAY"); - - // Pop the recurrence pattern off the serialization stack - SerializationContext.Pop(); - - return Encode(recur, string.Join(";", values)); - } - - /// - /// Deserializes an RRULE value string into an object. - /// - /// RFC5545, section 3.3.10: - /// The RRULE value type is a structured value consisting of a - /// list of one or more recurrence grammar parts. Each rule part is - /// defined by a NAME=VALUE pair. The rule parts are separated from - /// each other by the SEMICOLON character. The rule parts are not - /// ordered in any particular sequence. Individual rule parts MUST - /// only be specified once. Compliant applications MUST accept rule - /// parts ordered in any sequence. - /// - /// - /// An object or for invalid input. - /// - public override object? Deserialize(TextReader tr) - { - var value = tr.ReadToEnd(); - - // Instantiate the data type - var r = CreateAndAssociate() as RecurrencePattern; - var factory = GetService(); - - System.Diagnostics.Debug.Assert(r != null); - System.Diagnostics.Debug.Assert(factory != null); - - // Decode the value, if necessary - value = Decode(r, value); - if (value == null) return null; - - DeserializePattern(value, r, factory); - return r; - } - - /// - /// Deserializes the recurrence rule. - /// - /// Always throws on failure. - private void DeserializePattern(string value, RecurrencePattern r, ISerializerFactory factory) - { - var freqPartExists = false; - var keywordPairs = value.Split(';'); - foreach (var keywordPair in keywordPairs) - { - if (keywordPair.Length == 0) - { - // ignore subsequent semi-colons - continue; - } - - var keyValues = keywordPair.Split('='); - if (keyValues.Length != 2) - { - throw new ArgumentOutOfRangeException(nameof(value), - $"The recurrence rule part '{keywordPair}' is invalid."); - } - - if (keyValues[0].Equals("FREQ", StringComparison.OrdinalIgnoreCase)) - { - freqPartExists = true; - } - - ProcessKeyValuePair(keyValues[0].ToLower(), keyValues[1], r, factory); - } - - if (!freqPartExists) - { - throw new ArgumentOutOfRangeException(nameof(value), - "The recurrence rule must specify a valid FREQ part."); - } - CheckMutuallyExclusive("COUNT", "UNTIL", r.Count, r.Until); - CheckRanges(r); - } - - private void ProcessKeyValuePair(string key, string value, RecurrencePattern r, ISerializerFactory factory) - { - switch (key) - { - case "freq" when Enum.TryParse(value, true, out FrequencyType freq): - r.Frequency = freq; - break; - - case "until": - var serializer = factory.Build(typeof(CalDateTime), SerializationContext) as IStringSerializer; - r.Until = serializer?.Deserialize(new StringReader(value)) as CalDateTime; - break; - - case "count": - r.Count = Convert.ToInt32(value, CultureInfo.InvariantCulture); - break; - - case "interval": - r.Interval = Convert.ToInt32(value, CultureInfo.InvariantCulture); - break; - - case "bysecond": - AddInt32Values(r.BySecond, value); - break; - - case "byminute": - AddInt32Values(r.ByMinute, value); - break; - - case "byhour": - AddInt32Values(r.ByHour, value); - break; - - case "byday": - AddWeekDays(r.ByDay, value); - break; - - case "bymonthday": - AddInt32Values(r.ByMonthDay, value); - break; - - case "byyearday": - AddInt32Values(r.ByYearDay, value); - break; - - case "byweekno": - AddInt32Values(r.ByWeekNo, value); - break; - - case "bymonth": - AddInt32Values(r.ByMonth, value); - break; - - case "bysetpos": - AddInt32Values(r.BySetPosition, value); - break; - - case "wkst": - r.FirstDayOfWeek = GetDayOfWeek(value); - break; - - default: - throw new ArgumentOutOfRangeException(nameof(key), - $"The recurrence rule part '{key}' or its value {value} is not supported."); - } - } - - private static void AddWeekDays(IList byDay, string keyValue) - { - var days = keyValue.Split(','); - foreach (var day in days) - { - byDay.Add(new WeekDay(day)); - } - } - - private void CheckRanges(RecurrencePattern r) - { - CheckRange("INTERVAL", r.Interval, 1, int.MaxValue); - CheckRange("COUNT", r.Count, 1, int.MaxValue); - CheckRange("BYSECOND", r.BySecond, 0, 59); - CheckRange("BYMINUTE", r.ByMinute, 0, 59); - CheckRange("BYHOUR", r.ByHour, 0, 23); - CheckRange("BYMONTHDAY", r.ByMonthDay, -31, 31); - CheckRange("BYYEARDAY", r.ByYearDay, -366, 366); - CheckRange("BYWEEKNO", r.ByWeekNo, -53, 53); - CheckRange("BYMONTH", r.ByMonth, 1, 12); - CheckRange("BYSETPOS", r.BySetPosition, -366, 366); - } } diff --git a/Ical.Net/Serialization/DataTypes/RecurrenceRuleSerializer.cs b/Ical.Net/Serialization/DataTypes/RecurrenceRuleSerializer.cs new file mode 100644 index 000000000..4c4c29e39 --- /dev/null +++ b/Ical.Net/Serialization/DataTypes/RecurrenceRuleSerializer.cs @@ -0,0 +1,344 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using Ical.Net.DataTypes; + +namespace Ical.Net.Serialization.DataTypes; + +public class RecurrenceRuleSerializer : EncodableDataTypeSerializer +{ + public RecurrenceRuleSerializer() { } + + public RecurrenceRuleSerializer(SerializationContext ctx) : base(ctx) { } + + public static DayOfWeek GetDayOfWeek(string value) + { + return value.ToUpper() switch + { + "SU" => DayOfWeek.Sunday, + "MO" => DayOfWeek.Monday, + "TU" => DayOfWeek.Tuesday, + "WE" => DayOfWeek.Wednesday, + "TH" => DayOfWeek.Thursday, + "FR" => DayOfWeek.Friday, + "SA" => DayOfWeek.Saturday, + _ => throw new ArgumentOutOfRangeException(nameof(value), + $"{value} is not a valid iCal day-of-week indicator.") + }; + } + + protected static void AddInt32Values(IList list, string value) + { + var values = value.Split(','); + foreach (var v in values) + { + list.Add(Convert.ToInt32(v, CultureInfo.InvariantCulture)); + } + } + + public virtual void CheckRange(string name, IList values, int min, int max) + { + var allowZero = (min == 0 || max == 0); + foreach (var value in values) + { + CheckRange(name, value, min, max, allowZero); + } + } + + public virtual void CheckRange(string name, int value, int min, int max) + { + var allowZero = min == 0 || max == 0; + CheckRange(name, value, min, max, allowZero); + } + + public virtual void CheckRange(string name, int? value, int min, int max) + { + var allowZero = min == 0 || max == 0; + CheckRange(name, value, min, max, allowZero); + } + + public virtual void CheckRange(string name, int value, int min, int max, bool allowZero) + { + if ((value < min || value > max || (!allowZero && value == 0))) + { + throw new ArgumentOutOfRangeException(nameof(name), + $"{name} value {value} is out of range. Valid values are between {min} and {max}{(allowZero ? "" : ", excluding zero (0)")}."); + } + } + + public virtual void CheckRange(string name, int? value, int min, int max, bool allowZero) + { + if (value != null && (value < min || value > max || (!allowZero && value == 0))) + { + throw new ArgumentOutOfRangeException(nameof(name), + $"{name} value {value} is out of range. Valid values are between {min} and {max}{(allowZero ? "" : ", excluding zero (0)")}."); + } + } + + public virtual void CheckMutuallyExclusive(string name1, string name2, int? obj1, CalDateTime? obj2) + { + if ((obj1 == null) || (obj2 == null)) + { + return; + } + + throw new ArgumentOutOfRangeException(nameof(name1), + $"Both {name1} and {name2} cannot be supplied together; they are mutually exclusive."); + } + + private static void SerializeByValue(List aggregate, IList byValue, string name) + { + if (byValue.Any()) + { + aggregate.Add($"{name}={string.Join(",", byValue.Select(i => i.ToString(CultureInfo.InvariantCulture)))}"); + } + } + + public override Type TargetType => typeof(RecurrencePattern); + + public override string? SerializeToString(object? obj) + { + var factory = GetService(); + if (obj is not RecurrencePattern recur) + { + return null; + } + + // Push the recurrence pattern onto the serialization stack + SerializationContext.Push(recur); + var values = new List + { + $"FREQ={recur.Frequency.ToString().ToUpper()}" + }; + + //-- FROM RFC2445 -- + //The INTERVAL rule part contains a positive integer representing how + //often the recurrence rule repeats. The default value is "1", meaning + //every second for a SECONDLY rule, or every minute for a MINUTELY + //rule, every hour for an HOURLY rule, every day for a DAILY rule, + //every week for a WEEKLY rule, every month for a MONTHLY rule and + //every year for a YEARLY rule. + var interval = recur.Interval; + if (interval != 1) + { + values.Add($"INTERVAL={interval}"); + } + + if (recur.Until is not null + && factory.Build(typeof(CalDateTime), SerializationContext) is IStringSerializer serializer1) + { + values.Add($"UNTIL={serializer1.SerializeToString(recur.Until)}"); + } + + if (recur.FirstDayOfWeek != DayOfWeek.Monday) + { + values.Add($"WKST={Enum.GetName(typeof(DayOfWeek), recur.FirstDayOfWeek)?.ToUpper().Substring(0, 2)}"); + } + + if (recur.Count.HasValue) + { + values.Add($"COUNT={recur.Count}"); + } + + if (recur.ByDay.Count > 0) + { + var bydayValues = new List(recur.ByDay.Count); + + if (factory.Build(typeof(WeekDay), SerializationContext) is IStringSerializer serializer) + { + bydayValues.AddRange(recur.ByDay + .Select(byday => serializer.SerializeToString(byday)) + .Where(serialized => serialized != null) + // tell the compiler that the filtered values are not null after the Where clause + .Select(serialized => serialized!)); + } + + values.Add($"BYDAY={string.Join(",", bydayValues)}"); + } + + SerializeByValue(values, recur.ByHour, "BYHOUR"); + SerializeByValue(values, recur.ByMinute, "BYMINUTE"); + SerializeByValue(values, recur.ByMonth, "BYMONTH"); + SerializeByValue(values, recur.ByMonthDay, "BYMONTHDAY"); + SerializeByValue(values, recur.BySecond, "BYSECOND"); + SerializeByValue(values, recur.BySetPosition, "BYSETPOS"); + SerializeByValue(values, recur.ByWeekNo, "BYWEEKNO"); + SerializeByValue(values, recur.ByYearDay, "BYYEARDAY"); + + // Pop the recurrence pattern off the serialization stack + SerializationContext.Pop(); + + return Encode(recur, string.Join(";", values)); + } + + /// + /// Deserializes an RRULE value string into an object. + /// + /// RFC5545, section 3.3.10: + /// The RRULE value type is a structured value consisting of a + /// list of one or more recurrence grammar parts. Each rule part is + /// defined by a NAME=VALUE pair. The rule parts are separated from + /// each other by the SEMICOLON character. The rule parts are not + /// ordered in any particular sequence. Individual rule parts MUST + /// only be specified once. Compliant applications MUST accept rule + /// parts ordered in any sequence. + /// + /// + /// An object or for invalid input. + /// + public override object? Deserialize(TextReader tr) + { + var value = tr.ReadToEnd(); + + // Instantiate the data type + var r = CreateAndAssociate() as RecurrencePattern; + var factory = GetService(); + + System.Diagnostics.Debug.Assert(r != null); + System.Diagnostics.Debug.Assert(factory != null); + + // Decode the value, if necessary + value = Decode(r, value); + if (value == null) return null; + + DeserializePattern(value, r, factory); + return r; + } + + /// + /// Deserializes the recurrence rule. + /// + /// Always throws on failure. + private void DeserializePattern(string value, RecurrencePattern r, ISerializerFactory factory) + { + var freqPartExists = false; + var keywordPairs = value.Split(';'); + foreach (var keywordPair in keywordPairs) + { + if (keywordPair.Length == 0) + { + // ignore subsequent semi-colons + continue; + } + + var keyValues = keywordPair.Split('='); + if (keyValues.Length != 2) + { + throw new ArgumentOutOfRangeException(nameof(value), + $"The recurrence rule part '{keywordPair}' is invalid."); + } + + if (keyValues[0].Equals("FREQ", StringComparison.OrdinalIgnoreCase)) + { + freqPartExists = true; + } + + ProcessKeyValuePair(keyValues[0].ToLower(), keyValues[1], r, factory); + } + + if (!freqPartExists) + { + throw new ArgumentOutOfRangeException(nameof(value), + "The recurrence rule must specify a valid FREQ part."); + } + CheckMutuallyExclusive("COUNT", "UNTIL", r.Count, r.Until); + CheckRanges(r); + } + + private void ProcessKeyValuePair(string key, string value, RecurrencePattern r, ISerializerFactory factory) + { + switch (key) + { + case "freq" when Enum.TryParse(value, true, out FrequencyType freq): + r.Frequency = freq; + break; + + case "until": + var serializer = factory.Build(typeof(CalDateTime), SerializationContext) as IStringSerializer; + r.Until = serializer?.Deserialize(new StringReader(value)) as CalDateTime; + break; + + case "count": + r.Count = Convert.ToInt32(value, CultureInfo.InvariantCulture); + break; + + case "interval": + r.Interval = Convert.ToInt32(value, CultureInfo.InvariantCulture); + break; + + case "bysecond": + AddInt32Values(r.BySecond, value); + break; + + case "byminute": + AddInt32Values(r.ByMinute, value); + break; + + case "byhour": + AddInt32Values(r.ByHour, value); + break; + + case "byday": + AddWeekDays(r.ByDay, value); + break; + + case "bymonthday": + AddInt32Values(r.ByMonthDay, value); + break; + + case "byyearday": + AddInt32Values(r.ByYearDay, value); + break; + + case "byweekno": + AddInt32Values(r.ByWeekNo, value); + break; + + case "bymonth": + AddInt32Values(r.ByMonth, value); + break; + + case "bysetpos": + AddInt32Values(r.BySetPosition, value); + break; + + case "wkst": + r.FirstDayOfWeek = GetDayOfWeek(value); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(key), + $"The recurrence rule part '{key}' or its value {value} is not supported."); + } + } + + private static void AddWeekDays(IList byDay, string keyValue) + { + var days = keyValue.Split(','); + foreach (var day in days) + { + byDay.Add(new WeekDay(day)); + } + } + + private void CheckRanges(RecurrencePattern r) + { + CheckRange("INTERVAL", r.Interval, 1, int.MaxValue); + CheckRange("COUNT", r.Count, 1, int.MaxValue); + CheckRange("BYSECOND", r.BySecond, 0, 59); + CheckRange("BYMINUTE", r.ByMinute, 0, 59); + CheckRange("BYHOUR", r.ByHour, 0, 23); + CheckRange("BYMONTHDAY", r.ByMonthDay, -31, 31); + CheckRange("BYYEARDAY", r.ByYearDay, -366, 366); + CheckRange("BYWEEKNO", r.ByWeekNo, -53, 53); + CheckRange("BYMONTH", r.ByMonth, 1, 12); + CheckRange("BYSETPOS", r.BySetPosition, -366, 366); + } +} diff --git a/Ical.Net/Serialization/DataTypes/WeekDaySerializer.cs b/Ical.Net/Serialization/DataTypes/WeekDaySerializer.cs index ed725220e..c6f7549f7 100644 --- a/Ical.Net/Serialization/DataTypes/WeekDaySerializer.cs +++ b/Ical.Net/Serialization/DataTypes/WeekDaySerializer.cs @@ -75,7 +75,7 @@ public WeekDaySerializer(SerializationContext ctx) : base(ctx) { } ds.Offset *= -1; } } - ds.DayOfWeek = RecurrencePatternSerializer.GetDayOfWeek(match.Groups[3].Value); + ds.DayOfWeek = RecurrenceRuleSerializer.GetDayOfWeek(match.Groups[3].Value); return ds; } } diff --git a/Ical.Net/VTimeZoneInfo.cs b/Ical.Net/VTimeZoneInfo.cs index fda7d67d1..c30ed2f16 100644 --- a/Ical.Net/VTimeZoneInfo.cs +++ b/Ical.Net/VTimeZoneInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; @@ -125,12 +126,19 @@ internal IList RecurrenceDatesPeriodLists public virtual RecurrenceDates RecurrenceDates { get; private set; } = null!; + [Obsolete("Use RecurrenceRule instead. Support for multiple recurrence rules will be removed in a future version.")] public virtual IList RecurrenceRules { get => Properties.GetMany("RRULE"); set => Properties.Set("RRULE", value); } + public virtual RecurrenceRule? RecurrenceRule + { + get => RecurrenceRules?.FirstOrDefault(); + set => RecurrenceRules = value is null ? [] : [new(value)]; + } + /// /// Gets or sets the recurrence identifier for a specific instance of a recurring event. ///