Skip to content

Commit

Permalink
Add new collection helper classes to create EXDATE and RDATE
Browse files Browse the repository at this point in the history
- Introduced `ExceptionDateCollection`, `RecurrencePeriodCollection` and PeriodCollectionBase` classes.
- Updated serializers to handle the modified `PeriodList` implementation.

The main feature of these classes are the `ToRecurrenceDates` and `ToRecurrenceDates` methods.

They aggregate and convert the exception or recurrence `CalDateTime` and `Period` objects into a list of `PeriodList` objects. This ensures proper serialization, because the periods are grouped by their timezone IDs and period kinds in a way, that each `PeriodList` contains only
distinct periods.
  • Loading branch information
axunonb committed Dec 31, 2024
1 parent 8532029 commit 76a047e
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 126 deletions.
18 changes: 12 additions & 6 deletions Ical.Net.Tests/PeriodListTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions Ical.Net.Tests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3201,10 +3201,10 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange()
/// Evaluate relevancy and validity of the request.
/// Find a solution for issue #120 or close forever
/// </summary>
[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
Expand Down
6 changes: 5 additions & 1 deletion Ical.Net.Tests/RecurrenceWithExDateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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);
Expand Down
95 changes: 63 additions & 32 deletions Ical.Net.Tests/RecurrenceWithRDateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PeriodList> { recurrenceDates }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand All @@ -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
{
Expand Down Expand Up @@ -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))
Expand All @@ -95,7 +99,7 @@ public void RDate_MultipleDates_WithTimeZones_AreProcessedCorrectly()
{
Start = eventStart,
Duration = new Duration(hours: 2),
RecurrenceDates = new List<PeriodList> { recurrenceDates }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand All @@ -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))
Expand All @@ -131,7 +135,7 @@ public void RDate_PeriodsWithTimezone_AreProcessedCorrectly()
{
Start = eventStart,
Duration = new Duration(hours: 2),
RecurrenceDates = new List<PeriodList> { recurrenceDates }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand Down Expand Up @@ -176,20 +180,17 @@ 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))
};

var calendarEvent = new CalendarEvent
{
Start = eventStart,
Duration = new Duration(hours: 1),
RecurrenceDates = new List<PeriodList> { recurrenceDates1, recurrenceDates2 }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand All @@ -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<PeriodList> { recurrenceDates1, recurrenceDates2 }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand Down Expand Up @@ -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<PeriodList> { recurrenceDates1, recurrenceDates2 }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand All @@ -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))
Expand All @@ -306,7 +302,7 @@ public void RDate_OverlappingPeriods_AreProcessedCorrectly()
{
Start = eventStart,
Duration = new Duration(hours: 1),
RecurrenceDates = new List<PeriodList> { recurrenceDates }
RecurrenceDates = recDateCollection.ToRecurrenceDates()
};

cal.Events.Add(calendarEvent);
Expand Down Expand Up @@ -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()
{
Expand All @@ -377,7 +408,7 @@ public void AddingDifferentTimeZonesToPeriodList_ShouldThrow()
}

[Test]
public void AddingDifferentPeriodTypes_ShouldThrow()
public void AddingDifferentPeriodKinds_ShouldThrow()
{
Assert.Multiple(() =>
{
Expand Down
55 changes: 55 additions & 0 deletions Ical.Net/Collections/ExceptionDateCollection.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a collection of exception dates for calendar events.
/// <para>
/// This class is used to manage dates that should be excluded from recurring events.
/// </para>
/// <para>
/// The main feature of this class is the <see cref="ToExceptionDates"/> method, which aggregates
/// and converts the exception <see cref="CalDateTime"/> objects into a list of <see cref="PeriodList"/> objects.
/// This method ensures that the periods are grouped by their timezone IDs and period kinds, and that
/// each <see cref="PeriodList"/> contains only distinct periods.
/// </para>
/// </summary>
public class ExceptionDateCollection : PeriodCollectionBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionDateCollection"/> class.
/// </summary>
public ExceptionDateCollection()
{ }

/// <summary>
/// Initializes a new instance of the <see cref="ExceptionDateCollection"/> class with a single <see cref="CalDateTime"/>.
/// </summary>
/// <param name="dt">The <see cref="CalDateTime"/> to add to the collection.</param>
public ExceptionDateCollection(CalDateTime dt) : this()
{
Add(dt);
}

/// <summary>
/// Initializes a new instance of the <see cref="ExceptionDateCollection"/> class with a collection of <see cref="CalDateTime"/> objects.
/// </summary>
/// <param name="dtList">The collection of <see cref="CalDateTime"/> objects to add to the collection.</param>
public ExceptionDateCollection(IEnumerable<CalDateTime> dtList) : this()
{
AddRange(dtList);
}

/// <summary>
/// Aggregates and converts the exception <see cref="CalDateTime"/>s to a list of <see cref="PeriodList"/> objects.
/// </summary>
/// <returns>A list of <see cref="PeriodList"/> objects.</returns>
public List<PeriodList> ToExceptionDates() => ToListOfPeriodList();
}
Loading

0 comments on commit 76a047e

Please sign in to comment.