diff --git a/Ical.Net.Tests/PeriodListTest.cs b/Ical.Net.Tests/PeriodListTest.cs index 916f2b44..170bfb3b 100644 --- a/Ical.Net.Tests/PeriodListTest.cs +++ b/Ical.Net.Tests/PeriodListTest.cs @@ -37,7 +37,8 @@ public void GetSet_Period_ShouldReturnCorrectPeriod() var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)); var period2 = new Period(new CalDateTime(2025, 2, 1, 0, 0, 0), Duration.FromHours(1)); - periodList.AddPeriod(period1).AddPeriod(period1); + periodList.Add(period1); + periodList.Add(period1); // Act var retrievedPeriod = periodList[0]; @@ -57,9 +58,11 @@ public void Clear_ShouldRemoveAllPeriods() { // Arrange var periodList = new PeriodList(); - var pl = PeriodList - .FromDateTime(new CalDateTime(2025, 1, 2)) - .Add(new CalDateTime(2025, 1, 3)); + var pl = new PeriodList + { + new CalDateTime(2025, 1, 2), + new CalDateTime(2025, 1, 3) + }; var count = pl.Count; @@ -104,7 +107,8 @@ public void InsertAt_ShouldInsertPeriodAtCorrectPosition() var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)); var period2 = new Period(new CalDateTime(2025, 1, 2, 0, 0, 0), Duration.FromHours(1)); var period3 = new Period(new CalDateTime(2025, 1, 3, 0, 0, 0), Duration.FromHours(1)); - periodList.AddPeriod(period1).AddPeriod(period3); + periodList.Add(period1); + periodList.Add(period3); // Act periodList.Insert(1, period2); @@ -125,7 +129,9 @@ public void RemoveAt_ShouldRemovePeriodAtCorrectPosition() var period1 = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1)); var period2 = new Period(new CalDateTime(2025, 1, 2, 0, 0, 0), Duration.FromHours(1)); var period3 = new Period(new CalDateTime(2025, 1, 3, 0, 0, 0), Duration.FromHours(1)); - periodList.AddPeriod(period1).AddPeriod(period2).AddPeriod(period3); + periodList.Add(period1); + periodList.Add(period2); + periodList.Add(period3); // Act periodList.RemoveAt(1); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 73426949..4bb76f9c 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -3201,10 +3201,10 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() /// Evaluate relevancy and validity of the request. /// Find a solution for issue #120 or close forever /// - [Test, Ignore("No solution for issue #120 yet", Until = "2024-12-31")] + [Test, Ignore("No solution for issue #120 yet", Until = "2025-02-28")] public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet() { - //https://github.com/rianjs/ical.net/issues/120 dated Sep 5, 2016 + //https://github.com/ical-org/ical.net/issues/120 dated Sep 5, 2016 const string ical = @"BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN diff --git a/Ical.Net.Tests/RecurrenceWithExDateTests.cs b/Ical.Net.Tests/RecurrenceWithExDateTests.cs index c58dc90b..7a785ad0 100644 --- a/Ical.Net.Tests/RecurrenceWithExDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithExDateTests.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using Ical.Net.CalendarComponents; +using Ical.Net.Collections; using Ical.Net.DataTypes; using Ical.Net.Serialization; using NUnit.Framework; @@ -29,10 +30,13 @@ public void ShouldNotOccurOnLocalExceptionDate(bool useExDateWithTime) const string timeZoneId = "Europe/London"; // IANA Time Zone ID var start = new CalDateTime(2024, 10, 19, 18, 0, 0, timeZoneId); var end = new CalDateTime(2024, 10, 19, 19, 0, 0, timeZoneId); + var exceptionDate = useExDateWithTime ? new CalDateTime(2024, 10, 19, 21, 0, 0, timeZoneId) : new CalDateTime(2024, 10, 19); + var exDateCollection = new ExceptionDateCollection(exceptionDate); + var recurrencePattern = new RecurrencePattern(FrequencyType.Hourly) { Count = 2, @@ -47,7 +51,7 @@ public void ShouldNotOccurOnLocalExceptionDate(bool useExDateWithTime) End = end }; recurringEvent.RecurrenceRules.Add(recurrencePattern); - recurringEvent.ExceptionDates.Add(PeriodList.FromDateTime(exceptionDate)); + recurringEvent.ExceptionDates = exDateCollection.ToExceptionDates(); var calendar = new Calendar(); calendar.Events.Add(recurringEvent); diff --git a/Ical.Net.Tests/RecurrenceWithRDateTests.cs b/Ical.Net.Tests/RecurrenceWithRDateTests.cs index 83a44e72..a29f7cd8 100644 --- a/Ical.Net.Tests/RecurrenceWithRDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithRDateTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using Ical.Net.CalendarComponents; +using Ical.Net.Collections; using Ical.Net.DataTypes; using Ical.Net.Serialization; using NUnit.Framework; @@ -21,16 +22,16 @@ public void RDate_SingleDateTime_IsProcessedCorrectly() { var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); - PeriodList recurrenceDates = - [ + var recDateCollection = new RecurrencePeriodCollection + { new Period(new CalDateTime(2023, 10, 2, 10, 0, 0)) - ]; + }; var calendarEvent = new CalendarEvent { Start = eventStart, Duration = new Duration(hours: 1), - RecurrenceDates = new List { recurrenceDates } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -55,7 +56,10 @@ public void RDate_SingleDateOnly_IsProcessedCorrectly() var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); - var recurrenceDates = PeriodList.FromDateTime(new CalDateTime(2023, 10, 2)); + PeriodList recurrenceDates = + [ + new Period(new CalDateTime(2023, 10, 2)) + ]; var calendarEvent = new CalendarEvent { @@ -85,7 +89,7 @@ public void RDate_MultipleDates_WithTimeZones_AreProcessedCorrectly() const string tzId = "America/New_York"; var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0, tzId); - var recurrenceDates = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { new Period(new CalDateTime(2023, 10, 2, 10, 0, 0, tzId)), new Period(new CalDateTime(2023, 10, 3, 10, 0, 0, tzId)) @@ -95,7 +99,7 @@ public void RDate_MultipleDates_WithTimeZones_AreProcessedCorrectly() { Start = eventStart, Duration = new Duration(hours: 2), - RecurrenceDates = new List { recurrenceDates } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -121,7 +125,7 @@ public void RDate_PeriodsWithTimezone_AreProcessedCorrectly() const string tzId = "America/New_York"; var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0, tzId); - var recurrenceDates = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { new Period(new CalDateTime(2023, 10, 2, 10, 0, 0, tzId), new Duration(hours: 4)), new Period(new CalDateTime(2023, 10, 3, 10, 0, 0, tzId), new Duration(hours: 5)) @@ -131,7 +135,7 @@ public void RDate_PeriodsWithTimezone_AreProcessedCorrectly() { Start = eventStart, Duration = new Duration(hours: 2), - RecurrenceDates = new List { recurrenceDates } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -176,12 +180,9 @@ public void RDate_MixedDatesAndPeriods_AreProcessedCorrectly() { var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); - var recurrenceDates1 = new PeriodList - { - new Period(new CalDateTime(2023, 10, 2, 10, 0, 0)), - }; - var recurrenceDates2 = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { + new CalDateTime(2023, 10, 2, 10, 0, 0), new Period(new CalDateTime(2023, 10, 3, 10, 0, 0), new Duration(hours: 3)) }; @@ -189,7 +190,7 @@ public void RDate_MixedDatesAndPeriods_AreProcessedCorrectly() { Start = eventStart, Duration = new Duration(hours: 1), - RecurrenceDates = new List { recurrenceDates1, recurrenceDates2 } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -215,20 +216,17 @@ public void RDate_DifferentTimeZones_AreProcessedCorrectly() { var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0, "America/New_York"); - var recurrenceDates1 = new PeriodList - { - new Period(new CalDateTime(2023, 10, 2, 10, 0, 0, "America/Los_Angeles")) - }; - var recurrenceDates2 = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { - new Period(new CalDateTime(2023, 10, 3, 10, 0, 0, "Europe/London")) + new CalDateTime(2023, 10, 2, 10, 0, 0, "America/Los_Angeles"), + new CalDateTime(2023, 10, 3, 10, 0, 0, "Europe/London") }; - + var calendarEvent = new CalendarEvent { Start = eventStart, Duration = new Duration(hours: 1), - RecurrenceDates = new List { recurrenceDates1, recurrenceDates2 } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -256,20 +254,18 @@ public void RDate_DateOnlyWithDurationAndDateTime_AreProcessedCorrectly() { var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); - var recurrenceDates1 = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { + // Period and CalDateTime can be added in one go new Period(new CalDateTime(2023, 10, 2), new Duration(days: 1)), - }; - var recurrenceDates2 = new PeriodList - { - new Period(new CalDateTime(2023, 10, 3, 10, 0, 0)) + new CalDateTime(2023, 10, 3, 10, 0, 0) }; var calendarEvent = new CalendarEvent { Start = eventStart, Duration = new Duration(days: 2), - RecurrenceDates = new List { recurrenceDates1, recurrenceDates2 } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -296,7 +292,7 @@ public void RDate_OverlappingPeriods_AreProcessedCorrectly() { var cal = new Calendar(); var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); - var recurrenceDates = new PeriodList + var recDateCollection = new RecurrencePeriodCollection { new Period(new CalDateTime(2023, 10, 2, 10, 0, 0), new Duration(hours: 2)), new Period(new CalDateTime(2023, 10, 2, 11, 0, 0), new Duration(hours: 2)) @@ -306,7 +302,7 @@ public void RDate_OverlappingPeriods_AreProcessedCorrectly() { Start = eventStart, Duration = new Duration(hours: 1), - RecurrenceDates = new List { recurrenceDates } + RecurrenceDates = recDateCollection.ToRecurrenceDates() }; cal.Events.Add(calendarEvent); @@ -363,6 +359,41 @@ public void RDate_LargeNumberOfDates_ShouldBeLineFolded() }); } + [Test] + public void RDate_DuplicateDates_ShouldBeSerializedJustOnce() + { + var cal = new Calendar(); + var eventStart = new CalDateTime(2023, 10, 1, 10, 0, 0); + + var periodDuplicate = new Period(new CalDateTime(2023, 10, 2, 10, 0, 0), new Duration(hours: 2)); + var recDateCollection = new RecurrencePeriodCollection + { + periodDuplicate, periodDuplicate + }; + + var calendarEvent = new CalendarEvent + { + Start = eventStart, + Duration = new Duration(hours: 1), + RecurrenceDates = recDateCollection.ToRecurrenceDates() + }; + + cal.Events.Add(calendarEvent); + var serializer = new CalendarSerializer(); + var ics = serializer.SerializeToString(cal); + + var occurrences = calendarEvent.GetOccurrences().ToList(); + + Assert.Multiple(() => + { + Assert.That(occurrences, Has.Count.EqualTo(2)); + Assert.That(occurrences[0].Period.StartTime, Is.EqualTo(new CalDateTime(2023, 10, 1, 10, 0, 0))); + Assert.That(occurrences[1].Period.StartTime, Is.EqualTo(periodDuplicate.StartTime)); + + Assert.That(ics, Does.Contain("RDATE;VALUE=PERIOD:20231002T100000/PT2H")); + }); + } + [Test] public void AddingDifferentTimeZonesToPeriodList_ShouldThrow() { @@ -377,7 +408,7 @@ public void AddingDifferentTimeZonesToPeriodList_ShouldThrow() } [Test] - public void AddingDifferentPeriodTypes_ShouldThrow() + public void AddingDifferentPeriodKinds_ShouldThrow() { Assert.Multiple(() => { diff --git a/Ical.Net/Collections/ExceptionDateCollection.cs b/Ical.Net/Collections/ExceptionDateCollection.cs new file mode 100644 index 00000000..14f0f0ef --- /dev/null +++ b/Ical.Net/Collections/ExceptionDateCollection.cs @@ -0,0 +1,55 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +#nullable enable +using System.Collections.Generic; +using Ical.Net.DataTypes; + +namespace Ical.Net.Collections; + +/// +/// Represents a collection of exception dates for calendar events. +/// +/// This class is used to manage dates that should be excluded from recurring events. +/// +/// +/// The main feature of this class is the method, which aggregates +/// and converts the exception objects into a list of objects. +/// This method ensures that the periods are grouped by their timezone IDs and period kinds, and that +/// each contains only distinct periods. +/// +/// +public class ExceptionDateCollection : PeriodCollectionBase +{ + /// + /// Initializes a new instance of the class. + /// + public ExceptionDateCollection() + { } + + /// + /// Initializes a new instance of the class with a single . + /// + /// The to add to the collection. + public ExceptionDateCollection(CalDateTime dt) : this() + { + Add(dt); + } + + /// + /// Initializes a new instance of the class with a collection of objects. + /// + /// The collection of objects to add to the collection. + public ExceptionDateCollection(IEnumerable dtList) : this() + { + AddRange(dtList); + } + + /// + /// Aggregates and converts the exception s to a list of objects. + /// + /// A list of objects. + public List ToExceptionDates() => ToListOfPeriodList(); +} diff --git a/Ical.Net/Collections/PeriodCollectionBase.cs b/Ical.Net/Collections/PeriodCollectionBase.cs new file mode 100644 index 00000000..115d77e7 --- /dev/null +++ b/Ical.Net/Collections/PeriodCollectionBase.cs @@ -0,0 +1,108 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +#nullable enable +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Ical.Net.DataTypes; + +namespace Ical.Net.Collections; + +/// +/// Represents a base class for collections of periods. +/// +public abstract class PeriodCollectionBase : IEnumerable +{ + /// + /// The list of periods in the collection. + /// + protected readonly List Periods = new List(); + + /// + /// Adds a to the collection as a . + /// + /// The to add. + /// The current instance of . + public PeriodCollectionBase Add(CalDateTime dt) + { + Periods.Add(new Period(dt)); + return this; + } + + /// + /// Adds a range of objects to the collection as s. + /// + /// The collection of objects to add. + /// The current instance of . + public PeriodCollectionBase AddRange(IEnumerable dtList) + { + foreach (var date in dtList) + { + Periods.Add(new Period(date)); + } + return this; + } + + /// + /// Clears all periods from the collection. + /// + public void Clear() => Periods.Clear(); + + /// + /// Aggregates and converts the periods to a list of objects. + /// + /// The periods as a list of objects. + protected List ToListOfPeriodList() + { + var periodList = new List(); + foreach (var tzId in GetDistinctTzIds()) + { + foreach (var pKind in GetDistinctPeriodKindsByTzId(tzId)) + { + var distinctPeriodList = new PeriodList(); + foreach (var period in GetPeriodsByTzIdAndKind(tzId, pKind)) + { + if (!distinctPeriodList.Contains(period)) + distinctPeriodList.Add(period); + } + periodList.Add(distinctPeriodList); + } + } + return periodList; + } + + /// + /// Gets an of all distinct timezone IDs in the list of periods. + /// + /// An of distinct timezone IDs. + internal IEnumerable GetDistinctTzIds() => Periods.Select(p => p.TzId).Distinct(); + + /// + /// Gets all unique values for a given timezone ID. + /// + /// The timezone ID to filter periods by. + /// An of unique values that match the specified timezone ID. + internal IEnumerable GetDistinctPeriodKindsByTzId(string? tzId) => + Periods.Where(p => p.TzId == tzId).Select(p => p.GetPeriodKind()).Distinct(); + + /// + /// Gets an of periods with the same for a given timezone ID. + /// + /// The timezone ID to filter periods by. + /// The to filter periods by. + /// An of periods that match the specified timezone ID and . + internal IEnumerable GetPeriodsByTzIdAndKind(string? tzId, PeriodKind periodKind) => + Periods.Where(p => p.TzId == tzId && p.GetPeriodKind() == periodKind); + + /// + [ExcludeFromCodeCoverage] + public IEnumerator GetEnumerator() => Periods.GetEnumerator(); + + /// + [ExcludeFromCodeCoverage] + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/Ical.Net/Collections/RecurrencePeriodCollection.cs b/Ical.Net/Collections/RecurrencePeriodCollection.cs new file mode 100644 index 00000000..6656b8c2 --- /dev/null +++ b/Ical.Net/Collections/RecurrencePeriodCollection.cs @@ -0,0 +1,90 @@ +// +// Copyright ical.net project maintainers and contributors. +// Licensed under the MIT license. +// + +#nullable enable +using System.Collections.Generic; +using Ical.Net.DataTypes; + +namespace Ical.Net.Collections; + +/// +/// Represents a collection of recurrence dates and periods for calendar events. +/// +/// This class is used to manage dates and periods that should be included as recurring events. +/// +/// +/// The main feature of this class is the method, which aggregates +/// and converts the exception and objects into a +/// list of objects. This method ensures that the periods are grouped +/// by their timezone IDs and period kinds, and that each contains only +/// distinct periods. +/// +/// +public class RecurrencePeriodCollection : PeriodCollectionBase +{ + /// + /// Initializes a new instance of the class. + /// + public RecurrencePeriodCollection() + { } + + /// + /// Initializes a new instance of the class + /// with a single . + /// + /// + public RecurrencePeriodCollection(CalDateTime dt) : this() + { + Add(dt); + } + + /// + /// Initializes a new instance of the class + /// with a list of objects. + /// + /// + public RecurrencePeriodCollection(IEnumerable periods) : this() + { + AddRange(periods); + } + + /// + /// Initializes a new instance of the class + /// with a list of objects. + /// + /// + public RecurrencePeriodCollection(IEnumerable dtList) : this() + { + AddRange(dtList); + } + + /// + /// Adds a to the collection. + /// + /// + /// This instance. + public RecurrencePeriodCollection Add(Period period) + { + Periods.Add(period); + return this; + } + + /// + /// Adds a list of s to the collection. + /// + /// + /// + public RecurrencePeriodCollection AddRange(IEnumerable periods) + { + Periods.AddRange(periods); + return this; + } + + /// + /// Aggregates and converts the recurrence s and s to a list of objects. + /// + /// A list of objects. + public List ToRecurrenceDates() => ToListOfPeriodList(); +} diff --git a/Ical.Net/DataTypes/PeriodList.cs b/Ical.Net/DataTypes/PeriodList.cs index 3b749006..67db696a 100644 --- a/Ical.Net/DataTypes/PeriodList.cs +++ b/Ical.Net/DataTypes/PeriodList.cs @@ -21,18 +21,6 @@ namespace Ical.Net.DataTypes; /// public class PeriodList : EncodableDataType, IList { - /// - /// Gets the timezone ID of the .
- /// The timezone of the first item added determines the timezone for the list. - ///
- internal string? TzId { get; private set; } - - /// - /// Gets the kind that this is representing.
- /// Only s with the same can be added to the list. - ///
- internal PeriodKind PeriodListKind { get; private set; } - /// /// Gets the number of s of the list. /// @@ -47,7 +35,6 @@ public class PeriodList : EncodableDataType, IList public PeriodList() { SetService(new PeriodListEvaluator(this)); - TzId = null; } /// @@ -62,6 +49,7 @@ private PeriodList(StringReader value) { CopyFrom(deserialized); } + SetService(new PeriodListEvaluator(this)); } @@ -72,19 +60,6 @@ private PeriodList(StringReader value) /// public static PeriodList FromStringReader(StringReader value) => new PeriodList(value); - /// - /// Creates a new instance of a class from an object, - /// using the timezone from the . - /// - /// - /// A new instance of the . - /// - public static PeriodList FromDateTime(IDateTime value) - { - var pl = new PeriodList().Add(value); - return pl; - } - /// public override void CopyFrom(ICopyable obj) { @@ -105,7 +80,7 @@ public override void CopyFrom(ICopyable obj) /// /// public override string? ToString() => new PeriodListSerializer().SerializeToString(this); - + /// /// Used for equality comparison of two lists of periods. /// @@ -125,11 +100,10 @@ public static Dictionary> GetGroupedPeriods(IList>(StringComparer.OrdinalIgnoreCase); foreach (var periodList in periodLists) { - // Dictionary key cannot be null, so an empty string is used for the default bucket - var defaultBucket = string.IsNullOrWhiteSpace(periodList.TzId) ? string.Empty : periodList.TzId; foreach (var period in periodList) { - var bucketTzId = period.StartTime.TzId ?? defaultBucket; + // Dictionary key cannot be null, so an empty string is used for the default bucket + var bucketTzId = period.StartTime.TzId ?? string.Empty; if (!grouped.TryGetValue(bucketTzId, out var periods)) { @@ -144,8 +118,7 @@ public static Dictionary> GetGroupedPeriods(IList k.Key, v => (IList) v.Value.OrderBy(d => d.StartTime).ToList()); } - protected bool Equals(PeriodList other) => string.Equals(TzId, other.TzId, StringComparison.OrdinalIgnoreCase) - && CollectionHelpers.Equals(Periods, other.Periods); + protected bool Equals(PeriodList other) => CollectionHelpers.Equals(Periods, other.Periods); /// public override bool Equals(object? obj) @@ -156,15 +129,7 @@ public override bool Equals(object? obj) } /// - public override int GetHashCode() - { - unchecked - { - var hashCode = TzId?.GetHashCode() ?? 0; - hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Periods); - return hashCode; - } - } + public override int GetHashCode() => CollectionHelpers.GetHashCode(Periods); /// public Period this[int index] @@ -189,12 +154,11 @@ public Period this[int index] public void RemoveAt(int index) => Periods.RemoveAt(index); /// - /// Adds a for an 'RDATE' to the list.
- /// The timezone of the first value added determines the timezone for the list. + /// Adds a to the list.
+ /// The timezone period kind of the first value added determines the timezone for the whole list. /// - /// To add an 'EXDATE', use the method instead, - /// because s are not permitted for 'EXDATE' according to - /// RFC 5545 section 3.8.5.1. + /// Use and + /// to simplify creating 'EXDATE' and 'RDATE' properties. ///
/// The for an 'RDATE'. /// @@ -206,33 +170,18 @@ public void Add(Period item) /// /// Adds a DATE or DATE-TIME value for an 'EXDATE' or 'RDATE' to the list.
- /// The timezone of the first value added determines the timezone for the list. + /// The timezone period kind of the first value added determines the timezone for the whole list. /// - /// To add an 'RDATE' , use the method instead. + /// Use and + /// to simplify creating 'EXDATE' and 'RDATE' properties. ///
/// - /// This instance of the . /// - public PeriodList Add(IDateTime dt) + public void Add(IDateTime dt) { var p = new Period(dt); EnsureConsistentTimezoneAndPeriodKind(p); Periods.Add(p); - return this; - } - - /// - /// Adds a for an 'RDATE' to the list.
- /// The timezone of the first value added determines the timezone for the list. - ///
- /// - /// This instance of the . - /// - public PeriodList AddPeriod(Period item) - { - EnsureConsistentTimezoneAndPeriodKind(item); - Add(item); - return this; } /// @@ -246,24 +195,21 @@ public PeriodList AddPeriod(Period item) /// public IEnumerator GetEnumerator() => Periods.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Periods.GetEnumerator(); - + private void EnsureConsistentTimezoneAndPeriodKind(Period p) { - if (Count != 0 && p.GetPeriodKind() != PeriodListKind) - { - throw new ArgumentException($"All Periods of a PeriodList must have the same value type. Current ValueType: {PeriodListKind}, Provided ValueType: {p.GetPeriodKind()}"); - } - - if (Count != 0 && p.TzId != TzId) + if (Count != 0 && p.GetPeriodKind() != Periods[0].GetPeriodKind()) { - throw new ArgumentException($"All Periods of a PeriodList must have the same timezone. Current TzId: {TzId}, Provided TzId: {p.TzId}"); + throw new ArgumentException( + $"All Periods of a PeriodList must be of the same period kind. Current Kind: {Periods[0].GetPeriodKind()}, Provided Kind: {p.GetPeriodKind()}"); } - if (Count == 0) + if (Count != 0 && p.TzId != Periods[0].TzId) { - TzId = p.StartTime.TzId; - PeriodListKind = p.GetPeriodKind(); + throw new ArgumentException( + $"All Periods of a PeriodList must have the same timezone. Current TzId: {Periods[0].TzId}, Provided TzId: {p.TzId}"); } } } diff --git a/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs b/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs index 973bd453..5f78e190 100644 --- a/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Ical.Net.DataTypes; namespace Ical.Net.Serialization.DataTypes; @@ -37,12 +38,12 @@ public PeriodListSerializer(SerializationContext ctx) : base(ctx) { } var parts = new List(periodList.Count); // Set TzId before ValueType, so that it serializes first - if (!string.IsNullOrWhiteSpace(periodList.TzId)) + if (periodList.FirstOrDefault(pl => !string.IsNullOrEmpty(pl.TzId) ) != null) { - periodList.Parameters.Set("TZID", periodList.TzId); + periodList.Parameters.Set("TZID", periodList[0].TzId); } - if (periodList.PeriodListKind == PeriodKind.Period) + if (periodList.FirstOrDefault(pl => pl.GetPeriodKind() == PeriodKind.Period) != null) { periodList.SetValueType("PERIOD"); } diff --git a/Ical.Net/Serialization/PropertySerializer.cs b/Ical.Net/Serialization/PropertySerializer.cs index 09d474be..33601a53 100644 --- a/Ical.Net/Serialization/PropertySerializer.cs +++ b/Ical.Net/Serialization/PropertySerializer.cs @@ -64,14 +64,15 @@ public PropertySerializer(SerializationContext ctx) : base(ctx) { } parameterList = ((ICalendarDataType) v).Parameters; } - // This says that the TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. There's nothing in the spec that - // prohibits having multiple EXDATE or RDATE collections, each of which specifies a different TZID. - // What *should* happen during serialization is that we should work with a single collection of zoned datetime objects, and we should create distinct RDATE and EXDATE collections based on - // those values. - if (v is PeriodList { TzId: not null } periodList && periodList.TzId != "UTC" && + // The TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. + // It is allowed to have multiple EXDATE or RDATE collections, each with a different TZID. + // Using RecurrencePeriodCollection and ExceptionDateCollection ensures, that all Periods in the + // PeriodList have the same TZID and PeriodKind. Thus each PeriodList be serialized in one go. + // Here, to determine the timezone, we can safely use the first Period's timezone. + if (v is PeriodList periodList && periodList[0].TzId != null && periodList[0].TzId != "UTC" && parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) { - parameterList.Set("TZID", periodList.TzId); + parameterList.Set("TZID", periodList[0].TzId); } var sb = new StringBuilder();