From 0ddc8120db7c1ea089ea5874e2789c19bd4da013 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Mon, 24 Oct 2016 11:15:14 -0400 Subject: [PATCH] Fixed some bugs when comparing collections of periods (PeriodList) and generating hashcodes. Implemented IComparable where it made sense to do so. #181 --- v2/Ical.Net.nuspec | 2 +- v2/ical.NET.UnitTests/AttendeeTest.cs | 1 - .../CollectionHelpersTests.cs | 34 ++++++++++ v2/ical.NET.UnitTests/EventTest.cs | 65 ++++++++++++++++++- .../Ical.Net.UnitTests.csproj | 1 + v2/ical.NET.UnitTests/SerializationTests.cs | 2 - v2/ical.NET/Calendar.cs | 12 ++-- v2/ical.NET/CalendarCollection.cs | 10 +-- v2/ical.NET/Components/Event.cs | 27 ++++++-- v2/ical.NET/Components/UniqueComponent.cs | 9 ++- v2/ical.NET/DataTypes/Period.cs | 19 +++++- v2/ical.NET/DataTypes/PeriodList.cs | 10 +-- v2/ical.NET/General/CalendarProperty.cs | 1 - .../DataTypes/RecurrencePatternSerializer.cs | 1 - v2/ical.NET/Utility/CollectionHelpers.cs | 22 +++++-- .../{EventTest.cs => CalendarEventTest.cs} | 65 ++++++++++++++++++- .../CollectionHelpersTests.cs | 33 ++++++++++ .../EqualityAndHashingTests.cs | 4 +- .../Ical.Net.UnitTests.csproj | 3 +- v3/ical.NET.UnitTests/SerializationTests.cs | 1 - v3/ical.NET/Calendar.cs | 15 +++-- v3/ical.NET/CalendarCollection.cs | 10 +-- v3/ical.NET/Components/CalendarEvent.cs | 27 ++++++-- v3/ical.NET/Components/UniqueComponent.cs | 10 +-- v3/ical.NET/DataTypes/Period.cs | 16 ++--- v3/ical.NET/DataTypes/PeriodList.cs | 9 ++- v3/ical.NET/General/CalendarProperty.cs | 1 - .../DataTypes/RecurrencePatternSerializer.cs | 1 - v3/ical.NET/Utility/CollectionHelpers.cs | 18 ++++- 29 files changed, 339 insertions(+), 90 deletions(-) create mode 100644 v2/ical.NET.UnitTests/CollectionHelpersTests.cs rename v3/ical.NET.UnitTests/{EventTest.cs => CalendarEventTest.cs} (71%) create mode 100644 v3/ical.NET.UnitTests/CollectionHelpersTests.cs diff --git a/v2/Ical.Net.nuspec b/v2/Ical.Net.nuspec index b4ab4e6f5..3909efd90 100644 --- a/v2/Ical.Net.nuspec +++ b/v2/Ical.Net.nuspec @@ -2,7 +2,7 @@ Ical.Net - 2.2.18 + 2.2.19 Ical.Net Rian Stockbower, Douglas Day, M. David Peterson Rian Stockbower diff --git a/v2/ical.NET.UnitTests/AttendeeTest.cs b/v2/ical.NET.UnitTests/AttendeeTest.cs index 331314da6..a09f5e374 100644 --- a/v2/ical.NET.UnitTests/AttendeeTest.cs +++ b/v2/ical.NET.UnitTests/AttendeeTest.cs @@ -1,5 +1,4 @@ using Ical.Net.DataTypes; -using Ical.Net.Serialization; using Ical.Net.Serialization.iCalendar.Serializers; using NUnit.Framework; using System; diff --git a/v2/ical.NET.UnitTests/CollectionHelpersTests.cs b/v2/ical.NET.UnitTests/CollectionHelpersTests.cs new file mode 100644 index 000000000..45ba34905 --- /dev/null +++ b/v2/ical.NET.UnitTests/CollectionHelpersTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ical.Net.DataTypes; +using Ical.Net.Interfaces.DataTypes; +using NUnit.Framework; + +namespace Ical.Net.UnitTests +{ + internal class CollectionHelpersTests + { + private static readonly DateTime _now = DateTime.UtcNow; + private static readonly DateTime _later = _now.AddHours(1); + private static readonly string _uid = Guid.NewGuid().ToString(); + + private static List GetSimpleRecurrenceList() + => new List { new RecurrencePattern(FrequencyType.Daily, 1) { Count = 5 } }; + private static List GetExceptionDates() + => new List { new PeriodList { new Period(new CalDateTime(_now.AddDays(1).Date)) } }; + + [Test] + public void ExDateTests() + { + Assert.AreEqual(GetExceptionDates(), GetExceptionDates()); + Assert.AreNotEqual(GetExceptionDates(), null); + Assert.AreNotEqual(null, GetExceptionDates()); + + var changedPeriod = GetExceptionDates(); + changedPeriod.First().First().StartTime = new CalDateTime(_now.AddHours(-1)); + + Assert.AreNotEqual(GetExceptionDates(), changedPeriod); + } + } +} diff --git a/v2/ical.NET.UnitTests/EventTest.cs b/v2/ical.NET.UnitTests/EventTest.cs index b9ca63c43..478d7e2af 100644 --- a/v2/ical.NET.UnitTests/EventTest.cs +++ b/v2/ical.NET.UnitTests/EventTest.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; using Ical.Net.Interfaces.DataTypes; -using NUnit.Framework.Internal; +using Ical.Net.Serialization.iCalendar.Serializers; namespace Ical.Net.UnitTests { @@ -16,6 +16,7 @@ public class EventTest { private static readonly DateTime _now = DateTime.UtcNow; private static readonly DateTime _later = _now.AddHours(1); + private static readonly string _uid = Guid.NewGuid().ToString(); /// /// Ensures that events can be properly added to a calendar. @@ -222,6 +223,7 @@ public void EventWithExDateShouldNotBeEqualToSameEventWithoutExDate() { DtStart = new CalDateTime(_now), DtEnd = new CalDateTime(_later), + Uid = _uid, }; [Test] @@ -240,5 +242,66 @@ public void RrulesAreSignificantTests() Assert.AreNotEqual(simpleEvent, testRdate); Assert.AreNotEqual(simpleEvent.GetHashCode(), testRdate.GetHashCode()); } + + private static List GetSimpleRecurrenceList() + => new List {new RecurrencePattern(FrequencyType.Daily, 1) {Count = 5}}; + private static List GetExceptionDates() + => new List { new PeriodList { new Period(new CalDateTime(_now.AddDays(1).Date)) } }; + + [Test] + public void EventWithRecurrenceAndExceptionComparison() + { + var vEvent = GetSimpleEvent(); + vEvent.RecurrenceRules = GetSimpleRecurrenceList(); + vEvent.ExceptionDates = GetExceptionDates(); + + var calendar = new Calendar(); + calendar.Events.Add(vEvent); + + var vEvent2 = GetSimpleEvent(); + vEvent2.RecurrenceRules = GetSimpleRecurrenceList(); + vEvent2.ExceptionDates = GetExceptionDates(); + + var cal2 = new Calendar(); + cal2.Events.Add(vEvent2); + + var eventA = calendar.Events.First(); + var eventB = cal2.Events.First(); + + Assert.AreEqual(eventA.RecurrenceRules.First(), eventB.RecurrenceRules.First()); + Assert.AreEqual(eventA.RecurrenceRules.First().GetHashCode(), eventB.RecurrenceRules.First().GetHashCode()); + Assert.AreEqual(eventA.ExceptionDates.First(), eventB.ExceptionDates.First()); + Assert.AreEqual(eventA.ExceptionDates.First().GetHashCode(), eventB.ExceptionDates.First().GetHashCode()); + Assert.AreEqual(eventA.GetHashCode(), eventB.GetHashCode()); + Assert.AreEqual(eventA, eventB); + Assert.AreEqual(calendar, cal2); + } + + [Test] + public void AddingExdateToEventShouldNotBeEqualToOriginal() + { + //Create a calendar with an event with a recurrence rule + //Serialize to string, and deserialize + //Change the original calendar.Event to have an ExDate + //Serialize to string, and deserialize + //Event and Calendar hash codes and equality should NOT be the same + var serializer = new CalendarSerializer(); + + var vEvent = GetSimpleEvent(); + vEvent.RecurrenceRules = GetSimpleRecurrenceList(); + var cal1 = new Calendar(); + cal1.Events.Add(vEvent); + var serialized = serializer.SerializeToString(cal1); + var deserializedNoExDate = Calendar.LoadFromStream(new StringReader(serialized)).First() as Calendar; + Assert.AreEqual(cal1, deserializedNoExDate); + + vEvent.ExceptionDates = GetExceptionDates(); + serialized = serializer.SerializeToString(cal1); + var deserializedWithExDate = Calendar.LoadFromStream(new StringReader(serialized)).First() as Calendar; + + Assert.AreNotEqual(deserializedNoExDate.Events.First(), deserializedWithExDate.Events.First()); + Assert.AreNotEqual(deserializedNoExDate.Events.First().GetHashCode(), deserializedWithExDate.Events.First().GetHashCode()); + Assert.AreNotEqual(deserializedNoExDate, deserializedWithExDate); + } } } diff --git a/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj b/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj index 166015f2b..9341352d4 100644 --- a/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj +++ b/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj @@ -74,6 +74,7 @@ + diff --git a/v2/ical.NET.UnitTests/SerializationTests.cs b/v2/ical.NET.UnitTests/SerializationTests.cs index 3acf20dcc..3e2d9b0c6 100644 --- a/v2/ical.NET.UnitTests/SerializationTests.cs +++ b/v2/ical.NET.UnitTests/SerializationTests.cs @@ -6,11 +6,9 @@ using System.Linq; using System.Text.RegularExpressions; using Ical.Net.DataTypes; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces; using Ical.Net.Interfaces.Components; using Ical.Net.Interfaces.DataTypes; -using Ical.Net.Serialization; using Ical.Net.Serialization.iCalendar.Serializers; using Ical.Net.Serialization.iCalendar.Serializers.Other; using Ical.Net.UnitTests.ExtensionMethods; diff --git a/v2/ical.NET/Calendar.cs b/v2/ical.NET/Calendar.cs index 7eab2e56f..2b21723eb 100644 --- a/v2/ical.NET/Calendar.cs +++ b/v2/ical.NET/Calendar.cs @@ -180,12 +180,12 @@ protected override void OnDeserializing(StreamingContext context) protected bool Equals(Calendar other) { return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) - && UniqueComponents.SequenceEqual(other.UniqueComponents) - && Events.SequenceEqual(other.Events) - && Todos.SequenceEqual(other.Todos) - && Journals.SequenceEqual(other.Journals) - && FreeBusy.SequenceEqual(other.FreeBusy) - && TimeZones.SequenceEqual(other.TimeZones); + && CollectionHelpers.Equals(UniqueComponents, other.UniqueComponents) + && CollectionHelpers.Equals(Events, other.Events) + && CollectionHelpers.Equals(Todos, other.Todos) + && CollectionHelpers.Equals(Journals, other.Journals) + && CollectionHelpers.Equals(FreeBusy, other.FreeBusy) + && CollectionHelpers.Equals(TimeZones, other.TimeZones); } public override bool Equals(object obj) diff --git a/v2/ical.NET/CalendarCollection.cs b/v2/ical.NET/CalendarCollection.cs index 1a28477f7..d39ca0d34 100644 --- a/v2/ical.NET/CalendarCollection.cs +++ b/v2/ical.NET/CalendarCollection.cs @@ -123,15 +123,9 @@ public IFreeBusy GetFreeBusy(IOrganizer organizer, IAttendee[] contacts, IDateTi return this.Aggregate(null, (current, iCal) => CombineFreeBusy(current, iCal.GetFreeBusy(organizer, contacts, fromInclusive, toExclusive))); } - public override int GetHashCode() - { - return CollectionHelpers.GetHashCode(this); - } + public override int GetHashCode() => CollectionHelpers.GetHashCode(this); - protected bool Equals(CalendarCollection obj) - { - return this.SequenceEqual(obj); - } + protected bool Equals(CalendarCollection obj) => CollectionHelpers.Equals(this, obj); public override bool Equals(object obj) { diff --git a/v2/ical.NET/Components/Event.cs b/v2/ical.NET/Components/Event.cs index 18904be9a..5421e5785 100644 --- a/v2/ical.NET/Components/Event.cs +++ b/v2/ical.NET/Components/Event.cs @@ -23,7 +23,7 @@ namespace Ical.Net /// Create a TextCollection DataType for 'text' items separated by commas /// /// - public class Event : RecurringComponent, IEvent + public class Event : RecurringComponent, IEvent, IComparable { internal const string ComponentName = "VEVENT"; @@ -295,10 +295,10 @@ protected bool Equals(Event other) && Transparency.Equals(other.Transparency) && EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate && Attachments.SequenceEqual(other.Attachments) - && (ExceptionDates != null && CollectionHelpers.Equals(ExceptionDates, other.ExceptionDates)) - && (ExceptionRules != null && CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules)) - && (RecurrenceRules != null && CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules, true)) - && (RecurrenceDates != null && CollectionHelpers.Equals(RecurrenceDates, other.RecurrenceDates, true)); + && CollectionHelpers.Equals(ExceptionDates, other.ExceptionDates) + && CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules) + && CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules, true) + && CollectionHelpers.Equals(RecurrenceDates, other.RecurrenceDates, true); return result; } @@ -329,5 +329,22 @@ public override int GetHashCode() return hashCode; } } + + public int CompareTo(Event other) + { + if (DtStart.Equals(other.DtStart)) + { + return 0; + } + if (DtStart.LessThan(other.DtStart)) + { + return -1; + } + if (DtStart.GreaterThan(other.DtStart)) + { + return 1; + } + throw new Exception("An error occurred while comparing two CalDateTimes."); + } } } \ No newline at end of file diff --git a/v2/ical.NET/Components/UniqueComponent.cs b/v2/ical.NET/Components/UniqueComponent.cs index 0b0577117..8ed790213 100644 --- a/v2/ical.NET/Components/UniqueComponent.cs +++ b/v2/ical.NET/Components/UniqueComponent.cs @@ -11,7 +11,7 @@ namespace Ical.Net /// Represents a unique component, a component with a unique UID, /// which can be used to uniquely identify the component. /// - public class UniqueComponent : CalendarComponent, IUniqueComponent + public class UniqueComponent : CalendarComponent, IUniqueComponent, IComparable { // TODO: Add AddRelationship() public method. // This method will add the UID of a related component @@ -93,6 +93,8 @@ protected override void OnDeserialized(StreamingContext context) EnsureProperties(); } + public int CompareTo(UniqueComponent other) => string.Compare(Uid, other.Uid, StringComparison.OrdinalIgnoreCase); + public override bool Equals(object obj) { if (obj is RecurringComponent && obj != this) @@ -107,10 +109,7 @@ public override bool Equals(object obj) return base.Equals(obj); } - public override int GetHashCode() - { - return Uid?.GetHashCode() ?? base.GetHashCode(); - } + public override int GetHashCode() => Uid?.GetHashCode() ?? base.GetHashCode(); public virtual string Uid { diff --git a/v2/ical.NET/DataTypes/Period.cs b/v2/ical.NET/DataTypes/Period.cs index 86612c722..4f1be3f93 100644 --- a/v2/ical.NET/DataTypes/Period.cs +++ b/v2/ical.NET/DataTypes/Period.cs @@ -6,7 +6,7 @@ namespace Ical.Net.DataTypes { /// Represents an iCalendar period of time. - public class Period : EncodableDataType, IPeriod + public class Period : EncodableDataType, IPeriod, IComparable { public Period() { } @@ -84,6 +84,23 @@ public override int GetHashCode() } } + public int CompareTo(Period other) + { + if (StartTime.Equals(other.StartTime)) + { + return 0; + } + if (StartTime.LessThan(other.StartTime)) + { + return -1; + } + if (StartTime.GreaterThan(other.StartTime)) + { + return 1; + } + throw new Exception("An error occurred while comparing two Periods."); + } + public override string ToString() { var periodSerializer = new PeriodSerializer(); diff --git a/v2/ical.NET/DataTypes/PeriodList.cs b/v2/ical.NET/DataTypes/PeriodList.cs index 580440564..3a52e08f4 100644 --- a/v2/ical.NET/DataTypes/PeriodList.cs +++ b/v2/ical.NET/DataTypes/PeriodList.cs @@ -5,6 +5,7 @@ using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; +using Ical.Net.Utility; namespace Ical.Net.DataTypes { @@ -18,7 +19,6 @@ public class PeriodList : EncodableDataType, IPeriodList protected IList Periods { get; set; } = new List(64); - public PeriodList() { SetService(new PeriodListEvaluator(this)); @@ -32,22 +32,22 @@ public PeriodList(string value) : this() protected bool Equals(PeriodList other) { - return Equals(Periods, other.Periods) && string.Equals(TzId, other.TzId); + return string.Equals(TzId, other.TzId) && CollectionHelpers.Equals(Periods, other.Periods); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((PeriodList)obj); + if (obj.GetType() != this.GetType()) return false; + return Equals((PeriodList) obj); } public override int GetHashCode() { unchecked { - return ((Periods?.GetHashCode() ?? 0) * 397) ^ (TzId?.GetHashCode() ?? 0); + return ((TzId?.GetHashCode() ?? 0) * 397) ^ CollectionHelpers.GetHashCode(Periods); } } diff --git a/v2/ical.NET/General/CalendarProperty.cs b/v2/ical.NET/General/CalendarProperty.cs index 7c1bf8ede..f155659b8 100644 --- a/v2/ical.NET/General/CalendarProperty.cs +++ b/v2/ical.NET/General/CalendarProperty.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces.General; namespace Ical.Net.General diff --git a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs index 2d9d238c3..8c4f09310 100644 --- a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs +++ b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.RegularExpressions; using Ical.Net.DataTypes; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.Serialization; using Ical.Net.Interfaces.Serialization.Factory; diff --git a/v2/ical.NET/Utility/CollectionHelpers.cs b/v2/ical.NET/Utility/CollectionHelpers.cs index 30934537c..d9287904a 100644 --- a/v2/ical.NET/Utility/CollectionHelpers.cs +++ b/v2/ical.NET/Utility/CollectionHelpers.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Ical.Net.Utility { public static class CollectionHelpers { - internal static int GetHashCode(IEnumerable collection) + public static int GetHashCode(IEnumerable collection) { unchecked { @@ -14,7 +15,7 @@ internal static int GetHashCode(IEnumerable collection) } } - internal static bool Equals(IEnumerable left, IEnumerable right, bool orderSignificant = false) + public static bool Equals(IEnumerable left, IEnumerable right, bool orderSignificant = false) { if (ReferenceEquals(left, right)) { @@ -36,7 +37,20 @@ internal static bool Equals(IEnumerable left, IEnumerable right, bool o return left.SequenceEqual(right); } - return left.OrderBy(l => l).SequenceEqual(right.OrderBy(r => r)); + try + { + //Many things have natural IComparers defined, but some don't, because no natural comparer exists + return left.OrderBy(l => l).SequenceEqual(right.OrderBy(r => r)); + } + catch (Exception) + { + //It's not possible to sort some collections of things (like Calendars) in any meaningful way. Properties can be null, and there's no natural + //ordering for the contents therein. In cases like that, the best we can do is treat them like sets, and compare them. We don't maintain + //fidelity with respect to duplicates, but it seems better than doing nothing + var leftSet = new HashSet(left); + var rightSet = new HashSet(right); + return leftSet.SetEquals(rightSet); + } } } } diff --git a/v3/ical.NET.UnitTests/EventTest.cs b/v3/ical.NET.UnitTests/CalendarEventTest.cs similarity index 71% rename from v3/ical.NET.UnitTests/EventTest.cs rename to v3/ical.NET.UnitTests/CalendarEventTest.cs index 4245226fa..4ad1c8c91 100644 --- a/v3/ical.NET.UnitTests/EventTest.cs +++ b/v3/ical.NET.UnitTests/CalendarEventTest.cs @@ -5,14 +5,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Ical.Net.Serialization.iCalendar.Serializers; namespace Ical.Net.UnitTests { [TestFixture] - public class EventTest + public class CalendarEventTest { private static readonly DateTime _now = DateTime.UtcNow; private static readonly DateTime _later = _now.AddHours(1); + private static readonly string _uid = Guid.NewGuid().ToString(); /// /// Ensures that events can be properly added to a calendar. @@ -237,5 +239,66 @@ public void RrulesAreSignificantTests() Assert.AreNotEqual(simpleEvent, testRdate); Assert.AreNotEqual(simpleEvent.GetHashCode(), testRdate.GetHashCode()); } + + private static List GetSimpleRecurrenceList() + => new List { new RecurrencePattern(FrequencyType.Daily, 1) { Count = 5 } }; + private static List GetExceptionDates() + => new List { new PeriodList { new Period(new CalDateTime(_now.AddDays(1).Date)) } }; + + [Test] + public void EventWithRecurrenceAndExceptionComparison() + { + var vEvent = GetSimpleEvent(); + vEvent.RecurrenceRules = GetSimpleRecurrenceList(); + vEvent.ExceptionDates = GetExceptionDates(); + + var calendar = new Calendar(); + calendar.Events.Add(vEvent); + + var vEvent2 = GetSimpleEvent(); + vEvent2.RecurrenceRules = GetSimpleRecurrenceList(); + vEvent2.ExceptionDates = GetExceptionDates(); + + var cal2 = new Calendar(); + cal2.Events.Add(vEvent2); + + var eventA = calendar.Events.First(); + var eventB = cal2.Events.First(); + + Assert.AreEqual(eventA.RecurrenceRules.First(), eventB.RecurrenceRules.First()); + Assert.AreEqual(eventA.RecurrenceRules.First().GetHashCode(), eventB.RecurrenceRules.First().GetHashCode()); + Assert.AreEqual(eventA.ExceptionDates.First(), eventB.ExceptionDates.First()); + Assert.AreEqual(eventA.ExceptionDates.First().GetHashCode(), eventB.ExceptionDates.First().GetHashCode()); + Assert.AreEqual(eventA.GetHashCode(), eventB.GetHashCode()); + Assert.AreEqual(eventA, eventB); + Assert.AreEqual(calendar, cal2); + } + + [Test] + public void AddingExdateToEventShouldNotBeEqualToOriginal() + { + //Create a calendar with an event with a recurrence rule + //Serialize to string, and deserialize + //Change the original calendar.Event to have an ExDate + //Serialize to string, and deserialize + //Event and Calendar hash codes and equality should NOT be the same + var serializer = new CalendarSerializer(); + + var vEvent = GetSimpleEvent(); + vEvent.RecurrenceRules = GetSimpleRecurrenceList(); + var cal1 = new Calendar(); + cal1.Events.Add(vEvent); + var serialized = serializer.SerializeToString(cal1); + var deserializedNoExDate = Calendar.LoadFromStream(new StringReader(serialized)).First(); + Assert.AreEqual(cal1, deserializedNoExDate); + + vEvent.ExceptionDates = GetExceptionDates(); + serialized = serializer.SerializeToString(cal1); + var deserializedWithExDate = Calendar.LoadFromStream(new StringReader(serialized)).First(); + + Assert.AreNotEqual(deserializedNoExDate.Events.First(), deserializedWithExDate.Events.First()); + Assert.AreNotEqual(deserializedNoExDate.Events.First().GetHashCode(), deserializedWithExDate.Events.First().GetHashCode()); + Assert.AreNotEqual(deserializedNoExDate, deserializedWithExDate); + } } } diff --git a/v3/ical.NET.UnitTests/CollectionHelpersTests.cs b/v3/ical.NET.UnitTests/CollectionHelpersTests.cs new file mode 100644 index 000000000..2657fb64b --- /dev/null +++ b/v3/ical.NET.UnitTests/CollectionHelpersTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ical.Net.DataTypes; +using NUnit.Framework; + +namespace Ical.Net.UnitTests +{ + internal class CollectionHelpersTests + { + private static readonly DateTime _now = DateTime.UtcNow; + private static readonly DateTime _later = _now.AddHours(1); + private static readonly string _uid = Guid.NewGuid().ToString(); + + private static List GetSimpleRecurrenceList() + => new List { new RecurrencePattern(FrequencyType.Daily, 1) { Count = 5 } }; + private static List GetExceptionDates() + => new List { new PeriodList { new Period(new CalDateTime(_now.AddDays(1).Date)) } }; + + [Test] + public void ExDateTests() + { + Assert.AreEqual(GetExceptionDates(), GetExceptionDates()); + Assert.AreNotEqual(GetExceptionDates(), null); + Assert.AreNotEqual(null, GetExceptionDates()); + + var changedPeriod = GetExceptionDates(); + changedPeriod.First().First().StartTime = new CalDateTime(_now.AddHours(-1)); + + Assert.AreNotEqual(GetExceptionDates(), changedPeriod); + } + } +} diff --git a/v3/ical.NET.UnitTests/EqualityAndHashingTests.cs b/v3/ical.NET.UnitTests/EqualityAndHashingTests.cs index b602e6af8..a2f027155 100644 --- a/v3/ical.NET.UnitTests/EqualityAndHashingTests.cs +++ b/v3/ical.NET.UnitTests/EqualityAndHashingTests.cs @@ -202,8 +202,8 @@ public static IEnumerable Attendees_TestCases() [Test, TestCaseSource(nameof(CalendarCollection_TestCases))] public void CalendarCollection_Tests(string rawCalendar) { - var a = Calendar.LoadFromStream(new StringReader(IcsFiles.USHolidays)) as CalendarCollection; - var b = Calendar.LoadFromStream(new StringReader(IcsFiles.USHolidays)) as CalendarCollection; + var a = Calendar.LoadFromStream(new StringReader(IcsFiles.USHolidays)); + var b = Calendar.LoadFromStream(new StringReader(IcsFiles.USHolidays)); Assert.IsNotNull(a); Assert.IsNotNull(b); diff --git a/v3/ical.NET.UnitTests/Ical.Net.UnitTests.csproj b/v3/ical.NET.UnitTests/Ical.Net.UnitTests.csproj index 7c6d11bb4..7052d6c18 100644 --- a/v3/ical.NET.UnitTests/Ical.Net.UnitTests.csproj +++ b/v3/ical.NET.UnitTests/Ical.Net.UnitTests.csproj @@ -74,6 +74,7 @@ + @@ -81,7 +82,7 @@ - + diff --git a/v3/ical.NET.UnitTests/SerializationTests.cs b/v3/ical.NET.UnitTests/SerializationTests.cs index ffe88340e..83a450a47 100644 --- a/v3/ical.NET.UnitTests/SerializationTests.cs +++ b/v3/ical.NET.UnitTests/SerializationTests.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text.RegularExpressions; using Ical.Net.DataTypes; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces.Components; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Serialization.iCalendar.Serializers; diff --git a/v3/ical.NET/Calendar.cs b/v3/ical.NET/Calendar.cs index 0626d7398..a792ce966 100644 --- a/v3/ical.NET/Calendar.cs +++ b/v3/ical.NET/Calendar.cs @@ -178,13 +178,14 @@ protected override void OnDeserializing(StreamingContext context) protected bool Equals(Calendar other) { + var foo = CollectionHelpers.Equals(UniqueComponents, other.UniqueComponents); return string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) - && UniqueComponents.SequenceEqual(other.UniqueComponents) - && Events.SequenceEqual(other.Events) - && Todos.SequenceEqual(other.Todos) - && Journals.SequenceEqual(other.Journals) - && FreeBusy.SequenceEqual(other.FreeBusy) - && TimeZones.SequenceEqual(other.TimeZones); + && CollectionHelpers.Equals(UniqueComponents, other.UniqueComponents) + && CollectionHelpers.Equals(Events, other.Events) + && CollectionHelpers.Equals(Todos, other.Todos) + && CollectionHelpers.Equals(Journals, other.Journals) + && CollectionHelpers.Equals(FreeBusy, other.FreeBusy) + && CollectionHelpers.Equals(TimeZones, other.TimeZones); } public override bool Equals(object obj) @@ -201,7 +202,7 @@ public override bool Equals(object obj) { return false; } - return Equals((Calendar) obj); + return Equals((Calendar)obj); } public override int GetHashCode() diff --git a/v3/ical.NET/CalendarCollection.cs b/v3/ical.NET/CalendarCollection.cs index 63d90824d..6513beaaf 100644 --- a/v3/ical.NET/CalendarCollection.cs +++ b/v3/ical.NET/CalendarCollection.cs @@ -122,15 +122,9 @@ public FreeBusy GetFreeBusy(Organizer organizer, IEnumerable contacts, return this.Aggregate(null, (current, iCal) => CombineFreeBusy(current, iCal.GetFreeBusy(organizer, contacts, fromInclusive, toExclusive))); } - public override int GetHashCode() - { - return CollectionHelpers.GetHashCode(this); - } + public override int GetHashCode() => CollectionHelpers.GetHashCode(this); - protected bool Equals(CalendarCollection obj) - { - return this.SequenceEqual(obj); - } + protected bool Equals(CalendarCollection obj) => CollectionHelpers.Equals(this, obj); public override bool Equals(object obj) { diff --git a/v3/ical.NET/Components/CalendarEvent.cs b/v3/ical.NET/Components/CalendarEvent.cs index dcdfb1799..c0479f418 100644 --- a/v3/ical.NET/Components/CalendarEvent.cs +++ b/v3/ical.NET/Components/CalendarEvent.cs @@ -24,7 +24,7 @@ namespace Ical.Net /// Create a TextCollection DataType for 'text' items separated by commas /// /// - public class CalendarEvent : RecurringComponent, IAlarmContainer + public class CalendarEvent : RecurringComponent, IAlarmContainer, IComparable { internal const string ComponentName = "VEVENT"; @@ -296,10 +296,10 @@ protected bool Equals(CalendarEvent other) && Transparency.Equals(other.Transparency) && EvaluationIncludesReferenceDate == other.EvaluationIncludesReferenceDate && Attachments.SequenceEqual(other.Attachments) - && (ExceptionDates != null && CollectionHelpers.Equals(ExceptionDates, other.ExceptionDates)) - && (ExceptionRules != null && CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules)) - && (RecurrenceRules != null && CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules, true)) - && (RecurrenceDates != null && CollectionHelpers.Equals(RecurrenceDates, other.RecurrenceDates, true)); + && CollectionHelpers.Equals(ExceptionDates, other.ExceptionDates) + && CollectionHelpers.Equals(ExceptionRules, other.ExceptionRules) + && CollectionHelpers.Equals(RecurrenceRules, other.RecurrenceRules, true) + && CollectionHelpers.Equals(RecurrenceDates, other.RecurrenceDates, true); return result; } @@ -330,5 +330,22 @@ public override int GetHashCode() return hashCode; } } + + public int CompareTo(CalendarEvent other) + { + if (DtStart.Equals(other.DtStart)) + { + return 0; + } + if (DtStart.LessThan(other.DtStart)) + { + return -1; + } + if (DtStart.GreaterThan(other.DtStart)) + { + return 1; + } + throw new Exception("An error occurred while comparing two CalDateTimes."); + } } } \ No newline at end of file diff --git a/v3/ical.NET/Components/UniqueComponent.cs b/v3/ical.NET/Components/UniqueComponent.cs index 9d0838b32..fe667cdf9 100644 --- a/v3/ical.NET/Components/UniqueComponent.cs +++ b/v3/ical.NET/Components/UniqueComponent.cs @@ -11,7 +11,7 @@ namespace Ical.Net /// Represents a unique component, a component with a unique UID, /// which can be used to uniquely identify the component. /// - public class UniqueComponent : CalendarComponent, IUniqueComponent + public class UniqueComponent : CalendarComponent, IUniqueComponent, IComparable { // TODO: Add AddRelationship() public method. // This method will add the UID of a related component @@ -93,6 +93,9 @@ protected override void OnDeserialized(StreamingContext context) EnsureProperties(); } + public int CompareTo(UniqueComponent other) + => string.Compare(Uid, other.Uid, StringComparison.OrdinalIgnoreCase); + public override bool Equals(object obj) { if (obj is RecurringComponent && obj != this) @@ -107,10 +110,7 @@ public override bool Equals(object obj) return base.Equals(obj); } - public override int GetHashCode() - { - return Uid?.GetHashCode() ?? base.GetHashCode(); - } + public override int GetHashCode() => Uid?.GetHashCode() ?? base.GetHashCode(); public virtual string Uid { diff --git a/v3/ical.NET/DataTypes/Period.cs b/v3/ical.NET/DataTypes/Period.cs index 8309bd224..6562006f9 100644 --- a/v3/ical.NET/DataTypes/Period.cs +++ b/v3/ical.NET/DataTypes/Period.cs @@ -6,7 +6,7 @@ namespace Ical.Net.DataTypes { /// Represents an iCalendar period of time. - public class Period : EncodableDataType + public class Period : EncodableDataType, IComparable { public Period() { } @@ -178,25 +178,21 @@ public virtual bool CollidesWith(Period period) && ((period.StartTime != null && Contains(period.StartTime)) || (period.EndTime != null && Contains(period.EndTime))); } - public int CompareTo(Period p) + public int CompareTo(Period other) { - if (p == null) - { - throw new ArgumentNullException(nameof(p)); - } - if (Equals(p)) + if (StartTime.Equals(other.StartTime)) { return 0; } - if (StartTime.LessThan(p.StartTime)) + if (StartTime.LessThan(other.StartTime)) { return -1; } - if (StartTime.GreaterThanOrEqual(p.StartTime)) + if (StartTime.GreaterThan(other.StartTime)) { return 1; } - throw new Exception("An error occurred while comparing Period values."); + throw new Exception("An error occurred while comparing two Periods."); } } } \ No newline at end of file diff --git a/v3/ical.NET/DataTypes/PeriodList.cs b/v3/ical.NET/DataTypes/PeriodList.cs index be8276349..ba9eb93a1 100644 --- a/v3/ical.NET/DataTypes/PeriodList.cs +++ b/v3/ical.NET/DataTypes/PeriodList.cs @@ -5,6 +5,7 @@ using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; +using Ical.Net.Utility; namespace Ical.Net.DataTypes { @@ -18,7 +19,6 @@ public class PeriodList : EncodableDataType, IEnumerable protected IList Periods { get; set; } = new List(64); - public PeriodList() { SetService(new PeriodListEvaluator(this)); @@ -32,14 +32,14 @@ public PeriodList(string value) : this() protected bool Equals(PeriodList other) { - return Equals(Periods, other.Periods) && string.Equals(TzId, other.TzId); + return string.Equals(TzId, other.TzId) && CollectionHelpers.Equals(Periods, other.Periods); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; + if (obj.GetType() != this.GetType()) return false; return Equals((PeriodList)obj); } @@ -47,10 +47,9 @@ public override int GetHashCode() { unchecked { - return ((Periods?.GetHashCode() ?? 0) * 397) ^ (TzId?.GetHashCode() ?? 0); + return ((TzId?.GetHashCode() ?? 0) * 397) ^ CollectionHelpers.GetHashCode(Periods); } } - public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); diff --git a/v3/ical.NET/General/CalendarProperty.cs b/v3/ical.NET/General/CalendarProperty.cs index 7c1bf8ede..f155659b8 100644 --- a/v3/ical.NET/General/CalendarProperty.cs +++ b/v3/ical.NET/General/CalendarProperty.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces.General; namespace Ical.Net.General diff --git a/v3/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs b/v3/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs index df9c278e1..39cd6a16e 100644 --- a/v3/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs +++ b/v3/ical.NET/Serialization/iCalendar/Serializers/DataTypes/RecurrencePatternSerializer.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text.RegularExpressions; using Ical.Net.DataTypes; -using Ical.Net.ExtensionMethods; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.Serialization; using Ical.Net.Interfaces.Serialization.Factory; diff --git a/v3/ical.NET/Utility/CollectionHelpers.cs b/v3/ical.NET/Utility/CollectionHelpers.cs index efbd0b7f3..039e6355f 100644 --- a/v3/ical.NET/Utility/CollectionHelpers.cs +++ b/v3/ical.NET/Utility/CollectionHelpers.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Ical.Net.Utility @@ -36,7 +37,20 @@ public static bool Equals(IEnumerable left, IEnumerable right, bool ord return left.SequenceEqual(right); } - return left.OrderBy(l => l).SequenceEqual(right.OrderBy(r => r)); + try + { + //Many things have natural IComparers defined, but some don't, because no natural comparer exists + return left.OrderBy(l => l).SequenceEqual(right.OrderBy(r => r)); + } + catch (Exception) + { + //It's not possible to sort some collections of things (like Calendars) in any meaningful way. Properties can be null, and there's no natural + //ordering for the contents therein. In cases like that, the best we can do is treat them like sets, and compare them. We don't maintain + //fidelity with respect to duplicates, but it seems better than doing nothing + var leftSet = new HashSet(left); + var rightSet = new HashSet(right); + return leftSet.SetEquals(rightSet); + } } } }