Skip to content

Commit

Permalink
Better time zone handling:
Browse files Browse the repository at this point in the history
- Use local time zones instead of doing everything in terms of UTC
- Set and compare DateTimeKind during serialization and deserialization
- Get rid of IsUniversalTime property in IDateTime
- IsUtc property is get-only
- AsSystemLocal no longer needs to do contortions to answer the question
- ToTimeZone returns local or UTC time zones for BCL, serialization, and
  IANA zones
- Better heuristics for determining whether a CalDateTime is UTC or local
- TzId setter now does all the state maintenance for UTC vs local
  bookkeeping, and all other related properties are get-only
- Consistent, ordinal string comparison in GetZone
- Truncating parts of DateTimes doesn't suck anymore
- Slightly better intellisense documentation
- Fixed a broken unit test
- Better local vs UTC time zone handling for Unit RRULEs with unit tests
- Fixed a bug in the RecurrencePatternEvaluator where tzId wasn't taking
  into account
- MatchTimeZone is less awkward and shorter

Issues: #331, #330
  • Loading branch information
Rian Stockbower committed Nov 7, 2017
1 parent dd487ce commit c572071
Show file tree
Hide file tree
Showing 18 changed files with 344 additions and 167 deletions.
6 changes: 4 additions & 2 deletions net-core/Ical.Net/Ical.Net.UnitTests/AlarmTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ical.Net.DataTypes;
using Ical.Net.Interfaces.DataTypes;

using NUnit.Framework;

namespace Ical.Net.UnitTests
Expand All @@ -22,10 +22,12 @@ public void TestAlarm(string calendarString, List<IDateTime> dates, CalDateTime
// Poll all alarms that occurred between Start and End
var alarms = evt.PollAlarms(start, end);

var utcDates = new HashSet<DateTime>(dates.Select(d => d.AsUtc));

//Only compare the UTC values here, since we care about the time coordinate when the alarm fires, and nothing else
foreach (var alarm in alarms.Select(a => a.DateTime.AsUtc))
{
Assert.IsTrue(dates.Select(d => d.AsUtc).Contains(alarm), "Alarm triggers at " + alarm + ", but it should not.");
Assert.IsTrue(utcDates.Contains(alarm), "Alarm triggers at " + alarm + ", but it should not.");
}
Assert.IsTrue(dates.Count == alarms.Count, "There were " + alarms.Count + " alarm occurrences; there should have been " + dates.Count + ".");
}
Expand Down
68 changes: 68 additions & 0 deletions net-core/Ical.Net/Ical.Net.UnitTests/CalDateTimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using Ical.Net.DataTypes;
using NUnit.Framework;
using NUnit.Framework.Interfaces;

namespace Ical.Net.UnitTests
{
public class CalDateTimeTests
{
private static readonly DateTime _now = DateTime.Now;
private static readonly DateTime _later = _now.AddHours(1);
private static CalendarEvent GetEventWithRecurrenceRules(string tzId)
{
var dailyForFiveDays = new RecurrencePattern(FrequencyType.Daily, 1)
{
Count = 5,
};

var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(_now, tzId),
End = new CalDateTime(_later, tzId),
RecurrenceRules = new List<RecurrencePattern> { dailyForFiveDays },
Resources = new List<string>(new[] { "Foo", "Bar", "Baz" }),
};
return calendarEvent;
}

[Test, TestCaseSource(nameof(ToTimeZoneTestCases))]
public void ToTimeZoneTests(CalendarEvent calendarEvent, string targetTimeZone)
{
var startAsUtc = calendarEvent.Start.AsUtc;

var convertedStart = calendarEvent.Start.ToTimeZone(targetTimeZone);
var convertedAsUtc = convertedStart.AsUtc;

Assert.AreEqual(startAsUtc, convertedAsUtc);
}

public static IEnumerable<ITestCaseData> ToTimeZoneTestCases()
{
const string bclCst = "Central Standard Time";
const string bclEastern = "Eastern Standard Time";
var bclEvent = GetEventWithRecurrenceRules(bclCst);
yield return new TestCaseData(bclEvent, bclEastern)
.SetName($"BCL to BCL: {bclCst} to {bclEastern}");

const string ianaNy = "America/New_York";
const string ianaRome = "Europe/Rome";
var ianaEvent = GetEventWithRecurrenceRules(ianaNy);

yield return new TestCaseData(ianaEvent, ianaRome)
.SetName($"IANA to IANA: {ianaNy} to {ianaRome}");

const string utc = "UTC";
var utcEvent = GetEventWithRecurrenceRules(utc);
yield return new TestCaseData(utcEvent, utc)
.SetName("UTC to UTC");

yield return new TestCaseData(bclEvent, ianaRome)
.SetName($"BCL to IANA: {bclCst} to {ianaRome}");

yield return new TestCaseData(ianaEvent, bclCst)
.SetName($"IANA to BCL: {ianaNy} to {bclCst}");
}
}
}
2 changes: 1 addition & 1 deletion net-core/Ical.Net/Ical.Net.UnitTests/CalendarEventTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void EnsureDTSTAMPisOfTypeUTC()
};

