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