From 1b4fea794d6a7bad668a0b61e283f017d5a49bc6 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Wed, 5 Apr 2017 09:12:51 -0400 Subject: [PATCH 1/6] Utility methods in NugetTester --- NugetTester/NugetTester/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NugetTester/NugetTester/Program.cs b/NugetTester/NugetTester/Program.cs index ee6d972a3..53a0f8092 100644 --- a/NugetTester/NugetTester/Program.cs +++ b/NugetTester/NugetTester/Program.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Ical.Net; using Ical.Net.DataTypes; +using Ical.Net.Interfaces.Components; +using Ical.Net.Interfaces.DataTypes; +using Ical.Net.Serialization.iCalendar.Serializers; namespace NugetTester { @@ -27,5 +31,9 @@ private static CalendarCollection DeserializeCalendar(string ical) return Calendar.LoadFromStream(reader) as CalendarCollection; } } + + private static string SerializeToString(IEvent calendarEvent) => SerializeToString(new Calendar { Events = { calendarEvent } }); + + private static string SerializeToString(Calendar iCalendar) => new CalendarSerializer().SerializeToString(iCalendar); } } From b7ca3eddb9bca876749e1156c3d836adc05f5f5a Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Wed, 5 Apr 2017 09:37:48 -0400 Subject: [PATCH 2/6] Broken unit tests showing bad EXDATE serialization #259 --- .../Ical.Net.UnitTests/RecurrenceTests.cs | 51 ++++++++++++++++++ .../Ical.Net.UnitTests.csproj | 1 + v2/ical.NET.UnitTests/RecurrenceTests.cs | 52 +++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs b/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs index 3fa61077c..f6571bd21 100644 --- a/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs +++ b/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using Ical.Net.DataTypes; using Ical.Net.Evaluation; @@ -12,6 +13,7 @@ using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using Ical.Net.Utility; using NUnit.Framework; +using static Ical.Net.UnitTests.SerializationHelpers; namespace Ical.Net.UnitTests { @@ -3128,5 +3130,54 @@ private static CalendarEvent GetEventWithRecurrenceRules() }; return calendarEvent; } + + [Test] + public void ExDateFold_Tests() + { + var start = _now.AddYears(-1); + var end = start.AddHours(1); + var rrule = new RecurrencePattern(FrequencyType.Daily) { Until = start.AddYears(2) }; + var e = new CalendarEvent + { + DtStart = new CalDateTime(start), + DtEnd = new CalDateTime(end), + RecurrenceRules = new List { rrule } + }; + + var firstExclusion = new CalDateTime(start.AddDays(4)); + e.ExceptionDates = new List { new PeriodList { new Period(firstExclusion) } }; + var serialized = SerializeToString(e); + Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); + + var secondExclusion = new CalDateTime(start.AddDays(5)); + e.ExceptionDates.Add(new PeriodList { new Period(secondExclusion) }); + serialized = SerializeToString(e); + Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); + } + + [Test] + public void ExDateTimeZone_Tests() + { + const string tzid = "Europe/Stockholm"; + + //Repeat daily for 10 days + var rrule = new RecurrencePattern(FrequencyType.Daily, 1) { Count = 10 }; + + var e = new CalendarEvent + { + DtStart = new CalDateTime(_now, tzid), + DtEnd = new CalDateTime(_later, tzid), + RecurrenceRules = new List { rrule }, + }; + + var exceptionDateList = new PeriodList(); + exceptionDateList.TzId = tzid; + exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1), tzid))); + e.ExceptionDates.Add(exceptionDateList); + + var serialized = SerializeToString(e); + const string expected = "EXDATE;TZID=Europe/Stockholm:"; + Assert.AreEqual(1, Regex.Matches(serialized, expected).Count); + } } } diff --git a/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj b/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj index 435317543..a374442c1 100644 --- a/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj +++ b/v2/ical.NET.UnitTests/Ical.Net.UnitTests.csproj @@ -78,6 +78,7 @@ + diff --git a/v2/ical.NET.UnitTests/RecurrenceTests.cs b/v2/ical.NET.UnitTests/RecurrenceTests.cs index 991168b52..d4aca5bd5 100644 --- a/v2/ical.NET.UnitTests/RecurrenceTests.cs +++ b/v2/ical.NET.UnitTests/RecurrenceTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using Ical.Net.DataTypes; using Ical.Net.Evaluation; @@ -11,9 +12,11 @@ using Ical.Net.Interfaces.Components; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.Evaluation; +using Ical.Net.Serialization.iCalendar.Serializers; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using Ical.Net.Utility; using NUnit.Framework; +using static Ical.Net.UnitTests.SerializationHelpers; namespace Ical.Net.UnitTests { @@ -3130,5 +3133,54 @@ private static Event GetEventWithRecurrenceRules() }; return calendarEvent; } + + [Test] + public void ExDateFold_Tests() + { + var start = _now.AddYears(-1); + var end = start.AddHours(1); + var rrule = new RecurrencePattern(FrequencyType.Daily) { Until = start.AddYears(2) }; + var e = new Event + { + DtStart = new CalDateTime(start), + DtEnd = new CalDateTime(end), + RecurrenceRules = new List { rrule } + }; + + var firstExclusion = new CalDateTime(start.AddDays(4)); + e.ExceptionDates = new List { new PeriodList { new Period(firstExclusion) } }; + var serialized = SerializeToString(e); + Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); + + var secondExclusion = new CalDateTime(start.AddDays(5)); + e.ExceptionDates.Add(new PeriodList { new Period(secondExclusion) }); + serialized = SerializeToString(e); + Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); + } + + [Test] + public void ExDateTimeZone_Tests() + { + const string tzid = "Europe/Stockholm"; + + //Repeat daily for 10 days + var rrule = new RecurrencePattern(FrequencyType.Daily, 1) { Count = 10 }; + + var e = new Event + { + DtStart = new CalDateTime(_now, tzid), + DtEnd = new CalDateTime(_later, tzid), + RecurrenceRules = new List { rrule }, + }; + + var exceptionDateList = new PeriodList(); + exceptionDateList.TzId = tzid; + exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1), tzid))); + e.ExceptionDates.Add(exceptionDateList); + + var serialized = SerializeToString(e); + const string expected = "EXDATE;TZID=Europe/Stockholm:"; + Assert.AreEqual(1, Regex.Matches(serialized, expected).Count); + } } } From 72c89ed4db92cd204af0b14c2bb9f5491ceab17b Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 6 Apr 2017 09:54:59 -0400 Subject: [PATCH 3/6] Add serialization helpers #259 --- .../SerializationHelpers.cs | 28 ++++++++++++++++++ v2/ical.NET.UnitTests/RecurrenceTests.cs | 1 - v2/ical.NET.UnitTests/SerializationHelpers.cs | 29 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 net-core/Ical.Net/Ical.Net.UnitTests/SerializationHelpers.cs create mode 100644 v2/ical.NET.UnitTests/SerializationHelpers.cs diff --git a/net-core/Ical.Net/Ical.Net.UnitTests/SerializationHelpers.cs b/net-core/Ical.Net/Ical.Net.UnitTests/SerializationHelpers.cs new file mode 100644 index 000000000..ae36148a0 --- /dev/null +++ b/net-core/Ical.Net/Ical.Net.UnitTests/SerializationHelpers.cs @@ -0,0 +1,28 @@ +using System.IO; +using System.Linq; +using Ical.Net.Serialization.iCalendar.Serializers; + +namespace Ical.Net.UnitTests +{ + internal class SerializationHelpers + { + public static CalendarEvent DeserializeCalendarEvent(string ical) + { + var calendar = DeserializeCalendar(ical); + var calendarEvent = calendar.First().Events.First(); + return calendarEvent; + } + + public static CalendarCollection DeserializeCalendar(string ical) + { + using (var reader = new StringReader(ical)) + { + return Calendar.LoadFromStream(reader); + } + } + + public static string SerializeToString(CalendarEvent calendarEvent) => SerializeToString(new Calendar { Events = { calendarEvent } }); + + public static string SerializeToString(Calendar iCalendar) => new CalendarSerializer().SerializeToString(iCalendar); + } +} diff --git a/v2/ical.NET.UnitTests/RecurrenceTests.cs b/v2/ical.NET.UnitTests/RecurrenceTests.cs index d4aca5bd5..8fad53b64 100644 --- a/v2/ical.NET.UnitTests/RecurrenceTests.cs +++ b/v2/ical.NET.UnitTests/RecurrenceTests.cs @@ -12,7 +12,6 @@ using Ical.Net.Interfaces.Components; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.Evaluation; -using Ical.Net.Serialization.iCalendar.Serializers; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using Ical.Net.Utility; using NUnit.Framework; diff --git a/v2/ical.NET.UnitTests/SerializationHelpers.cs b/v2/ical.NET.UnitTests/SerializationHelpers.cs new file mode 100644 index 000000000..c98f17470 --- /dev/null +++ b/v2/ical.NET.UnitTests/SerializationHelpers.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Linq; +using Ical.Net.Interfaces.Components; +using Ical.Net.Serialization.iCalendar.Serializers; + +namespace Ical.Net.UnitTests +{ + internal class SerializationHelpers + { + public static Event DeserializeCalendarEvent(string ical) + { + var calendar = DeserializeCalendar(ical); + var calendarEvent = calendar.First().Events.First() as Event; + return calendarEvent; + } + + public static CalendarCollection DeserializeCalendar(string ical) + { + using (var reader = new StringReader(ical)) + { + return Calendar.LoadFromStream(reader) as CalendarCollection; + } + } + + public static string SerializeToString(IEvent calendarEvent) => SerializeToString(new Calendar { Events = { calendarEvent } }); + + public static string SerializeToString(Calendar iCalendar) => new CalendarSerializer().SerializeToString(iCalendar); + } +} From df83136671bed1a262feb3cf7b2565e6cf35ad95 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 6 Apr 2017 13:20:11 -0400 Subject: [PATCH 4/6] Fix EXDATE and RDATE serialization #259 --- .../Ical.Net.UnitTests/CalendarEventTest.cs | 78 +++++++++---------- .../DateTimeSerializerTests.cs | 25 +----- .../Ical.Net.UnitTests/RecurrenceTests.cs | 15 ++-- .../DataTypes/DateTimeSerializer.cs | 13 ++-- .../DataTypes/PeriodListSerializer.cs | 15 ++-- .../Serializers/PropertySerializer.cs | 16 +++- .../DateTimeSerializerTests.cs | 25 +----- v2/ical.NET.UnitTests/EventTest.cs | 60 +++++++------- v2/ical.NET.UnitTests/RecurrenceTests.cs | 15 ++-- .../DataTypes/DateTimeSerializer.cs | 13 ++-- .../DataTypes/PeriodListSerializer.cs | 12 +-- .../Serializers/PropertySerializer.cs | 16 +++- 12 files changed, 146 insertions(+), 157 deletions(-) diff --git a/net-core/Ical.Net/Ical.Net.UnitTests/CalendarEventTest.cs b/net-core/Ical.Net/Ical.Net.UnitTests/CalendarEventTest.cs index cd1dbae87..7489282a7 100644 --- a/net-core/Ical.Net/Ical.Net.UnitTests/CalendarEventTest.cs +++ b/net-core/Ical.Net/Ical.Net.UnitTests/CalendarEventTest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using Ical.Net.Serialization.iCalendar.Serializers; +using NUnit.Framework.Interfaces; namespace Ical.Net.UnitTests { @@ -19,10 +20,10 @@ public class CalendarEventTest /// /// Ensures that events can be properly added to a calendar. /// - [Test, Category("Event")] + [Test, Category("CalendarEvent")] public void Add1() { - Calendar cal = new Calendar(); + var cal = new Calendar(); var evt = new CalendarEvent { @@ -33,16 +34,16 @@ public void Add1() cal.Events.Add(evt); Assert.AreEqual(1, cal.Children.Count); - Assert.AreSame(evt, cal.Children[0]); + Assert.AreSame(evt, cal.Children[0]); } /// /// Ensures that events can be properly removed from a calendar. /// - [Test, Category("Event")] + [Test, Category("CalendarEvent")] public void Remove1() { - Calendar cal = new Calendar(); + var cal = new Calendar(); var evt = new CalendarEvent { @@ -63,10 +64,10 @@ public void Remove1() /// /// Ensures that events can be properly removed from a calendar. /// - [Test, Category("Event")] + [Test, Category("CalendarEvent")] public void Remove2() { - Calendar cal = new Calendar(); + var cal = new Calendar(); var evt = new CalendarEvent { @@ -87,10 +88,10 @@ public void Remove2() /// /// Ensures that event DTSTAMP is set. /// - [Test, Category("Event")] + [Test, Category("CalendarEvent")] public void EnsureDTSTAMPisNotNull() { - Calendar cal = new Calendar(); + var cal = new Calendar(); // Do not set DTSTAMP manually var evt = new CalendarEvent @@ -107,10 +108,10 @@ public void EnsureDTSTAMPisNotNull() /// /// Ensures that automatically set DTSTAMP property is of kind UTC. /// - [Test, Category("Event")] + [Test, Category("CalendarEvent")] public void EnsureDTSTAMPisOfTypeUTC() { - Calendar cal = new Calendar(); + var cal = new Calendar(); var evt = new CalendarEvent { @@ -124,40 +125,39 @@ public void EnsureDTSTAMPisOfTypeUTC() } /// - /// Ensures that correct set DTSTAMP property is being serialized with kind UTC. + /// Ensures that automatically set DTSTAMP property is being serialized with kind UTC. /// - [Test, Category("Deserialization")] - public void EnsureCorrectSetDTSTAMPisSerializedAsKindUTC() + [Test, Category("Deserialization"), TestCaseSource(nameof(EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_TestCases))] + public bool EnsureAutomaticallySetDTSTAMPisSerializedAsKindUTC(string serialized) { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.CalendarEvent(); - evt.DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)); - ical.Events.Add(evt); - - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); - - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); + var lines = serialized.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual("DTSTAMP:20160817T023000Z", result); + + //Both of these are correct, since the library no longer asserts that UTC must elide an explicit TZID in favor of the Z suffix on UTC times + return !result.Contains("TZID=") && result.EndsWith("Z") + || result.Contains("TZID=") && !result.EndsWith("Z"); } - /// - /// Ensures that automatically set DTSTAMP property is being serialized with kind UTC. - /// - [Test, Category("Deserialization")] - public void EnsureAutomaticallySetDTSTAMPisSerializedAsKindUTC() + public static IEnumerable EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_TestCases() { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.CalendarEvent(); - ical.Events.Add(evt); + var emptyCalendar = new Calendar(); + var evt = new CalendarEvent(); + emptyCalendar.Events.Add(evt); - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); + var serializer = new CalendarSerializer(); + yield return new TestCaseData(serializer.SerializeToString(emptyCalendar)) + .SetName("Empty calendar with empty event returns true") + .Returns(true); - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); - var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual($"DTSTAMP:{evt.DtStamp.Year}{evt.DtStamp.Month:00}{evt.DtStamp.Day:00}T{evt.DtStamp.Hour:00}{evt.DtStamp.Minute:00}{evt.DtStamp.Second:00}Z", result); + var explicitDtStampCalendar = new Calendar(); + var explicitDtStampEvent = new CalendarEvent + { + DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)) + }; + explicitDtStampCalendar.Events.Add(explicitDtStampEvent); + yield return new TestCaseData(serializer.SerializeToString(explicitDtStampCalendar)) + .SetName("CalendarEvent with explicitly-set DTSTAMP property returns true") + .Returns(true); } [Test] @@ -221,6 +221,7 @@ public void EventWithExDateShouldNotBeEqualToSameEventWithoutExDate() { DtStart = new CalDateTime(_now), DtEnd = new CalDateTime(_later), + Uid = _uid, }; [Test] @@ -281,7 +282,7 @@ public void AddingExdateToEventShouldNotBeEqualToOriginal() //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 + //CalendarEvent and Calendar hash codes and equality should NOT be the same var serializer = new CalendarSerializer(); var vEvent = GetSimpleEvent(); @@ -423,7 +424,6 @@ public void HourMinuteSecondOffsetParsingTest() END:STANDARD END:VTIMEZONE END:VCALENDAR"; - using (var reader = new StringReader(ical)) { var timezones = Calendar.LoadFromStream(reader) diff --git a/net-core/Ical.Net/Ical.Net.UnitTests/DateTimeSerializerTests.cs b/net-core/Ical.Net/Ical.Net.UnitTests/DateTimeSerializerTests.cs index d1b6d3d63..e5e1f7409 100644 --- a/net-core/Ical.Net/Ical.Net.UnitTests/DateTimeSerializerTests.cs +++ b/net-core/Ical.Net/Ical.Net.UnitTests/DateTimeSerializerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Ical.Net.DataTypes; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using NUnit.Framework; @@ -9,38 +8,16 @@ namespace Ical.Net.UnitTests.Serialization.iCalendar.Serializers.DataTypes [TestFixture] public class DateTimeSerializerTests { - [Test, Category("Deserialization")] - public void TZIDPropertyMustNotBeAppliedToUtcDateTime() - { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.CalendarEvent(); - evt.DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)); - ical.Events.Add(evt); - - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); - - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); - var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual("DTSTAMP:20160817T023000Z", result); - } - [Test, Category("Deserialization")] public void TZIDPropertyShouldBeAppliedForLocalTimezones() { // see http://www.ietf.org/rfc/rfc2445.txt p.36 - var result = DateTimeSerializer() + var result = new DateTimeSerializer() .SerializeToString( new CalDateTime(new DateTime(1997, 7, 14, 13, 30, 0, DateTimeKind.Local), "US-Eastern")); // TZID is applied elsewhere - just make sure this doesn't have 'Z' appended. Assert.AreEqual("19970714T133000", result); } - - - private static DateTimeSerializer DateTimeSerializer() - { - return new DateTimeSerializer(); - } } } \ No newline at end of file diff --git a/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs b/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs index f6571bd21..9caffa38a 100644 --- a/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs +++ b/net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs @@ -3150,7 +3150,7 @@ public void ExDateFold_Tests() Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); var secondExclusion = new CalDateTime(start.AddDays(5)); - e.ExceptionDates.Add(new PeriodList { new Period(secondExclusion) }); + e.ExceptionDates.First().Add(new Period(secondExclusion)); serialized = SerializeToString(e); Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); } @@ -3170,14 +3170,17 @@ public void ExDateTimeZone_Tests() RecurrenceRules = new List { rrule }, }; - var exceptionDateList = new PeriodList(); - exceptionDateList.TzId = tzid; - exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1), tzid))); + var exceptionDateList = new PeriodList { TzId = tzid }; + exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1)))); e.ExceptionDates.Add(exceptionDateList); var serialized = SerializeToString(e); - const string expected = "EXDATE;TZID=Europe/Stockholm:"; - Assert.AreEqual(1, Regex.Matches(serialized, expected).Count); + const string expected = "TZID=Europe/Stockholm"; + Assert.AreEqual(3, Regex.Matches(serialized, expected).Count); + + e.ExceptionDates.First().Add(new Period(new CalDateTime(_now.AddDays(2)))); + serialized = SerializeToString(e); + Assert.AreEqual(3, Regex.Matches(serialized, expected).Count); } } } diff --git a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs index d08e0973f..e54501d3d 100644 --- a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs +++ b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs @@ -44,12 +44,11 @@ public override string SerializeToString(object obj) { var dt = obj as IDateTime; - // Assign the TZID for the date/time value. - if (dt.IsUniversalTime) - { - dt.Parameters.Remove("TZID"); - } - else if (dt.TzId != null) + //Historically, dday.ical substituted TZID=UTC with Z suffixes on DateTimes. However this behavior isn't part of the spec. Some popular libraries + //like Telerik's RadSchedule components will only understand the DateTimeKind.Utc if the ical text says TZID=UTC. Anything but that is treated as + //DateTimeKind.Unspecified, which is problematic. + + if (!string.IsNullOrWhiteSpace(dt.TzId)) { dt.Parameters.Set("TZID", dt.TzId); } @@ -68,7 +67,7 @@ public override string SerializeToString(object obj) if (dt.HasTime) { value.Append($"T{dt.Hour:00}{dt.Minute:00}{dt.Second:00}"); - if (dt.IsUniversalTime) + if (dt.IsUniversalTime && string.IsNullOrWhiteSpace(dt.Parameters.Get("TZID"))) { value.Append("Z"); } diff --git a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs index a46227f37..1f701d536 100644 --- a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs +++ b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs @@ -18,22 +18,23 @@ public PeriodListSerializer(SerializationContext ctx) : base(ctx) { } public override string SerializeToString(object obj) { - var rdt = obj as PeriodList; + var periodList = obj as PeriodList; var factory = GetService(); - if (rdt == null || factory == null) + if (periodList == null || factory == null) { return null; } - var dtSerializer = factory.Build(typeof (IDateTime), SerializationContext) as IStringSerializer; - var periodSerializer = factory.Build(typeof (Period), SerializationContext) as IStringSerializer; + var dtSerializer = factory.Build(typeof(IDateTime), SerializationContext) as IStringSerializer; + var periodSerializer = factory.Build(typeof(Period), SerializationContext) as IStringSerializer; if (dtSerializer == null || periodSerializer == null) { return null; } - var parts = new List(rdt.Count); - foreach (var p in rdt) + var parts = new List(periodList.Count); + + foreach (var p in periodList) { if (p.EndTime != null) { @@ -45,7 +46,7 @@ public override string SerializeToString(object obj) } } - return Encode(rdt, string.Join(",", parts)); + return Encode(periodList, string.Join(",", parts)); } public override object Deserialize(TextReader tr) diff --git a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/PropertySerializer.cs b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/PropertySerializer.cs index d69b08613..53fa4ffb1 100644 --- a/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/PropertySerializer.cs +++ b/net-core/Ical.Net/Ical.Net/Serialization/iCalendar/Serializers/PropertySerializer.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using Ical.Net.DataTypes; using Ical.Net.General; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; @@ -62,6 +63,20 @@ public override string SerializeToString(object obj) parameterList = (v as ICalendarDataType).Parameters; } + //This says that the TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. There's nothing in the spec that + //prohibits having multiple EXDATE or RDATE collections, each of which specifies a different TZID. What *should* happen during serialization is + //that we should work with a single collection of zoned datetime objects, and we should create distinct RDATE and EXDATE collections based on + //those values. Right now, if you add CalDateTime objects, each of which specifies a different time zone, the first one "wins". This means + //application developers will need to handle those cases outside the library. + if (v is PeriodList) + { + var typed = (PeriodList)v; + if (!string.IsNullOrWhiteSpace(typed.TzId) && parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) + { + parameterList.Set("TZID", typed.TzId); + } + } + var sb = new StringBuilder(256); sb.Append(prop.Name); if (parameterList.Any()) @@ -79,7 +94,6 @@ public override string SerializeToString(object obj) sb.Append(":"); sb.Append(value); - //result.Append(TextUtil.WrapLines(sb.ToString())); result.Append(TextUtil.FoldLines(sb.ToString())); } diff --git a/v2/ical.NET.UnitTests/DateTimeSerializerTests.cs b/v2/ical.NET.UnitTests/DateTimeSerializerTests.cs index 962259d43..e5e1f7409 100644 --- a/v2/ical.NET.UnitTests/DateTimeSerializerTests.cs +++ b/v2/ical.NET.UnitTests/DateTimeSerializerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using Ical.Net.DataTypes; using Ical.Net.Serialization.iCalendar.Serializers.DataTypes; using NUnit.Framework; @@ -9,38 +8,16 @@ namespace Ical.Net.UnitTests.Serialization.iCalendar.Serializers.DataTypes [TestFixture] public class DateTimeSerializerTests { - [Test, Category("Deserialization")] - public void TZIDPropertyMustNotBeAppliedToUtcDateTime() - { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.Event(); - evt.DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)); - ical.Events.Add(evt); - - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); - - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); - var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual("DTSTAMP:20160817T023000Z", result); - } - [Test, Category("Deserialization")] public void TZIDPropertyShouldBeAppliedForLocalTimezones() { // see http://www.ietf.org/rfc/rfc2445.txt p.36 - var result = DateTimeSerializer() + var result = new DateTimeSerializer() .SerializeToString( new CalDateTime(new DateTime(1997, 7, 14, 13, 30, 0, DateTimeKind.Local), "US-Eastern")); // TZID is applied elsewhere - just make sure this doesn't have 'Z' appended. Assert.AreEqual("19970714T133000", result); } - - - private static DateTimeSerializer DateTimeSerializer() - { - return new DateTimeSerializer(); - } } } \ No newline at end of file diff --git a/v2/ical.NET.UnitTests/EventTest.cs b/v2/ical.NET.UnitTests/EventTest.cs index 0cd274a13..5bbf82a4b 100644 --- a/v2/ical.NET.UnitTests/EventTest.cs +++ b/v2/ical.NET.UnitTests/EventTest.cs @@ -1,13 +1,14 @@ -using Ical.Net.DataTypes; -using Ical.Net.ExtensionMethods; -using Ical.Net.Interfaces; -using NUnit.Framework; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Ical.Net.DataTypes; +using Ical.Net.ExtensionMethods; +using Ical.Net.Interfaces; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Serialization.iCalendar.Serializers; +using NUnit.Framework; +using NUnit.Framework.Interfaces; namespace Ical.Net.UnitTests { @@ -126,40 +127,39 @@ public void EnsureDTSTAMPisOfTypeUTC() } /// - /// Ensures that correct set DTSTAMP property is being serialized with kind UTC. + /// Ensures that automatically set DTSTAMP property is being serialized with kind UTC. /// - [Test, Category("Deserialization")] - public void EnsureCorrectSetDTSTAMPisSerializedAsKindUTC() + [Test, Category("Deserialization"), TestCaseSource(nameof(EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_TestCases))] + public bool EnsureAutomaticallySetDTSTAMPisSerializedAsKindUTC(string serialized) { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.Event(); - evt.DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)); - ical.Events.Add(evt); - - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); - - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); + var lines = serialized.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual("DTSTAMP:20160817T023000Z", result); + + //Both of these are correct, since the library no longer asserts that UTC must elide an explicit TZID in favor of the Z suffix on UTC times + return !result.Contains("TZID=") && result.EndsWith("Z") + || result.Contains("TZID=") && !result.EndsWith("Z"); } - /// - /// Ensures that automatically set DTSTAMP property is being serialized with kind UTC. - /// - [Test, Category("Deserialization")] - public void EnsureAutomaticallySetDTSTAMPisSerializedAsKindUTC() + public static IEnumerable EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_TestCases() { - var ical = new Ical.Net.Calendar(); - var evt = new Ical.Net.Event(); - ical.Events.Add(evt); + var emptyCalendar = new Calendar(); + var evt = new Event(); + emptyCalendar.Events.Add(evt); - var serializer = new Ical.Net.Serialization.iCalendar.Serializers.CalendarSerializer(); - var serializedCalendar = serializer.SerializeToString(ical); + var serializer = new CalendarSerializer(); + yield return new TestCaseData(serializer.SerializeToString(emptyCalendar)) + .SetName("Empty calendar with empty event returns true") + .Returns(true); - var lines = serializedCalendar.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); - var result = lines.First(s => s.StartsWith("DTSTAMP")); - Assert.AreEqual($"DTSTAMP:{evt.DtStamp.Year}{evt.DtStamp.Month:00}{evt.DtStamp.Day:00}T{evt.DtStamp.Hour:00}{evt.DtStamp.Minute:00}{evt.DtStamp.Second:00}Z", result); + var explicitDtStampCalendar = new Calendar(); + var explicitDtStampEvent = new Event + { + DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)) + }; + explicitDtStampCalendar.Events.Add(explicitDtStampEvent); + yield return new TestCaseData(serializer.SerializeToString(explicitDtStampCalendar)) + .SetName("Event with explicitly-set DTSTAMP property returns true") + .Returns(true); } [Test] diff --git a/v2/ical.NET.UnitTests/RecurrenceTests.cs b/v2/ical.NET.UnitTests/RecurrenceTests.cs index 8fad53b64..28d05dd6d 100644 --- a/v2/ical.NET.UnitTests/RecurrenceTests.cs +++ b/v2/ical.NET.UnitTests/RecurrenceTests.cs @@ -3152,7 +3152,7 @@ public void ExDateFold_Tests() Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); var secondExclusion = new CalDateTime(start.AddDays(5)); - e.ExceptionDates.Add(new PeriodList { new Period(secondExclusion) }); + e.ExceptionDates.First().Add(new Period(secondExclusion)); serialized = SerializeToString(e); Assert.AreEqual(1, Regex.Matches(serialized, "EXDATE:").Count); } @@ -3172,14 +3172,17 @@ public void ExDateTimeZone_Tests() RecurrenceRules = new List { rrule }, }; - var exceptionDateList = new PeriodList(); - exceptionDateList.TzId = tzid; - exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1), tzid))); + var exceptionDateList = new PeriodList {TzId = tzid}; + exceptionDateList.Add(new Period(new CalDateTime(_now.AddDays(1)))); e.ExceptionDates.Add(exceptionDateList); var serialized = SerializeToString(e); - const string expected = "EXDATE;TZID=Europe/Stockholm:"; - Assert.AreEqual(1, Regex.Matches(serialized, expected).Count); + const string expected = "TZID=Europe/Stockholm"; + Assert.AreEqual(3, Regex.Matches(serialized, expected).Count); + + e.ExceptionDates.First().Add(new Period(new CalDateTime(_now.AddDays(2)))); + serialized = SerializeToString(e); + Assert.AreEqual(3, Regex.Matches(serialized, expected).Count); } } } diff --git a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs index 55fe47916..0263c6f21 100644 --- a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs +++ b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/DateTimeSerializer.cs @@ -45,12 +45,11 @@ public override string SerializeToString(object obj) { var dt = obj as IDateTime; - // Assign the TZID for the date/time value. - if (dt.IsUniversalTime) - { - dt.Parameters.Remove("TZID"); - } - else if (dt.TzId != null) + //Historically, dday.ical substituted TZID=UTC with Z suffixes on DateTimes. However this behavior isn't part of the spec. Some popular libraries + //like Telerik's RadSchedule components will only understand the DateTimeKind.Utc if the ical text says TZID=UTC. Anything but that is treated as + //DateTimeKind.Unspecified, which is problematic. + + if (!string.IsNullOrWhiteSpace(dt.TzId)) { dt.Parameters.Set("TZID", dt.TzId); } @@ -69,7 +68,7 @@ public override string SerializeToString(object obj) if (dt.HasTime) { value.Append($"T{dt.Hour:00}{dt.Minute:00}{dt.Second:00}"); - if (dt.IsUniversalTime) + if (dt.IsUniversalTime && string.IsNullOrWhiteSpace(dt.Parameters.Get("TZID"))) { value.Append("Z"); } diff --git a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs index bbf5853a6..111242c26 100644 --- a/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs +++ b/v2/ical.NET/Serialization/iCalendar/Serializers/DataTypes/PeriodListSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Ical.Net.DataTypes; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.Serialization; @@ -18,9 +19,9 @@ public PeriodListSerializer(ISerializationContext ctx) : base(ctx) { } public override string SerializeToString(object obj) { - var rdt = obj as IPeriodList; + var periodList = obj as IPeriodList; var factory = GetService(); - if (rdt == null || factory == null) + if (periodList == null || factory == null) { return null; } @@ -32,8 +33,9 @@ public override string SerializeToString(object obj) return null; } - var parts = new List(rdt.Count); - foreach (var p in rdt) + var parts = new List(periodList.Count); + + foreach (var p in periodList) { if (p.EndTime != null) { @@ -45,7 +47,7 @@ public override string SerializeToString(object obj) } } - return Encode(rdt, string.Join(",", parts)); + return Encode(periodList, string.Join(",", parts)); } public override object Deserialize(TextReader tr) diff --git a/v2/ical.NET/Serialization/iCalendar/Serializers/PropertySerializer.cs b/v2/ical.NET/Serialization/iCalendar/Serializers/PropertySerializer.cs index adb91be44..8936132df 100644 --- a/v2/ical.NET/Serialization/iCalendar/Serializers/PropertySerializer.cs +++ b/v2/ical.NET/Serialization/iCalendar/Serializers/PropertySerializer.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Text; +using Ical.Net.DataTypes; using Ical.Net.General; using Ical.Net.Interfaces.DataTypes; using Ical.Net.Interfaces.General; @@ -62,6 +63,20 @@ public override string SerializeToString(object obj) parameterList = (v as ICalendarDataType).Parameters; } + //This says that the TZID property of an RDATE/EXDATE collection is owned by the PeriodList that contains it. There's nothing in the spec that + //prohibits having multiple EXDATE or RDATE collections, each of which specifies a different TZID. What *should* happen during serialization is + //that we should work with a single collection of zoned datetime objects, and we should create distinct RDATE and EXDATE collections based on + //those values. Right now, if you add CalDateTime objects, each of which specifies a different time zone, the first one "wins". This means + //application developers will need to handle those cases outside the library. + if (v is PeriodList) + { + var typed = (PeriodList) v; + if (!string.IsNullOrWhiteSpace(typed.TzId) && parameterList.All(p => string.Equals("TZID", p.Value, StringComparison.OrdinalIgnoreCase))) + { + parameterList.Set("TZID", typed.TzId); + } + } + var sb = new StringBuilder(256); sb.Append(prop.Name); if (parameterList.Any()) @@ -79,7 +94,6 @@ public override string SerializeToString(object obj) sb.Append(":"); sb.Append(value); - //result.Append(TextUtil.WrapLines(sb.ToString())); result.Append(TextUtil.FoldLines(sb.ToString())); } From f1194f5d6b073ecf0081e6739f9d7cb2fab6d716 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 6 Apr 2017 13:44:31 -0400 Subject: [PATCH 5/6] ical v3 PeriodList implements IList instead of IEnumerable #259 --- .../Ical.Net/Ical.Net/DataTypes/PeriodList.cs | 29 ++++++++++++++++++- .../Interfaces/DataTypes/IPeriodList.cs | 1 - 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/net-core/Ical.Net/Ical.Net/DataTypes/PeriodList.cs b/net-core/Ical.Net/Ical.Net/DataTypes/PeriodList.cs index ba9eb93a1..6d755d43d 100644 --- a/net-core/Ical.Net/Ical.Net/DataTypes/PeriodList.cs +++ b/net-core/Ical.Net/Ical.Net/DataTypes/PeriodList.cs @@ -12,7 +12,7 @@ namespace Ical.Net.DataTypes /// /// An iCalendar list of recurring dates (or date exclusions) /// - public class PeriodList : EncodableDataType, IEnumerable + public class PeriodList : EncodableDataType, IList { public string TzId { get; set; } public int Count => Periods.Count; @@ -80,5 +80,32 @@ public Period this[int index] public IEnumerator GetEnumerator() => Periods.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Periods.GetEnumerator(); + public void Clear() + { + Periods.Clear(); + } + + public bool Contains(Period item) => Periods.Contains(item); + + public void CopyTo(Period[] array, int arrayIndex) + { + Periods.CopyTo(array, arrayIndex); + } + + public bool Remove(Period item) => Periods.Remove(item); + + public bool IsReadOnly => Periods.IsReadOnly; + + public int IndexOf(Period item) => Periods.IndexOf(item); + + public void Insert(int index, Period item) + { + Periods.Insert(index, item); + } + + public void RemoveAt(int index) + { + Periods.RemoveAt(index); + } } } \ No newline at end of file diff --git a/v2/ical.NET/Interfaces/DataTypes/IPeriodList.cs b/v2/ical.NET/Interfaces/DataTypes/IPeriodList.cs index 9ebcfe2a6..2ec6ab3f2 100644 --- a/v2/ical.NET/Interfaces/DataTypes/IPeriodList.cs +++ b/v2/ical.NET/Interfaces/DataTypes/IPeriodList.cs @@ -8,7 +8,6 @@ public interface IPeriodList : IEncodableDataType, IEnumerable IPeriod this[int index] { get; } void Add(IDateTime dt); void Add(IPeriod item); - IEnumerator GetEnumerator(); int Count { get; } } } \ No newline at end of file From b7337e4b137d448cef556894ad846495a69380b3 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 6 Apr 2017 13:59:16 -0400 Subject: [PATCH 6/6] Nump nuspec versions and update release notes #259 --- net-core/Ical.Net/Ical.Net.nuspec | 2 +- release-notes.md | 2 ++ v2/Ical.Net.nuspec | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/net-core/Ical.Net/Ical.Net.nuspec b/net-core/Ical.Net/Ical.Net.nuspec index a84939670..cad7d0865 100644 --- a/net-core/Ical.Net/Ical.Net.nuspec +++ b/net-core/Ical.Net/Ical.Net.nuspec @@ -2,7 +2,7 @@ Ical.Net - 3.0.1-net-core-alpha + 3.0.3-net-core-alpha Ical.Net Rian Stockbower, Douglas Day, M. David Peterson Rian Stockbower diff --git a/release-notes.md b/release-notes.md index 5f2b2c813..b022a5b5d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,11 +4,13 @@ A listing of what each [Nuget package](https://www.nuget.org/packages/Ical.Net) ### v3 +* 3.0.3-net-core-alpha: Bringing in 2.2.34's changes: serializing `EXDATE`s and `RDATE`s now includes time zone information when specified. UTC timestamps are now allowed to be specified either in `Z`-suffixed form (which was mandatory before), OR explicitly with a time zone id (`TZID=UTC`). This allows greater interop with third-party libraries like Telerik's RadSchedule. [#259](https://github.com/rianjs/ical.net/issues/259) `PeriodList` now implements `IList`. * 3.0.2-net-core-alpha: Fix nuspec file to declare NodaTime as dependency * 3.0.1-alpha: Initial publishing of .NET Core release ### v2 +* 2.2.34: Serializing `EXDATE`s and `RDATE`s now includes time zone information when specified. UTC timestamps are now allowed to be specified either in `Z`-suffixed form (which was mandatory before), OR explicitly with a time zone id (`TZID=UTC`). This allows greater interop with third-party libraries like Telerik's RadSchedule. [#259](https://github.com/rianjs/ical.net/issues/259) * 2.2.33: Bugfix for [#235](https://github.com/rianjs/ical.net/issues/235) when years have 53 weeks. Contains a new deserializer that's twice as fast as the default ANTLR implementation, and several other (smaller) performance enhancements. _This will become the default deserializer in a future release._ [PR 246](https://github.com/rianjs/ical.net/pull/246), [PR 247](https://github.com/rianjs/ical.net/pull/247) * 2.2.31: .NET's UTC offset parsing semantics don't match the RFC (which allows for `hhmmss` UTC offsets), so I extended the offset serializer to account for these differences. ([#102](https://github.com/rianjs/ical.net/issues/102), [#236](https://github.com/rianjs/ical.net/issues/236)) * 2.2.30: `Event.Resources` is an `IList` again. Event.Resources wasn't being deserialized, so I have reverted back to an IList to fix this. Sorry everyone. I'm intentionally violating my own semver rules. In the future, I'll call version n+1 "vNext" so I can more freely rev the major version number. diff --git a/v2/Ical.Net.nuspec b/v2/Ical.Net.nuspec index b328576c9..bdf80a61d 100644 --- a/v2/Ical.Net.nuspec +++ b/v2/Ical.Net.nuspec @@ -2,7 +2,7 @@ Ical.Net - 2.2.33 + 2.2.34 Ical.Net Rian Stockbower, Douglas Day, M. David Peterson Rian Stockbower