diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index b3ca3191..5a35ebb4 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -10,6 +10,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; namespace Ical.Net.Tests; @@ -73,56 +74,21 @@ public static IEnumerable ToTimeZoneTestCases() .SetName($"IANA to BCL: {ianaNy} to {bclCst}"); } - [TestCaseSource(nameof(AsDateTimeOffsetTestCases))] - public DateTimeOffset AsDateTimeOffsetTests(CalDateTime incoming) - => incoming.AsDateTimeOffset; - - public static IEnumerable AsDateTimeOffsetTestCases() - { - const string nyTzId = "America/New_York"; - var summerDate = DateTime.Parse("2018-05-15T11:00"); - - var nySummerOffset = TimeSpan.FromHours(-4); - var nySummer = new CalDateTime(summerDate, nyTzId); - yield return new TestCaseData(nySummer) - .SetName("NY Summer DateTime returns DateTimeOffset with UTC-4") - .Returns(new DateTimeOffset(summerDate, nySummerOffset)); - - var utc = new CalDateTime(summerDate, "UTC"); - yield return new TestCaseData(utc) - .SetName("UTC summer DateTime returns a DateTimeOffset with UTC-0") - .Returns(new DateTimeOffset(summerDate, TimeSpan.Zero)); - - var convertedToNySummer = new CalDateTime(summerDate, "UTC"); - convertedToNySummer.TzId = nyTzId; - yield return new TestCaseData(convertedToNySummer) - .SetName( - "Summer UTC DateTime converted to NY time zone by setting TzId returns a DateTimeOffset with UTC-4") - .Returns(new DateTimeOffset(summerDate, nySummerOffset)); - - var noTz = new CalDateTime(summerDate); - var currentSystemOffset = TimeZoneInfo.Local.GetUtcOffset(summerDate); - yield return new TestCaseData(noTz) - .SetName( - $"Summer DateTime with no time zone information returns the system-local's UTC offset ({currentSystemOffset})") - .Returns(new DateTimeOffset(summerDate, currentSystemOffset)); - } - - [Test(Description = "Calling AsUtc should always return the proper UTC time, even if the TzId has changed")] - public void TestTzidChanges() + [Test(Description = "A certain date/time value applied to different timezones should return the same UTC date/time")] + public void SameDateTimeWithDifferentTzIdShouldReturnSameUtc() { var someTime = DateTimeOffset.Parse("2018-05-21T11:35:00-04:00"); - var someDt = new CalDateTime(someTime.DateTime) { TzId = "America/New_York" }; + var someDt = new CalDateTime(someTime.DateTime, "America/New_York"); var firstUtc = someDt.AsUtc; Assert.That(firstUtc, Is.EqualTo(someTime.UtcDateTime)); - someDt.TzId = "Europe/Berlin"; + someDt = new CalDateTime(someTime.DateTime, "Europe/Berlin"); var berlinUtc = someDt.AsUtc; Assert.That(berlinUtc, Is.Not.EqualTo(firstUtc)); } - [Test, TestCaseSource(nameof(DateTimeKindOverrideTestCases))] + [Test, TestCaseSource(nameof(DateTimeKindOverrideTestCases)), Description("DateTimeKind of values is always DateTimeKind.Unspecified")] public DateTimeKind DateTimeKindOverrideTests(DateTime dateTime, string tzId) => new CalDateTime(dateTime, tzId).Value.Kind; @@ -132,12 +98,12 @@ public static IEnumerable DateTimeKindOverrideTestCases() var localDt = DateTime.SpecifyKind(DateTime.Parse("2018-05-21T11:35:33"), DateTimeKind.Unspecified); yield return new TestCaseData(localDt, "UTC") - .Returns(DateTimeKind.Utc) - .SetName("Explicit tzid = UTC time zone returns DateTimeKind.Utc"); + .Returns(DateTimeKind.Unspecified) + .SetName("Explicit tzid = UTC time zone returns DateTimeKind.Unspecified"); yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Utc), null) - .Returns(DateTimeKind.Utc) - .SetName("DateTime with Kind = Utc and no tzid returns DateTimeKind.Utc"); + .Returns(DateTimeKind.Unspecified) + .SetName("DateTime with Kind = Utc and no tzid returns DateTimeKind.Unspecified"); yield return new TestCaseData(localDt, localTz) .Returns(DateTimeKind.Unspecified) @@ -155,9 +121,9 @@ public static IEnumerable DateTimeKindOverrideTestCases() .Returns(DateTimeKind.Unspecified) .SetName("DateTime with Kind = Local with null tzid returns DateTimeKind.Unspecified"); - yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Unspecified), null) + yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Local), null) .Returns(DateTimeKind.Unspecified) - .SetName("DateTime with Kind = Unspecified and null tzid returns DateTimeKind.Unspecified"); + .SetName("DateTime with Kind = Local and null tzid returns DateTimeKind.Unspecified"); } [Test, TestCaseSource(nameof(ToStringTestCases))] @@ -170,8 +136,8 @@ public static IEnumerable ToStringTestCases() .Returns("2024-08-30T10:30:00.0000000+12:00 Pacific/Auckland") .SetName("Date and time with 'O' format arg, default culture"); - yield return new TestCaseData(new CalDateTime(2024, 8, 30, tzId: "Pacific/Auckland"), "O", null) - .Returns("08/30/2024 Pacific/Auckland") + yield return new TestCaseData(new CalDateTime(2024, 8, 30), "O", null) + .Returns("2024-08-30") // Date only cannot have timezone .SetName("Date only with 'O' format arg, default culture"); yield return new TestCaseData(new CalDateTime(2024, 8, 30, 10, 30, 0, tzId: "Pacific/Auckland"), "O", @@ -189,58 +155,146 @@ public static IEnumerable ToStringTestCases() .Returns("08/30/2024 10:30:00 Pacific/Auckland") .SetName("Date and time with format and 'FR' CultureInfo"); - yield return new TestCaseData(new CalDateTime(2024, 8, 30, tzId: "Pacific/Auckland"), null, - CultureInfo.GetCultureInfo("IT")) - .Returns("30/08/2024 Pacific/Auckland") + yield return new TestCaseData(new CalDateTime(2024, 8, 30), null, + CultureInfo.GetCultureInfo("IT")) // Date only cannot have timezone + .Returns("30/08/2024") .SetName("Date only with 'IT' CultureInfo and default format arg"); } - [Test] - public void SetValue_AppliesSameRulesAsWith_CTOR() + [Test, TestCaseSource(nameof(DateTimeArithmeticTestCases))] + public DateTime DateTimeArithmeticTests(Func operation) { - var dateTime = new DateTime(2024, 8, 30, 10, 30, 0, DateTimeKind.Unspecified); - var tzId = "Europe/Berlin"; + var result = operation(new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: CalDateTime.UtcTzId)); + return result.Value; + } - var dt1 = new CalDateTime(dateTime, tzId); - var dt2 = new CalDateTime(DateTime.Now, tzId); - dt2.Value = dateTime; + public static IEnumerable DateTimeArithmeticTestCases() + { + var dateTime = new DateTime(2025, 1, 15, 10, 20, 30); - Assert.Multiple(() => - { - // TzId changes the DateTimeKind to Unspecified - Assert.That(dt1.Value.Kind, Is.EqualTo(dateTime.Kind)); - Assert.That(dt1.Value.Kind, Is.EqualTo(dt2.Value.Kind)); - Assert.That(dt1.TzId, Is.EqualTo(dt2.TzId)); - }); + yield return new TestCaseData(new Func(dt => dt.AddHours(1))) + .Returns(dateTime.AddHours(1)) + .SetName($"{nameof(IDateTime.AddHours)} 1 hour"); + + yield return new TestCaseData(new Func(dt => dt.Add(TimeSpan.FromSeconds(30)))) + .Returns(dateTime.Add(TimeSpan.FromSeconds(30))) + .SetName($"{nameof(IDateTime.Add)} 30 seconds"); + + yield return new TestCaseData(new Func(dt => dt.Add(TimeSpan.FromMilliseconds(100)))) + .Returns(dateTime.Add(TimeSpan.FromMilliseconds(0))) + .SetName($"{nameof(IDateTime.Add)} 100 milliseconds round down"); + + yield return new TestCaseData(new Func(dt => dt.AddMinutes(70))) + .Returns(dateTime.AddMinutes(70)) + .SetName($"{nameof(IDateTime.AddMinutes)} 70 minutes"); + } + + [Test, TestCaseSource(nameof(EqualityTestCases))] + public bool EqualityTests(Func operation) + { + return operation(new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: CalDateTime.UtcTzId)); + } + + public static IEnumerable EqualityTestCases() + { + yield return new TestCaseData(new Func(dt => (CalDateTime) dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: CalDateTime.UtcTzId))) + .Returns(true) + .SetName("== operator 2 UTC timezones"); + + yield return new TestCaseData(new Func(dt => (CalDateTime) dt != new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: "Europe/Berlin"))) + .Returns(true) + .SetName("!= operator 2 timezones"); + + yield return new TestCaseData(new Func(dt => (CalDateTime) dt == new CalDateTime(2025, 1, 15, 10, 20, 30, tzId: null))) + .Returns(false) + .SetName("== operator UTC vs. floating"); } [Test] - public void SetValue_LeavesExistingPropertiesUnchanged() + public void EqualityShouldBeTransitive() { - var cal = new Calendar(); - var dateTime = new DateTime(2024, 8, 30, 10, 30, 0, DateTimeKind.Unspecified); - var tzId = "Europe/Berlin"; + var seq1 = + new CalDateTime[] + { + new("20241204T000000", tzId: "Europe/Vienna"), + new("20241204T000000", tzId: null), + new("20241204T000000", tzId: "America/New_York") + }.Distinct(); + + var seq2 = + new CalDateTime[] + { + new("20241204T000000", tzId: "America/New_York"), + new("20241204T000000", tzId: "Europe/Vienna"), + new("20241204T000000", tzId: null) + }.Distinct(); + + // Equality using GetHashCode() + Assert.That(seq1.Count(), Is.EqualTo(seq2.Count())); + } - var dt = new CalDateTime(dateTime, tzId, false) - { - AssociatedObject = cal - }; - var hasTimeInitial = dt.HasTime; - dt.Value = DateTime.MinValue; + [Test, TestCaseSource(nameof(DateOnlyArithmeticTestCases))] + public (DateTime Value, bool HasTime) DateOnlyArithmeticTests(Func operation) + { + var result = operation(new CalDateTime(2025, 1, 15)); + return (result.Value, result.HasTime); + } - // Properties should remain unchanged - Assert.Multiple(() => - { - Assert.That(dt.HasTime, Is.EqualTo(hasTimeInitial)); - Assert.That(dt.TzId, Is.EqualTo(tzId)); - Assert.That(dt.Calendar, Is.SameAs(cal)); - }); + public static IEnumerable DateOnlyArithmeticTestCases() + { + var dateTime = new DateTime(2025, 1, 15); + + yield return new TestCaseData(new Func(dt => dt.Subtract(TimeSpan.FromDays(1)))) + .Returns((dateTime.AddDays(-1), false)) + .SetName($"{nameof(IDateTime.Subtract)} 1 day TimeSpan HasTime=false"); + + yield return new TestCaseData(new Func(dt => dt.AddYears(1))) + .Returns((dateTime.AddYears(1), false)) + .SetName($"{nameof(IDateTime.AddYears)} 1 year"); + + yield return new TestCaseData(new Func(dt => dt.AddMonths(2))) + .Returns((dateTime.AddMonths(2), false)) + .SetName($"{nameof(IDateTime.AddMonths)} 2 months"); + + yield return new TestCaseData(new Func(dt => dt.AddDays(7))) + .Returns((dateTime.AddDays(7), false)) + .SetName($"{nameof(IDateTime.AddDays)} 7 days"); + + yield return new TestCaseData(new Func(dt => dt.AddHours(24))) + .Returns((dateTime.AddHours(24), false)) + .SetName($"{nameof(IDateTime.AddHours)} 24 hours HasTime=false"); + + yield return new TestCaseData(new Func(dt => dt.AddHours(1))) + .Returns((dateTime.AddHours(1), true)) + .SetName($"{nameof(IDateTime.AddHours)} 1 hour HasTime=true"); + + yield return new TestCaseData(new Func(dt => dt.AddMinutes(24 * 60))) + .Returns((dateTime.AddMinutes(24 * 60), false)) + .SetName($"{nameof(IDateTime.AddMinutes)} 1 day in minutes HasTime=false"); + + yield return new TestCaseData(new Func(dt => dt.AddMinutes(23 * 60))) + .Returns((dateTime.AddMinutes(23 * 60), true)) + .SetName($"{nameof(IDateTime.AddMinutes)} 23 hours in minutes HasTime=true"); + + yield return new TestCaseData(new Func(dt => dt.AddSeconds(TimeSpan.FromDays(1).Seconds))) + .Returns((dateTime.AddSeconds(TimeSpan.FromDays(1).Seconds), false)) + .SetName($"{nameof(IDateTime.AddSeconds)} 1 day in seconds HasTime=false"); + + yield return new TestCaseData(new Func(dt => dt.Add(TimeSpan.FromDays(1)))) + .Returns((dateTime.Add(TimeSpan.FromDays(1)), false)) + .SetName($"{nameof(IDateTime.Add)} 1 day TimeSpan HasTime=false"); + + yield return new TestCaseData(new Func(dt => dt.Add(TimeSpan.FromSeconds(30)))) + .Returns((dateTime.Add(TimeSpan.FromSeconds(30)), true)) + .SetName($"{nameof(IDateTime.Add)} 30 seconds TimeSpan HasTime=true"); } [Test] public void Simple_PropertyAndMethod_HasTime_Tests() { + // A collection of tests that are not covered elsewhere + var dt = new DateTime(2025, 1, 2, 10, 20, 30, DateTimeKind.Utc); var c = new CalDateTime(dt, tzId: "Europe/Berlin"); @@ -251,74 +305,15 @@ public void Simple_PropertyAndMethod_HasTime_Tests() Assert.Multiple(() => { Assert.That(c2.Value, Is.EqualTo(c3.Value)); - Assert.That(c2.Ticks, Is.EqualTo(c3.Ticks)); Assert.That(c2.TzId, Is.EqualTo(c3.TzId)); - Assert.That(CalDateTime.UtcNow.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + Assert.That(CalDateTime.UtcNow.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified)); Assert.That(CalDateTime.Today.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified)); - Assert.That(c.Millisecond, Is.EqualTo(0)); - Assert.That(c.Ticks, Is.EqualTo(dt.Ticks)); Assert.That(c.DayOfYear, Is.EqualTo(dt.DayOfYear)); - Assert.That(c.TimeOfDay, Is.EqualTo(dt.TimeOfDay)); + Assert.That(c.Time?.ToTimeSpan(), Is.EqualTo(dt.TimeOfDay)); Assert.That(c.Subtract(TimeSpan.FromSeconds(dt.Second)).Value.Second, Is.EqualTo(0)); - Assert.That(c.AddYears(1).Value, Is.EqualTo(dt.AddYears(1))); - Assert.That(c.AddMonths(1).Value, Is.EqualTo(dt.AddMonths(1))); - Assert.That(c.AddDays(1).Value, Is.EqualTo(dt.AddDays(1))); - Assert.That(c.AddHours(1).Value, Is.EqualTo(dt.AddHours(1))); - Assert.That(c.AddMinutes(1).Value, Is.EqualTo(dt.AddMinutes(1))); - Assert.That(c.AddSeconds(15).Value, Is.EqualTo(dt.AddSeconds(15))); - Assert.That(c.AddMilliseconds(100).Value, Is.EqualTo(dt.AddMilliseconds(0))); // truncated - Assert.That(c.AddMilliseconds(1000).Value, Is.EqualTo(dt.AddMilliseconds(1000))); - Assert.That(c.AddTicks(1).Value, Is.EqualTo(dt.AddTicks(0))); // truncated - Assert.That(c.AddTicks(TimeSpan.FromMinutes(1).Ticks).Value, Is.EqualTo(dt.AddTicks(TimeSpan.FromMinutes(1).Ticks))); - Assert.That(c.DateOnlyValue, Is.EqualTo(new DateOnly(dt.Year, dt.Month, dt.Day))); - Assert.That(c.TimeOnlyValue, Is.EqualTo(new TimeOnly(dt.Hour, dt.Minute, dt.Second))); Assert.That(c.ToString("dd.MM.yyyy"), Is.EqualTo("02.01.2025 Europe/Berlin")); - }); - } - - [Test] - public void Simple_PropertyAndMethod_NotHasTime_Tests() - { - var dt = new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc); - var c = new CalDateTime(dt, tzId: "Europe/Berlin", hasTime: false); - - // Adding time to a date-only value should not change the HasTime property - Assert.Multiple(() => - { - var result = c.AddHours(1); - Assert.That(result.HasTime, Is.True); - - result = c.AddMinutes(1); - Assert.That(result.HasTime, Is.True); - - result = c.AddSeconds(1); - Assert.That(result.HasTime, Is.True); - - result = c.AddMilliseconds(1000); - Assert.That(result.HasTime, Is.True); - - result = c.AddTicks(TimeSpan.FromMinutes(1).Ticks); - Assert.That(result.HasTime, Is.True); - }); - } - - [Test] - public void Toggling_HasDate_ShouldSucceed() - { - var dateTime = new DateTime(2025, 1, 2, 10, 20, 30, DateTimeKind.Utc); - var dt = new CalDateTime(dateTime); - Assert.Multiple(() => - { - Assert.That(dt.HasTime, Is.True); - Assert.That(dt.HasDate, Is.True); - - dt.HasDate = false; - Assert.That(dt.HasDate, Is.False); - Assert.That(dt.DateOnlyValue.HasValue, Is.False); - Assert.That(() => dt.Value, Throws.InstanceOf()); - - dt.HasDate = true; - Assert.That(dt.HasDate, Is.True); + // Create a date-only CalDateTime from a CalDateTime + Assert.That(new CalDateTime(new CalDateTime(2025, 1, 1)), Is.EqualTo(new CalDateTime(2025, 1, 1))); }); } } diff --git a/Ical.Net.Tests/CalendarEventTest.cs b/Ical.Net.Tests/CalendarEventTest.cs index de1522b0..bb199ce5 100644 --- a/Ical.Net.Tests/CalendarEventTest.cs +++ b/Ical.Net.Tests/CalendarEventTest.cs @@ -478,8 +478,8 @@ public void TestGetEffectiveDuration() var evt = new CalendarEvent() { - DtStart = new CalDateTime(now) { HasTime = true }, - DtEnd = new CalDateTime(now.AddHours(1)) { HasTime = true }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now), TimeOnly.FromDateTime(now)), + DtEnd = new CalDateTime(DateOnly.FromDateTime(now.AddHours(1)), TimeOnly.FromDateTime(now.AddHours(1))) }; Assert.Multiple(() => @@ -491,8 +491,8 @@ public void TestGetEffectiveDuration() evt = new CalendarEvent() { - DtStart = new CalDateTime(now.Date) { HasTime = true }, - DtEnd = new CalDateTime(now.Date.AddHours(1)) { HasTime = true }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now.Date), TimeOnly.FromDateTime(now.Date)), + DtEnd = new CalDateTime(DateOnly.FromDateTime(now.Date.AddHours(1)), TimeOnly.FromDateTime(now.Date.AddHours(1))) }; Assert.Multiple(() => @@ -503,7 +503,7 @@ public void TestGetEffectiveDuration() evt = new CalendarEvent() { - DtStart = new CalDateTime(now.Date) { HasTime = false }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now)), }; Assert.Multiple(() => @@ -514,7 +514,7 @@ public void TestGetEffectiveDuration() evt = new CalendarEvent() { - DtStart = new CalDateTime(now) { HasTime = true }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now), TimeOnly.FromDateTime(now)), Duration = TimeSpan.FromHours(2), }; @@ -526,7 +526,7 @@ public void TestGetEffectiveDuration() evt = new CalendarEvent() { - DtStart = new CalDateTime(now.Date) { HasTime = true }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now.Date), TimeOnly.FromDateTime(now.Date)), Duration = TimeSpan.FromHours(2), }; @@ -537,7 +537,7 @@ public void TestGetEffectiveDuration() evt = new CalendarEvent() { - DtStart = new CalDateTime(now.Date) { HasTime = false }, + DtStart = new CalDateTime(DateOnly.FromDateTime(now)), Duration = TimeSpan.FromDays(1), }; diff --git a/Ical.Net.Tests/ComponentTest.cs b/Ical.Net.Tests/ComponentTest.cs index eef17a8b..8dcf0b90 100644 --- a/Ical.Net.Tests/ComponentTest.cs +++ b/Ical.Net.Tests/ComponentTest.cs @@ -22,29 +22,4 @@ public void UniqueComponent1() Assert.That(evt.Created, Is.Null); // We don't want this to be set automatically Assert.That(evt.DtStamp, Is.Not.Null); } - - [Test, Category("Components")] - public void ChangeCalDateTimeValue() - { - var e = new CalendarEvent - { - Start = new CalDateTime(2017, 11, 22, 11, 00, 01), - End = new CalDateTime(2017, 11, 22, 11, 30, 01), - }; - - var firstStartAsUtc = e.Start.AsUtc; - var firstEndAsUtc = e.End.AsUtc; - - e.Start.Value = new DateTime(2017, 11, 22, 11, 30, 01); - e.End.Value = new DateTime(2017, 11, 22, 12, 00, 01); - - var secondStartAsUtc = e.Start.AsUtc; - var secondEndAsUtc = e.End.AsUtc; - - Assert.Multiple(() => - { - Assert.That(secondStartAsUtc, Is.Not.EqualTo(firstStartAsUtc)); - Assert.That(secondEndAsUtc, Is.Not.EqualTo(firstEndAsUtc)); - }); - } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index 9e33befc..2e3e76da 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -325,8 +325,8 @@ public void Google1() var evt = iCal.Events["594oeajmftl3r9qlkb476rpr3c@google.com"]; Assert.That(evt, Is.Not.Null); - IDateTime dtStart = new CalDateTime(2006, 12, 18, tzId); - IDateTime dtEnd = new CalDateTime(2006, 12, 23, tzId); + IDateTime dtStart = new CalDateTime(2006, 12, 18); + IDateTime dtEnd = new CalDateTime(2006, 12, 23); var occurrences = iCal.GetOccurrences(dtStart, dtEnd).OrderBy(o => o.Period.StartTime).ToList(); var dateTimes = new[] diff --git a/Ical.Net.Tests/ProgramTest.cs b/Ical.Net.Tests/ProgramTest.cs index f842f0c9..4e797742 100644 --- a/Ical.Net.Tests/ProgramTest.cs +++ b/Ical.Net.Tests/ProgramTest.cs @@ -52,8 +52,8 @@ public void Merge1() // Get occurrences for the first event var occurrences = evt1.GetOccurrences( - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2000, 1, 1, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + new CalDateTime(1996, 1, 1), + new CalDateTime(2000, 1, 1)).OrderBy(o => o.Period.StartTime).ToList(); var dateTimes = new[] { @@ -104,8 +104,8 @@ public void Merge1() // Get occurrences for the 2nd event occurrences = evt2.GetOccurrences( - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 4, 1, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 4, 1)).OrderBy(o => o.Period.StartTime).ToList(); var dateTimes1 = new[] { @@ -180,4 +180,4 @@ public void SystemTimeZone3() }, Throws.Nothing, "Time zone should be found."); } } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 9dc7fc98..201d67f8 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -93,8 +93,8 @@ public void YearlyComplex1() ProgramTest.TestCal(iCal); var evt = iCal.Events.First(); var occurrences = evt.GetOccurrences( - new CalDateTime(2006, 1, 1, _tzid), - new CalDateTime(2011, 1, 1, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + new CalDateTime(2006, 1, 1), + new CalDateTime(2011, 1, 1)).OrderBy(o => o.Period.StartTime).ToList(); IDateTime dt = new CalDateTime(2007, 1, 1, 8, 30, 0, _tzid); var i = 0; @@ -128,8 +128,8 @@ public void DailyCount1() var iCal = Calendar.Load(IcsFiles.DailyCount1); EventOccurrenceTest( iCal, - new CalDateTime(2006, 7, 1, _tzid), - new CalDateTime(2006, 9, 1, _tzid), + new CalDateTime(2006, 7, 1), + new CalDateTime(2006, 9, 1), new[] { new CalDateTime(2006, 07, 18, 10, 00, 00, _tzid), @@ -158,8 +158,8 @@ public void DailyUntil1() var evt = iCal.Events.First(); var occurrences = evt.GetOccurrences( - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1998, 1, 1, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + new CalDateTime(1997, 9, 1), + new CalDateTime(1998, 1, 1)).OrderBy(o => o.Period.StartTime).ToList(); IDateTime dt = new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid); var i = 0; @@ -172,8 +172,8 @@ public void DailyUntil1() { Assert.That(occurrences[i].Period.StartTime, Is.EqualTo(dt), "Event should occur at " + dt); Assert.That( - (dt.LessThan(new CalDateTime(1997, 10, 26, _tzid)) && dt.TimeZoneName == "US-Eastern") || - (dt.GreaterThan(new CalDateTime(1997, 10, 26, _tzid)) && dt.TimeZoneName == "US-Eastern"), + (dt.LessThan(new CalDateTime(1997, 10, 26)) && dt.TimeZoneName == "US-Eastern") || + (dt.GreaterThan(new CalDateTime(1997, 10, 26)) && dt.TimeZoneName == "US-Eastern"), Is.True, "Event " + dt + " doesn't occur in the correct time zone (including Daylight & Standard time zones)"); }); @@ -193,8 +193,8 @@ public void Daily1() var iCal = Calendar.Load(IcsFiles.Daily1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1997, 12, 4, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1997, 12, 4), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -307,8 +307,8 @@ public void DailyCount2() var iCal = Calendar.Load(IcsFiles.DailyCount2); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1998, 1, 1, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1998, 1, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -332,8 +332,8 @@ public void ByMonth1() var evt = iCal.Events.First(); var occurrences = evt.GetOccurrences( - new CalDateTime(1998, 1, 1, _tzid), - new CalDateTime(2000, 12, 31, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + new CalDateTime(1998, 1, 1), + new CalDateTime(2000, 12, 31)).OrderBy(o => o.Period.StartTime).ToList(); IDateTime dt = new CalDateTime(1998, 1, 1, 9, 0, 0, _tzid); var i = 0; @@ -385,8 +385,8 @@ public void WeeklyCount1() var iCal = Calendar.Load(IcsFiles.WeeklyCount1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1998, 1, 1, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1998, 1, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -425,8 +425,8 @@ public void WeeklyUntil1() var iCal = Calendar.Load(IcsFiles.WeeklyUntil1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -479,8 +479,8 @@ public void WeeklyWkst1() var iCal = Calendar.Load(IcsFiles.WeeklyWkst1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1998, 1, 31, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1998, 1, 31), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -521,8 +521,8 @@ public void WeeklyUntilWkst1() var iCal = Calendar.Load(IcsFiles.WeeklyUntilWkst1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1997, 9, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -571,8 +571,8 @@ public void WeeklyUntilWkst2() var iCal = Calendar.Load(IcsFiles.WeeklyUntilWkst2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 3, 9, 0, 0, _tzid), @@ -640,8 +640,8 @@ public void WeeklyUntilWkst2_1() var iCal = Calendar.Load(IcsFiles.WeeklyUntilWkst2); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 9, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1997, 9, 9), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 15, 9, 0, 0, _tzid), @@ -704,8 +704,8 @@ public void WeeklyCountWkst2() var iCal = Calendar.Load(IcsFiles.WeeklyCountWkst2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -730,8 +730,8 @@ public void MonthlyCountByDay1() var iCal = Calendar.Load(IcsFiles.MonthlyCountByDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 5, 9, 0, 0, _tzid), @@ -770,8 +770,8 @@ public void MonthlyUntilByDay1() var iCal = Calendar.Load(IcsFiles.MonthlyUntilByDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 5, 9, 0, 0, _tzid), @@ -798,8 +798,8 @@ public void MonthlyCountByDay2() var iCal = Calendar.Load(IcsFiles.MonthlyCountByDay2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 7, 9, 0, 0, _tzid), @@ -838,8 +838,8 @@ public void MonthlyCountByDay3() var iCal = Calendar.Load(IcsFiles.MonthlyCountByDay3); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 1, 1), new[] { new CalDateTime(1997, 9, 22, 9, 0, 0, _tzid), @@ -870,8 +870,8 @@ public void ByMonthDay1() var iCal = Calendar.Load(IcsFiles.ByMonthDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 3, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 3, 1), new[] { new CalDateTime(1997, 9, 28, 9, 0, 0, _tzid), @@ -903,8 +903,8 @@ public void MonthlyCountByMonthDay1() EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 3, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 3, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -943,8 +943,8 @@ public void MonthlyCountByMonthDay2() var iCal = Calendar.Load(IcsFiles.MonthlyCountByMonthDay2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 3, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 3, 1), new[] { new CalDateTime(1997, 9, 30, 9, 0, 0, _tzid), @@ -983,8 +983,8 @@ public void MonthlyCountByMonthDay3() var iCal = Calendar.Load(IcsFiles.MonthlyCountByMonthDay3); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2000, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2000, 1, 1), new[] { new CalDateTime(1997, 9, 10, 9, 0, 0, _tzid), @@ -1023,8 +1023,8 @@ public void MonthlyByDay1() var iCal = Calendar.Load(IcsFiles.MonthlyByDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 4, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 4, 1), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -1079,8 +1079,8 @@ public void YearlyByMonth1() var iCal = Calendar.Load(IcsFiles.YearlyByMonth1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2002, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2002, 1, 1), new[] { new CalDateTime(1997, 6, 10, 9, 0, 0, _tzid), @@ -1107,8 +1107,8 @@ public void YearlyCountByMonth1() var iCal = Calendar.Load(IcsFiles.YearlyCountByMonth1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2003, 4, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2003, 4, 1), new[] { new CalDateTime(1997, 3, 10, 9, 0, 0, _tzid), @@ -1135,8 +1135,8 @@ public void YearlyCountByYearDay1() var iCal = Calendar.Load(IcsFiles.YearlyCountByYearDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2007, 1, 1, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2007, 1, 1), new[] { new CalDateTime(1997, 1, 1, 9, 0, 0, _tzid), @@ -1175,8 +1175,8 @@ public void YearlyByDay1() var iCal = Calendar.Load(IcsFiles.YearlyByDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 5, 19, 9, 0, 0, _tzid), @@ -1213,8 +1213,8 @@ public void YearlyByWeekNo1() var iCal = Calendar.Load(IcsFiles.YearlyByWeekNo1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 5, 12, 9, 0, 0, _tzid), @@ -1239,8 +1239,8 @@ public void YearlyByWeekNo2() var iCal = Calendar.Load(IcsFiles.YearlyByWeekNo2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 5, 12, 9, 0, 0, _tzid), @@ -1264,8 +1264,8 @@ public void YearlyByWeekNo3() var iCal = Calendar.Load(IcsFiles.YearlyByWeekNo3); EventOccurrenceTest( iCal, - new CalDateTime(2001, 1, 1, _tzid), - new CalDateTime(2003, 1, 31, _tzid), + new CalDateTime(2001, 1, 1), + new CalDateTime(2003, 1, 31), new[] { new CalDateTime(2002, 1, 1, 10, 0, 0, _tzid), @@ -1287,8 +1287,8 @@ public void YearlyByWeekNo4() var iCal = Calendar.Load(IcsFiles.YearlyByWeekNo4); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 5, 12, 9, 0, 0, _tzid), @@ -1331,8 +1331,8 @@ public void YearlyByWeekNo5() var iCal = Calendar.Load(IcsFiles.YearlyByWeekNo5); EventOccurrenceTest( iCal, - new CalDateTime(2001, 1, 1, _tzid), - new CalDateTime(2003, 1, 31, _tzid), + new CalDateTime(2001, 1, 1), + new CalDateTime(2003, 1, 31), new[] { new CalDateTime(2002, 1, 1, 10, 0, 0, _tzid), @@ -1362,8 +1362,8 @@ public void YearlyByMonth2() var iCal = Calendar.Load(IcsFiles.YearlyByMonth2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 3, 13, 9, 0, 0, _tzid), @@ -1391,8 +1391,8 @@ public void YearlyByMonth3() var iCal = Calendar.Load(IcsFiles.YearlyByMonth3); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1999, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1999, 12, 31), new[] { new CalDateTime(1997, 6, 5, 9, 0, 0, _tzid), @@ -1450,8 +1450,8 @@ public void MonthlyByMonthDay1() var iCal = Calendar.Load(IcsFiles.MonthlyByMonthDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2000, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2000, 12, 31), new[] { new CalDateTime(1998, 2, 13, 9, 0, 0, _tzid), @@ -1480,8 +1480,8 @@ public void MonthlyByMonthDay2() var iCal = Calendar.Load(IcsFiles.MonthlyByMonthDay2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 6, 30, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 6, 30), new[] { new CalDateTime(1997, 9, 13, 9, 0, 0, _tzid), @@ -1520,8 +1520,8 @@ public void YearlyByMonthDay1() var iCal = Calendar.Load(IcsFiles.YearlyByMonthDay1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2004, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2004, 12, 31), new[] { new CalDateTime(1996, 11, 5, 9, 0, 0, _tzid), @@ -1541,8 +1541,8 @@ public void MonthlyBySetPos1() var iCal = Calendar.Load(IcsFiles.MonthlyBySetPos1); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(2004, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(2004, 12, 31), new[] { new CalDateTime(1997, 9, 4, 9, 0, 0, _tzid), @@ -1567,8 +1567,8 @@ public void MonthlyBySetPos2() var iCal = Calendar.Load(IcsFiles.MonthlyBySetPos2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 3, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 3, 31), new[] { new CalDateTime(1997, 9, 29, 9, 0, 0, _tzid), @@ -1603,8 +1603,8 @@ public void HourlyUntil1() var iCal = Calendar.Load(IcsFiles.HourlyUntil1); EventOccurrenceTest( iCal, - fromDate: new CalDateTime(1996, 1, 1, _tzid), - toDate: new CalDateTime(1998, 3, 31, _tzid), + fromDate: new CalDateTime(1996, 1, 1), + toDate: new CalDateTime(1998, 3, 31), dateTimes: new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -1625,8 +1625,8 @@ public void MinutelyCount1() var iCal = Calendar.Load(IcsFiles.MinutelyCount1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 2, _tzid), - new CalDateTime(1997, 9, 3, _tzid), + new CalDateTime(1997, 9, 2), + new CalDateTime(1997, 9, 3), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -1649,8 +1649,8 @@ public void MinutelyCount2() var iCal = Calendar.Load(IcsFiles.MinutelyCount2); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 12, 31), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -1671,8 +1671,8 @@ public void MinutelyCount3() var iCal = Calendar.Load(IcsFiles.MinutelyCount3); EventOccurrenceTest( iCal, - new CalDateTime(2010, 8, 27, _tzid), - new CalDateTime(2010, 8, 28, _tzid), + new CalDateTime(2010, 8, 27), + new CalDateTime(2010, 8, 28), new[] { new CalDateTime(2010, 8, 27, 11, 0, 0, _tzid), @@ -1699,8 +1699,8 @@ public void MinutelyCount4() var iCal = Calendar.Load(IcsFiles.MinutelyCount4); EventOccurrenceTest( iCal, - new CalDateTime(2010, 8, 27, _tzid), - new CalDateTime(2010, 8, 28, _tzid), + new CalDateTime(2010, 8, 27), + new CalDateTime(2010, 8, 28), new[] { new CalDateTime(2010, 8, 27, 11, 0, 0, _tzid), @@ -1727,8 +1727,8 @@ public void DailyByHourMinute1() var iCal = Calendar.Load(IcsFiles.DailyByHourMinute1); EventOccurrenceTest( iCal, - new CalDateTime(1997, 9, 2, _tzid), - new CalDateTime(1997, 9, 4, _tzid), + new CalDateTime(1997, 9, 2), + new CalDateTime(1997, 9, 4), new[] { new CalDateTime(1997, 9, 2, 9, 0, 0, _tzid), @@ -1797,8 +1797,8 @@ public void MinutelyByHour1() var evt1 = iCal1.Events.First(); var evt2 = iCal2.Events.First(); - var evt1Occ = evt1.GetOccurrences(new CalDateTime(1997, 9, 1, _tzid), new CalDateTime(1997, 9, 3, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); - var evt2Occ = evt2.GetOccurrences(new CalDateTime(1997, 9, 1, _tzid), new CalDateTime(1997, 9, 3, _tzid)).OrderBy(o => o.Period.StartTime).ToList(); + var evt1Occ = evt1.GetOccurrences(new CalDateTime(1997, 9, 1), new CalDateTime(1997, 9, 3)).OrderBy(o => o.Period.StartTime).ToList(); + var evt2Occ = evt2.GetOccurrences(new CalDateTime(1997, 9, 1), new CalDateTime(1997, 9, 3)).OrderBy(o => o.Period.StartTime).ToList(); Assert.That(evt1Occ.Count == evt2Occ.Count, Is.True, "MinutelyByHour1() does not match DailyByHourMinute1() as it should"); for (var i = 0; i < evt1Occ.Count; i++) Assert.That(evt2Occ[i].Period, Is.EqualTo(evt1Occ[i].Period), "PERIOD " + i + " from DailyByHourMinute1 (" + evt1Occ[i].Period + ") does not match PERIOD " + i + " from MinutelyByHour1 (" + evt2Occ[i].Period + ")"); @@ -1813,8 +1813,8 @@ public void WeeklyCountWkst3() var iCal = Calendar.Load(IcsFiles.WeeklyCountWkst3); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 12, 31), new[] { new CalDateTime(1997, 8, 5, 9, 0, 0, _tzid), @@ -1836,8 +1836,8 @@ public void WeeklyCountWkst4() var iCal = Calendar.Load(IcsFiles.WeeklyCountWkst4); EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1, _tzid), - new CalDateTime(1998, 12, 31, _tzid), + new CalDateTime(1996, 1, 1), + new CalDateTime(1998, 12, 31), new[] { new CalDateTime(1997, 8, 5, 9, 0, 0, _tzid), @@ -1859,8 +1859,8 @@ public void Bug1741093() var iCal = Calendar.Load(IcsFiles.Bug1741093); EventOccurrenceTest( iCal, - new CalDateTime(2007, 7, 1, _tzid), - new CalDateTime(2007, 8, 1, _tzid), + new CalDateTime(2007, 7, 1), + new CalDateTime(2007, 8, 1), new[] { new CalDateTime(2007, 7, 2, 8, 0, 0, _tzid), @@ -2327,8 +2327,8 @@ public void Bug2912657() // Weekly with UNTIL value EventOccurrenceTest( iCal, - new CalDateTime(2009, 12, 4, localTzid), - new CalDateTime(2009, 12, 10, localTzid), + new CalDateTime(2009, 12, 4), + new CalDateTime(2009, 12, 10), new[] { new CalDateTime(2009, 12, 4, 2, 00, 00, localTzid) @@ -2340,8 +2340,8 @@ public void Bug2912657() // Weekly with COUNT=2 EventOccurrenceTest( iCal, - new CalDateTime(2009, 12, 4, localTzid), - new CalDateTime(2009, 12, 12, localTzid), + new CalDateTime(2009, 12, 4), + new CalDateTime(2009, 12, 12), new[] { new CalDateTime(2009, 12, 4, 2, 00, 00, localTzid), @@ -2476,7 +2476,7 @@ public void Bug3007244() { var iCal = Calendar.Load(IcsFiles.Bug3007244); - // CalDateTimes.HasTime = false + // date only cannot have a time zone EventOccurrenceTest( cal: iCal, fromDate: new CalDateTime(2010, 7, 18), @@ -2486,7 +2486,7 @@ public void Bug3007244() eventIndex: 0 ); - // CalDateTimes.HasTime = false + // date only cannot have a time zone EventOccurrenceTest( cal: iCal, fromDate: new CalDateTime(2011, 7, 18), @@ -2691,9 +2691,8 @@ public void Issue432_AllDay() { var vEvent = new CalendarEvent { - Start = new CalDateTime(DateTime.Parse("2020-01-11T00:00")), + Start = new CalDateTime(DateTime.Parse("2020-01-11")), // no time means all day End = new CalDateTime(DateTime.Parse("2020-01-11T00:00")), - IsAllDay = true, }; var occurrences = vEvent.GetOccurrences(DateTime.Parse("2020-01-10T00:00"), DateTime.Parse("2020-01-11T00:00")); @@ -3003,8 +3002,8 @@ public void ExDateShouldFilterOutAllPeriods() END:VCALENDAR"; var calendar = Calendar.Load(ical); var firstEvent = calendar.Events.First(); - var startSearch = new CalDateTime(2010, 1, 1, _tzid); - var endSearch = new CalDateTime(2016, 12, 31, _tzid); + var startSearch = new CalDateTime(2010, 1, 1); + var endSearch = new CalDateTime(2016, 12, 31); var occurrences = firstEvent.GetOccurrences(startSearch, endSearch).Select(o => o.Period).ToList(); Assert.That(occurrences.Count == 0, Is.True); @@ -3675,15 +3674,15 @@ private static IEnumerable ParseTestCaseFile(string fileCont break; case "DTSTART": - current.DtStart = new CalDateTime(val) { TzId = "UTC" }; + current.DtStart = new CalDateTime(val, "UTC"); break; case "START-AT": - current.StartAt = new CalDateTime(val) { TzId = "UTC" }; + current.StartAt = new CalDateTime(val, "UTC"); break; case "INSTANCES": - current.Instances = val.Split(',').Select(dt => new CalDateTime(dt) { TzId = "UTC" }).ToList(); + current.Instances = val.Split(',').Select(dt => new CalDateTime(dt, "UTC")).ToList(); break; case "EXCEPTION": @@ -3786,5 +3785,28 @@ private static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch) } return dt.Value; } -} + [Test] + public void GetOccurrenceShouldExcludeDtEndFloating() + { + var ical = """ + BEGIN:VCALENDAR + VERSION:2.0 + PRODID:-//github.com/ical-org/ical.net//NONSGML ical.net 5.0//EN + BEGIN:VEVENT + UID:123456 + DTSTAMP:20240630T000000Z + DTSTART;VALUE=DATE:20241001 + DTEND;VALUE=DATE:20241202 + SUMMARY:Don't include the end date of this event + END:VEVENT + END:VCALENDAR + """; + + var calendar = Calendar.Load(ical); + // Set start date for occurrences to search to the end date of the event + var occurrences = calendar.GetOccurrences(new CalDateTime(2024, 12, 2)); + + Assert.That(occurrences, Is.Empty); + } +} diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index cd3321e5..b4c20dd2 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -176,8 +176,8 @@ public void TimeZoneSerialize() var evt = new CalendarEvent { Summary = "Testing", - Start = new CalDateTime(2016, 7, 14, tz.TzId), - End = new CalDateTime(2016, 7, 15, tz.TzId) + Start = new CalDateTime(2016, 7, 14), + End = new CalDateTime(2016, 7, 15) }; cal.Events.Add(evt); diff --git a/Ical.Net.Tests/SimpleDeserializationTests.cs b/Ical.Net.Tests/SimpleDeserializationTests.cs index ea20f11b..f2d6f754 100644 --- a/Ical.Net.Tests/SimpleDeserializationTests.cs +++ b/Ical.Net.Tests/SimpleDeserializationTests.cs @@ -327,8 +327,8 @@ public void Google1() var evt = iCal.Events["594oeajmftl3r9qlkb476rpr3c@google.com"]; Assert.That(evt, Is.Not.Null); - IDateTime dtStart = new CalDateTime(2006, 12, 18, tzId); - IDateTime dtEnd = new CalDateTime(2006, 12, 23, tzId); + IDateTime dtStart = new CalDateTime(2006, 12, 18); + IDateTime dtEnd = new CalDateTime(2006, 12, 23); var occurrences = iCal.GetOccurrences(dtStart, dtEnd).OrderBy(o => o.Period.StartTime).ToList(); var dateTimes = new[] diff --git a/Ical.Net.Tests/TodoTest.cs b/Ical.Net.Tests/TodoTest.cs index 664468e9..7fa59dc6 100644 --- a/Ical.Net.Tests/TodoTest.cs +++ b/Ical.Net.Tests/TodoTest.cs @@ -25,8 +25,7 @@ public void ActiveTodo_Tests(string calendarString, IListThe date for which to return occurrences. Time is ignored on this parameter. /// A list of occurrences that occur on the given date (). public virtual HashSet GetOccurrences(IDateTime dt) - => GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1))); + { + return GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1))); + } /// public virtual HashSet GetOccurrences(DateTime dt) - => GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1))); + { + return GetOccurrences(new CalDateTime(DateOnly.FromDateTime(dt)), new CalDateTime(DateOnly.FromDateTime(dt.Date.AddDays(1)))); + } /// /// Returns a list of occurrences of each recurring component @@ -223,7 +227,7 @@ public virtual HashSet GetOccurrences(IDateTime startTime, IDateTime /// public virtual HashSet GetOccurrences(DateTime startTime, DateTime endTime) - => GetOccurrences(new CalDateTime(startTime), new CalDateTime(endTime)); + => GetOccurrences(new CalDateTime(DateOnly.FromDateTime(startTime), TimeOnly.FromDateTime(startTime)), new CalDateTime(DateOnly.FromDateTime(endTime), TimeOnly.FromDateTime(endTime))); /// /// Returns all occurrences of components of type T that start on the date provided. @@ -238,11 +242,15 @@ public virtual HashSet GetOccurrences(DateTime startTime, DateTime e /// The date for which to return occurrences. Time is ignored on this parameter. /// A list of Periods representing the occurrences of this object. public virtual HashSet GetOccurrences(IDateTime dt) where T : IRecurringComponent - => GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddTicks(-1))); + { + return GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1))); + } /// public virtual HashSet GetOccurrences(DateTime dt) where T : IRecurringComponent - => GetOccurrences(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddTicks(-1))); + { + return GetOccurrences(new CalDateTime(DateOnly.FromDateTime(dt)), new CalDateTime(DateOnly.FromDateTime(dt.Date.AddDays(1)))); + } /// public virtual HashSet GetOccurrences(DateTime startTime, DateTime endTime) where T : IRecurringComponent diff --git a/Ical.Net/CalendarComponents/CalendarEvent.cs b/Ical.Net/CalendarComponents/CalendarEvent.cs index 48d45f8c..dd1812ea 100644 --- a/Ical.Net/CalendarComponents/CalendarEvent.cs +++ b/Ical.Net/CalendarComponents/CalendarEvent.cs @@ -150,25 +150,6 @@ public virtual IDateTime End public virtual bool IsAllDay { get => !Start.HasTime; - set - { - // Set whether or not the start date/time - // has a time value. - if (Start != null) - { - Start.HasTime = !value; - } - if (End != null) - { - End.HasTime = !value; - } - - if (value && Start != null && End != null && Equals(Start.Date, End.Date)) - { - Duration = default(TimeSpan); - End = Start.AddDays(1); - } - } } /// @@ -213,7 +194,7 @@ public string Status /// /// The transparency of the event. In other words, - /// whether or not the period of time this event + /// whether the period of time this event /// occupies can contain other events (transparent), /// or if the time cannot be scheduled for anything /// else (opaque). @@ -242,7 +223,7 @@ private void Initialize() /// - /// Determines whether or not the is actively displayed + /// Determines whether the is actively displayed /// as an upcoming or occurred event. /// /// True if the event has not been cancelled, False otherwise. diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index a0d6b666..6a23c15d 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -17,7 +17,10 @@ namespace Ical.Net.DataTypes; /// The iCalendar equivalent of the .NET class. /// /// In addition to the features of the class, the -/// class handles timezones, and integrates seamlessly into the iCalendar framework. +/// class handles timezones, floating date/times and integrates seamlessly into the iCalendar framework. +/// +/// Any values are always rounded to the nearest second. +/// This is because RFC 5545, Section 3.3.5, does not allow for fractional seconds. /// /// public sealed class CalDateTime : EncodableDataType, IDateTime @@ -25,22 +28,28 @@ public sealed class CalDateTime : EncodableDataType, IDateTime // The date part that is used to return the Value property. private DateOnly _dateOnly; // The time part that is used to return the Value property. - private TimeOnly _timeOnly; + private TimeOnly? _timeOnly; - private const string UtcTzId = "UTC"; + /// + /// The timezone ID for Universal Coordinated Time (UTC). + /// + public const string UtcTzId = "UTC"; /// - /// Gets the current date/time in the local timezone. + /// Creates a new instance of the class + /// with the current date/time and sets the to . /// public static CalDateTime Now => new CalDateTime(DateTime.Now, null, true); /// - /// Gets the current date in the local timezone. + /// Creates a new instance of the class + /// with the current date and sets the to . /// public static CalDateTime Today => new CalDateTime(DateTime.Today, null, false); /// - /// Gets the current date/time in the Coordinated Universal Time (UTC) timezone. + /// Creates a new instance of the class + /// with the current date/time in the Coordinated Universal Time (UTC) timezone. /// public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow, UtcTzId, true); @@ -51,6 +60,8 @@ public CalDateTime() { } /// /// Creates a new instance of the class. + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// It will represent an RFC 5545, Section 3.3.4, DATE value, if is . /// /// public CalDateTime(IDateTime value) @@ -62,34 +73,39 @@ public CalDateTime(IDateTime value) } /// - /// Creates a new instance of the class - /// and sets the to "UTC" if the - /// has , otherwise it will be left as . + /// Creates a new instance of the class. + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// It will represent an RFC 5545, Section 3.3.4, DATE value, if is . /// - /// The timezone will be set to UTC if the has . - /// Else, the timezone will be . - /// - /// - /// Set to (default), if the must be included. + /// The will be set to "UTC" if the + /// has and is . + /// Otherwise will be . + /// + /// The value. Its will be ignored. + /// + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// It will represent an RFC 5545, Section 3.3.4, DATE value, if is . + /// public CalDateTime(DateTime value, bool hasTime = true) : this(value, value.Kind == DateTimeKind.Utc ? UtcTzId : null, hasTime) { } /// - /// Creates a new instance of the class using the specified timezone. - /// - /// - /// The specified value will override value's property. - /// If the timezone specified is UTC, the underlying will be - /// . If a non-UTC timezone or no timezone is specified, the - /// will be used. A timezone of represents - /// the system's local timezone. + /// Creates a new instance of the class. + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// It will represent an RFC 5545, Section 3.3.4, DATE value, if is . + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// + /// The value. Its will be ignored. + /// A timezone of represents + /// a floating date/time, which is the same in all timezones. () represents the Coordinated Universal Time. + /// Other values determine the timezone of the date/time. + /// + /// + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value, if is . + /// It will represent an RFC 5545, Section 3.3.4, DATE value, if is . /// - /// Set to (default), if the must be included. public CalDateTime(DateTime value, string? tzId, bool hasTime = true) { - if (value.Kind == DateTimeKind.Utc && tzId is null or UtcTzId) - tzId = UtcTzId; - if (hasTime) Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), tzId); else @@ -98,12 +114,11 @@ public CalDateTime(DateTime value, string? tzId, bool hasTime = true) /// /// Creates a new instance of the class using the specified timezone. + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value. /// - /// The specified value will determine the property. - /// If the timezone specified is UTC, the underlying will be - /// . If a non-UTC timezone or no timezone is specified, the underlying - /// will be used. A timezone of represents - /// the system's local timezone. + /// A timezone of represents + /// a floating date/time, which is the same in all timezones. () represents the Coordinated Universal Time. + /// Other values determine the timezone of the date/time. /// /// /// @@ -117,31 +132,36 @@ public CalDateTime(int year, int month, int day, int hour, int minute, int secon } /// - /// Creates a new instance of the class using the specified timezone. - /// Sets for the property. + /// Creates a new instance of the class with set to . + /// The instance will represent an RFC 5545, Section 3.3.4, DATE value, + /// and thus it cannot have a timezone. /// - /// The specified value will determine the property. - /// If the timezone specified is UTC, the underlying will be - /// . If a non-UTC timezone or no timezone is specified, the underlying - /// will be used. A timezone of represents - /// the system's local timezone. - /// /// /// /// - public CalDateTime(int year, int month, int day, string? tzId = null) + public CalDateTime(int year, int month, int day) { - Initialize(new DateOnly(year, month, day), null, tzId); + Initialize(new DateOnly(year, month, day), null, null); + } + + /// + /// Creates a new instance of the class with set to . + /// The instance will represent an RFC 5545, Section 3.3.4, DATE value, + /// and thus it cannot have a timezone. + /// + /// + public CalDateTime(DateOnly date) + { + Initialize(date, null, null); } /// /// Creates a new instance of the class using the specified timezone. + /// The instance will represent an RFC 5545, Section 3.3.5, DATE-TIME value. /// - /// The specified value will determine the property. - /// If the timezone specified is UTC, the underlying will be - /// . If a non-UTC timezone or no timezone is specified, the underlying - /// will be used. A timezone of represents - /// the system's local timezone. + /// A timezone of represents + /// a floating date/time, which is the same in all timezones. () represents the Coordinated Universal Time. + /// Other values determine the timezone of the date/time. /// /// /// @@ -154,29 +174,36 @@ public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null) /// Creates a new instance of the class by parsing /// using the . /// - /// An iCalendar-compatible date or date-time string. - /// The specified value will determine the property. - /// If the timezone specified is UTC, the underlying will be - /// . If a non-UTC timezone or no timezone is specified, the underlying - /// will be used. A timezone of represents - /// the system's local timezone. + /// An iCalendar-compatible date or date-time string. + /// + /// If the parsed string represents an RFC 5545, Section 3.3.4, DATE value, + /// it cannot have a timezone, and the will be ignored. + /// + /// If the parsed string represents an RFC 5545, DATE-TIME value, the will be used. + /// + /// A timezone of represents + /// a floating date/time, which is the same in all timezones. () represents the Coordinated Universal Time. + /// Other values determine the timezone of the date/time. /// public CalDateTime(string value, string? tzId = null) { var serializer = new DateTimeSerializer(); CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable ?? throw new InvalidOperationException("Failure deserializing value")); - TzId = tzId; + // The string may contain a date only, meaning that the tzId should be ignored. + _tzId = HasTime ? tzId : null; } private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId) { - HasTime = timeOnly.HasValue; - HasDate = true; _dateOnly = dateOnly; - _timeOnly = timeOnly ?? new TimeOnly(); + _timeOnly = TruncateTimeToSeconds(timeOnly); - _tzId = string.Equals(UtcTzId, tzId, StringComparison.OrdinalIgnoreCase) ? UtcTzId : tzId; + _tzId = tzId switch + { + _ when !timeOnly.HasValue => null, + _ => tzId // can also be UtcTzId + }; } /// @@ -206,10 +233,8 @@ public override void CopyFrom(ICopyable obj) { // Maintain the private date/time backing fields _dateOnly = calDt._dateOnly; - _timeOnly = calDt._timeOnly; + _timeOnly = TruncateTimeToSeconds(calDt._timeOnly); _tzId = calDt._tzId; - HasTime = calDt.HasTime; - HasDate = calDt.HasDate; } AssociateWith(dt); @@ -228,7 +253,7 @@ public override int GetHashCode() unchecked { var hashCode = Value.GetHashCode(); - hashCode = (hashCode * 397) ^ HasDate.GetHashCode(); + hashCode = (hashCode * 397) ^ HasTime.GetHashCode(); hashCode = (hashCode * 397) ^ AsUtc.GetHashCode(); hashCode = (hashCode * 397) ^ (TzId != null ? TzId.GetHashCode() : 0); return hashCode; @@ -236,27 +261,45 @@ public override int GetHashCode() } public static bool operator <(CalDateTime? left, IDateTime? right) - => left != null && right != null && left.AsUtc < right.AsUtc; + => left != null + && right != null + && (left.IsFloating || right.IsFloating ? left.Value < right.Value : left.AsUtc < right.AsUtc); public static bool operator >(CalDateTime? left, IDateTime? right) - => left != null && right != null && left.AsUtc > right.AsUtc; - + => left != null + && right != null + && (left.IsFloating || right.IsFloating ? left.Value > right.Value : left.AsUtc > right.AsUtc); + public static bool operator <=(CalDateTime? left, IDateTime? right) - => left != null && right != null && left.AsUtc <= right.AsUtc; + => left != null + && right != null + && (left.IsFloating || right.IsFloating ? left.Value <= right.Value : left.AsUtc <= right.AsUtc); public static bool operator >=(CalDateTime? left, IDateTime? right) - => left != null && right != null && left.AsUtc >= right.AsUtc; - + => left != null + && right != null + && (left.IsFloating || right.IsFloating ? left.Value >= right.Value : left.AsUtc >= right.AsUtc); + public static bool operator ==(CalDateTime? left, IDateTime? right) { - return ReferenceEquals(left, null) || ReferenceEquals(right, null) - ? ReferenceEquals(left, right) - : right is CalDateTime calDateTime - && left.Value.Equals(calDateTime.Value) - && left.HasDate == calDateTime.HasDate - && left.HasTime == calDateTime.HasTime - && left.AsUtc.Equals(calDateTime.AsUtc) - && string.Equals(left.TzId, calDateTime.TzId, StringComparison.OrdinalIgnoreCase); + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left is null || right is null) + { + return false; + } + + if (left.IsFloating != right.IsFloating) + { + return false; + } + + return left.Value.Equals(right.Value) + && left.HasTime == right.HasTime + && string.Equals(left.TzId, right.TzId, StringComparison.OrdinalIgnoreCase); } public static bool operator !=(CalDateTime? left, IDateTime? right) @@ -266,17 +309,22 @@ public override int GetHashCode() /// Subtracts a from the . /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the is not a multiple of 24 hours. /// public static IDateTime operator -(CalDateTime left, TimeSpan right) { var copy = left.Copy(); - if ((right.Ticks % TimeSpan.TicksPerDay) != 0) + var newValue = copy.Value - right; + if (!copy.HasTime && (right.Ticks % TimeSpan.TicksPerDay) == 0) { - copy.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); + } + else + { + copy._dateOnly = DateOnly.FromDateTime(newValue); + copy._timeOnly = TruncateTimeToSeconds(newValue); } - copy.Value -= right; return copy; } @@ -284,17 +332,22 @@ public override int GetHashCode() /// Adds a to the . /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the is not a multiple of 24 hours. /// public static IDateTime operator +(CalDateTime left, TimeSpan right) { var copy = left.Copy(); - if ((right.Ticks % TimeSpan.TicksPerDay) != 0) + var newValue = copy.Value + right; + if (!copy.HasTime && (right.Ticks % TimeSpan.TicksPerDay) == 0) + { + copy._dateOnly = DateOnly.FromDateTime(newValue); + } + else { - copy.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); + copy._timeOnly = TruncateTimeToSeconds(newValue); } - copy.Value += right; return copy; } @@ -303,111 +356,51 @@ public override int GetHashCode() /// public static implicit operator CalDateTime(DateTime left) => new CalDateTime(left); - /// - /// Converts the date/time to the date/time of the computer running the program. - /// If the DateTimeKind is Unspecified, it's assumed that the underlying - /// Value already represents the system's datetime. - /// - public DateTime AsSystemLocal => AsDateTimeOffset.LocalDateTime; - - /// - /// Returns a representation of the in UTC. - /// - public DateTime AsUtc => AsDateTimeOffset.UtcDateTime; - - /// - /// Gets the underlying of . - /// - public DateOnly? DateOnlyValue => HasDate ? _dateOnly : null; - - /// - /// Gets the underlying of . - /// - public TimeOnly? TimeOnlyValue => HasTime ? _timeOnly : null; +/// + public DateTime AsUtc => DateTime.SpecifyKind(ToTimeZone(UtcTzId).Value, DateTimeKind.Utc); - /// - /// Gets the underlying . - /// Depending on setting, - /// the returned has - /// set to midnight or the time from initialization. The precision of the time part is up to seconds. - /// - /// See also and for the date and time parts. - /// + /// public DateTime Value { get { // HasDate and HasTime both have setters, so they can be changed. - if (HasDate && HasTime) + if (_timeOnly.HasValue) { return new DateTime(_dateOnly.Year, _dateOnly.Month, - _dateOnly.Day, _timeOnly.Hour, _timeOnly.Minute, _timeOnly.Second, - IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); + _dateOnly.Day, _timeOnly.Value.Hour, _timeOnly.Value.Minute, _timeOnly.Value.Second, + DateTimeKind.Unspecified); } - if (HasDate) // but no time - return new DateTime(_dateOnly.Year, _dateOnly.Month, _dateOnly.Day, - 0, 0, 0, - IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); - - throw new InvalidOperationException($"Cannot create DateTime when {nameof(HasDate)} is false."); - } - - set - { - // Initialize, keeping the HasTime setting - if (HasTime) - Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), _tzId); - else - Initialize(DateOnly.FromDateTime(value), null, _tzId); + // No time part + return new DateTime(_dateOnly.Year, _dateOnly.Month, _dateOnly.Day, + 0, 0, 0, + DateTimeKind.Unspecified); } } - /// - /// Returns true if the underlying is in UTC. - /// - public bool IsUtc => string.Equals(_tzId, UtcTzId, StringComparison.OrdinalIgnoreCase); + /// + public bool IsFloating => _tzId is null; - /// - /// if the underlying has a 'date' part (year, month, day). - /// - public bool HasDate { get; set; } = true; + /// + public bool IsUtc => string.Equals(_tzId, UtcTzId, StringComparison.OrdinalIgnoreCase); /// /// if the underlying has a 'time' part (hour, minute, second). /// - public bool HasTime { get; set; } = true; + public bool HasTime => _timeOnly.HasValue; - private string? _tzId = string.Empty; + private string? _tzId; /// - /// Setting the to a local timezone will set to . - /// Setting to UTC will set to . - /// If the value is set to , will be , - /// and the system's local timezone will be used. - /// - /// Setting the will initialize in the same way aw with the .
- /// To convert to another timezone, use . + /// Gets the IANA timezone ID of this instance. + /// It can be for Coordinated Universal Time, + /// or for a floating date/time, or value for a specific timezone. ///
- public string? TzId - { - get => _tzId; - set - { - if (string.Equals(_tzId, value, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - _tzId = value; - Initialize(_dateOnly, HasTime ? _timeOnly : null, value); - } - } + public string? TzId => _tzId; - /// - /// Gets the timezone name, if it references a timezone. - /// This is an alias for . - /// + /// + /// This is an alias for public string? TimeZoneName => TzId; /// @@ -428,53 +421,57 @@ public string? TzId /// public int Second => Value.Second; - /// - public int Millisecond => Value.Millisecond; - - /// - public long Ticks => Value.Ticks; - /// public DayOfWeek DayOfWeek => Value.DayOfWeek; /// public int DayOfYear => Value.DayOfYear; - /// - public DateTime Date => Value.Date; + /// + public DateOnly Date => _dateOnly; - /// - public TimeSpan TimeOfDay => Value.TimeOfDay; + /// + public TimeOnly? Time => _timeOnly; /// - /// Returns a representation of the in the timezone + /// Any values are always rounded to the nearest second. + /// RFC 5545, Section 3.3.5 does not allow for fractional seconds. /// - public IDateTime ToTimeZone(string? tzId) + private static TimeOnly? TruncateTimeToSeconds(TimeOnly? time) { - // If TzId is empty, it's a system-local datetime, so we should use the system timezone as the starting point. - var originalTzId = TzId ?? TimeZoneInfo.Local.Id; - - var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, originalTzId); - var converted = zonedOriginal.WithZone(DateUtil.GetZone(tzId)); + if (time is null) + { + return null; + } - return converted.Zone == DateTimeZone.Utc - ? new CalDateTime(converted.ToDateTimeUtc(), tzId) - : new CalDateTime(DateTime.SpecifyKind(converted.ToDateTimeUnspecified(), DateTimeKind.Unspecified), tzId); + return new TimeOnly(time.Value.Hour, time.Value.Minute, time.Value.Second); } /// - /// Returns a representation of the . - /// If a TzId is specified, it will use that timezone's UTC offset, otherwise it will use the - /// system-local timezone. + /// Any values are always rounded to the nearest second. + /// RFC 5545, Section 3.3.5 does not allow for fractional seconds. /// - public DateTimeOffset AsDateTimeOffset => - TzId is null - ? new DateTimeOffset(Value) - : DateUtil.ToZonedDateTimeLeniently(Value, TzId).ToDateTimeOffset(); + private static TimeOnly? TruncateTimeToSeconds(DateTime dateTime) + { + return new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second); + } + + /// + public IDateTime ToTimeZone(string otherTzId) + { + if (IsFloating) return new CalDateTime(_dateOnly, _timeOnly, otherTzId); + + var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, TzId); + var converted = zonedOriginal.WithZone(DateUtil.GetZone(otherTzId)); + + return converted.Zone == DateTimeZone.Utc + ? new CalDateTime(converted.ToDateTimeUtc(), UtcTzId) + : new CalDateTime(converted.ToDateTimeUnspecified(), otherTzId); + } /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the hours are not a multiple of 24. /// public IDateTime Add(TimeSpan ts) => this + ts; @@ -486,24 +483,13 @@ TzId is null /// Returns a new by subtracting the specified from the value of this instance. /// An interval. /// An object whose value is the difference of the date and time represented by this instance and the time interval represented by . - /// - /// This will also set to , - /// if the hours are not a multiple of 24. - /// public IDateTime Subtract(TimeSpan ts) => this - ts; - [Obsolete("This operator will be removed in a future version.", true)] - public static TimeSpan? operator -(CalDateTime? left, IDateTime? right) - { - left?.AssociateWith(right); // Should not be done in operator overloads - return left?.AsUtc - right?.AsUtc; - } - /// public IDateTime AddYears(int years) { var dt = Copy(); - dt.Value = Value.AddYears(years); + dt._dateOnly = dt._dateOnly.AddYears(years); return dt; } @@ -511,7 +497,7 @@ public IDateTime AddYears(int years) public IDateTime AddMonths(int months) { var dt = Copy(); - dt.Value = Value.AddMonths(months); + dt._dateOnly = dt._dateOnly.AddMonths(months); return dt; } @@ -519,93 +505,71 @@ public IDateTime AddMonths(int months) public IDateTime AddDays(int days) { var dt = Copy(); - dt.Value = Value.AddDays(days); + dt._dateOnly = dt._dateOnly.AddDays(days); return dt; } /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the hours are not a multiple of 24. /// public IDateTime AddHours(int hours) { - var dt = Copy(); - if (!dt.HasTime && hours % 24 > 0) + var copy = Copy(); + var newValue = copy.Value.AddHours(hours); + if (!copy.HasTime && (hours % 24 == 0)) { - dt.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); } - dt.Value = Value.AddHours(hours); - return dt; + else + { + copy._dateOnly = DateOnly.FromDateTime(newValue); + copy._timeOnly = TruncateTimeToSeconds(newValue); + } + return copy; } /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the minutes are not a multiple of 1440. /// public IDateTime AddMinutes(int minutes) { - var dt = Copy(); - if (!dt.HasTime && minutes % 1440 > 0) + var copy = Copy(); + var newValue = copy.Value.AddMinutes(minutes); + if (!copy.HasTime && (minutes % 1440 == 0)) { - dt.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); } - dt.Value = Value.AddMinutes(minutes); - return dt; + else + { + copy._dateOnly = DateOnly.FromDateTime(newValue); + copy._timeOnly = TruncateTimeToSeconds(newValue); + } + return copy; } /// /// - /// This will also set to , + /// This will also add a part that did not exist before the operation, /// if the seconds are not a multiple of 86400. /// public IDateTime AddSeconds(int seconds) { - var dt = Copy(); - if (!dt.HasTime && seconds % 86400 > 0) - { - dt.HasTime = true; - } - dt.Value = Value.AddSeconds(seconds); - return dt; - } - - /// - /// - /// This will also set to - /// if the milliseconds are not a multiple of 86400000. - /// - /// Milliseconds less than full seconds get truncated. - /// - public IDateTime AddMilliseconds(int milliseconds) - { - var dt = Copy(); - if (!dt.HasTime && milliseconds % 86400000 > 0) + var copy = Copy(); + var newValue = copy.Value.AddSeconds(seconds); + if (!copy.HasTime && (seconds % 86400 == 0)) { - dt.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); } - dt.Value = Value.AddMilliseconds(milliseconds); - return dt; - } - - /// - /// - /// This will also set to . - /// if ticks do not result in multiple of full days. - /// - /// Ticks less than full seconds get truncated. - /// - public IDateTime AddTicks(long ticks) - { - var dt = Copy(); - if (!dt.HasTime && (ticks % TimeSpan.TicksPerDay) != 0) + else { - dt.HasTime = true; + copy._dateOnly = DateOnly.FromDateTime(newValue); + copy._timeOnly = TruncateTimeToSeconds(newValue); } - - dt.Value = Value.AddTicks(ticks); - return dt; + return copy; } /// @@ -628,7 +592,6 @@ public IDateTime AddTicks(long ticks) /// public bool GreaterThanOrEqual(IDateTime dt) => this >= dt; - /// /// Associates the current instance with the specified object. /// @@ -685,20 +648,17 @@ public int CompareTo(IDateTime? dt) public string ToString(string? format, IFormatProvider? formatProvider) { formatProvider ??= CultureInfo.InvariantCulture; - var dateTimeOffset = AsDateTimeOffset; + var dateTimeOffset = DateUtil.ToZonedDateTimeLeniently(Value, _tzId).ToDateTimeOffset(); // Use the .NET format options to format the DateTimeOffset - - if (HasTime && !HasDate) - { - return $"{dateTimeOffset.TimeOfDay.ToString(format, formatProvider)} {_tzId}"; - } + var tzIdString = _tzId is not null ? $" {_tzId}" : string.Empty; if (HasTime) { - return $"{dateTimeOffset.ToString(format, formatProvider)} {_tzId}"; + return $"{dateTimeOffset.ToString(format, formatProvider)}{tzIdString}"; } - return $"{dateTimeOffset.ToString("d", formatProvider)} {_tzId}"; + // No time part + return $"{DateOnly.FromDateTime(dateTimeOffset.Date).ToString(format ?? "d", formatProvider)}{tzIdString}"; } } diff --git a/Ical.Net/DataTypes/IDateTime.cs b/Ical.Net/DataTypes/IDateTime.cs index ef791d5b..9f79a333 100644 --- a/Ical.Net/DataTypes/IDateTime.cs +++ b/Ical.Net/DataTypes/IDateTime.cs @@ -10,22 +10,14 @@ namespace Ical.Net.DataTypes; public interface IDateTime : IEncodableDataType, IComparable, IFormattable, ICalendarDataType { - /// - /// Converts the date/time to this computer's local date/time. - /// - DateTime AsSystemLocal { get; } - /// /// Converts the date/time to UTC (Coordinated Universal Time) + /// If == + /// it means that the is considered as local time for every timezone: + /// The returned is unchanged, but with . /// DateTime AsUtc { get; } - /// - /// Returns a DateTimeOffset representation of the Value. If a TzId is specified, it will use that time zone's UTC offset, otherwise it will use the - /// system-local time zone. - /// - DateTimeOffset AsDateTimeOffset { get; } - /// /// Gets/sets whether the Value of this date/time represents /// a universal time. @@ -33,106 +25,108 @@ public interface IDateTime : IEncodableDataType, IComparable, IFormat bool IsUtc { get; } /// - /// Gets the time zone name this time is in, if it references a time zone. + /// Gets the timezone name this time is in, if it references a timezone. /// string? TimeZoneName { get; } /// - /// Gets/sets the underlying DateTime value stored. This should always - /// use DateTimeKind.Utc, regardless of its actual representation. - /// Use IsUtc along with the TZID to control how this - /// date/time is handled. + /// Gets the date and time value in the ISO calendar as a type with . + /// The value has no associated timezone.
+ /// The precision of the time part is up to seconds. + /// + /// Use along with and + /// to control how this date/time is handled. ///
- DateTime Value { get; set; } + DateTime Value { get; } /// - /// Gets/sets whether or not this date/time value contains a 'date' part. + /// Returns , if the date/time value contains a 'time' part. /// - bool HasDate { get; set; } + bool HasTime { get; } /// - /// Gets/sets whether or not this date/time value contains a 'time' part. + /// Returns , if the date/time value is floating. + /// + /// A floating date/time value does not include a timezone identifier or UTC offset, + /// so it is interpreted as local time in the context where it is used. + /// + /// A floating date/time value is useful when the exact timezone is not + /// known or when the event should be interpreted in the local timezone of + /// the user or system processing the calendar data. /// - bool HasTime { get; set; } + bool IsFloating { get; } /// - /// Gets/sets the time zone ID for this date/time value. + /// Gets the timezone ID that applies to the . /// - string? TzId { get; set; } + string? TzId { get; } /// - /// Gets the year for this date/time value. + /// Gets the year that applies to the . /// int Year { get; } /// - /// Gets the month for this date/time value. + /// Gets the month that applies to the . /// int Month { get; } /// - /// Gets the day for this date/time value. + /// Gets the day that applies to the . /// int Day { get; } /// - /// Gets the hour for this date/time value. + /// Gets the hour that applies to the . /// int Hour { get; } /// - /// Gets the minute for this date/time value. + /// Gets the minute that applies to the . /// int Minute { get; } /// - /// Gets the second for this date/time value. + /// Gets the second that applies to the . /// int Second { get; } /// - /// Gets the millisecond for this date/time value. + /// Gets the DayOfWeek that applies to the . /// - int Millisecond { get; } - - /// - /// Gets the ticks for this date/time value. - /// - long Ticks { get; } + DayOfWeek DayOfWeek { get; } /// - /// Gets the DayOfWeek for this date/time value. + /// Gets the date portion of the . /// - DayOfWeek DayOfWeek { get; } + DateOnly Date { get; } /// - /// Gets the date portion of the date/time value. + /// Gets the time portion of the , or if the is a pure date. /// - DateTime Date { get; } + TimeOnly? Time { get; } /// - /// Converts the date/time value to a local time - /// within the specified time zone. + /// Converts the to a date/time + /// within the specified timezone. + /// + /// If == + /// it means that the is considered as local time for every timezone: + /// The returned is unchanged and the is set as . /// - IDateTime ToTimeZone(string tzId); - + IDateTime ToTimeZone(string otherTzId); IDateTime Add(TimeSpan ts); IDateTime Subtract(TimeSpan ts); TimeSpan Subtract(IDateTime dt); - IDateTime AddYears(int years); IDateTime AddMonths(int months); IDateTime AddDays(int days); IDateTime AddHours(int hours); IDateTime AddMinutes(int minutes); IDateTime AddSeconds(int seconds); - IDateTime AddMilliseconds(int milliseconds); - IDateTime AddTicks(long ticks); - bool LessThan(IDateTime dt); bool GreaterThan(IDateTime dt); bool LessThanOrEqual(IDateTime dt); bool GreaterThanOrEqual(IDateTime dt); - void AssociateWith(IDateTime dt); } diff --git a/Ical.Net/DataTypes/Trigger.cs b/Ical.Net/DataTypes/Trigger.cs index 35df092f..d527c290 100644 --- a/Ical.Net/DataTypes/Trigger.cs +++ b/Ical.Net/DataTypes/Trigger.cs @@ -37,7 +37,11 @@ public virtual IDateTime DateTime Duration = null; // Ensure date/time has a time part - _mDateTime.HasTime = true; + if (!_mDateTime.HasTime) + { + _mDateTime = new CalDateTime(_mDateTime.Date, new TimeOnly(), _mDateTime.TzId) + { AssociatedObject = _mDateTime.AssociatedObject }; + } } } diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index c0686a7d..92e8e68a 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -953,9 +953,7 @@ public override HashSet Evaluate(IDateTime referenceDate, DateTime perio { // This case is not defined by RFC 5545. We handle it by evaluating the rule // as if referenceDate had a time (i.e. set to midnight). - - referenceDate = referenceDate.Copy(); - referenceDate.HasTime = true; + referenceDate = new CalDateTime(referenceDate.Date, new TimeOnly(), referenceDate.TzId) { AssociatedObject = referenceDate.AssociatedObject }; } // Create a recurrence pattern suitable for use during evaluation. @@ -988,6 +986,6 @@ private static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) return dt1.IsUtc ? new CalDateTime(copy.AsUtc) - : new CalDateTime(copy.AsSystemLocal); + : copy; } } diff --git a/Ical.Net/Evaluation/RecurrenceUtil.cs b/Ical.Net/Evaluation/RecurrenceUtil.cs index 3062d337..3e467ce7 100644 --- a/Ical.Net/Evaluation/RecurrenceUtil.cs +++ b/Ical.Net/Evaluation/RecurrenceUtil.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. // +using System; using System.Collections.Generic; using System.Linq; using Ical.Net.CalendarComponents; @@ -13,8 +14,11 @@ namespace Ical.Net.Evaluation; internal class RecurrenceUtil { - public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) => GetOccurrences(recurrable, - new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); + public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) + { + return GetOccurrences(recurrable, + new CalDateTime(dt.Date, dt.Time), new CalDateTime(dt.Date.AddDays(1)), includeReferenceDateInResults); + } public static HashSet GetOccurrences(IRecurrable recurrable, IDateTime periodStart, IDateTime periodEnd, bool includeReferenceDateInResults) { @@ -31,8 +35,8 @@ public static HashSet GetOccurrences(IRecurrable recurrable, IDateTi // Change the time zone of periodStart/periodEnd as needed // so they can be used during the evaluation process. - periodStart.TzId = start.TzId; - periodEnd.TzId = start.TzId; + periodStart = new CalDateTime(periodStart.Date, periodStart.Time, start.TzId); + periodEnd = new CalDateTime(periodEnd.Date, periodEnd.Time, start.TzId); var periods = evaluator.Evaluate(start, DateUtil.GetSimpleDateTimeData(periodStart), DateUtil.GetSimpleDateTimeData(periodEnd), includeReferenceDateInResults); diff --git a/Ical.Net/Evaluation/TodoEvaluator.cs b/Ical.Net/Evaluation/TodoEvaluator.cs index adc593cb..c89c5787 100644 --- a/Ical.Net/Evaluation/TodoEvaluator.cs +++ b/Ical.Net/Evaluation/TodoEvaluator.cs @@ -73,7 +73,7 @@ private void DetermineStartingRecurrence(RecurrencePattern recur, ref IDateTime { var dtVal = referenceDateTime.Value; IncrementDate(ref dtVal, recur, -recur.Interval); - referenceDateTime.Value = dtVal; + referenceDateTime = new CalDateTime(DateOnly.FromDateTime(dtVal), TimeOnly.FromDateTime(dtVal)) { AssociatedObject = referenceDateTime.AssociatedObject }; } } diff --git a/Ical.Net/ILoadable.cs b/Ical.Net/ILoadable.cs index 37b90ac2..da19f509 100644 --- a/Ical.Net/ILoadable.cs +++ b/Ical.Net/ILoadable.cs @@ -10,7 +10,7 @@ namespace Ical.Net; public interface ILoadable { /// - /// Gets whether or not the object has been loaded. + /// Gets whether the object has been loaded. /// bool IsLoaded { get; } @@ -23,4 +23,4 @@ public interface ILoadable /// Fires the Loaded event. ///
void OnLoaded(); -} \ No newline at end of file +} diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index 4e0ed1f1..7d45ec06 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -99,7 +99,7 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } // The associated object is an ICalendarObject of type CalendarProperty // that contains any timezone ("TZID" property) deserialized in a prior step - dt.TzId = dt.Parameters.Get("TZID"); + var timeZoneId = dt.Parameters.Get("TZID"); // Decode the value as necessary value = Decode(dt, value); @@ -132,16 +132,11 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } } var isUtc = match.Groups[9].Success; - var kind = isUtc - ? DateTimeKind.Utc - : DateTimeKind.Unspecified; - - if (isUtc) dt.TzId = "UTC"; + if (isUtc) timeZoneId = "UTC"; - dt.Value = timePart.HasValue - ? new DateTime(datePart.Year, datePart.Month, datePart.Day, timePart.Value.Hour, timePart.Value.Minute, timePart.Value.Second, kind) - : new DateTime(datePart.Year, datePart.Month, datePart.Day, 0, 0, 0, kind); - dt.HasTime = timePart.HasValue; + dt = timePart.HasValue + ? new CalDateTime(datePart, timePart.Value, timeZoneId) { AssociatedObject = dt.AssociatedObject } + : new CalDateTime(datePart) { AssociatedObject = dt.AssociatedObject }; return dt; } diff --git a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs index 5395a52f..de00fe31 100644 --- a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs @@ -147,8 +147,9 @@ public override string SerializeToString(object obj) var serializer = factory.Build(typeof(IDateTime), SerializationContext) as IStringSerializer; if (serializer != null) { - IDateTime until = new CalDateTime(recur.Until); - until.HasTime = true; + var until = new CalDateTime(DateOnly.FromDateTime(recur.Until), TimeOnly.FromDateTime(recur.Until), + recur.Until.Kind == DateTimeKind.Utc ? "UTC" : null); + values.Add("UNTIL=" + serializer.SerializeToString(until)); } } @@ -269,7 +270,7 @@ public override object Deserialize(TextReader tr) var dt = serializer?.Deserialize(new StringReader(keyValue)) as IDateTime; if (dt != null) { - r.Until = dt.Value; + r.Until = DateTime.SpecifyKind(dt.Value, dt.IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); } } break; diff --git a/Ical.Net/Utility/DateUtil.cs b/Ical.Net/Utility/DateUtil.cs index 941c9ca2..d716fe97 100644 --- a/Ical.Net/Utility/DateUtil.cs +++ b/Ical.Net/Utility/DateUtil.cs @@ -19,7 +19,7 @@ public static IDateTime StartOfDay(IDateTime dt) => dt.AddHours(-dt.Hour).AddMinutes(-dt.Minute).AddSeconds(-dt.Second); public static IDateTime EndOfDay(IDateTime dt) - => StartOfDay(dt).AddDays(1).AddTicks(-1); + => StartOfDay(dt).AddDays(1).AddSeconds(-1); public static DateTime GetSimpleDateTimeData(IDateTime dt) => dt.Value;