cal.Events.Add(evt);
Assert.IsTrue(evt.DtStamp.IsUniversalTime, "DTSTAMP should always be of type UTC.");
Assert.IsTrue(evt.DtStamp.IsUtc, "DTSTAMP should always be of type UTC.");
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void Daily_Test()
//Recur daily through the end of the day, July 31, 2016
var recurrenceRule = new RecurrencePattern(FrequencyType.Daily, 1)
{
Until = DateTime.Parse("2016-07-31T11:59:59")
Until = DateTime.Parse("2016-07-31T23:59:59")
};

vEvent.RecurrenceRules = new List<RecurrencePattern> {recurrenceRule};
Expand Down
20 changes: 12 additions & 8 deletions net-core/Ical.Net/Ical.Net.UnitTests/GetOccurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using Ical.Net.DataTypes;
using Ical.Net.Interfaces.DataTypes;
using Ical.Net.Utility;
using NUnit.Framework;

Expand Down Expand Up @@ -68,16 +69,19 @@ public void SkippedOccurrenceOnWeeklyPattern()
var intervalStart = eventStart;
var intervalEnd = intervalStart.AddDays(7 * evaluationsCount);

var occurrences = RecurrenceUtil.GetOccurrences(vEvent, intervalStart, intervalEnd, false)
.Select(o => o.Period.StartTime)
.OrderBy(dt => dt)
.ToList();
Assert.AreEqual(evaluationsCount, occurrences.Count);
var occurrences = RecurrenceUtil.GetOccurrences(
recurrable: vEvent,
periodStart: intervalStart,
periodEnd: intervalEnd,
includeReferenceDateInResults: false);
var occurrenceSet = new HashSet<IDateTime>(occurrences.Select(o => o.Period.StartTime));

for (var currentOccurrence = intervalStart.AsUtc; currentOccurrence.CompareTo(intervalEnd.AsUtc) < 0; currentOccurrence = currentOccurrence.AddDays(7))
Assert.AreEqual(evaluationsCount, occurrenceSet.Count);

for (var currentOccurrence = intervalStart; currentOccurrence.CompareTo(intervalEnd) < 0; currentOccurrence = (CalDateTime)currentOccurrence.AddDays(7))
{
Assert.IsTrue(occurrences.Contains(new CalDateTime(currentOccurrence)),
$"Collection does not contain {currentOccurrence}, but it is a {currentOccurrence.DayOfWeek}");
var contains = occurrenceSet.Contains(currentOccurrence);
Assert.IsTrue(contains, $"Collection does not contain {currentOccurrence}, but it is a {currentOccurrence.DayOfWeek}");
}
}

Expand Down
42 changes: 42 additions & 0 deletions net-core/Ical.Net/Ical.Net.UnitTests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
using Ical.Net.Interfaces.DataTypes;
using Ical.Net.Interfaces.Evaluation;
using Ical.Net.Serialization;
using Ical.Net.Serialization.iCalendar.Serializers;
using Ical.Net.Serialization.iCalendar.Serializers.DataTypes;
using Ical.Net.Utility;
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using static Ical.Net.UnitTests.SerializationHelpers;

