-
Notifications
You must be signed in to change notification settings - Fork 232
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add wrapper classes for
ExceptionDates
and RecurrenceDates
- Add internal properties `PeriodKind` and `TzId` to `PeriodList`. - Modify `Period` indexer and `Insert` to ensure consistency for timezone and `PeriodKind` - Prepare for `PeriodList` to become `internal` - Update `PeriodListSerializer` and `PeriodSerializer` classes for `Period` serialization - Introduce `ExceptionDates`, `RecurrenceDates`, and `PeriodListWrapperBase` to become wrappers around these properties of `RecurringComponent` - Add unit tests in `PeriodListWrapperTests` for `ExceptionDates` and `RecurrenceDates`. Note: The wrappers are not yet implemented for `IRecurringComponent`s.
- Loading branch information
Showing
8 changed files
with
623 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,337 @@ | ||
// | ||
// 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\r\nEXDATE:20250102T140000Z\r\nEXDATE;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\r\nRDATE;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\r\nRDATE;VALUE=PERIOD:20250202T000000Z/20250202T060000Z,20250601T120000Z/2025\r\n 0601T140000Z\r\nRDATE;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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// This class is used to manage ICalendar <c>EXDATE</c> properties, which can be date-time and date-only. | ||
/// <remarks> | ||
/// The class is a wrapper around a list of <c>PeriodList</c> objects. | ||
/// Specifically, it is used to group periods by their <c>TzId</c>, <c>PeriodKind</c> and <c>date-time/date-only</c> | ||
/// in way that serialization conforms to the RFC 5545 standard. | ||
/// </remarks> | ||
/// </summary> | ||
public class ExceptionDates : PeriodListWrapperBase | ||
{ | ||
internal ExceptionDates(IList<PeriodList> listOfPeriodList) : base(listOfPeriodList) | ||
{ } | ||
|
||
/// <summary> | ||
/// Adds a date to the list, if it doesn't already exist. | ||
/// </summary> | ||
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; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a range of dates to the list, if they don't already exist. | ||
/// </summary> | ||
public ExceptionDates AddRange(IEnumerable<IDateTime> dates) | ||
{ | ||
foreach (var dt in dates) | ||
{ | ||
Add(dt); | ||
} | ||
|
||
return this; | ||
} | ||
} |
Oops, something went wrong.