Skip to content

Commit

Permalink
Replace method PeriodList.GetGroupedPeriods
Browse files Browse the repository at this point in the history
- No duplicate `Period` instances can be added or inserted into the `PeriodList`.
- `RecurrenceDates.GetAllDates()`, `PeriodListWrapperBase.GetAllDates()`  and `PeriodListWrapperBase.GetAllPeriodsByKind` return a flattened and ordered list of distinct objects
- Update `CalendarEvent.Equals` and `CalendarEvent.GetHashCode` using `ExceptionDates` and `RecurrenceDates` instead of `PeriodList.GetGroupedPeriods`
- Remove `PeriodList.GetGroupedPeriods`
  • Loading branch information
axunonb committed Jan 11, 2025
1 parent f2e9c19 commit 14b0187
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 86 deletions.
48 changes: 47 additions & 1 deletion Ical.Net.Tests/PeriodListTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void GetSet_Period_ShouldReturnCorrectPeriod()
var period2 = new Period(new CalDateTime(2025, 2, 1, 0, 0, 0), Duration.FromHours(1));

periodList.Add(period1);
periodList.Add(period1);
periodList.Add(period2);

// Act
var retrievedPeriod = periodList[0];
Expand All @@ -54,6 +54,52 @@ public void GetSet_Period_ShouldReturnCorrectPeriod()
});
}

[Test]
public void ExistingPeriod_ShouldNotBeAddedAgain()
{
// Arrange
var periodList = new PeriodList();
var period = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0), Duration.FromHours(1));

periodList.Add(period);
periodList.Add(period);
periodList.Insert(0, period);

// Assert
Assert.That(periodList, Has.Count.EqualTo(1));
}

[Test]
public void AddPeriodWithInconsistentTimezoneOrPeriodKind_ShouldThrow()
{
// Arrange
var periodList = new PeriodList();
var dateTimeUtcPeriod = new Period(new CalDateTime(2023, 1, 1, 12, 0, 0, CalDateTime.UtcTzId));
var durationPeriod = new Period(new CalDateTime(2023, 1, 2, 12, 0, 0, CalDateTime.UtcTzId), new Duration(1, 0, 0, 0));
var dateTimeLocalPeriod = new Period(new CalDateTime(2023, 1, 2, 12, 0, 0, "America/New_York"));
var dateOnlyPeriod = new Period(new CalDateTime(2023, 1, 3));

// The first period determines timezone and period kind
periodList.Add(dateTimeUtcPeriod);

// Act & Assert
Assert.Multiple(() =>
{
Assert.That(periodList.Count, Is.EqualTo(1));

// Test adding a period with inconsistent PeriodKind
Assert.That(() => periodList.Add(durationPeriod), Throws.ArgumentException);

// Test adding a period with inconsistent TzId
Assert.That(() => periodList.Add(dateTimeLocalPeriod), Throws.ArgumentException);

// Test adding period with inconsistent PeriodKind
Assert.That(() => periodList.Insert(0, dateOnlyPeriod), Throws.ArgumentException);
;
Assert.That(periodList.Count, Is.EqualTo(1));
});

}
[Test]
public void Clear_ShouldRemoveAllPeriods()
{
Expand Down
31 changes: 10 additions & 21 deletions Ical.Net/CalendarComponents/CalendarEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,29 +285,18 @@ protected bool Equals(CalendarEvent? other)
return false;
}

// RDATEs and EXDATEs are all List<PeriodList>, because the spec allows for multiple declarations of collections.
// Consequently we have to contrive a normalized representation before we can determine whether two events are equal

var exDates = PeriodList.GetGroupedPeriods(ExceptionDatesPeriodLists);
var otherExDates = PeriodList.GetGroupedPeriods(other.ExceptionDatesPeriodLists);
if (exDates.Keys.Count != otherExDates.Keys.Count || !exDates.Keys.OrderBy(k => k).SequenceEqual(otherExDates.Keys.OrderBy(k => k)))
{
return false;
}

if (exDates.Any(exDate => !exDate.Value.OrderBy(d => d).SequenceEqual(otherExDates[exDate.Key].OrderBy(d => d))))
{
return false;
}