namespace Ical.Net.UnitTests
Expand Down Expand Up @@ -3378,5 +3380,45 @@ public void ManyExclusionDatesEqualityTesting()
Assert.AreEqual(exDatesA, exDatesB);

}

[Test, TestCaseSource(nameof(UntilTimeZoneSerializationTestCases))]
public void UntilTimeZoneSerializationTests(string tzid, DateTimeKind expectedKind)
{
var until = DateTime.SpecifyKind(_now.AddDays(7), expectedKind);

var rrule = new RecurrencePattern(FrequencyType.Daily)
{
Until = until,
};
var e = new CalendarEvent
{
Start = new CalDateTime(_now, tzid),
End = new CalDateTime(_later, tzid)
};
e.RecurrenceRules.Add(rrule);
var calendar = new Calendar
{
Events = { e },
};

var serializer = new CalendarSerializer();

var serialized = serializer.SerializeToString(calendar);
var deserializedKind = (serializer.Deserialize(new StringReader(serialized)) as CalendarCollection).First()
.Events.First()
.RecurrenceRules.First().Until.Kind;

Assert.AreEqual(expectedKind, deserializedKind);
}

public static IEnumerable<ITestCaseData> UntilTimeZoneSerializationTestCases()
{
yield return new TestCaseData("America/New_York", DateTimeKind.Local)
.SetName("IANA time time zone results in a local DateTimeKind");
yield return new TestCaseData("Eastern Standard Time", DateTimeKind.Local)
.SetName("BCL time zone results in a Local DateTimeKind");
yield return new TestCaseData("UTC", DateTimeKind.Utc)
.SetName("UTC results in DateTimeKind.Utc");
}
}
}
2 changes: 1 addition & 1 deletion net-core/Ical.Net/Ical.Net.UnitTests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public static string InspectSerializedSection(string serialized, string sectionN
static string CalDateString(IDateTime cdt)
{
var returnVar = $"{cdt.Year}{cdt.Month:D2}{cdt.Day:D2}T{cdt.Hour:D2}{cdt.Minute:D2}{cdt.Second:D2}";
if (cdt.IsUniversalTime)
if (cdt.IsUtc)
{
return returnVar + 'Z';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Ical.Net.DataTypes;
using Ical.Net.General;
using Ical.Net.Interfaces;
using Ical.Net.Interfaces.Components;
using Ical.Net.Interfaces.DataTypes;
using Ical.Net.Interfaces.General;
using Ical.Net.Serialization;
Expand Down
11 changes: 5 additions & 6 deletions net-core/Ical.Net/Ical.Net/Components/UniqueComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Ical.Net.DataTypes;
using Ical.Net.Interfaces.Components;
using Ical.Net.Interfaces.DataTypes;
using Ical.Net.Utility;

namespace Ical.Net
{
Expand Down Expand Up @@ -41,12 +42,10 @@ private void EnsureProperties()
// See https://sourceforge.net/projects/dday-ical/forums/forum/656447/topic/3754354
if (DtStamp == null)
{
// Here, we don't simply set to DateTime.Now because DateTime.Now contains milliseconds, and
// the iCalendar standard doesn't care at all about milliseconds. Therefore, when comparing
// two calendars, one generated, and one loaded from file, they may be functionally identical,
// but be determined to be different due to millisecond differences.
var now = DateTime.SpecifyKind(DateTime.Today.Add(DateTime.UtcNow.TimeOfDay), DateTimeKind.Utc);
DtStamp = new CalDateTime(now);
// icalendar RFC doesn't care about sub-second time resolution, so shave off everything smaller than seconds.

var utcNow = DateTime.UtcNow.Truncate(TimeSpan.FromSeconds(1)); //DateTimeKind.Utc is preserved
DtStamp = new CalDateTime(utcNow, "UTC");
}
}

Expand Down
Loading

0 comments on commit c572071

Please sign in to comment.