diff --git a/Ical.Net.Tests/CalendarPropertiesTest.cs b/Ical.Net.Tests/CalendarPropertiesTest.cs
index f416bb93..1549cca9 100644
--- a/Ical.Net.Tests/CalendarPropertiesTest.cs
+++ b/Ical.Net.Tests/CalendarPropertiesTest.cs
@@ -61,4 +61,4 @@ public void PropertySetValueMustAllowNull()
var property = new CalendarProperty();
Assert.DoesNotThrow(() => property.SetValue(null));
}
-}
\ No newline at end of file
+}
diff --git a/Ical.Net.Tests/PeriodListWrapperTests.cs b/Ical.Net.Tests/PeriodListWrapperTests.cs
new file mode 100644
index 00000000..211cf71b
--- /dev/null
+++ b/Ical.Net.Tests/PeriodListWrapperTests.cs
@@ -0,0 +1,349 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+#nullable enable
+using System.Linq;
+using Ical.Net.CalendarComponents;
+using Ical.Net.DataTypes;
+using Ical.Net.Serialization;
+using NUnit.Framework;
+
+namespace Ical.Net.Tests;
+
+[TestFixture]
+public class PeriodListWrapperTests
+{
+ #region ** ExceptionDates **
+
+ [Test]
+ public void AddExDateTime_ShouldCreate_DedicatePeriodList()
+ {
+ var cal = new Calendar();
+ var evt = new CalendarEvent();
+ cal.Events.Add(evt);
+ var exDates = new ExceptionDates(evt.ExceptionDates);
+
+ exDates // Add date-only
+ .Add(new CalDateTime(2025, 1, 1))
+ .Add(new CalDateTime(2025, 1, 2))
+ // duplicate
+ .Add(new CalDateTime(2025, 1, 2))
+ // Should go to a new PeriodList
+ .Add(new CalDateTime(2025, 1, 2, 14, 0, 0, CalDateTime.UtcTzId));
+
+ exDates.AddRange([ // Add date-time
+ new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin"),
+ new CalDateTime(2025, 1, 1, 10, 11, 13, "Europe/Berlin"),
+ // duplicate
+ new CalDateTime(2025, 1, 1, 10, 11, 13, "Europe/Berlin")
+ ]);
+
+ var serialized = new CalendarSerializer(cal).SerializeToString();
+
+ Assert.Multiple(() =>
+ {
+ // 2 dedicate PeriodList objects
+ Assert.That(evt.ExceptionDates, Has.Count.EqualTo(3));
+
+ // First PeriodList is date-only
+ Assert.That(evt.ExceptionDates[0], Has.Count.EqualTo(2));
+ Assert.That(evt.ExceptionDates[0].TzId, Is.Null);
+ Assert.That(evt.ExceptionDates[0].PeriodKind, Is.EqualTo(PeriodKind.DateOnly));
+
+ // Second PeriodList is date-time UTC
+ Assert.That(evt.ExceptionDates[1], Has.Count.EqualTo(1));
+ Assert.That(evt.ExceptionDates[1].TzId, Is.EqualTo(CalDateTime.UtcTzId));
+ Assert.That(evt.ExceptionDates[1].PeriodKind, Is.EqualTo(PeriodKind.DateTime));
+
+ // Second PeriodList is date-time
+ Assert.That(evt.ExceptionDates[2], Has.Count.EqualTo(2));
+ Assert.That(evt.ExceptionDates[2].TzId, Is.EqualTo("Europe/Berlin"));
+ Assert.That(evt.ExceptionDates[2].PeriodKind, Is.EqualTo(PeriodKind.DateTime));
+
+ Assert.That(serialized,
+ Does.Contain("""
+ EXDATE;VALUE=DATE:20250101,20250102
+ EXDATE:20250102T140000Z
+ EXDATE;TZID=Europe/Berlin:20250101T101112,20250101T101113
+ """));
+
+ // A flattened list of all dates
+ Assert.That(exDates.GetAllDates().Count(), Is.EqualTo(5));
+ });
+ }
+
+ [Test]
+ public void RemoveExDateTime_ShouldRemove_FromPeriodList()
+ {
+ var evt = new CalendarEvent();
+ var exDates = new ExceptionDates(evt.ExceptionDates);
+
+ var dateOnly = new CalDateTime(2025, 1, 1);
+ var dateTime = new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin");
+
+ exDates.Add(dateOnly);
+ exDates.Add(dateOnly.AddDays(1));
+ exDates.Add(dateTime);
+
+ var dateTimeSuccess = exDates.Remove(dateTime);
+ var dateOnlySuccess = exDates.Remove(dateOnly);
+ var dateOnlyFail = !exDates.Remove(dateOnly); // already removed
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(dateOnlySuccess, Is.True);
+ Assert.That(dateTimeSuccess, Is.True);
+ Assert.That(dateOnlyFail, Is.True);
+ Assert.That(evt.ExceptionDates[0], Has.Count.EqualTo(1));
+ Assert.That(evt.ExceptionDates[1], Is.Empty);
+ // Empty lists should work as well
+ evt.ExceptionDates.Clear();
+ Assert.That(() => exDates.Remove(dateTime), Is.False);
+ });
+ }
+
+ #endregion
+
+ #region ** RecurrenceDates **
+
+ [Test]
+ public void AddRDateTime_ShouldCreate_DedicatePeriodList()
+ {
+ var cal = new Calendar();
+ var evt = new CalendarEvent();
+ cal.Events.Add(evt);
+ var recDates = new RecurrenceDates(evt.RecurrenceDates);
+
+ recDates // Add date-only
+ .Add(new CalDateTime(2025, 1, 1))
+ .Add(new CalDateTime(2025, 1, 2))
+ .Add(new CalDateTime(2025, 1, 2)); // duplicate
+
+ recDates.AddRange([ // Add date-time
+ new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin"),
+ new CalDateTime(2025, 1, 1, 10, 11, 13, "Europe/Berlin"),
+ new CalDateTime(2025, 1, 1, 10, 11, 13, "Europe/Berlin") // duplicate
+ ]);
+
+ var serialized = new CalendarSerializer(cal).SerializeToString();
+
+ Assert.Multiple(() =>
+ {
+ // 2 dedicate PeriodList objects
+ Assert.That(evt.RecurrenceDates, Has.Count.EqualTo(2));
+
+ // First PeriodList is date-only
+ Assert.That(evt.RecurrenceDates[0], Has.Count.EqualTo(2));
+ Assert.That(evt.RecurrenceDates[0].TzId, Is.Null);
+ Assert.That(evt.RecurrenceDates[0].PeriodKind, Is.EqualTo(PeriodKind.DateOnly));
+
+ // Third PeriodList is date-time
+ Assert.That(evt.RecurrenceDates[1], Has.Count.EqualTo(2));
+ Assert.That(evt.RecurrenceDates[1].TzId, Is.EqualTo("Europe/Berlin"));
+ Assert.That(evt.RecurrenceDates[1].PeriodKind, Is.EqualTo(PeriodKind.DateTime));
+
+ Assert.That(serialized,
+ Does.Contain("""
+ RDATE;VALUE=DATE:20250101,20250102
+ RDATE;TZID=Europe/Berlin:20250101T101112,20250101T101113
+ """));
+
+ // A flattened list of all dates
+ Assert.That(recDates.GetAllDates().Count(), Is.EqualTo(4));
+ });
+ }
+
+ [Test]
+ public void AddRPeriod_ShouldCreate_DedicatePeriodList()
+ {
+ var cal = new Calendar();
+ var evt = new CalendarEvent();
+ cal.Events.Add(evt);
+ var recPeriod = new RecurrenceDates(evt.RecurrenceDates);
+
+ recPeriod
+ // Add date-only period
+ .Add(Period.Create(new CalDateTime(2025, 1, 2), new CalDateTime(2025, 1, 5)))
+ // Add zoned date-time period
+ .Add(Period.Create(new CalDateTime(2025, 2, 2, 0, 0, 0, CalDateTime.UtcTzId),
+ new CalDateTime(2025, 2, 2, 6, 0, 0, CalDateTime.UtcTzId)))
+ // duplicate
+ .Add(Period.Create(new CalDateTime(2025, 2, 2, 0, 0, 0, CalDateTime.UtcTzId),
+ new CalDateTime(2025, 2, 2, 6, 0, 0, CalDateTime.UtcTzId)));
+
+ recPeriod.AddRange([
+ // Add date-only period with end time
+ Period.Create(new CalDateTime(2025, 5, 1), new CalDateTime(2025, 5, 10)),
+ // Add zoned date-time period with end time
+ Period.Create(new CalDateTime(2025, 6, 1, 12, 0, 0, CalDateTime.UtcTzId), new CalDateTime(2025, 6, 1, 14, 0, 0, CalDateTime.UtcTzId)),
+ // duplicate
+ Period.Create(new CalDateTime(2025, 6, 1, 12, 0, 0, CalDateTime.UtcTzId), new CalDateTime(2025, 6, 1, 14, 0, 0, CalDateTime.UtcTzId)),
+ // Add date-only with duration
+ Period.Create(new CalDateTime(2025, 5, 1), duration: Duration.FromDays(9)),
+ // Add zoned date-time period with duration
+ Period.Create(new CalDateTime(2025, 6, 1, 12, 0, 0, "Europe/Vienna"), duration: Duration.FromHours(8))
+ ]);
+
+ var serializer = new CalendarSerializer(cal);
+ var serialized = serializer.SerializeToString();
+ // Assign the deserialized event
+ cal = Calendar.Load(serialized);
+ evt = cal.Events[0];
+
+ // Assert the serialized string and the deserialized event
+ Assert.Multiple(() =>
+ {
+ // 2 dedicate PeriodList objects
+ Assert.That(evt.RecurrenceDates, Has.Count.EqualTo(3));
+
+ // First PeriodList has date-only periods
+ Assert.That(evt.RecurrenceDates[0], Has.Count.EqualTo(3));
+ Assert.That(evt.RecurrenceDates[0].TzId, Is.Null);
+ Assert.That(evt.RecurrenceDates[0].PeriodKind, Is.EqualTo(PeriodKind.Period));
+
+ // Second PeriodList has UTC date-time periods
+ Assert.That(evt.RecurrenceDates[1], Has.Count.EqualTo(2));
+ Assert.That(evt.RecurrenceDates[1].TzId, Is.EqualTo("UTC"));
+ Assert.That(evt.RecurrenceDates[1].PeriodKind, Is.EqualTo(PeriodKind.Period));
+
+ // Third PeriodList has zoned date-time with duration
+ Assert.That(evt.RecurrenceDates[2], Has.Count.EqualTo(1));
+ Assert.That(evt.RecurrenceDates[2].TzId, Is.EqualTo("Europe/Vienna"));
+ Assert.That(evt.RecurrenceDates[2].PeriodKind, Is.EqualTo(PeriodKind.Period));
+
+ Assert.That(serialized,
+ Does.Contain("""
+ RDATE;VALUE=PERIOD:20250102/20250105,20250501/20250510,20250501/P9D
+ RDATE;VALUE=PERIOD:20250202T000000Z/20250202T060000Z,20250601T120000Z/2025
+ 0601T140000Z
+ RDATE;TZID=Europe/Vienna;VALUE=PERIOD:20250601T120000/PT8H
+ """));
+
+ // A flattened list of all dates
+ Assert.That(recPeriod.GetAllDates().Count(), Is.EqualTo(0));
+ // A flattened list of all periods
+ Assert.That(recPeriod.GetAllPeriods().Count(), Is.EqualTo(6));
+ });
+ }
+
+ [Test]
+ public void RemoveRDateTime_ShouldRemove_FromPeriodList()
+ {
+ var evt = new CalendarEvent();
+ var recDates = new RecurrenceDates(evt.RecurrenceDates);
+
+ var period1 = new Period(new CalDateTime(2025, 1, 1), Duration.FromDays(5));
+ var period2 = new Period(new CalDateTime(2025, 1, 1, 10, 0, 0, "Europe/Berlin"), Duration.FromHours(6));
+
+ recDates.Add(period1).Add(period2);
+ recDates.Add(new Period(period1.StartTime.AddDays(1), Duration.FromDays(5)));
+
+ var period1Success = recDates.Remove(period1);
+ var period2Success = recDates.Remove(period2);
+ var period2Fail = !recDates.Remove(period2); // already removed
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(period2Success, Is.True);
+ Assert.That(period1Success, Is.True);
+ Assert.That(period2Fail, Is.True);
+ Assert.That(evt.RecurrenceDates[0], Has.Count.EqualTo(1));
+ Assert.That(evt.RecurrenceDates[1], Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Contains_ShouldReturnTrue_IfPeriodExists()
+ {
+ var evt = new CalendarEvent();
+ var recDates = new RecurrenceDates(evt.RecurrenceDates);
+
+ var period1 = new Period(new CalDateTime(2025, 1, 1), Duration.FromDays(5));
+ var period2 = new Period(new CalDateTime(2025, 1, 1, 10, 0, 0, "Europe/Berlin"), Duration.FromHours(6));
+
+ recDates.Add(period1).Add(period2);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(recDates.Contains(period1), Is.True);
+ Assert.That(recDates.Contains(period2), Is.True);
+ });
+ }
+
+ [Test]
+ public void Contains_ShouldReturnFalse_IfPeriodDoesNotExist()
+ {
+ var evt = new CalendarEvent();
+ var recDates = new RecurrenceDates(evt.RecurrenceDates);
+
+ var period1 = new Period(new CalDateTime(2025, 1, 1), Duration.FromDays(5));
+ var period2 = new Period(new CalDateTime(2025, 1, 1, 10, 0, 0, "Europe/Berlin"), Duration.FromHours(6));
+
+ recDates.AddRange([period1, period2]);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(recDates.Contains(new Period(period1.StartTime.AddDays(1), Duration.FromDays(5))), Is.False);
+ Assert.That(recDates.Contains(new Period(period2.StartTime.AddDays(1), Duration.FromHours(6))), Is.False);
+ });
+ }
+
+ #endregion
+
+ #region ** PeriodListWrapperBase **
+
+ [Test]
+ public void Clear_ShouldRemoveAllPeriods()
+ {
+ var evt = new CalendarEvent();
+ var exDates = new ExceptionDates(evt.ExceptionDates);
+
+ exDates
+ .Add(new CalDateTime(2025, 1, 1))
+ .Add(new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin"));
+
+ exDates.Clear();
+
+ Assert.That(evt.ExceptionDates, Is.Empty);
+ }
+
+ [Test]
+ public void Contains_ShouldReturnTrue_IfDateExists()
+ {
+ var evt = new CalendarEvent();
+ var exDates = new ExceptionDates(evt.ExceptionDates);
+
+ var dateOnly = new CalDateTime(2025, 1, 1);
+ var dateTime = new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin");
+
+ exDates.Add(dateOnly).Add(dateTime);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exDates.Contains(dateOnly), Is.True);
+ Assert.That(exDates.Contains(dateTime), Is.True);
+ });
+ }
+
+ [Test]
+ public void Contains_ShouldReturnFalse_IfDateDoesNotExist()
+ {
+ var evt = new CalendarEvent();
+ var exDates = new ExceptionDates(evt.ExceptionDates);
+
+ var dateOnly = new CalDateTime(2025, 1, 1);
+ var dateTime = new CalDateTime(2025, 1, 1, 10, 11, 12, "Europe/Berlin");
+
+ exDates.AddRange([dateOnly, dateTime]);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exDates.Contains(dateOnly.AddDays(1)), Is.False);
+ Assert.That(exDates.Contains(dateTime.AddDays(1)), Is.False);
+ });
+ }
+
+ #endregion
+}
diff --git a/Ical.Net/DataTypes/ExceptionDates.cs b/Ical.Net/DataTypes/ExceptionDates.cs
new file mode 100644
index 00000000..21a3913f
--- /dev/null
+++ b/Ical.Net/DataTypes/ExceptionDates.cs
@@ -0,0 +1,54 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ical.Net.DataTypes;
+
+///
+/// This class is used to manage ICalendar EXDATE properties, which can be date-time and date-only.
+///
+/// The class is a wrapper around a list of PeriodList objects.
+/// Specifically, it is used to group periods by their TzId, PeriodKind and date-time/date-only
+/// in way that serialization conforms to the RFC 5545 standard.
+///
+///
+public class ExceptionDates : PeriodListWrapperBase
+{
+ internal ExceptionDates(IList listOfPeriodList) : base(listOfPeriodList)
+ { }
+
+ ///
+ /// Adds a date to the list, if it doesn't already exist.
+ ///
+ public ExceptionDates Add(IDateTime dt)
+ {
+ var periodList = GetOrCreate(dt);
+
+ // Don't add the same date twice.
+ if (periodList.FirstOrDefault(period => Equals(period.StartTime, dt)) != null)
+ return this;
+
+ var dtPeriod = new Period(dt);
+ periodList.Add(dtPeriod);
+
+ return this;
+ }
+
+ ///
+ /// Adds a range of dates to the list, if they don't already exist.
+ ///
+ public ExceptionDates AddRange(IEnumerable dates)
+ {
+ foreach (var dt in dates)
+ {
+ Add(dt);
+ }
+
+ return this;
+ }
+}
diff --git a/Ical.Net/DataTypes/PeriodList.cs b/Ical.Net/DataTypes/PeriodList.cs
index 5a006830..7698e624 100644
--- a/Ical.Net/DataTypes/PeriodList.cs
+++ b/Ical.Net/DataTypes/PeriodList.cs
@@ -21,6 +21,10 @@ namespace Ical.Net.DataTypes;
///
public class PeriodList : EncodableDataType, IList
{
+ internal PeriodKind PeriodKind => Count == 0 ? PeriodKind.Undefined : Periods[0].PeriodKind;
+
+ internal string? TzId => Count == 0 ? null : Periods[0].TzId;
+
///
/// Gets the number of s of the list.
///
@@ -135,7 +139,11 @@ public override bool Equals(object? obj)
public Period this[int index]
{
get => Periods[index];
- set => Periods[index] = value;
+ set
+ {
+ EnsureConsistentTimezoneAndPeriodKind(value);
+ Periods[index] = value;
+ }
}
///
@@ -148,7 +156,11 @@ public Period this[int index]
public int IndexOf(Period item) => Periods.IndexOf(item);
///
- public void Insert(int index, Period item) => Periods.Insert(index, item);
+ public void Insert(int index, Period item)
+ {
+ EnsureConsistentTimezoneAndPeriodKind(item);
+ Periods.Insert(index, item);
+ }
///
public void RemoveAt(int index) => Periods.RemoveAt(index);
diff --git a/Ical.Net/DataTypes/PeriodListWrapperBase.cs b/Ical.Net/DataTypes/PeriodListWrapperBase.cs
new file mode 100644
index 00000000..bf3f22be
--- /dev/null
+++ b/Ical.Net/DataTypes/PeriodListWrapperBase.cs
@@ -0,0 +1,102 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ical.Net.DataTypes;
+
+/// This base class is used to manage ICalendar EXDATE and RATE properties.
+///
+/// The class is a wrapper around a list of PeriodList objects.
+/// Specifically, it is used to group periods by their TzId and PeriodKind
+/// is way that serialization conforms to the RFC 5545 standard.
+///
+public abstract class PeriodListWrapperBase
+{
+ protected IList ListOfPeriodList;
+
+ private protected PeriodListWrapperBase(IList periodList) => ListOfPeriodList = periodList;
+
+ ///
+ /// Gets a flattened list of all dates in the list.
+ ///
+ public IEnumerable GetAllDates()
+ => ListOfPeriodList.SelectMany(pl => pl.Where(p => p.PeriodKind is PeriodKind.DateOnly or PeriodKind.DateTime).Select(p => p.StartTime));
+
+ ///
+ /// Clears all elements from the list.
+ ///
+ public void Clear() => ListOfPeriodList.Clear();
+
+ ///
+ /// Determines whether the list contains the .
+ ///
+ public bool Contains(IDateTime dt)
+ {
+ var periodList = GetPeriodListForTzIdKind(dt);
+
+ return periodList?
+ .FirstOrDefault(period => Equals(period.StartTime, dt)) != null;
+ }
+
+ ///
+ public bool Remove(IDateTime dt)
+ {
+ var periodList = GetPeriodListForTzIdKind(dt);
+
+ if (periodList == null) return false;
+
+ var dtPeriod = new Period(dt);
+
+ return periodList.Remove(dtPeriod);
+ }
+
+ protected PeriodList GetOrCreate(IDateTime dt)
+ => GetOrCreatePeriodList(dt);
+
+ protected PeriodList GetOrCreate(Period period)
+ => GetOrCreatePeriodList(period);
+
+ protected PeriodList GetOrCreatePeriodList(IDateTime dt)
+ {
+ // The number of periods is expected to be small, so a linear search is acceptable.
+ var periodList = GetPeriodListForTzIdKind(dt);
+
+ if (periodList != null) return periodList;
+
+ periodList = new PeriodList();
+ ListOfPeriodList.Add(periodList);
+ return periodList;
+ }
+
+ protected PeriodList GetOrCreatePeriodList(Period period)
+ {
+ // The number of periods is expected to be small, so a linear search is acceptable.
+ var periodList = GetPeriodListForTzIdKind(period);
+
+ if (periodList != null) return periodList;
+
+ periodList = new PeriodList();
+ ListOfPeriodList.Add(periodList);
+ return periodList;
+ }
+
+ protected PeriodList? GetPeriodListForTzIdKind(IDateTime dt)
+ {
+ return ListOfPeriodList
+ .FirstOrDefault(p =>
+ p.TzId == dt.TzId
+ && p.PeriodKind == (dt.HasTime ? PeriodKind.DateTime : PeriodKind.DateOnly));
+ }
+
+ protected PeriodList? GetPeriodListForTzIdKind(Period period)
+ {
+ return ListOfPeriodList
+ .FirstOrDefault(p =>
+ p.TzId == period.TzId && p.PeriodKind == period.PeriodKind && p[0].StartTime.HasTime == period.StartTime.HasTime);
+ }
+}
diff --git a/Ical.Net/DataTypes/RecurrenceDates.cs b/Ical.Net/DataTypes/RecurrenceDates.cs
new file mode 100644
index 00000000..d7706d75
--- /dev/null
+++ b/Ical.Net/DataTypes/RecurrenceDates.cs
@@ -0,0 +1,110 @@
+//
+// Copyright ical.net project maintainers and contributors.
+// Licensed under the MIT license.
+//
+
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ical.Net.DataTypes;
+
+///
+/// This class is used to manage ICalendar RDATE properties, which can be date-time, date-only and period.
+///
+/// The class is a wrapper around a list of PeriodList objects.
+/// Specifically, it is used to group periods by their TzId, PeriodKind and date-time/date-only
+/// in way that serialization conforms to the RFC 5545 standard.
+///
+///
+public class RecurrenceDates : PeriodListWrapperBase
+{
+ internal RecurrenceDates(IList listOfPeriodList) : base(listOfPeriodList)
+ { }
+
+ ///
+ /// Adds a date to the list, if it doesn't already exist.
+ ///
+ public RecurrenceDates Add(IDateTime dt)
+ {
+ var periodList = GetOrCreate(dt);
+
+ // Don't add the same date twice.
+ if (periodList.FirstOrDefault(period => Equals(period.StartTime, dt)) != null)
+ return this;
+
+ var dtPeriod = new Period(dt);
+ periodList.Add(dtPeriod);
+
+ return this;
+ }
+
+ ///
+ /// Adds a period to the list, if it doesn't already exist.
+ ///
+ public RecurrenceDates Add(Period period)
+ {
+ var periodList = GetOrCreate(period);
+
+ // Don't add the same period twice.
+ if (periodList.FirstOrDefault(p => Equals(p, period)) != null)
+ return this;
+
+ periodList.Add(period);
+
+ return this;
+ }
+
+ ///
+ /// Adds a range of dates to the list, if they don't already exist.
+ ///
+ public RecurrenceDates AddRange(IEnumerable dates)
+ {
+ foreach (var dt in dates)
+ {
+ Add(dt);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Adds a range of periods to the list, if they don't already exist.
+ ///
+ public RecurrenceDates AddRange(IEnumerable periods)
+ {
+ foreach (var period in periods)
+ {
+ Add(period);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Determines whether the list contains the .
+ ///
+ public bool Contains(Period period)
+ {
+ var periodList = GetPeriodListForTzIdKind(period);
+
+ return periodList?
+ .FirstOrDefault(p => Equals(p, period)) != null;
+ }
+
+ ///
+ public bool Remove(Period period)
+ {
+ var periodList = GetPeriodListForTzIdKind(period);
+
+ if (periodList == null) return false;
+
+ return periodList.Remove(period);
+ }
+
+ ///
+ /// Gets a flattened list of all periods in the list.
+ ///
+ public IEnumerable GetAllPeriods()
+ => ListOfPeriodList.SelectMany(pl => pl.Where(p => p.PeriodKind is PeriodKind.Period));
+}
diff --git a/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs b/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs
index 80a35533..0bedd905 100644
--- a/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs
+++ b/Ical.Net/Serialization/DataTypes/PeriodListSerializer.cs
@@ -40,7 +40,7 @@ public PeriodListSerializer(SerializationContext ctx) : base(ctx) { }
var firstPeriod = periodList.FirstOrDefault();
// Set TzId before ValueType, so that it serializes first
- if (firstPeriod != null && !string.IsNullOrEmpty(firstPeriod.TzId))
+ if (firstPeriod != null && !string.IsNullOrEmpty(firstPeriod.TzId) && firstPeriod.TzId != "UTC")
{
periodList.Parameters.Set("TZID", periodList[0].TzId);
}
diff --git a/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs b/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs
index cd72255f..374ef334 100644
--- a/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs
+++ b/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs
@@ -51,17 +51,17 @@ public PeriodSerializer(SerializationContext ctx) : base(ctx) { }
// "DTEND" nor "DURATION" property, the event’s duration is taken to
// be one day:
- if (p.EndTime is { HasTime: true })
+ if (p.EndTime is { } endtime)
{
// Serialize the end date and time...
sb.Append('/');
- sb.Append(dtSerializer.SerializeToString(p.EndTime));
+ sb.Append(dtSerializer.SerializeToString(endtime));
}
- if (p.Duration != null)
+ if (p.Duration is { } duration)
{
// Serialize the duration
sb.Append('/');
- sb.Append(durationSerializer.SerializeToString(p.Duration));
+ sb.Append(durationSerializer.SerializeToString(duration));
}
// else, just the start time gets serialized to comply with the RFC 5545 section 3.6.1