var rDates = PeriodList.GetGroupedPeriods(RecurrenceDatesPeriodLists);
var otherRDates = PeriodList.GetGroupedPeriods(other.RecurrenceDatesPeriodLists);
if (rDates.Keys.Count != otherRDates.Keys.Count || !rDates.Keys.OrderBy(k => k).SequenceEqual(otherRDates.Keys.OrderBy(k => k)))
// exDates and otherExDates are filled with a sorted list of distinct periods
var exDates = ExceptionDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime).ToList();
var otherExDates = other.ExceptionDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime).ToList();
if (exDates.Count != otherExDates.Count || !exDates.SequenceEqual(otherExDates))
{
return false;
}

if (rDates.Any(exDate => !exDate.Value.OrderBy(d => d).SequenceEqual(otherRDates[exDate.Key].OrderBy(d => d))))
// rDates and otherRDates are filled with a sorted list of distinct periods
var rDates = RecurrenceDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime).ToList();
var otherRDates = other.RecurrenceDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime).ToList(); ;
if (rDates.Count != otherRDates.Count && !rDates.SequenceEqual(otherRDates))
{
return false;
}
Expand Down Expand Up @@ -339,9 +328,9 @@ public override int GetHashCode()
hashCode = (hashCode * 397) ^ Transparency?.GetHashCode() ?? 0;
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Attachments);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(Resources);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCodeForNestedCollection(ExceptionDatesPeriodLists);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(ExceptionDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime));
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(ExceptionRules);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCodeForNestedCollection(RecurrenceDatesPeriodLists);
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(RecurrenceDates.GetAllPeriodsByKind(PeriodKind.Period, PeriodKind.DateOnly, PeriodKind.DateTime));
hashCode = (hashCode * 397) ^ CollectionHelpers.GetHashCode(RecurrenceRules);
return hashCode;
}
Expand Down
5 changes: 0 additions & 5 deletions Ical.Net/DataTypes/ExceptionDates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

#nullable enable
using System.Collections.Generic;
using System.Linq;

namespace Ical.Net.DataTypes;

Expand All @@ -29,10 +28,6 @@ public ExceptionDates Add(IDateTime dt)
{
var periodList = GetOrCreatePeriodList(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);

Expand Down
53 changes: 10 additions & 43 deletions Ical.Net/DataTypes/PeriodList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ical.Net.Evaluation;
using Ical.Net.Serialization.DataTypes;
using Ical.Net.Utility;
Expand Down Expand Up @@ -85,43 +84,6 @@ public override void CopyFrom(ICopyable obj)
/// <returns></returns>
public override string? ToString() => new PeriodListSerializer().SerializeToString(this);

/// <summary>
/// Used for equality comparison of two lists of periods.
/// </summary>
public static Dictionary<string, IList<Period>> GetGroupedPeriods(IList<PeriodList> periodLists)
{
// In order to know if two events are equal, a semantic understanding of exdates, rdates, rrules, and exrules is required. This could be done by
// computing the complete recurrence set (expensive) while being time-zone sensitive, or by comparing each List<Period> in each IPeriodList.

// For example, events containing these rules generate the same recurrence set, including having the same time zone for each occurrence, so
// they're the same:
// Event A:
// RDATE:20170302T060000Z,20170303T060000Z
// Event B:
// RDATE:20170302T060000Z
// RDATE:20170303T060000Z

var grouped = new Dictionary<string, HashSet<Period>>(StringComparer.OrdinalIgnoreCase);
foreach (var periodList in periodLists)
{
foreach (var period in periodList)
{
// 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))
{
periods = new HashSet<Period>();
grouped.Add(bucketTzId, periods);
}

periods.Add(period);
}
}

return grouped.ToDictionary(k => k.Key, v => (IList<Period>) v.Value.OrderBy(d => d.StartTime).ToList());
}

protected bool Equals(PeriodList other) => CollectionHelpers.Equals(Periods, other.Periods);

/// <inheritdoc/>
Expand Down Expand Up @@ -155,39 +117,44 @@ public Period this[int index]
/// <inheritdoc/>
public int IndexOf(Period item) => Periods.IndexOf(item);

/// <inheritdoc/>
/// <summary>
/// Inserts a <see cref="Period"/> to the list if it does not already exist.<br/>
/// The timezone period kind of the first value added determines the timezone for the whole list.
/// </summary>
/// <exception cref="ArgumentException"></exception>
public void Insert(int index, Period item)
{
EnsureConsistentTimezoneAndPeriodKind(item);
if (Periods.Contains(item)) return;
Periods.Insert(index, item);
}

/// <inheritdoc/>
public void RemoveAt(int index) => Periods.RemoveAt(index);

/// <summary>
/// Adds a <see cref="Period"/> to the list.<br/>
/// Adds a <see cref="Period"/> to the list if it does not already exist.<br/>
/// The timezone period kind of the first value added determines the timezone for the whole list.
/// </summary>
/// <param name="item">The <see cref="Period"/> for an 'RDATE'.</param>
/// <exception cref="ArgumentException"></exception>
public void Add(Period item)
{
EnsureConsistentTimezoneAndPeriodKind(item);
if (Periods.Contains(item)) return;
Periods.Add(item);
}

/// <summary>
/// Adds a DATE or DATE-TIME value for an 'EXDATE' or 'RDATE' to the list.<br/>
/// Adds a DATE or DATE-TIME value for an 'EXDATE' or 'RDATE' to the list if it does not already exist.<br/>
/// The timezone period kind of the first value added determines the timezone for the whole list.
/// </summary>
/// <param name="dt"></param>
/// <exception cref="ArgumentException"></exception>
public void Add(IDateTime dt)
{
var p = new Period(dt);
EnsureConsistentTimezoneAndPeriodKind(p);
Periods.Add(p);
Add(p);
}

/// <inheritdoc/>
Expand Down
13 changes: 9 additions & 4 deletions Ical.Net/DataTypes/PeriodListWrapperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Ical.Net.Utility;

namespace Ical.Net.DataTypes;

Expand All @@ -22,10 +23,13 @@ public abstract class PeriodListWrapperBase
private protected PeriodListWrapperBase(IList<PeriodList> periodList) => ListOfPeriodList = periodList;

/// <summary>
/// Gets a flattened list of all dates in the list.
/// Gets a flattened and ordered list of all distinct dates in the list
/// </summary>
public IEnumerable<IDateTime> GetAllDates()
=> ListOfPeriodList.SelectMany(pl => pl.Where(p => p.PeriodKind is PeriodKind.DateOnly or PeriodKind.DateTime).Select(p => p.StartTime));
=> ListOfPeriodList.SelectMany(pl =>
pl.Where(p => p.PeriodKind is PeriodKind.DateOnly or PeriodKind.DateTime)
.Select(p => p.StartTime))
.OrderedDistinct();

/// <summary>
/// Clears all elements from the list.
Expand Down Expand Up @@ -97,8 +101,9 @@ private protected PeriodList GetOrCreatePeriodList(Period period)
}

/// <summary>
/// Gets a flattened list of all periods with <see cref="PeriodKind.Period"/>, <see cref="PeriodKind.DateOnly"/> and <see cref="PeriodKind.DateTime"/>.
/// Gets a flattened and ordered list of all distinct periods with
/// <see cref="PeriodKind.Period"/>, <see cref="PeriodKind.DateOnly"/> and <see cref="PeriodKind.DateTime"/>.
/// </summary>
internal IEnumerable<Period> GetAllPeriodsByKind(params PeriodKind[] periodKinds)
=> ListOfPeriodList.SelectMany(pl => pl.Where(p => periodKinds.Contains(p.PeriodKind)));
=> ListOfPeriodList.SelectMany(pl => pl.Where(p => periodKinds.Contains(p.PeriodKind))).OrderedDistinct();
}
16 changes: 4 additions & 12 deletions Ical.Net/DataTypes/RecurrenceDates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Ical.Net.Utility;

namespace Ical.Net.DataTypes;

Expand All @@ -28,11 +29,6 @@ internal RecurrenceDates(IList<PeriodList> listOfPeriodList) : base(listOfPeriod
public RecurrenceDates Add(IDateTime dt)
{
var periodList = GetOrCreatePeriodList(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);

Expand All @@ -45,11 +41,6 @@ public RecurrenceDates Add(IDateTime dt)
public RecurrenceDates Add(Period period)
{
var periodList = GetOrCreatePeriodList(period);

// Don't add the same period twice.
if (periodList.FirstOrDefault(p => Equals(p, period)) != null)
return this;

periodList.Add(period);

return this;
Expand Down Expand Up @@ -103,8 +94,9 @@ public bool Remove(Period period)
}

/// <summary>
/// Gets a flattened list of all periods in the list.
/// Gets a flattened and ordered list of all distinct periods in the list.
/// </summary>
public IEnumerable<Period> GetAllPeriods()
=> ListOfPeriodList.SelectMany(pl => pl.Where(p => p.PeriodKind is PeriodKind.Period));
=> ListOfPeriodList.
SelectMany(pl => pl.Where(p => p.PeriodKind is PeriodKind.Period)).OrderedDistinct();
}

0 comments on commit 14b0187

Please sign in to comment.