diff --git a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs index 9fcd2172..94c646bb 100644 --- a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs +++ b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -12,23 +12,13 @@ namespace Ical.Net.Benchmarks; public class CalDateTimePerfTests { private const string _aTzid = "Australia/Sydney"; - private const string _bTzid = "America/New_York"; [Benchmark] public CalDateTime EmptyTzid() => CalDateTime.Now; [Benchmark] - public CalDateTime SpecifiedTzid() => new CalDateTime(DateTime.Now, _aTzid); - - [Benchmark] - public CalDateTime UtcDateTime() => new CalDateTime(DateTime.UtcNow); - - [Benchmark] - public CalDateTime EmptyTzidToTzid() => EmptyTzid().ToTimeZone(_bTzid); - - [Benchmark] - public CalDateTime SpecifiedTzidToDifferentTzid() => SpecifiedTzid().ToTimeZone(_bTzid); + public CalDateTime SpecifiedTzid() => CalDateTime.FromDateTime(DateTime.Now, _aTzid); [Benchmark] - public CalDateTime UtcToDifferentTzid() => UtcDateTime().ToTimeZone(_bTzid); + public CalDateTime UtcDateTime() => CalDateTime.FromDateTime(DateTime.UtcNow); } diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index 070175b2..d14d9634 100644 --- a/Ical.Net.Benchmarks/OccurencePerfTests.cs +++ b/Ical.Net.Benchmarks/OccurencePerfTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -40,13 +40,13 @@ public void GetOccurrences() public void MultipleEventsWithUntilOccurrencesSearchingByWholeCalendar() { var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .InZoneLeniently(tz) @@ -59,13 +59,13 @@ public void MultipleEventsWithUntilOccurrencesSearchingByWholeCalendar() public void MultipleEventsWithUntilOccurrences() { var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .InZoneLeniently(tz) @@ -80,13 +80,13 @@ public void MultipleEventsWithUntilOccurrences() public void MultipleEventsWithUntilOccurrencesEventsAsParallel() { var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .PlusDays(10) @@ -178,7 +178,7 @@ private static Calendar GetFourCalendarEventsWithUntilRule() End = new(startTime.PlusMinutes(10), tzid), RecurrenceRule = new(FrequencyType.Daily, 1) { - Until = new(startTime.PlusDays(10).InUtc().ToInstant()), + Until = CalDateTime.FromZonedDateTime(startTime.PlusDays(10).InUtc()), }, }; startTime = startTime.PlusDays(1); @@ -195,13 +195,13 @@ public void MultipleEventsWithCountOccurrencesSearchingByWholeCalendar() { var calendar = GetFourCalendarEventsWithCountRule(); var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .InZoneLeniently(tz) @@ -215,13 +215,13 @@ public void MultipleEventsWithCountOccurrences() { var calendar = GetFourCalendarEventsWithCountRule(); var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .InZoneLeniently(tz) @@ -237,13 +237,13 @@ public void MultipleEventsWithCountOccurrencesEventsAsParallel() { var calendar = GetFourCalendarEventsWithCountRule(); var searchStart = _calendarFourEvents.Events.First().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(-1) .InZoneLeniently(tz); var searchEnd = _calendarFourEvents.Events.Last().DtStart! - .ToZonedDateTime(tz) + .ToZonedOrDefault(tz) .LocalDateTime .PlusYears(1) .PlusDays(10) @@ -270,8 +270,8 @@ private static Calendar GetFourCalendarEventsWithCountRule() { var e = new CalendarEvent { - Start = new CalDateTime(startTime.AddMinutes(5), tzid), - End = new CalDateTime(startTime.AddMinutes(10), tzid), + Start = CalDateTime.FromDateTime(startTime.AddMinutes(5), tzid), + End = CalDateTime.FromDateTime(startTime.AddMinutes(10), tzid), RecurrenceRule = new(FrequencyType.Daily, 1) { Count = 100, diff --git a/Ical.Net.Benchmarks/SerializationPerfTests.cs b/Ical.Net.Benchmarks/SerializationPerfTests.cs index 094046e4..8081a1de 100644 --- a/Ical.Net.Benchmarks/SerializationPerfTests.cs +++ b/Ical.Net.Benchmarks/SerializationPerfTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -80,8 +80,8 @@ private static Calendar CreateSimpleCalendar() var simpleCalendar = new Calendar(); var calendarEvent = new CalendarEvent { - Start = new CalDateTime(DateTime.Now, timeZoneId), - End = new CalDateTime(DateTime.Now + TimeSpan.FromHours(1), timeZoneId), + Start = CalDateTime.FromDateTime(DateTime.Now, timeZoneId), + End = CalDateTime.FromDateTime(DateTime.Now + TimeSpan.FromHours(1), timeZoneId), RecurrenceRule = new(FrequencyType.Daily, 1) { Count = 100, diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index b98b8c38..6dc97fde 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -1,111 +1,40 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // -using Ical.Net.CalendarComponents; -using Ical.Net.DataTypes; -using NUnit.Framework; -using NUnit.Framework.Constraints; using System; using System.Collections; using System.Globalization; using System.Linq; +using Ical.Net.DataTypes; +using NodaTime; +using NUnit.Framework; +using NUnit.Framework.Constraints; namespace Ical.Net.Tests; public class CalDateTimeTests { - private static readonly DateTime _now = DateTime.Now; - private static readonly DateTime _later = _now.AddHours(1); - - private static CalendarEvent GetEventWithRecurrenceRule(string tzId) - { - var calendarEvent = new CalendarEvent - { - Start = new CalDateTime(_now, tzId), - End = new CalDateTime(_later, tzId), - RecurrenceRule = new(FrequencyType.Daily, 1) - { - Count = 5, - }, - Resources = ["Foo", "Bar", "Baz"], - }; - return calendarEvent; - } - - [Test] - public void ToTimeZoneFloating() - { - var dt = new CalDateTime(2024, 12, 28, 17, 45, 05, "Europe/Vienna"); - var floating = dt.ToTimeZone(null); - var dt2 = floating.ToTimeZone("Europe/Vienna"); - - using (Assert.EnterMultipleScope()) - { - Assert.That(dt, Is.EqualTo(dt2)); - Assert.That(floating.TzId, Is.Null); - Assert.That(floating.Value, Is.EqualTo(dt.Value)); - } - } - - [Test, TestCaseSource(nameof(ToTimeZoneTestCases))] - public void ToTimeZoneTests(CalendarEvent calendarEvent, string targetTimeZone) - { - var startAsUtc = calendarEvent.Start!.ToInstant(); - - var convertedStart = calendarEvent.Start.ToTimeZone(targetTimeZone); - var convertedAsUtc = convertedStart.ToInstant(); - - Assert.That(convertedAsUtc, Is.EqualTo(startAsUtc)); - } - - public static IEnumerable ToTimeZoneTestCases() - { - const string bclCst = "Central Standard Time"; - const string bclEastern = "Eastern Standard Time"; - var bclEvent = GetEventWithRecurrenceRule(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 = GetEventWithRecurrenceRule(ianaNy); - - yield return new TestCaseData(ianaEvent, ianaRome) - .SetName($"IANA to IANA: {ianaNy} to {ianaRome}"); - - const string utc = "UTC"; - var utcEvent = GetEventWithRecurrenceRule(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}"); - } - [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", CultureInfo.InvariantCulture); - var someDt = new CalDateTime(someTime.DateTime, "America/New_York"); - var firstUtc = someDt.ToInstant(); - Assert.That(firstUtc, Is.EqualTo(NodaTime.Instant.FromDateTimeOffset(someTime))); + var someDt = CalDateTime.FromDateTime(someTime.DateTime, "America/New_York"); + var firstUtc = someDt.ToZonedDateTime().ToDateTimeOffset(); + Assert.That(firstUtc, Is.EqualTo(someTime)); - someDt = new CalDateTime(someTime.DateTime, "Europe/Berlin"); - var berlinUtc = someDt.ToInstant(); + someDt = CalDateTime.FromDateTime(someTime.DateTime, "Europe/Berlin"); + var berlinUtc = someDt.ToZonedDateTime().ToDateTimeOffset(); Assert.That(berlinUtc, Is.Not.EqualTo(firstUtc)); } [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; + => CalDateTime.FromDateTime(dateTime, tzId).ToDateTimeUnspecified().Kind; - public static IEnumerable DateTimeKindOverrideTestCases() + private static IEnumerable DateTimeKindOverrideTestCases() { const string localTz = "America/New_York"; var localDt = DateTime.SpecifyKind(DateTime.Parse("2018-05-21T11:35:33", CultureInfo.InvariantCulture), DateTimeKind.Unspecified); @@ -224,18 +153,17 @@ 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"); + var c = CalDateTime.FromDateTime(dt, tzId: "Europe/Berlin"); var c2 = new CalDateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, c.TzId); - var c3 = new CalDateTime(new NodaTime.LocalDate(dt.Year, dt.Month, dt.Day), - new NodaTime.LocalTime(dt.Hour, dt.Minute, dt.Second), c.TzId); + var c3 = new CalDateTime(new NodaTime.LocalDateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second), c.TzId); using (Assert.EnterMultipleScope()) { - Assert.That(c2.Value, Is.EqualTo(c3.Value)); + Assert.That(c2.ToDateTimeUnspecified(), Is.EqualTo(c3.ToDateTimeUnspecified())); Assert.That(c2.TzId, Is.EqualTo(c3.TzId)); - Assert.That(CalDateTime.UtcNow.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified)); - Assert.That(CalDateTime.Today.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified)); + Assert.That(CalDateTime.UtcNow.ToDateTimeUnspecified().Kind, Is.EqualTo(DateTimeKind.Unspecified)); + Assert.That(CalDateTime.Today.ToDateTimeUnspecified().Kind, Is.EqualTo(DateTimeKind.Unspecified)); Assert.That(c.DayOfYear, Is.EqualTo(dt.DayOfYear)); Assert.That(c.Time, Is.EqualTo(NodaTime.LocalDateTime.FromDateTime(dt).TimeOfDay)); Assert.That(c.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture), Is.EqualTo("02.01.2025 Europe/Berlin")); @@ -246,17 +174,15 @@ public void Simple_PropertyAndMethod_HasTime_Tests() [ new TestCaseData(DateTimeKind.Unspecified, Is.EqualTo(new CalDateTime(2024, 12, 30, 10, 44, 50, null))), new TestCaseData(DateTimeKind.Utc, Is.EqualTo(new CalDateTime(2024, 12, 30, 10, 44, 50, "UTC"))), - new TestCaseData(DateTimeKind.Local, Throws.ArgumentException), + new TestCaseData(DateTimeKind.Local, Is.EqualTo(new CalDateTime(2024, 12, 30, 10, 44, 50))), ]; [Test, TestCaseSource(nameof(CalDateTime_FromDateTime_HandlesKindCorrectlyTestCases))] - - public void CalDateTime_FromDateTime_HandlesKindCorrectly(DateTimeKind kind, IResolveConstraint constraint) { var dt = new DateTime(2024, 12, 30, 10, 44, 50, kind); - Assert.That(() => new CalDateTime(dt), constraint); + Assert.That(() => CalDateTime.FromDateTime(dt), constraint); } [TestCase("20250703T060000Z", null)] @@ -266,7 +192,7 @@ public void ConstructorWithIso8601UtcString_ShouldResultInUtc(string value, stri var dt = new CalDateTime(value, tzId); using (Assert.EnterMultipleScope()) { - Assert.That(dt.Value, Is.EqualTo(new DateTime(2025, 7, 3, 6, 0, 0, DateTimeKind.Utc))); + Assert.That(dt.ToDateTimeUnspecified(), Is.EqualTo(new DateTime(2025, 7, 3, 6, 0, 0, DateTimeKind.Utc))); #pragma warning disable CA1305 Assert.That(dt.ToString("yyyy-MM-dd HH:mm:ss"), Is.EqualTo("2025-07-03 06:00:00 UTC")); #pragma warning restore CA1305 @@ -277,4 +203,105 @@ public void ConstructorWithIso8601UtcString_ShouldResultInUtc(string value, stri [Test] public void ConstructorWithIso8601UtcString_ButDifferentTzId_ShouldThrow() => Assert.That(() => _ = new CalDateTime("20250703T060000Z", "CEST"), Throws.ArgumentException); + + [Test] + public void ThrowsWhenDateYearIsInvalid() + { + var d = new LocalDate(0, 1, 1); + + Assert.Throws(() => + { + new CalDateTime(d); + }); + } + + [Test] + public void ThrowsWhenStringValueIsInvalid() + { + Assert.Throws(() => + { + new CalDateTime("invalid"); + }); + } + +#if NET6_0_OR_GREATER + [Test] + public void ConvertsToAndFromDateOnly() + { + var d = new DateOnly(2026, 4, 19); + var c = CalDateTime.FromDateOnly(d); + + Assert.That(c.ToDateOnly(), Is.EqualTo(d)); + } + + [Test] + public void ConvertsToAndFromTimeOnly() + { + var t = new TimeOnly(6, 30, 2); + var d = new DateOnly(2026, 4, 19).ToDateTime(t); + + var c = CalDateTime.FromDateTime(d); + + Assert.That(c.ToTimeOnly(), Is.EqualTo(t)); + } + + [Test] + public void TimeOnlyIsNullForDateValues() + { + var d = new DateOnly(2026, 4, 19); + var c = CalDateTime.FromDateOnly(d); + + Assert.That(c.ToTimeOnly(), Is.Null); + } +#endif + + [Test] + public void ThrowsWhenConvertingToZonedDateTimeWithoutTimeZone() + { + var c = new CalDateTime(2026, 4, 19, 5, 0, 0); + + Assert.Throws(() => + { + c.ToZonedDateTime(); + }); + } + + [Test] + public void DayOfWeekIsCorrect() + { + var c = new CalDateTime(2026, 4, 19, 5, 0, 0); + Assert.That(c.DayOfWeek, Is.EqualTo(DayOfWeek.Sunday)); + } + + [Test] + public void OtherTypeIsNotEqual() + { + var d = new CalDateTime(2026, 4, 19); + + var isEqual = Equals(d, new DateTime(2026, 4, 19)); + + Assert.That(isEqual, Is.False); + } + + [Test] + public void DateValueDoesNotEqualDateTimeAtMidnight() + { + var a = new CalDateTime(2026, 4, 19, 0, 0, 0); + var b = new CalDateTime(2026, 4, 19); + + Assert.That(a, Is.Not.EqualTo(b)); + } + + [Test] + public void CaseInsensitiveTzIdHasSameHashCode() + { + var a = new CalDateTime(2026, 4, 19, 3, 0, 0, "Utc"); + var b = new CalDateTime(2026, 4, 19, 3, 0, 0, "UTC"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(a, Is.EqualTo(b)); + Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode())); + } + } } diff --git a/Ical.Net.Tests/CalendarEventTest.cs b/Ical.Net.Tests/CalendarEventTest.cs index d54b7b29..ebd912b6 100644 --- a/Ical.Net.Tests/CalendarEventTest.cs +++ b/Ical.Net.Tests/CalendarEventTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -165,7 +165,7 @@ public static IEnumerable EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_Tes var explicitDtStampCalendar = new Calendar(); var explicitDtStampEvent = new CalendarEvent { - DtStamp = new CalDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)) + DtStamp = CalDateTime.FromDateTime(new DateTime(2016, 8, 17, 2, 30, 0, DateTimeKind.Utc)) }; explicitDtStampCalendar.Events.Add(explicitDtStampEvent); yield return new TestCaseData(serializer.SerializeToString(explicitDtStampCalendar)) @@ -175,8 +175,8 @@ public static IEnumerable EnsureAutomaticallySetDtStampIsSerializedAsUtcKind_Tes private static CalendarEvent GetSimpleEvent() => new CalendarEvent { - DtStart = new CalDateTime(_now), - DtEnd = new CalDateTime(_later), + DtStart = CalDateTime.FromDateTime(_now), + DtEnd = CalDateTime.FromDateTime(_later), Uid = _uid, }; diff --git a/Ical.Net.Tests/CalendarPropertiesTest.cs b/Ical.Net.Tests/CalendarPropertiesTest.cs index c50c46e2..a4d8b2a6 100644 --- a/Ical.Net.Tests/CalendarPropertiesTest.cs +++ b/Ical.Net.Tests/CalendarPropertiesTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -40,8 +40,8 @@ public void PropertySerialization() var end = start.AddHours(1); var @event = new CalendarEvent { - Start = new CalDateTime(start), - End = new CalDateTime(end), + Start = CalDateTime.FromDateTime(start), + End = CalDateTime.FromDateTime(end), Description = "This is a description", }; var property = new CalendarProperty("X-ALT-DESC", propValue); diff --git a/Ical.Net.Tests/CollectionHelpersTests.cs b/Ical.Net.Tests/CollectionHelpersTests.cs index e94e461c..1599a2b6 100644 --- a/Ical.Net.Tests/CollectionHelpersTests.cs +++ b/Ical.Net.Tests/CollectionHelpersTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -17,7 +17,7 @@ internal class CollectionHelpersTests private static readonly DateTime _now = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified); private static List GetExceptionDates() - => new List { new PeriodList { new Period(new CalDateTime(_now.AddDays(1).Date)) } }; + => new List { new PeriodList { new Period(CalDateTime.FromDateTime(_now.AddDays(1).Date)) } }; [Test] public void ExDateTests() @@ -29,7 +29,7 @@ public void ExDateTests() } var changedPeriod = GetExceptionDates(); - changedPeriod[0][0] = new Period(new CalDateTime(_now.AddHours(-1)), changedPeriod[0][0].EndTime); + changedPeriod[0][0] = new Period(CalDateTime.FromDateTime(_now.AddHours(-1)), changedPeriod[0][0].EndTime); Assert.That(changedPeriod, Is.Not.EqualTo(GetExceptionDates())); } @@ -47,17 +47,6 @@ public void TestMerge(IList seq1, IList seq2, IList expected) Assert.That(result, Is.EqualTo(expected)); } - [TestCase(new int[] { }, new int[] { }, new int[] { })] - [TestCase(new int[] { }, new[] { 2, 4, 6 }, new int[] { })] - [TestCase(new[] { 2, 4, 6 }, new int[] { }, new[] { 2, 4, 6 })] - [TestCase(new[] { 1, 2, 3, 5, 6, 7 }, new[] { 2, 4, 6 }, new[] { 1, 3, 5, 7 })] - public void TestMergeExclude(IList seq, IList exclude, IList expected) - { - var result = seq.OrderedExclude(exclude).ToList(); - - Assert.That(result, Is.EqualTo(expected)); - } - private static IEnumerable GetNaturalNumbers() { var i = 1; @@ -73,14 +62,6 @@ public void TestMergeIndefinite() Assert.That(result, Is.EqualTo(new[] { 2, 3, 4, 6, 6, 8, 9 })); } - [Test] - public void TestMergeExcludeIndefinite() - { - var result = GetNaturalNumbers().Select(x => x * 3).OrderedExclude(GetNaturalNumbers().Select(x => x * 2)) - .Take(4); - Assert.That(result, Is.EqualTo(new[] { 3, 9, 15, 21 })); - } - [Test] public void TestMergeMulti() { diff --git a/Ical.Net.Tests/CopyComponentTests.cs b/Ical.Net.Tests/CopyComponentTests.cs index 7cac4384..9da117ca 100644 --- a/Ical.Net.Tests/CopyComponentTests.cs +++ b/Ical.Net.Tests/CopyComponentTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -24,8 +24,8 @@ public class CopyComponentTests private static CalendarEvent GetSimpleEvent() => new CalendarEvent { - DtStart = new CalDateTime(_now), - DtEnd = new CalDateTime(_later), + DtStart = CalDateTime.FromDateTime(_now), + DtEnd = CalDateTime.FromDateTime(_later), }; private static string SerializeEvent(CalendarEvent e) => new CalendarSerializer().SerializeToString(new Calendar { Events = { e } })!; @@ -73,8 +73,8 @@ public void CopyFreeBusyTest() { var orig = new FreeBusy { - Start = new CalDateTime(_now), - End = new CalDateTime(_later), + Start = CalDateTime.FromDateTime(_now), + End = CalDateTime.FromDateTime(_later), Entries = { new FreeBusyEntry(new Period(new CalDateTime(2024, 10, 1), Duration.FromDays(1)), FreeBusyStatus.Busy) { Language = "English" }} }; @@ -119,7 +119,7 @@ public void CopyTodoTest() { Summary = "Test Todo", Description = "This is a test todo", - Due = new CalDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified).AddDays(10)), + Due = CalDateTime.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified).AddDays(10)), Priority = 1, Contacts = new[] { "John", "Paul" }, Status = "NeedsAction" @@ -145,7 +145,7 @@ public void CopyJournalTest() { Summary = "Test Journal", Description = "This is a test journal", - DtStart = new CalDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified)), + DtStart = CalDateTime.FromDateTime(DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Unspecified)), Categories = new List { "Category1", "Category2" }, Priority = 1, Status = "Draft" diff --git a/Ical.Net.Tests/DateTimeSerializerTests.cs b/Ical.Net.Tests/DateTimeSerializerTests.cs index 888eea6a..00a4b6a9 100644 --- a/Ical.Net.Tests/DateTimeSerializerTests.cs +++ b/Ical.Net.Tests/DateTimeSerializerTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -19,9 +19,9 @@ public void TZIDPropertyShouldBeAppliedForLocalTimezones() // see http://www.ietf.org/rfc/rfc2445.txt p.36 var result = new DateTimeSerializer() .SerializeToString( - new CalDateTime(new DateTime(1997, 7, 14, 13, 30, 0, DateTimeKind.Local), "US-Eastern")); + CalDateTime.FromDateTime(new DateTime(1997, 7, 14, 13, 30, 0, DateTimeKind.Local), "US-Eastern")); // TZID is applied elsewhere - just make sure this doesn't have 'Z' appended. Assert.That(result, Is.EqualTo("19970714T133000")); } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index d0a59a89..269b197f 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -375,7 +375,7 @@ public void RecurrenceDates1() new CalDateTime(1997, 12, 25) }; - var expectedEndTime = new CalDateTime(new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc)); + var expectedEndTime = CalDateTime.FromDateTime(new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc)); var actualStartTimes = iCal.Events[0]!.RecurrenceDates.GetAllPeriods() .Select(p => p.StartTime) diff --git a/Ical.Net.Tests/DocumentationExamples.cs b/Ical.Net.Tests/DocumentationExamples.cs index af79b061..5cac0c5e 100644 --- a/Ical.Net.Tests/DocumentationExamples.cs +++ b/Ical.Net.Tests/DocumentationExamples.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -49,8 +49,8 @@ public void EveryOtherTuesdayUntilTheEndOfTheYear_Test() // An event taking place between 07:00 and 08:00, beginning July 5 (a Tuesday) var vEvent = new CalendarEvent { - DtStart = new CalDateTime(DateTime.Parse("2016-07-05T07:00", CultureInfo.InvariantCulture)), - DtEnd = new CalDateTime(DateTime.Parse("2016-07-05T08:00",CultureInfo.InvariantCulture)), + DtStart = CalDateTime.FromDateTime(DateTime.Parse("2016-07-05T07:00", CultureInfo.InvariantCulture)), + DtEnd = CalDateTime.FromDateTime(DateTime.Parse("2016-07-05T08:00",CultureInfo.InvariantCulture)), // Recurring every other Tuesday until Dec 31 RecurrenceRule = new(FrequencyType.Weekly, 2) @@ -75,8 +75,8 @@ public void FourthThursdayOfNovember_Tests() // An event taking place between 07:00 and 19:00, beginning July 5 (a Tuesday) var vEvent = new CalendarEvent { - DtStart = new CalDateTime(DateTime.Parse("2000-11-23T07:00", CultureInfo.InvariantCulture)), - DtEnd = new CalDateTime(DateTime.Parse("2000-11-23T19:00", CultureInfo.InvariantCulture)), + DtStart = CalDateTime.FromDateTime(DateTime.Parse("2000-11-23T07:00", CultureInfo.InvariantCulture)), + DtEnd = CalDateTime.FromDateTime(DateTime.Parse("2000-11-23T19:00", CultureInfo.InvariantCulture)), // Recurring every other Tuesday until Dec 31 RecurrenceRule = new(FrequencyType.Yearly, 1) diff --git a/Ical.Net.Tests/EqualityAndHashingTests.cs b/Ical.Net.Tests/EqualityAndHashingTests.cs index 16c02a23..6ee35e28 100644 --- a/Ical.Net.Tests/EqualityAndHashingTests.cs +++ b/Ical.Net.Tests/EqualityAndHashingTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -28,7 +28,7 @@ public void CalDateTime_Tests(CalDateTime incomingDt, CalDateTime expectedDt) { using (Assert.EnterMultipleScope()) { - Assert.That(expectedDt.Value, Is.EqualTo(incomingDt.Value)); + Assert.That(expectedDt.ToDateTimeUnspecified(), Is.EqualTo(incomingDt.ToDateTimeUnspecified())); Assert.That(expectedDt.GetHashCode(), Is.EqualTo(incomingDt.GetHashCode())); Assert.That(expectedDt.TzId, Is.EqualTo(incomingDt.TzId)); Assert.That(incomingDt.Equals(expectedDt), Is.True); @@ -37,17 +37,17 @@ public void CalDateTime_Tests(CalDateTime incomingDt, CalDateTime expectedDt) public static IEnumerable CalDateTime_TestCases() { - var nowCalDt = new CalDateTime(_nowTime); - yield return new TestCaseData(nowCalDt, new CalDateTime(_nowTime)).SetName("Now, no time zone"); + var nowCalDt = CalDateTime.FromDateTime(_nowTime); + yield return new TestCaseData(nowCalDt, CalDateTime.FromDateTime(_nowTime)).SetName("Now, no time zone"); - var nowCalDtWithTz = new CalDateTime(_nowTime, TzId); - yield return new TestCaseData(nowCalDtWithTz, new CalDateTime(_nowTime, TzId)).SetName("Now, with time zone"); + var nowCalDtWithTz = CalDateTime.FromDateTime(_nowTime, TzId); + yield return new TestCaseData(nowCalDtWithTz, CalDateTime.FromDateTime(_nowTime, TzId)).SetName("Now, with time zone"); } private static CalendarEvent GetSimpleEvent() => new CalendarEvent { - DtStart = new CalDateTime(_nowTime), - DtEnd = new CalDateTime(_later), + DtStart = CalDateTime.FromDateTime(_nowTime), + DtEnd = CalDateTime.FromDateTime(_later), }; private static string SerializeEvent(CalendarEvent e) => new CalendarSerializer().SerializeToString(new Calendar { Events = { e } })!; @@ -108,7 +108,7 @@ public void PeriodTests(Period a, Period b) public static IEnumerable PeriodTestCases() { - yield return new TestCaseData(new Period(new CalDateTime(_nowTime)), new Period(new CalDateTime(_nowTime))) + yield return new TestCaseData(new Period(CalDateTime.FromDateTime(_nowTime)), new Period(CalDateTime.FromDateTime(_nowTime))) .SetName("Two identical CalDateTimes are equal"); } @@ -123,7 +123,7 @@ public void PeriodListTests() new DateTime(2017, 04, 28, 06, 00, 00), new DateTime(2017, 05, 01, 06, 00, 00) } - .Select(dt => new Period(new CalDateTime(dt))).ToList(); + .Select(dt => new Period(CalDateTime.FromDateTime(dt))).ToList(); var a = new PeriodList(); foreach (var period in startTimesA) @@ -141,7 +141,7 @@ public void PeriodListTests() new DateTime(2017, 05, 01, 06, 00, 00), new DateTime(2017, 04, 28, 06, 00, 00) } - .Select(dt => new Period(new CalDateTime(dt))).ToList(); + .Select(dt => new Period(CalDateTime.FromDateTime(dt))).ToList(); var b = new PeriodList(); @@ -168,8 +168,8 @@ public void CalDateTimeTests() var nowLocal = DateTime.Now; var nowUtc = nowLocal.ToUniversalTime(); - var asLocal = new CalDateTime(nowLocal, "America/New_York"); - var asUtc = new CalDateTime(nowUtc, "UTC"); + var asLocal = CalDateTime.FromDateTime(nowLocal, "America/New_York"); + var asUtc = CalDateTime.FromDateTime(nowUtc, "UTC"); Assert.That(asUtc, Is.Not.EqualTo(asLocal)); } diff --git a/Ical.Net.Tests/FreeBusyTest.cs b/Ical.Net.Tests/FreeBusyTest.cs index 27bf2671..eb04a225 100644 --- a/Ical.Net.Tests/FreeBusyTest.cs +++ b/Ical.Net.Tests/FreeBusyTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -38,13 +38,13 @@ public void GetFreeBusyStatusByDateTime() using (Assert.EnterMultipleScope()) { - Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 7, 59, 59).ToTimeZone("America/New_York")), + Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 7, 59, 59, "America/New_York")), Is.EqualTo(FreeBusyStatus.Free)); - Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 0, 0).ToTimeZone("America/New_York")), + Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 0, 0, "America/New_York")), Is.EqualTo(FreeBusyStatus.Busy)); - Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 59, 59).ToTimeZone("America/New_York")), + Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 8, 59, 59, "America/New_York")), Is.EqualTo(FreeBusyStatus.Busy)); - Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 9, 0, 0).ToTimeZone("America/New_York")), + Assert.That(freeBusy.GetFreeBusyStatus(new CalDateTime(2025, 10, 1, 9, 0, 0, "America/New_York")), Is.EqualTo(FreeBusyStatus.Free)); } } @@ -103,7 +103,7 @@ public void Contains_Tests() var dtAfter = start.InUtc().PlusHours(1).PlusSeconds(1).ToInstant(); // Period with duration: effective end time = start + 1 hour (exclusive) - var periodWithDuration = new FreeBusyEntry(new(new(start), Duration.FromHours(1)), FreeBusyStatus.Free); + var periodWithDuration = new FreeBusyEntry(new(start.InUtc().ToCalDateTime(), Duration.FromHours(1)), FreeBusyStatus.Free); using (Assert.EnterMultipleScope()) { Assert.That(periodWithDuration.Contains(null), Is.False, "Contains should return false for null dt."); @@ -263,7 +263,7 @@ public void CreateFiltersOccurrencesByRequestAttendees() { Assert.That(freeBusy.Entries, Has.Count.EqualTo(1)); Assert.That(freeBusy.Entries[0].Status, Is.EqualTo(FreeBusyStatus.Busy)); - Assert.That(freeBusy.Entries[0].StartTime.AsUtc, Is.EqualTo(busyEvent.Start.AsUtc)); + Assert.That(freeBusy.Entries[0].StartTime.ToDateTimeUtc(), Is.EqualTo(busyEvent.Start.ToDateTimeUtc())); } } diff --git a/Ical.Net.Tests/MatchTimeZoneTests.cs b/Ical.Net.Tests/MatchTimeZoneTests.cs index 72c5d558..06675726 100644 --- a/Ical.Net.Tests/MatchTimeZoneTests.cs +++ b/Ical.Net.Tests/MatchTimeZoneTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -82,7 +82,7 @@ public void MatchTimeZone_LocalTimeAustraliaWithTimeZone(string inputUntil, int var evt = calendar.Events.First(); var until = evt.RecurrenceRule!.Until; - var expectedUntil = new CalDateTime(DateTime.ParseExact(inputUntil, "yyyyMMddTHHmmssZ", + var expectedUntil = CalDateTime.FromDateTime(DateTime.ParseExact(inputUntil, "yyyyMMddTHHmmssZ", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal)); diff --git a/Ical.Net.Tests/PeriodTests.cs b/Ical.Net.Tests/PeriodTests.cs index 6138ff33..b10ad589 100644 --- a/Ical.Net.Tests/PeriodTests.cs +++ b/Ical.Net.Tests/PeriodTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -87,15 +87,18 @@ public void CompareTo_ReturnsExpectedValues() var dt = new CalDateTime(2025, 6, 1, 0, 0, 0, "Europe/Vienna") .ToZonedDateTime(); + var end = new CalDateTime(2025, 6, 2, 0, 0, 0, "Europe/Vienna") + .ToZonedDateTime(); + using (Assert.EnterMultipleScope()) { - Assert.That(new EvaluationPeriod(dt).CompareTo(null), + Assert.That(new EvaluationPeriod(dt, end).CompareTo(null), Is.EqualTo(1)); - Assert.That(new EvaluationPeriod(dt).CompareTo(new EvaluationPeriod(dt)), + Assert.That(new EvaluationPeriod(dt, end).CompareTo(new EvaluationPeriod(dt, end)), Is.EqualTo(0)); - Assert.That(new EvaluationPeriod(dt).CompareTo(new EvaluationPeriod(dt.PlusHours(-1))), + Assert.That(new EvaluationPeriod(dt, end).CompareTo(new EvaluationPeriod(dt.PlusHours(-1), end)), Is.EqualTo(1)); - Assert.That(new EvaluationPeriod(dt).CompareTo(new EvaluationPeriod(dt.PlusHours(1))), + Assert.That(new EvaluationPeriod(dt, end).CompareTo(new EvaluationPeriod(dt.PlusHours(1), end)), Is.EqualTo(-1)); } } diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 4f4e2b4b..865f241d 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -86,8 +86,8 @@ public void EventOccurrenceTest( .Select(p => (Period) periodSerializer.Deserialize(new StringReader(p))!) .Select(p => p.Duration is null - ? new Period(p.StartTime.ToTimeZone(tzid), p.EndTime) - : new Period(p.StartTime.ToTimeZone(tzid), p.Duration.Value)) + ? new Period(p.StartTime.ToLocalDateTime().ToCalDateTime(tzid), p.EndTime) + : new Period(p.StartTime.ToLocalDateTime().ToCalDateTime(tzid), p.Duration.Value)) .ToArray(); EventOccurrenceTest(cal, null, null, periods, 0); @@ -691,8 +691,8 @@ public void ByMonthDay1() var iCal = Calendar.Load(IcsFiles.ByMonthDay1)!; EventOccurrenceTest( iCal, - new CalDateTime(1996, 1, 1).ToTimeZone(_tzid), - new CalDateTime(1998, 3, 1).ToTimeZone(_tzid), + new CalDateTime(1996, 1, 1, 0, 0, 0, _tzid), + new CalDateTime(1998, 3, 1, 0, 0, 0, _tzid), new[] { new Period(new CalDateTime(1997, 9, 28, 9, 0, 0, _tzid), Duration.FromHours(1)), @@ -1586,11 +1586,11 @@ public void Secondly_DefinedNumberOfOccurrences_ShouldSucceed() var periods = new List(); for (var dt = start; dt.ToInstant() < end.ToInstant(); dt = dt.PlusSeconds(1)) { - periods.Add(new Period(new CalDateTime(dt), Duration.FromHours(9))); + periods.Add(new Period(CalDateTime.FromZonedDateTime(dt), Duration.FromHours(9))); } - var calStart = new CalDateTime(start); - var calEnd = new CalDateTime(end); // End period is exclusive, not inclusive. + var calStart = CalDateTime.FromZonedDateTime(start); + var calEnd = CalDateTime.FromZonedDateTime(end); // End period is exclusive, not inclusive. EventOccurrenceTest(iCal, calStart, calEnd, periods.ToArray()); } @@ -1607,11 +1607,11 @@ public void Minutely_DefinedNumberOfOccurrences_ShouldSucceed() var periods = new List(); for (var dt = start; dt.ToInstant() < end.ToInstant(); dt = dt.PlusMinutes(1)) { - periods.Add(new Period(new CalDateTime(dt), Duration.FromHours(9))); + periods.Add(new Period(CalDateTime.FromZonedDateTime(dt), Duration.FromHours(9))); } - var calStart = new CalDateTime(start); - var calEnd = new CalDateTime(end); + var calStart = CalDateTime.FromZonedDateTime(start); + var calEnd = CalDateTime.FromZonedDateTime(end); EventOccurrenceTest(iCal, calStart, calEnd, periods.ToArray()); } @@ -1628,11 +1628,11 @@ public void Hourly_DefinedNumberOfOccurrences_ShouldSucceed() var periods = new List(); for (var dt = start; dt.ToInstant() < end.ToInstant(); dt = dt.PlusHours(1)) { - periods.Add(new Period(new CalDateTime(dt), Duration.FromHours(9))); + periods.Add(new Period(CalDateTime.FromZonedDateTime(dt), Duration.FromHours(9))); } - var calStart = new CalDateTime(start); - var calEnd = new CalDateTime(end); + var calStart = CalDateTime.FromZonedDateTime(start); + var calEnd = CalDateTime.FromZonedDateTime(end); EventOccurrenceTest(iCal, calStart, calEnd, periods.ToArray()); } @@ -2021,8 +2021,8 @@ public void Bug2912657() // Weekly with UNTIL value EventOccurrenceTest( iCal, - new CalDateTime(2009, 12, 4).ToTimeZone(localTzid), - new CalDateTime(2009, 12, 10).ToTimeZone(localTzid), + new CalDateTime(2009, 12, 4, 0, 0, 0, localTzid), + new CalDateTime(2009, 12, 10, 0, 0, 0, localTzid), new[] { new Period(new CalDateTime(2009, 12, 4, 2, 00, 00, localTzid), Duration.FromMinutes(30)) @@ -2033,8 +2033,8 @@ public void Bug2912657() // Weekly with COUNT=2 EventOccurrenceTest( iCal, - new CalDateTime(2009, 12, 4).ToTimeZone(localTzid), - new CalDateTime(2009, 12, 12).ToTimeZone(localTzid), + new CalDateTime(2009, 12, 4, 0, 0, 0, localTzid), + new CalDateTime(2009, 12, 12, 0, 0, 0, localTzid), new[] { new Period(new CalDateTime(2009, 12, 4, 2, 00, 00, localTzid), Duration.FromMinutes(30)), @@ -2266,11 +2266,11 @@ public void DurationOfRecurrencesOverDst(string dtStart, string dtEnd, string? d for (var index = 0; index < expectedPeriods.Length; index++) { var p = expectedPeriods[index]; - var newStart = p.StartTime.ToTimeZone(start!.TzId); + var newStart = p.StartTime.ToLocalDateTime().ToCalDateTime(start!.TzId); if (p.EndTime is not null) { - expectedPeriods[index] = new Period(newStart, p.EndTime.ToTimeZone(start!.TzId)); + expectedPeriods[index] = new Period(newStart, p.EndTime.ToLocalDateTime().ToCalDateTime(start!.TzId)); } else { @@ -2300,11 +2300,11 @@ public void BugByWeekNoNotWorking() var start = new CalDateTime(2019, 1, 1); var end = new CalDateTime(2019, 12, 31); - var recurringPeriods = new RecurrenceRule("FREQ=YEARLY;BYDAY=MO;BYWEEKNO=2") + var occurrences = new RecurrenceRule("FREQ=YEARLY;BYDAY=MO;BYWEEKNO=2") .Evaluate(start, start.ToZonedDateTime(_tzid)).TakeWhileBefore(end).ToList(); - Assert.That(recurringPeriods, Has.Count.EqualTo(1)); - Assert.That(recurringPeriods.First().Start, Is.EqualTo(new CalDateTime(2019, 1, 7).ToZonedDateTime(_tzid))); + Assert.That(occurrences, Has.Count.EqualTo(1)); + Assert.That(occurrences.First(), Is.EqualTo(new CalDateTime(2019, 1, 7).ToZonedDateTime(_tzid))); } /// @@ -2316,16 +2316,16 @@ public void BugByMonthWhileFreqIsWeekly() var start = new CalDateTime(2020, 1, 1); var end = new CalDateTime(2020, 12, 31); - var recurringPeriods = new RecurrenceRule("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1") + var occurrences = new RecurrenceRule("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1") .Evaluate(start, start.ToZonedDateTime(_tzid)).TakeWhileBefore(end).ToList(); - Assert.That(recurringPeriods, Has.Count.EqualTo(4)); + Assert.That(occurrences, Has.Count.EqualTo(4)); using (Assert.EnterMultipleScope()) { - Assert.That(recurringPeriods[0].Start, Is.EqualTo(new CalDateTime(2020, 1, 6).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[1].Start, Is.EqualTo(new CalDateTime(2020, 1, 13).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[2].Start, Is.EqualTo(new CalDateTime(2020, 1, 20).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[3].Start, Is.EqualTo(new CalDateTime(2020, 1, 27).ToZonedDateTime(_tzid))); + Assert.That(occurrences[0], Is.EqualTo(new CalDateTime(2020, 1, 6).ToZonedDateTime(_tzid))); + Assert.That(occurrences[1], Is.EqualTo(new CalDateTime(2020, 1, 13).ToZonedDateTime(_tzid))); + Assert.That(occurrences[2], Is.EqualTo(new CalDateTime(2020, 1, 20).ToZonedDateTime(_tzid))); + Assert.That(occurrences[3], Is.EqualTo(new CalDateTime(2020, 1, 27).ToZonedDateTime(_tzid))); } } @@ -2343,7 +2343,7 @@ public void ReccurenceRule_MaxDate_StopsOnCount() }; var occurrences = evt.GetOccurrences(new CalDateTime(2018, 1, 1)) - .TakeWhileBefore(new CalDateTime(DateTime.MaxValue, false)).ToList(); + .TakeWhileBefore(CalDateTime.FromDateTimeDate(DateTime.MaxValue)).ToList(); Assert.That(occurrences, Has.Count.EqualTo(10), "There should be 10 occurrences of this event."); } @@ -2356,16 +2356,16 @@ public void BugByMonthWhileFreqIsMonthly() var start = new CalDateTime(2020, 1, 1); var end = new CalDateTime(2020, 12, 31); - var recurringPeriods = new RecurrenceRule("FREQ=MONTHLY;BYDAY=MO;BYMONTH=1") + var occurrences = new RecurrenceRule("FREQ=MONTHLY;BYDAY=MO;BYMONTH=1") .Evaluate(start, start.ToZonedDateTime(_tzid)).TakeWhileBefore(end).ToList(); - Assert.That(recurringPeriods, Has.Count.EqualTo(4)); + Assert.That(occurrences, Has.Count.EqualTo(4)); using (Assert.EnterMultipleScope()) { - Assert.That(recurringPeriods[0].Start, Is.EqualTo(new CalDateTime(2020, 1, 6).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[1].Start, Is.EqualTo(new CalDateTime(2020, 1, 13).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[2].Start, Is.EqualTo(new CalDateTime(2020, 1, 20).ToZonedDateTime(_tzid))); - Assert.That(recurringPeriods[3].Start, Is.EqualTo(new CalDateTime(2020, 1, 27).ToZonedDateTime(_tzid))); + Assert.That(occurrences[0], Is.EqualTo(new CalDateTime(2020, 1, 6).ToZonedDateTime(_tzid))); + Assert.That(occurrences[1], Is.EqualTo(new CalDateTime(2020, 1, 13).ToZonedDateTime(_tzid))); + Assert.That(occurrences[2], Is.EqualTo(new CalDateTime(2020, 1, 20).ToZonedDateTime(_tzid))); + Assert.That(occurrences[3], Is.EqualTo(new CalDateTime(2020, 1, 27).ToZonedDateTime(_tzid))); } } @@ -2380,12 +2380,12 @@ public void Bug3119920() var start = new CalDateTime(2010, 11, 27, 9, 0, 0); var serializer = new RecurrenceRuleSerializer(); var rp = (RecurrenceRule) serializer.Deserialize(sr)!; - var recurringPeriods = rp.Evaluate(start, start.ToZonedDateTime(_tzid)) - .TakeWhileBefore(rp.Until!).ToList(); - var period = recurringPeriods.ElementAt(recurringPeriods.Count - 1); + var lastOccurrence = rp.Evaluate(start, start.ToZonedDateTime(_tzid)) + .TakeWhileBefore(rp.Until!) + .Last(); - Assert.That(period.Start, Is.EqualTo(new CalDateTime(2025, 11, 24, 9, 0, 0).ToZonedDateTime(_tzid))); + Assert.That(lastOccurrence, Is.EqualTo(new CalDateTime(2025, 11, 24, 9, 0, 0).ToZonedDateTime(_tzid))); } /// @@ -2475,9 +2475,9 @@ public void Issue432_AllDay() { var vEvent = new CalendarEvent { - Start = new CalDateTime(DateTime.Parse("2020-01-11", + Start = CalDateTime.FromDateTime(DateTime.Parse("2020-01-11", CultureInfo.InvariantCulture)), // no time means all day - End = new CalDateTime(DateTime.Parse("2020-01-11T00:00", CultureInfo.InvariantCulture)), + End = CalDateTime.FromDateTime(DateTime.Parse("2020-01-11T00:00", CultureInfo.InvariantCulture)), }; var occurrences = vEvent.GetOccurrences(new CalDateTime(2020, 01, 10, 0, 0, 0)) @@ -2562,13 +2562,13 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) evt.Summary = "Event summary"; // Start at midnight, UTC time - evt.Start = new CalDateTime(DateTime.UtcNow.Date, false); + evt.Start = CalDateTime.FromDateTimeDate(DateTime.UtcNow); // This case (DTSTART of type DATE and FREQ=MINUTELY) is undefined in RFC 5545. // ical.net handles the case by pretending DTSTART has the time set to midnight. evt.RecurrenceRule = new($"FREQ={freq};INTERVAL=10;COUNT=5"); - var occurrences = evt.GetOccurrences(evt.Start.ToZonedDateTime().PlusHours(-24)) + var occurrences = evt.GetOccurrences(evt.Start.ToLocalDateTime().InUtc().PlusHours(-24)) .TakeWhileBefore(evt.Start.Date.PlusDays(100).AtMidnight().InUtc().ToInstant()) .ToList(); @@ -2604,10 +2604,10 @@ public void RecurrenceRule1() Assert.That(occurrences, Has.Count.EqualTo(4)); using (Assert.EnterMultipleScope()) { - Assert.That(occurrences[0].Start, Is.EqualTo(new CalDateTime(2008, 3, 30, 23, 59, 40).ToZonedDateTime(_tzid))); - Assert.That(occurrences[1].Start, Is.EqualTo(new CalDateTime(2008, 3, 30, 23, 59, 50).ToZonedDateTime(_tzid))); - Assert.That(occurrences[2].Start, Is.EqualTo(new CalDateTime(2008, 3, 31, 00, 00, 00).ToZonedDateTime(_tzid))); - Assert.That(occurrences[3].Start, Is.EqualTo(new CalDateTime(2008, 3, 31, 00, 00, 10).ToZonedDateTime(_tzid))); + Assert.That(occurrences[0], Is.EqualTo(new CalDateTime(2008, 3, 30, 23, 59, 40).ToZonedDateTime(_tzid))); + Assert.That(occurrences[1], Is.EqualTo(new CalDateTime(2008, 3, 30, 23, 59, 50).ToZonedDateTime(_tzid))); + Assert.That(occurrences[2], Is.EqualTo(new CalDateTime(2008, 3, 31, 00, 00, 00).ToZonedDateTime(_tzid))); + Assert.That(occurrences[3], Is.EqualTo(new CalDateTime(2008, 3, 31, 00, 00, 10).ToZonedDateTime(_tzid))); } } @@ -2620,9 +2620,9 @@ public void RecurrenceRule2() var us = new CultureInfo("en-US"); - var startDate = new CalDateTime(DateTime.Parse("3/31/2008 12:00:10 AM", us)); - var fromDate = new CalDateTime(DateTime.Parse("4/1/2008 10:08:10 AM", us)); - var toDate = new CalDateTime(DateTime.Parse("4/1/2008 10:43:23 AM", us)); + var startDate = CalDateTime.FromDateTime(DateTime.Parse("3/31/2008 12:00:10 AM", us)); + var fromDate = CalDateTime.FromDateTime(DateTime.Parse("4/1/2008 10:08:10 AM", us)); + var toDate = CalDateTime.FromDateTime(DateTime.Parse("4/1/2008 10:43:23 AM", us)); var occurrences = pattern.Evaluate(startDate, fromDate) .TakeWhileBefore(toDate); @@ -2696,7 +2696,7 @@ public void Test2() var cal = new Calendar(); var evt = cal.Create(); evt.Summary = "Event summary"; - evt.Start = new CalDateTime(DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc)); + evt.Start = CalDateTime.FromDateTime(DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc)); var recur = new RecurrenceRule(); recur.Frequency = FrequencyType.Daily; @@ -2727,23 +2727,23 @@ public void Test4() var evtEnd = new CalDateTime(2007, 1, 1); // Add the exception dates - var periods = rpattern.Evaluate(evtStart, evtStart) + var occurrences = rpattern.Evaluate(evtStart, evtStart) .TakeWhileBefore(evtEnd) .ToList(); - Assert.That(periods, Has.Count.EqualTo(10)); + Assert.That(occurrences, Has.Count.EqualTo(10)); using (Assert.EnterMultipleScope()) { - Assert.That(periods[0].Start.Day, Is.EqualTo(2)); - Assert.That(periods[1].Start.Day, Is.EqualTo(3)); - Assert.That(periods[2].Start.Day, Is.EqualTo(9)); - Assert.That(periods[3].Start.Day, Is.EqualTo(10)); - Assert.That(periods[4].Start.Day, Is.EqualTo(16)); - Assert.That(periods[5].Start.Day, Is.EqualTo(17)); - Assert.That(periods[6].Start.Day, Is.EqualTo(23)); - Assert.That(periods[7].Start.Day, Is.EqualTo(24)); - Assert.That(periods[8].Start.Day, Is.EqualTo(30)); - Assert.That(periods[9].Start.Day, Is.EqualTo(31)); + Assert.That(occurrences[0].Day, Is.EqualTo(2)); + Assert.That(occurrences[1].Day, Is.EqualTo(3)); + Assert.That(occurrences[2].Day, Is.EqualTo(9)); + Assert.That(occurrences[3].Day, Is.EqualTo(10)); + Assert.That(occurrences[4].Day, Is.EqualTo(16)); + Assert.That(occurrences[5].Day, Is.EqualTo(17)); + Assert.That(occurrences[6].Day, Is.EqualTo(23)); + Assert.That(occurrences[7].Day, Is.EqualTo(24)); + Assert.That(occurrences[8].Day, Is.EqualTo(30)); + Assert.That(occurrences[9].Day, Is.EqualTo(31)); } } @@ -2840,9 +2840,9 @@ public void RDateShouldBeUnionedWithRecurrenceSet() var calendar = Calendar.Load(ical)!; var firstEvent = calendar.Events.First(); - var startSearch = new CalDateTime(DateTime.Parse("2015-08-28T07:00:00", CultureInfo.InvariantCulture), _tzid) + var startSearch = CalDateTime.FromDateTime(DateTime.Parse("2015-08-28T07:00:00", CultureInfo.InvariantCulture), _tzid) .ToZonedDateTime(); - var endSearch = new CalDateTime(DateTime.Parse("2016-08-28T07:00:00", CultureInfo.InvariantCulture).AddDays(7), _tzid) + var endSearch = CalDateTime.FromDateTime(DateTime.Parse("2016-08-28T07:00:00", CultureInfo.InvariantCulture).AddDays(7), _tzid) .ToZonedDateTime() .ToInstant(); @@ -2850,13 +2850,13 @@ public void RDateShouldBeUnionedWithRecurrenceSet() .Select(o => o.Period) .ToList(); - var firstExpectedOccurrence = new CalDateTime(DateTime.Parse("2016-08-29T08:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); + var firstExpectedOccurrence = CalDateTime.FromDateTime(DateTime.Parse("2016-08-29T08:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); Assert.That(occurrences.First().Start, Is.EqualTo(firstExpectedOccurrence)); - var firstExpectedRDate = new CalDateTime(DateTime.Parse("2016-08-30T10:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); + var firstExpectedRDate = CalDateTime.FromDateTime(DateTime.Parse("2016-08-30T10:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); Assert.That(occurrences[1].Start.Equals(firstExpectedRDate), Is.True); - var secondExpectedRDate = new CalDateTime(DateTime.Parse("2016-08-31T10:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); + var secondExpectedRDate = CalDateTime.FromDateTime(DateTime.Parse("2016-08-31T10:00:00", CultureInfo.InvariantCulture), _tzid).ToZonedDateTime(); Assert.That(occurrences[2].Start.Equals(secondExpectedRDate), Is.True); } @@ -2888,8 +2888,8 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() var end = start.AddHours(1); var e = new CalendarEvent { - DtStart = new CalDateTime(start, "UTC"), - DtEnd = new CalDateTime(end, "UTC"), + DtStart = CalDateTime.FromDateTime(start, "UTC"), + DtEnd = CalDateTime.FromDateTime(end, "UTC"), RecurrenceRule = rrule, Summary = "This is an event", Uid = "abab717c-1786-4efc-87dd-6859c2b48eb6", @@ -2902,10 +2902,10 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() Assert.That(firstEvent, Is.EqualTo(e)); - var startSearch = new CalDateTime(DateTime.Parse("2016-07-01T00:00:00", CultureInfo.InvariantCulture), "UTC"); - var endSearch = new CalDateTime(DateTime.Parse("2016-08-31T07:00:00", CultureInfo.InvariantCulture), "UTC"); + var startSearch = CalDateTime.FromDateTime(DateTime.Parse("2016-07-01T00:00:00", CultureInfo.InvariantCulture), "UTC"); + var endSearch = CalDateTime.FromDateTime(DateTime.Parse("2016-08-31T07:00:00", CultureInfo.InvariantCulture), "UTC"); - var lastExpected = new CalDateTime(DateTime.Parse("2016-08-31T07:00:00", CultureInfo.InvariantCulture), "UTC").ToZonedDateTime(); + var lastExpected = CalDateTime.FromDateTime(DateTime.Parse("2016-08-31T07:00:00", CultureInfo.InvariantCulture), "UTC").ToZonedDateTime(); var occurrences = firstEvent.GetOccurrences(startSearch.ToZonedDateTime()) .TakeWhileBefore(endSearch.ToZonedDateTime().ToInstant()) .Select(o => o.Period) @@ -2914,7 +2914,7 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() Assert.That(occurrences.Last().Start.Equals(lastExpected), Is.False); //Create 1 second of overlap - endSearch = new CalDateTime(endSearch.Value.AddSeconds(1), "UTC"); + endSearch = CalDateTime.FromDateTime(endSearch.ToDateTimeUnspecified().AddSeconds(1), "UTC"); occurrences = firstEvent.GetOccurrences(startSearch.ToZonedDateTime()).TakeWhileBefore(endSearch.ToZonedDateTime().ToInstant()) .Select(o => o.Period) .ToList(); @@ -2988,16 +2988,16 @@ public void EventWithZonedRecurrenceId_Should_ReplaceOriginalEvent_Occurrence() .Take(10) .ToList(); - var expectedSept1Start = new CalDateTime(DateTime.Parse("2016-09-01T16:30:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); - var expectedSept1End = new CalDateTime(DateTime.Parse("2016-09-01T22:00:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); + var expectedSept1Start = CalDateTime.FromDateTime(DateTime.Parse("2016-09-01T16:30:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); + var expectedSept1End = CalDateTime.FromDateTime(DateTime.Parse("2016-09-01T22:00:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); using (Assert.EnterMultipleScope()) { Assert.That(orderedOccurrences[3].Start, Is.EqualTo(expectedSept1Start)); Assert.That(orderedOccurrences[3].End, Is.EqualTo(expectedSept1End)); } - var expectedSept3Start = new CalDateTime(DateTime.Parse("2016-09-03T07:00:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); - var expectedSept3End = new CalDateTime(DateTime.Parse("2016-09-03T12:30:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); + var expectedSept3Start = CalDateTime.FromDateTime(DateTime.Parse("2016-09-03T07:00:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); + var expectedSept3End = CalDateTime.FromDateTime(DateTime.Parse("2016-09-03T12:30:00", CultureInfo.InvariantCulture), "Europe/Bucharest").ToZonedDateTime(); using (Assert.EnterMultipleScope()) { Assert.That(orderedOccurrences[5].Start, Is.EqualTo(expectedSept3Start)); @@ -3075,23 +3075,26 @@ public void ExDateTimeZone_Tests() { const string tzid = "Europe/Stockholm"; + var now = _now.ToLocalDateTime(); + var later = _later.ToLocalDateTime(); + var e = new CalendarEvent { - DtStart = new CalDateTime(_now.Date, _now.Time, tzid), - DtEnd = new CalDateTime(_later.Date, _later.Time, tzid), + DtStart = new CalDateTime(now, tzid), + DtEnd = new CalDateTime(later, tzid), RecurrenceRule = new(FrequencyType.Daily, 1) { Count = 10 } }; - e.ExceptionDates.Add(new CalDateTime(_now.Date.PlusDays(1), _now.Time, tzid)); + e.ExceptionDates.Add(new CalDateTime(now.PlusDays(1), tzid)); var serialized = SerializationHelpers.SerializeToString(e); const string expected = "TZID=Europe/Stockholm"; Assert.That(Regex.Matches(serialized, expected), Has.Count.EqualTo(3)); - e.ExceptionDates.Add(new CalDateTime(_now.Date.PlusDays(2), _now.Time, tzid)); + e.ExceptionDates.Add(new CalDateTime(now.PlusDays(2), tzid)); serialized = SerializationHelpers.SerializeToString(e); Assert.That(Regex.Matches(serialized, expected), Has.Count.EqualTo(3)); } @@ -3101,8 +3104,8 @@ public void OneDayRange() { var vEvent = new CalendarEvent { - Start = new CalDateTime(DateTime.Parse("2019-06-07 0:00:00", CultureInfo.InvariantCulture)), - End = new CalDateTime(DateTime.Parse("2019-06-08 00:00:00", CultureInfo.InvariantCulture)) + Start = CalDateTime.FromDateTime(DateTime.Parse("2019-06-07 0:00:00", CultureInfo.InvariantCulture)), + End = CalDateTime.FromDateTime(DateTime.Parse("2019-06-08 00:00:00", CultureInfo.InvariantCulture)) }; var tz = DateUtil.GetZone("America/New_York"); @@ -3128,8 +3131,8 @@ public void SpecificMinute() { var vEvent = new CalendarEvent { - Start = new CalDateTime(DateTime.Parse("2009-01-01 09:00:00", CultureInfo.InvariantCulture)), - End = new CalDateTime(DateTime.Parse("2009-01-01 17:00:00", CultureInfo.InvariantCulture)), + Start = CalDateTime.FromDateTime(DateTime.Parse("2009-01-01 09:00:00", CultureInfo.InvariantCulture)), + End = CalDateTime.FromDateTime(DateTime.Parse("2009-01-01 17:00:00", CultureInfo.InvariantCulture)), RecurrenceRule = new(FrequencyType.Daily) }; @@ -3198,9 +3201,9 @@ public void InclusiveRruleUntil() var calendar = Calendar.Load(icalText)!; var firstEvent = calendar.Events.First(); var startSearch = - new CalDateTime(DateTime.Parse("2017-07-01T00:00:00", CultureInfo.InvariantCulture), timeZoneId); + CalDateTime.FromDateTime(DateTime.Parse("2017-07-01T00:00:00", CultureInfo.InvariantCulture), timeZoneId); var endSearch = - new CalDateTime(DateTime.Parse("2018-07-01T00:00:00", CultureInfo.InvariantCulture), timeZoneId); + CalDateTime.FromDateTime(DateTime.Parse("2018-07-01T00:00:00", CultureInfo.InvariantCulture), timeZoneId); var occurrences = firstEvent.GetOccurrences(startSearch).TakeWhileBefore(endSearch).ToList(); Assert.That(occurrences, Has.Count.EqualTo(5)); @@ -3368,7 +3371,7 @@ RecurrenceRule GetRule() // the local date and time of the expected values. var timeZone = DateUtil.GetZone("UTC"); - var startAt = testCase.StartAt?.ToZonedDateTime(timeZone).ToInstant(); + var startAt = testCase.StartAt?.ToZonedOrDefault(timeZone).ToInstant(); IEnumerable GetOccurrences() => evt.GetOccurrences(timeZone, startAt); @@ -3392,10 +3395,10 @@ RecurrenceRule GetRule() var expectedInstances = testCase.Instances?.Skip(instanceOffs).ToList(); if (testCase.StartAt != null) - expectedInstances = expectedInstances?.Where(x => x.Value >= testCase.StartAt.Value).ToList(); + expectedInstances = expectedInstances?.Where(x => x.ToDateTimeUnspecified() >= testCase.StartAt.ToDateTimeUnspecified()).ToList(); var startDates = occurrences.Select(x => x.Start).ToList(); - Assert.That(startDates, Is.EqualTo(expectedInstances?.Select(x => x.ToZonedDateTime(timeZone)).ToList())); + Assert.That(startDates, Is.EqualTo(expectedInstances?.Select(x => x.ToZonedOrDefault(timeZone)).ToList())); } private static IEnumerable GetTestFileBasedRecurrenceTestCasesWithIndividualDtStarts(RecurrenceTestCase testCase) @@ -3885,7 +3888,7 @@ public void GetOccurrences_WithPeriodStart_ShouldConsiderTzCorrectly(string dtSt var firstFewOccurrences = cal.GetOccurrences(tz).Take(3).ToList(); var periodStart = new CalDateTime(periodStartStr, periodStartTzId); - var zonedStart = periodStart.ToZonedDateTime(tz); + var zonedStart = periodStart.ToZonedOrDefault(tz); Assert.That(cal.GetOccurrences(zonedStart).First(), Is.EqualTo(firstFewOccurrences[1])); var nextPeriodStart = periodStart.HasTime ? zonedStart.PlusSeconds(1) : zonedStart.PlusHours(24); @@ -3934,7 +3937,7 @@ public void EventWithRecurrenceId_Should_ReplaceOriginalEvent_Occurrence(string Assert.That(occurrences, Has.Count.EqualTo(4)); Assert.That(overrideOcc, Is.Not.Null); Assert.That(overrideOcc!.Start, - Is.EqualTo(new CalDateTime(dtStart.Year, dtStart.Month, dtStart.Day).ToZonedDateTime(tz))); + Is.EqualTo(new CalDateTime(dtStart.Year, dtStart.Month, dtStart.Day).ToZonedOrDefault(tz))); Assert.That(((CalendarEvent) overrideOcc.Source).Summary, Is.EqualTo("Override Event")); if (overrideOcc.Start.Year == 2028) Assert.That(occurrences.IndexOf(overrideOcc), Is.EqualTo(2)); } @@ -3986,7 +3989,7 @@ public void EventWithRecurrenceId_LatestInOrderOverride_ShouldBeTaken() Assert.That(occurrences, Has.Count.EqualTo(4)); Assert.That(overrideOcc, Is.Not.Null); Assert.That(overrideOcc!.Start, - Is.EqualTo(new CalDateTime(2025, 11, 5).ToZonedDateTime(tz))); + Is.EqualTo(new CalDateTime(2025, 11, 5).ToZonedOrDefault(tz))); // The last override in the calendar is taken Assert.That(((CalendarEvent) overrideOcc.Source).Summary, Is.EqualTo("Override Event 2")); } @@ -4040,7 +4043,7 @@ public void EventWithRecurrenceId_LatestSequence_ShouldBeTaken() Assert.That(occurrences, Has.Count.EqualTo(4)); Assert.That(overrideOcc, Is.Not.Null); Assert.That(overrideOcc!.Start, - Is.EqualTo(new CalDateTime(2025, 11, 4).ToZonedDateTime(tz))); + Is.EqualTo(new CalDateTime(2025, 11, 4).ToZonedOrDefault(tz))); // The override with the highest SEQUENCE is taken, even if it comes earlier in the calendar Assert.That(((CalendarEvent) overrideOcc.Source).Summary, Is.EqualTo("Override Event 2")); } @@ -4050,8 +4053,8 @@ public void EventWithRecurrenceId_LatestSequence_ShouldBeTaken() public void SkippedOccurrenceOnWeeklyRule() // Test moved from former GetOccurrencesTests { const int evaluationsCount = 1000; - var eventStart = new CalDateTime(new DateTime(2016, 1, 1, 10, 0, 0, DateTimeKind.Utc)); - var eventEnd = new CalDateTime(new DateTime(2016, 1, 1, 11, 0, 0, DateTimeKind.Utc)); + var eventStart = CalDateTime.FromDateTime(new DateTime(2016, 1, 1, 10, 0, 0, DateTimeKind.Utc)); + var eventEnd = CalDateTime.FromDateTime(new DateTime(2016, 1, 1, 11, 0, 0, DateTimeKind.Utc)); var vEvent = new CalendarEvent { DtStart = eventStart, @@ -4067,7 +4070,7 @@ public void SkippedOccurrenceOnWeeklyRule() // Test moved from former GetOccurre var tz = DateUtil.GetZone("UTC"); - var intervalStart = eventStart.ToZonedDateTime(tz); + var intervalStart = eventStart.ToZonedOrDefault(tz); var intervalEnd = intervalStart.LocalDateTime .PlusDays(7 * evaluationsCount) .InZoneLeniently(tz) @@ -4094,12 +4097,12 @@ public void SkippedOccurrenceOnWeeklyRule() // Test moved from former GetOccurre [Test] public void GetOccurrences_ShouldReturnCorrectStartAndEndTimes() { - var firstStart = new CalDateTime(DateTime.Parse("2016-01-01", CultureInfo.InvariantCulture)); - var firstEnd = new CalDateTime(DateTime.Parse("2016-01-05", CultureInfo.InvariantCulture)); + var firstStart = CalDateTime.FromDateTime(DateTime.Parse("2016-01-01", CultureInfo.InvariantCulture)); + var firstEnd = CalDateTime.FromDateTime(DateTime.Parse("2016-01-05", CultureInfo.InvariantCulture)); var vEvent = new CalendarEvent { DtStart = firstStart, DtEnd = firstEnd, }; - var secondStart = new CalDateTime(DateTime.Parse("2016-03-01", CultureInfo.InvariantCulture)); - var secondEnd = new CalDateTime(DateTime.Parse("2016-03-05", CultureInfo.InvariantCulture)); + var secondStart = CalDateTime.FromDateTime(DateTime.Parse("2016-03-01", CultureInfo.InvariantCulture)); + var secondEnd = CalDateTime.FromDateTime(DateTime.Parse("2016-03-05", CultureInfo.InvariantCulture)); var vEvent2 = new CalendarEvent { DtStart = secondStart, DtEnd = secondEnd, }; var calendar = new Calendar(); @@ -4108,13 +4111,13 @@ public void GetOccurrences_ShouldReturnCorrectStartAndEndTimes() var tz = DateUtil.GetZone(_tzid); - var searchStart = new CalDateTime(2015, 12, 29).ToZonedDateTime(tz); - var searchEnd = new CalDateTime(2017, 02, 10).ToZonedDateTime(tz).ToInstant(); + var searchStart = new CalDateTime(2015, 12, 29).ToZonedOrDefault(tz); + var searchEnd = new CalDateTime(2017, 02, 10).ToZonedOrDefault(tz).ToInstant(); var occurrences = calendar.GetOccurrences(searchStart).TakeWhileBefore(searchEnd).ToList(); var firstOccurrence = occurrences.First(); - var firstStartCopy = firstStart.ToZonedDateTime(tz); - var firstEndCopy = firstEnd.ToZonedDateTime(tz); + var firstStartCopy = firstStart.ToZonedOrDefault(tz); + var firstEndCopy = firstEnd.ToZonedOrDefault(tz); using (Assert.EnterMultipleScope()) { Assert.That(firstOccurrence.Start, Is.EqualTo(firstStartCopy)); @@ -4122,8 +4125,8 @@ public void GetOccurrences_ShouldReturnCorrectStartAndEndTimes() } var secondOccurrence = occurrences.Last(); - var secondStartCopy = secondStart.ToZonedDateTime(tz); - var secondEndCopy = secondEnd.ToZonedDateTime(tz); + var secondStartCopy = secondStart.ToZonedOrDefault(tz); + var secondEndCopy = secondEnd.ToZonedOrDefault(tz); using (Assert.EnterMultipleScope()) { Assert.That(secondOccurrence.Start, Is.EqualTo(secondStartCopy)); @@ -4190,7 +4193,7 @@ public void GetOccurrencesWithRecurrenceIdShouldEnumerate() var tz = DateUtil.GetZone("W. Europe Standard Time"); var collection = Calendar.Load(ical)!; - var startCheck = new CalDateTime(2016, 11, 11).ToZonedDateTime(tz); + var startCheck = new CalDateTime(2016, 11, 11).ToZonedOrDefault(tz); var occurrences = collection.GetOccurrences(startCheck) .TakeWhileBefore(startCheck.LocalDateTime.PlusMonths(1).InZoneLeniently(tz).ToInstant()).ToList(); @@ -4209,7 +4212,7 @@ public void GetOccurrencesWithRecurrenceIdShouldEnumerate() // Specify end time that is between the original occurrence at 20161128T0001 and the overridden one at 20161128T0030. // The overridden one shouldn't be returned, because it was replaced and the other one is in the future. var occurrences2 = collection.GetOccurrences(startCheck) - .TakeWhileBefore(new CalDateTime("20161128T002000", "W. Europe Standard Time").ToInstant()) + .TakeWhileBefore(tz.AtStrictly(new(2016, 11, 28, 0, 20)).ToInstant()) .ToList(); using (Assert.EnterMultipleScope()) @@ -4249,13 +4252,13 @@ public void GetOccurrencesWithRecurrenceId_DateOnly_ShouldEnumerate() var tz = DateUtil.GetZone(_tzid); var collection = Calendar.Load(ical)!; - var startCheck = new CalDateTime(2023, 10, 1).ToZonedDateTime(tz); + var startCheck = new CalDateTime(2023, 10, 1).ToZonedOrDefault(tz); var occurrences = collection.GetOccurrences(startCheck) .TakeWhileBefore(startCheck.LocalDateTime.PlusMonths(1).InZoneLeniently(tz).ToInstant()) .ToList(); var occurrences2 = collection.GetOccurrences(startCheck) - .TakeWhileBefore(new CalDateTime(2023, 12, 31).ToZonedDateTime(tz).ToInstant()) + .TakeWhileBefore(new CalDateTime(2023, 12, 31).ToZonedOrDefault(tz).ToInstant()) .ToList(); var expectedStartDates = new List @@ -4263,7 +4266,7 @@ public void GetOccurrencesWithRecurrenceId_DateOnly_ShouldEnumerate() new(2023, 10, 1), new(2023, 11, 15), // the replaced occurrence new(2023, 12, 1) - }.Select(x => x.ToZonedDateTime(tz)).ToList(); + }.Select(x => x.ToZonedOrDefault(tz)).ToList(); using (Assert.EnterMultipleScope()) { @@ -4567,7 +4570,7 @@ public void MonthlyUtcEvent_OnForwardDaylightSaving_HourShiftsAfterTransition() cal.Events.Add(evt); var tz = DateUtil.GetZone("America/New_York"); - var result = cal.GetOccurrences(tz, evt.Start.ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4596,7 +4599,7 @@ public void MonthlyByDayByHour_AroundForwardDaylightSaving_CountIsCorrect() cal.Events.Add(evt); var tz = DateUtil.GetZone("America/New_York"); - var result = cal.GetOccurrences(tz, evt.Start.ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4625,7 +4628,7 @@ public void MonthlyByDayByHourByMinute_AroundForwardDaylightSaving_CountIsCorrec cal.Events.Add(evt); var tz = DateUtil.GetZone("America/New_York"); - var result = cal.GetOccurrences(tz, evt.Start.ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4659,7 +4662,7 @@ public void MonthlyByDayByHour_AroundBackwardDaylightSaving_CountIsCorrect() cal.Events.Add(evt); var tz = DateUtil.GetZone("America/New_York"); - var result = cal.GetOccurrences(tz, evt.Start.ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); diff --git a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs index 754c08fc..fe96fe8c 100644 --- a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs +++ b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -279,8 +279,8 @@ public void CheckCalendarZone() calendar.Events.Add(new CalendarEvent { Summary = "Unavailable Before Work", - DtStart = new CalDateTime(blockDate, zone), - DtEnd = new CalDateTime(blockDate.Add(new TimeSpan(8, 0, 0)), zone), + DtStart = CalDateTime.FromDateTime(blockDate, zone), + DtEnd = CalDateTime.FromDateTime(blockDate.Add(new TimeSpan(8, 0, 0)), zone), RecurrenceRule = new(FrequencyType.Weekly, 1) { ByDay = [new WeekDay(DayOfWeek.Monday)] @@ -300,8 +300,8 @@ public void Daylight_Savings_Changes_567() { // GetOccurrences Creates event with invalid time due to Daylight Savings changes #567 - var calStart = new CalDateTime(DateTimeOffset.Parse("2023-01-14T19:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); - var calFinish = new CalDateTime(DateTimeOffset.Parse("2023-03-14T18:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); + var calStart = CalDateTime.FromDateTime(DateTimeOffset.Parse("2023-01-14T19:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); + var calFinish = CalDateTime.FromDateTime(DateTimeOffset.Parse("2023-03-14T18:21:03.700Z", CultureInfo.InvariantCulture).UtcDateTime, "UTC"); var tz = "Pacific Standard Time"; //Adjust the date to today so that the times remain constant @@ -311,8 +311,8 @@ public void Daylight_Savings_Changes_567() var ev = new CalendarEvent { Class = "PUBLIC", - Start = new CalDateTime(localTzStartAdjust, tz), - End = new CalDateTime(localTzFinishAdjust, tz), + Start = CalDateTime.FromDateTime(localTzStartAdjust, tz), + End = CalDateTime.FromDateTime(localTzFinishAdjust, tz), Sequence = 0, RecurrenceRule = new("FREQ=WEEKLY;BYDAY=SU,MO,TU,WE") }; @@ -328,8 +328,8 @@ private static void CheckDates(DateTime startDate, DateTime endDate, CalDateTime { var calendarEvent = new CalendarEvent { - DtStart = new CalDateTime(startDate), - DtEnd = new CalDateTime(endDate), + DtStart = CalDateTime.FromDateTime(startDate), + DtEnd = CalDateTime.FromDateTime(endDate), RecurrenceRule = new(FrequencyType.Weekly, 2) { ByDay = @@ -345,10 +345,10 @@ private static void CheckDates(DateTime startDate, DateTime endDate, CalDateTime } }; - var occurrences = calendarEvent.GetOccurrences(new CalDateTime(startDate)).TakeWhileBefore(new CalDateTime(endDate)); + var occurrences = calendarEvent.GetOccurrences(CalDateTime.FromDateTime(startDate)).TakeWhileBefore(CalDateTime.FromDateTime(endDate)); var occurrencesDates = occurrences.Select(o => o.Start.Date).ToList(); - var sortedExpectedDates = expectedDates.Select(x => x.ToZonedDateTime().Date).ToList(); + var sortedExpectedDates = expectedDates.Select(x => x.Date).ToList(); Assert.That(occurrencesDates, Is.EquivalentTo(sortedExpectedDates)); } @@ -470,7 +470,7 @@ public void Weekly_ByDay_ByMonth() var start = referenceDate.ToZonedDateTime(); var recurringPeriods = rp.Evaluate(referenceDate, start); - var result = recurringPeriods.FirstOrDefault()?.Start; + var result = recurringPeriods.FirstOrDefault(); var expected = new LocalDate(2026, 05, 01).AtStartOfDayInZone(start.Zone); Assert.That(result, Is.EqualTo(expected)); diff --git a/Ical.Net.Tests/RecurrenceWithExDateTests.cs b/Ical.Net.Tests/RecurrenceWithExDateTests.cs index e940be28..8ad1be70 100644 --- a/Ical.Net.Tests/RecurrenceWithExDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithExDateTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -9,6 +9,7 @@ using Ical.Net.DataTypes; using Ical.Net.Serialization; using Ical.Net.Utility; +using NodaTime; using NUnit.Framework; namespace Ical.Net.Tests; @@ -118,7 +119,7 @@ public void ShouldNotOccurOnUtcExceptionDate() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.ToInstant().Equals(o.Start.ToInstant()))), Is.True); + .Any(ex => ex.ToZonedOrDefault(DateTimeZone.Utc).ToInstant().Equals(o.Start.ToInstant()))), Is.True); Assert.That(ics, Does.Contain("EXDATE:20241019T190000Z")); } } @@ -177,7 +178,7 @@ public void MultipleExclusionDatesSameTimeZoneShouldBeExcluded() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.ToInstant().Equals(o.Start.ToInstant()))), Is.True); + .Any(ex => ex.ToZonedOrDefault(DateTimeZone.Utc).ToInstant().Equals(o.Start.ToInstant()))), Is.True); Assert.That(ics, Does.Contain("EXDATE;TZID=Europe/Berlin:20231029T090000,20231105T090000,20231112T090000")); } } @@ -230,7 +231,7 @@ public void MultipleExclusionDatesDifferentZoneShouldBeExcluded() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.ToInstant().Equals(o.Start.ToInstant()))), Is.True); + .Any(ex => ex.ToZonedOrDefault(DateTimeZone.Utc).ToInstant().Equals(o.Start.ToInstant()))), Is.True); } } } diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index aaa55a6b..a1f7af42 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -107,22 +107,6 @@ public static string InspectSerializedSection(string serialized, string sectionN return searchRegion; } - //3 formats - UTC, local time as defined in vTimeZone, and floating, - //at some point it would be great to independently unit test string serialization of an CalDateTime object, into its 3 forms - //http://www.kanzaki.com/docs/ical/dateTime.html - private static string CalDateString(CalDateTime cdt) - { - var returnVar = $"{cdt.Year}{cdt.Month:D2}{cdt.Day:D2}T{cdt.Hour:D2}{cdt.Minute:D2}{cdt.Second:D2}"; - if (cdt.IsUtc) - { - return returnVar + 'Z'; - } - - return string.IsNullOrEmpty(cdt.TzId) - ? returnVar - : $"TZID={cdt.TzId}:{returnVar}"; - } - //This method needs renaming private static Dictionary GetValues(string serialized, string name, string value) { @@ -232,11 +216,17 @@ public void EventPropertiesSerialized() InspectSerializedSection(serializedCalendar, "VEVENT", [ - "CLASS:" + evt.Class, "CREATED:" + CalDateString(evt.Created), "DTSTAMP:" + CalDateString(evt.DtStamp), - "LAST-MODIFIED:" + CalDateString(evt.LastModified), "SEQUENCE:" + evt.Sequence, "UID:" + evt.Uid, + "CLASS:" + evt.Class, + "CREATED:20250325T125335", + "DTSTAMP:20250325T125335", + "LAST-MODIFIED:20250327T135335", + "SEQUENCE:" + evt.Sequence, + "UID:" + evt.Uid, "PRIORITY:" + evt.Priority, - "LOCATION:" + evt.Location, "SUMMARY:" + evt.Summary, "DTSTART:" + CalDateString(evt.DtStart), - "DTEND:" + CalDateString(evt.DtEnd), + "LOCATION:" + evt.Location, + "SUMMARY:" + evt.Summary, + "DTSTART:20250325T125000", + "DTEND:20250325T131000", "TRANSP:" + TransparencyType.Opaque.ToUpperInvariant(), "STATUS:" + EventStatus.Confirmed.ToUpperInvariant() ]); @@ -465,8 +455,8 @@ public void TestRRuleUntilSerialization() const string someTz = "Europe/Volgograd"; var e = new CalendarEvent { - Start = _nowTime.ToTimeZone(someTz), - End = _nowTime.ToLocalDateTime().PlusHours(1).ToCalDateTime().ToTimeZone(someTz), + Start = _nowTime.ToLocalDateTime().ToCalDateTime(someTz), + End = _nowTime.ToLocalDateTime().PlusHours(1).ToCalDateTime(someTz), RecurrenceRule = new(FrequencyType.Daily) { Until = _nowTime.ToLocalDateTime().PlusDays(7).ToCalDateTime(), diff --git a/Ical.Net.Tests/SymmetricSerializationTests.cs b/Ical.Net.Tests/SymmetricSerializationTests.cs index f70509c6..a5413268 100644 --- a/Ical.Net.Tests/SymmetricSerializationTests.cs +++ b/Ical.Net.Tests/SymmetricSerializationTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -28,9 +28,9 @@ public class SymmetricSerializationTests private static string SerializeToString(Calendar c) => GetNewSerializer().SerializeToString(c)!; private static CalendarEvent GetSimpleEvent(bool useDtEnd = true) { - var evt = new CalendarEvent { DtStart = new CalDateTime(_nowTime) }; + var evt = new CalendarEvent { DtStart = CalDateTime.FromDateTime(_nowTime) }; if (useDtEnd) - evt.DtEnd = new CalDateTime(_later); + evt.DtEnd = CalDateTime.FromDateTime(_later); else evt.Duration = Duration.FromTimeSpanExact(_later - _nowTime); diff --git a/Ical.Net.Tests/TestExtensions.cs b/Ical.Net.Tests/TestExtensions.cs index 58472d3b..2bc9c87e 100644 --- a/Ical.Net.Tests/TestExtensions.cs +++ b/Ical.Net.Tests/TestExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -23,16 +23,16 @@ public static IEnumerable GetOccurrences(this CalendarEvent calendar public static IEnumerable TakeWhileBefore(this IEnumerable sequence, CalDateTime periodEnd) => sequence.TakeWhile(p => p.Start.ToInstant() < periodEnd.ToZonedDateTime("America/New_York").ToInstant()); - public static IEnumerable TakeWhileBefore(this IEnumerable sequence, CalDateTime periodEnd) - => sequence.TakeWhile(p => p.Start.ToInstant() < periodEnd.ToZonedDateTime("America/New_York").ToInstant()); + public static IEnumerable TakeWhileBefore(this IEnumerable sequence, CalDateTime periodEnd) + => sequence.TakeWhile(p => p.ToInstant() < periodEnd.ToZonedDateTime("America/New_York").ToInstant()); - public static IEnumerable Evaluate(this RecurrenceRule pattern, CalDateTime referenceDate, CalDateTime periodStart, EvaluationOptions? options = null) + public static IEnumerable Evaluate(this RecurrenceRule pattern, CalDateTime referenceDate, CalDateTime periodStart, EvaluationOptions? options = null) { var zonedStart = periodStart.ToZonedDateTime("America/New_York"); return pattern.Evaluate(referenceDate, zonedStart, options); } - public static IEnumerable Evaluate(this RecurrenceRule pattern, CalDateTime referenceDate, ZonedDateTime periodStart, EvaluationOptions? options = null) + public static IEnumerable Evaluate(this RecurrenceRule pattern, CalDateTime referenceDate, ZonedDateTime periodStart, EvaluationOptions? options = null) { return new RecurrenceRuleEvaluator(pattern, referenceDate, periodStart, options).Evaluate(); } @@ -41,4 +41,10 @@ public static ZonedDateTime InZoneLeniently(this LocalDateTime value, string zon { return value.InZoneLeniently(DateUtil.GetZone(zoneId)); } + + public static ZonedDateTime ToZonedDateTime(this CalDateTime value, string zoneId) + { + var tz = DateUtil.GetZone(zoneId); + return value.ToZonedOrDefault(tz).WithZone(tz); + } } diff --git a/Ical.Net.Tests/TestHelpers/OccurrenceTester.cs b/Ical.Net.Tests/TestHelpers/OccurrenceTester.cs index 2452e4f5..90584b1b 100644 --- a/Ical.Net.Tests/TestHelpers/OccurrenceTester.cs +++ b/Ical.Net.Tests/TestHelpers/OccurrenceTester.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -27,11 +27,11 @@ public static void AssertOccurrences( var evt = cal.Events.Skip(eventIndex).First(); var tz = DateUtil.GetZone(timeZone ?? _tzid); - var start = fromDate?.ToZonedDateTime(tz).ToInstant(); + var start = fromDate?.ToZonedOrDefault(tz).ToInstant(); var occurrences = toDate == null ? evt.GetOccurrences(tz, start).ToList() - : evt.GetOccurrences(tz, start).TakeWhileBefore(toDate.ToZonedDateTime(tz).ToInstant()).ToList(); + : evt.GetOccurrences(tz, start).TakeWhileBefore(toDate.ToZonedOrDefault(tz).ToInstant()).ToList(); using (Assert.EnterMultipleScope()) { @@ -44,7 +44,7 @@ public static void AssertOccurrences( { for (var i = 0; i < expectedPeriods.Length; i++) { - var start2 = expectedPeriods[i].StartTime.ToZonedDateTime(tz); + var start2 = expectedPeriods[i].StartTime.ToZonedOrDefault(tz).WithZone(tz); ZonedDateTime end; @@ -57,7 +57,7 @@ public static void AssertOccurrences( } else if (expectedPeriods[i].EndTime is { } periodEnd) { - end = periodEnd.ToZonedDateTime(tz); + end = periodEnd.ToZonedOrDefault(tz).WithZone(tz); } else { diff --git a/Ical.Net.Tests/VTimeZoneTest.cs b/Ical.Net.Tests/VTimeZoneTest.cs index e7d34bb2..caa445ed 100644 --- a/Ical.Net.Tests/VTimeZoneTest.cs +++ b/Ical.Net.Tests/VTimeZoneTest.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -315,8 +315,8 @@ private static Calendar CreateTestCalendar(string tzId, DateTime? earliestTime = var calEvent = new CalendarEvent { Description = "Test Recurring Event", - Start = new CalDateTime(DateTime.Now, tzId), - End = new CalDateTime(DateTime.Now.AddHours(1), tzId), + Start = CalDateTime.FromDateTime(DateTime.Now, tzId), + End = CalDateTime.FromDateTime(DateTime.Now.AddHours(1), tzId), RecurrenceRule = new(FrequencyType.Daily) }; iCal.Events.Add(calEvent); @@ -324,8 +324,8 @@ private static Calendar CreateTestCalendar(string tzId, DateTime? earliestTime = var calEvent2 = new CalendarEvent { Description = "Test Recurring Event 2", - Start = new CalDateTime(DateTime.Now.AddHours(2), tzId), - End = new CalDateTime(DateTime.Now.AddHours(3), tzId), + Start = CalDateTime.FromDateTime(DateTime.Now.AddHours(2), tzId), + End = CalDateTime.FromDateTime(DateTime.Now.AddHours(3), tzId), RecurrenceRule = new(FrequencyType.Daily) }; iCal.Events.Add(calEvent2); diff --git a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs index 38edc716..cea607a2 100644 --- a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs +++ b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs @@ -186,7 +186,7 @@ public void YearlyByMonthDayUntil() Until = start.ToLocalDateTime() .PlusYears(2) .InZoneLeniently("Europe/Zurich") - .ToInstant() + .WithZone(DateTimeZone.Utc) .ToCalDateTime() }; @@ -344,7 +344,7 @@ public void HourlyUntilExDate() var calendarEvent = new CalendarEvent { DtStart = start, - DtEnd = new(start.ToZonedDateTime().PlusMinutes(15)), + DtEnd = start.ToZonedDateTime().PlusMinutes(15).ToCalDateTime(), RecurrenceRule = recurrence }; // Add the exception date to the series. @@ -440,7 +440,7 @@ public void DailyIntervalCountMoved() Summary = "Short after lunch walk", // Set new start and end time. DtStart = startMoved, - DtEnd = new(startMoved.ToZonedDateTime().PlusMinutes(13)), + DtEnd = startMoved.ToZonedDateTime().PlusMinutes(13).ToCalDateTime(), // Set the original date of the occurrence (2025-07-14 09:00:00). RecurrenceId = new(2025, 07, 14, 09, 00, 00, timeZoneId), // The first change for this RecurrenceId diff --git a/Ical.Net/Calendar.cs b/Ical.Net/Calendar.cs index dae95730..a986935e 100644 --- a/Ical.Net/Calendar.cs +++ b/Ical.Net/Calendar.cs @@ -237,7 +237,7 @@ public virtual IEnumerable GetOccurrences(DateTimeZone timeZone, { return children.OfType() .Where(r => r.RecurrenceId != null) - .Select(r => (Component: r as IUniqueComponent, Uid: (r as IUniqueComponent)?.Uid, RecurrenceId: r.RecurrenceId!.Value)) + .Select(r => (Component: r as IUniqueComponent, Uid: (r as IUniqueComponent)?.Uid, RecurrenceId: r.RecurrenceId!.ToDateTimeUnspecified())) .Where(x => x is { Uid: not null, Component: not null }) // Assure we have only one component per (UID, RECURRENCE-ID) pair .GroupBy(x => (x.Uid, x.RecurrenceId)) @@ -266,14 +266,14 @@ private static bool IsUnmodifiedOccurrence(Occurrence r, Dictionary<(string? Uid // If the occurrence is a modified instance (has RecurrenceId and Uid) // and the source is the last modified instance for this RecurrenceId/Uid, IUniqueComponent { Uid: not null } uc when r.Source.RecurrenceId != null => - recurrenceIdsAndUids.TryGetValue((uc.Uid, r.Source.RecurrenceId.Value), + recurrenceIdsAndUids.TryGetValue((uc.Uid, r.Source.RecurrenceId.ToDateTimeUnspecified()), out var lastComponent) && ReferenceEquals(lastComponent, r.Source), // If not a modified occurrence, keep if: // - It is not a unique component, or // - There is no replacement for this UID/StartTime in recurrenceIdsAndUids IUniqueComponent uc => - !recurrenceIdsAndUids.ContainsKey((uc.Uid, r.DtStart?.Value ?? default)), + !recurrenceIdsAndUids.ContainsKey((uc.Uid, r.DtStart?.ToDateTimeUnspecified() ?? default)), // If not a unique component, always keep _ => true diff --git a/Ical.Net/CalendarComponents/Alarm.cs b/Ical.Net/CalendarComponents/Alarm.cs index a8539d1f..23c49ae1 100644 --- a/Ical.Net/CalendarComponents/Alarm.cs +++ b/Ical.Net/CalendarComponents/Alarm.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -90,7 +90,7 @@ public virtual IList GetOccurrences(IRecurringComponent rc, Dat // Ensure that "FromDate" has already been set if (fromDate == null) { - fromDate = rc.Start?.ToZonedDateTime(timeZone).ToInstant(); + fromDate = rc.Start?.ToZonedOrDefault(timeZone).ToInstant(); } var triggerDuration = Trigger.Duration!.Value.ToPeriod(); @@ -112,7 +112,7 @@ public virtual IList GetOccurrences(IRecurringComponent rc, Dat } else { - var dt = Trigger?.DateTime?.ToZonedDateTime(); + var dt = Trigger?.DateTime?.ToZonedOrDefault(timeZone); if (dt != null) { occurrences.Add(new AlarmOccurrence(this, dt.Value, rc)); diff --git a/Ical.Net/CalendarComponents/FreeBusy.cs b/Ical.Net/CalendarComponents/FreeBusy.cs index 7f2eadab..a365a457 100644 --- a/Ical.Net/CalendarComponents/FreeBusy.cs +++ b/Ical.Net/CalendarComponents/FreeBusy.cs @@ -1,11 +1,10 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using Ical.Net.DataTypes; using Ical.Net.Evaluation; @@ -24,8 +23,16 @@ public class FreeBusy : UniqueComponent, IMergeable return null; } - var occurrences = occ.GetOccurrences(timeZone, freeBusyRequest.Start?.ToZonedDateTime(timeZone).ToInstant(), options) - .TakeWhile(p => (freeBusyRequest.End == null) || (p.Start.ToInstant() < freeBusyRequest.End.ToInstant())); + var startInstant = freeBusyRequest.Start? + .ToZonedOrDefault(timeZone) + .ToInstant(); + + var endInstant = freeBusyRequest.End? + .ToZonedOrDefault(timeZone) + .ToInstant(); + + var occurrences = occ.GetOccurrences(timeZone, startInstant, options) + .TakeWhile(p => endInstant == null || p.Start.ToInstant() < endInstant); var attendeeContacts = BuildAttendeeContacts(freeBusyRequest); var isFilteredByAttendees = attendeeContacts.Count > 0; @@ -192,7 +199,7 @@ public virtual FreeBusyStatus GetFreeBusyStatus(DataTypes.Period? period) /// public virtual FreeBusyStatus GetFreeBusyStatus(CalDateTime? dt) { - return GetFreeBusyStatus(dt?.ToInstant()); + return GetFreeBusyStatus(dt?.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); } public virtual FreeBusyStatus GetFreeBusyStatus(Instant? dt) diff --git a/Ical.Net/CalendarComponents/VTimeZone.cs b/Ical.Net/CalendarComponents/VTimeZone.cs index d5eb7672..06b02325 100644 --- a/Ical.Net/CalendarComponents/VTimeZone.cs +++ b/Ical.Net/CalendarComponents/VTimeZone.cs @@ -191,7 +191,7 @@ private static VTimeZoneInfo CreateTimeZoneInfo(List matchedInterv timeZoneInfo.TimeZoneName = oldestInterval.Name; var start = oldestInterval.IsoLocalStart.ToDateTimeUnspecified() + delta; - timeZoneInfo.Start = new CalDateTime(start, true); + timeZoneInfo.Start = CalDateTime.FromDateTime(start); if (isRRule) { diff --git a/Ical.Net/CollectionExtensions.cs b/Ical.Net/CollectionExtensions.cs index 51aa2d7d..84ae2347 100644 --- a/Ical.Net/CollectionExtensions.cs +++ b/Ical.Net/CollectionExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -34,7 +34,7 @@ public static class CollectionExtensions /// [Obsolete("Use NodaTime.Instant to specify period end.")] public static IEnumerable TakeWhileBefore(this IEnumerable sequence, CalDateTime periodEnd) - => sequence.TakeWhile(p => p.Start.ToInstant() < periodEnd.ToInstant()); + => sequence.TakeWhile(p => p.Start.ToInstant() < periodEnd.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); public static IEnumerable TakeWhileBefore(this IEnumerable sequence, Instant periodEnd) => sequence.TakeWhile(p => p.Start.ToInstant() < periodEnd); diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index d76e0d76..a72364c4 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -16,47 +16,42 @@ namespace Ical.Net.DataTypes; /// -/// The iCalendar equivalent of the .NET class. -/// -/// In addition to the features of the class, the -/// class handles timezones, floating date/times and integrates seamlessly into the iCalendar framework. +/// Represents a DATE, DATE-TIME, or DATE-TIME with a time zone. /// -/// Any values are always rounded to the nearest second. -/// This is because RFC 5545, Section 3.3.5, does not allow for fractional seconds. -/// +/// values are rounded to the nearest second. +/// Time zone offset is not stored. +/// +/// See RFC 5545, Section 3.3.4 and 3.3.5. /// public sealed class CalDateTime : IFormattable, IEquatable { - // The date part that is used to return the Value property. private readonly LocalDate _localDate; - // The time part that is used to return the Value property. private readonly LocalTime? _localTime; - private readonly string? _tzId; /// - /// The timezone ID for Universal Coordinated Time (UTC). + /// The time zone ID for Universal Coordinated Time (UTC). /// public const string UtcTzId = "UTC"; /// - /// Creates a new instance of the class - /// with the current date/time and sets the to . + /// Creates a + /// with the current local date/time and no time zone. /// - public static CalDateTime Now => new CalDateTime(DateTime.Now, null, true); + public static CalDateTime Now => FromDateTime(DateTime.Now); /// - /// Creates a new instance of the class - /// with the current date and sets the to . + /// Creates a + /// with the current local date and no time or time zone. /// - public static CalDateTime Today => new CalDateTime(DateTime.Today, null, false); + public static CalDateTime Today => FromDateTimeDate(DateTime.Now); /// - /// Creates a new instance of the class - /// with the current date/time in the Coordinated Universal Time (UTC) timezone. + /// Creates a + /// with the current date/time in the UTC time zone. /// - public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow, UtcTzId, true); + public static CalDateTime UtcNow => FromDateTime(DateTime.UtcNow); /// /// This constructor is required for the SerializerFactory to work. @@ -68,115 +63,46 @@ private 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 . - /// - /// The will be set to "UTC" if the - /// has kind and is . - /// It will be set to if the kind is kind - /// and will throw otherwise. - /// - /// 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 . - /// - /// If the specified value's kind is - public CalDateTime(DateTime value, bool hasTime = true) : this( - value, - value.Kind switch - { - DateTimeKind.Utc => UtcTzId, - DateTimeKind.Unspecified => null, - _ => throw new ArgumentException($"An instance of {nameof(CalDateTime)} can only be initialized from a {nameof(DateTime)} of kind {nameof(DateTimeKind.Utc)} or {nameof(DateTimeKind.Unspecified)}.") - }, - hasTime) - { } - - /// - /// 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 . + /// Creates a representing a DATE-TIME value + /// with an optional time zone. /// - /// 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 . - /// - public CalDateTime(DateTime value, string? tzId, bool hasTime = true) : this( - LocalDate.FromDateTime(value), - hasTime ? LocalDateTime.FromDateTime(value).TimeOfDay : null, - tzId) - { } - - /// - /// 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. - /// - /// 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(int year, int month, int day, int hour, int minute, int second, string? tzId = null) //NOSONAR - must keep this signature + /// The time zone ID. + public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null) : this(new LocalDate(year, month, day), new LocalTime(hour, minute, second), tzId) { } /// - /// 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. + /// Creates a representing a DATE value. /// /// /// /// public CalDateTime(int year, int month, int day) - : this(new LocalDate(year, month, day), null, null) - { } - - public CalDateTime(LocalDate value, string? tzId = null) - : this(value, null, tzId) - { } - - public CalDateTime(LocalDateTime value, string? tzId = null) - : this(value.Date, value.TimeOfDay, tzId) + : this(new LocalDate(year, month, day)) { } /// - /// Note that this drops the time zone offset, - /// so the value is not always exactly the same - /// when converting back to ZonedDateTime. + /// Creates a representing a DATE-TIME value + /// with an optional time zone. /// - /// - internal CalDateTime(ZonedDateTime value) - : this(value.LocalDateTime, value.Zone.Id) - { } - - public CalDateTime(Instant instant) : this(instant.InUtc()) + /// The local date and time. + /// The time zone ID. + public CalDateTime(LocalDateTime value, string? tzId = null) + : this(value.Date, value.TimeOfDay, tzId) { } /// - /// 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. + /// Creates a representing a DATE value. /// - /// 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(LocalDate date, LocalTime? time, string? tzId = null) + /// The local date. + /// Year must be a positive number. + public CalDateTime(LocalDate date) { // NodaTime supports year values <1 (BCE). Make sure these // years are considered invalid. @@ -186,30 +112,36 @@ public CalDateTime(LocalDate date, LocalTime? time, string? tzId = null) } _localDate = date; - _localTime = TruncateTimeToSeconds(time); + } - _tzId = tzId switch - { - _ when !time.HasValue => null, - _ => tzId // can also be UtcTzId - }; + /// + /// Creates a representing a DATE-TIME value + /// with an optional time zone. + /// + /// The local date. + /// The local time. + /// The time zone ID. + private CalDateTime(LocalDate date, LocalTime time, string? tzId = null) + : this(date) + { + // RFC 5545, Section 3.3.5 does not allow for fractional seconds. + _localTime = TimeAdjusters.TruncateToSecond(time); + + _tzId = tzId; } /// - /// Creates a new instance of the class by parsing + /// Creates a by parsing /// using the . /// /// 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. + /// it cannot have a time zone, 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. + /// If the parsed string represents an RFC 5545, DATE-TIME value, the will be used. /// + /// The time zone ID. public CalDateTime(string value, string? tzId = null) { var serializer = new DateTimeSerializer(); @@ -228,13 +160,6 @@ public CalDateTime(string value, string? tzId = null) } } -#if NET6_0_OR_GREATER - public CalDateTime(DateOnly date) : this(date, null, null) { } - - public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null) - : this(date.ToLocalDate(), time?.ToLocalTime(), tzId) { } -#endif - public bool Equals(CalDateTime? other) => this == other; /// @@ -242,7 +167,10 @@ public override bool Equals(object? obj) => obj is CalDateTime other && this == other; /// - public override int GetHashCode() => HashCode.Combine(_localDate, _localTime, _tzId); + public override int GetHashCode() => HashCode.Combine( + _localDate, + _localTime, + _tzId == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(_tzId)); public static bool operator ==(CalDateTime? left, CalDateTime? right) { @@ -267,56 +195,36 @@ public override bool Equals(object? obj) } /// - /// Converts the date/time to UTC (Coordinated Universal Time) - /// If == - /// it means that the value is considered as local time for every timezone: - /// The returned value is unchanged, but with . - /// - /// - public DateTime AsUtc => ToInstant().ToDateTimeUtc(); - - /// - /// 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. + /// Returns if the date/time value is floating. /// - /// Use along with and - /// to control how this date/time is handled. - ///
- public DateTime Value => ToLocalDateTime().ToDateTimeUnspecified(); - - /// - /// Returns , if the date/time value is floating. - /// - /// A floating date/time value does not include a timezone identifier or UTC offset, + /// A floating date/time value does not include a time zone ID 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 + /// A floating date/time value is useful when the exact time zone is not + /// known or when the event should be interpreted in the local time zone of /// the user or system processing the calendar data. /// public bool IsFloating => _tzId is null; /// - /// Gets/sets whether the Value of this date/time represents - /// a universal time. + /// Returns if the time zone is UTC. /// public bool IsUtc => string.Equals(_tzId, UtcTzId, StringComparison.OrdinalIgnoreCase); /// - /// if the underlying has a 'time' part (hour, minute, second). + /// Returns if is not null. /// public bool HasTime => _localTime.HasValue; /// - /// Gets the timezone ID of this instance. - /// It can be for Coordinated Universal Time, - /// or for a floating date/time, or a value for a specific timezone. + /// Gets the time zone ID. + /// A value indicates a floating date/time. /// public string? TzId => _tzId; /// - /// Gets the timezone name this time is in, if it references a timezone. + /// Gets the time zone ID. + /// A value indicates a floating date/time. /// /// This is an alias for public string? TimeZoneName => TzId; @@ -336,21 +244,6 @@ public override bool Equals(object? obj) ///
public int Day => _localDate.Day; - /// - /// Gets the hour. - /// - public int Hour => _localTime?.Hour ?? 0; - - /// - /// Gets the minute. - /// - public int Minute => _localTime?.Minute ?? 0; - - /// - /// Gets the second. - /// - public int Second => _localTime?.Second ?? 0; - /// /// Gets the DayOfWeek. /// @@ -374,7 +267,13 @@ public override bool Equals(object? obj) #if NET6_0_OR_GREATER /// - /// Gets the date.. + /// Converts a to a representing a DATE value. + /// + /// The local date. + public static CalDateTime FromDateOnly(DateOnly date) => new(date.ToLocalDate()); + + /// + /// Gets the date. /// public DateOnly ToDateOnly() => _localDate.ToDateOnly(); @@ -385,83 +284,109 @@ public override bool Equals(object? obj) #endif /// - /// Any values are truncated to seconds, because - /// RFC 5545, Section 3.3.5 does not allow for fractional seconds. + /// Converts a of any kind to a + /// representing a DATE-TIME value with an optional time zone. + /// + /// Time zone will be UTC if is and + /// kind is . /// - private static LocalTime? TruncateTimeToSeconds(LocalTime? time) + /// The value to copy the local date and time from. + /// The time zone ID. + public static CalDateTime FromDateTime(DateTime value, string? tzId = null) { - if (time is null) + tzId ??= value.Kind switch { - return null; - } + DateTimeKind.Utc => UtcTzId, + _ => null + }; - return TimeAdjusters.TruncateToSecond(time.Value); + return new CalDateTime(LocalDateTime.FromDateTime(value), tzId); } + /// + /// Converts a of any kind to a representing a DATE value. + /// + /// The value to copy the local date from. + /// A new with same date as the specified . + public static CalDateTime FromDateTimeDate(DateTime value) => new(LocalDate.FromDateTime(value)); + + /// + /// Converts this value to with . + /// + /// DATE values will default to . + /// + /// Values with a time zone will be converted to the UTC time zone. + /// Values without a time zone () will have the same local time. + /// + /// A value representing this value converted to the UTC time zone. + public DateTime ToDateTimeUtc() => ToZonedOrDefault(DateTimeZone.Utc).ToDateTimeUtc(); + + /// + /// Returns the local date and time as a with . + /// + /// DATE values will default to . + /// + /// A value with the same date and time (or midnight) as this value. + public DateTime ToDateTimeUnspecified() => ToLocalDateTime().ToDateTimeUnspecified(); + + /// + /// Constructs a from this value's date and time. + /// + /// DATE values will default to . + /// + /// A local date time with the same date and time (or midnight) as this value. public LocalDateTime ToLocalDateTime() => _localDate.At(_localTime ?? LocalTime.Midnight); - public Instant ToInstant() => ToZonedDateTime().ToInstant(); + /// + /// Converts a to a representing a DATE-TIME value + /// with a time zone. + /// + /// The time zone offset from the is ignored, + /// so converting back to may produce a + /// different value. + /// + /// The value to copy the date, time, and time zone ID from. + public static CalDateTime FromZonedDateTime(ZonedDateTime value) => new(value.LocalDateTime, value.Zone.Id); + /// + /// Converts this value to . + /// + /// Values without a time zone will throw an . + /// Use to handle floating values. + /// + /// Ambiguous local time will return the earlier time and skipped local time will be shifted foward. + /// + /// A zoned date time representing this value as close as possible. + /// Time zone is null public ZonedDateTime ToZonedDateTime() { if (_tzId is null) { - return ToLocalDateTime().InUtc(); - } - else - { - return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); - } - } - - public ZonedDateTime ToZonedDateTime(DateTimeZone timeZone) - { - if (_tzId is null) - { - return ToLocalDateTime().InZoneLeniently(timeZone); - } - else - { - return DateUtil.GetZone(_tzId) - .AtLeniently(ToLocalDateTime()) - .WithZone(timeZone); + throw new InvalidOperationException("CalDateTime must have a time zone to convert to ZonedDateTime"); } - } - public ZonedDateTime ToZonedDateTime(string zoneId) - { - return ToZonedDateTime(DateUtil.GetZone(zoneId)); - } - - public ZonedDateTime AsZonedOrDefault(DateTimeZone timeZone) - { - if (_tzId is null) - { - return ToLocalDateTime().InZoneLeniently(timeZone); - } - else - { - return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); - } + return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } /// - /// Converts the to a date/time - /// within the specified timezone. + /// Converts this value to . + /// + /// DATE values will default to . /// - /// If == - /// it means that the is considered as local time for every timezone: - /// The returned is unchanged and the is set as . + /// Values without a time zone will be treated as being in the specified time zone. + /// Ambiguous local time will return the earlier time and skipped local time will be shifted foward. /// - public CalDateTime ToTimeZone(string? otherTzId) + /// The time zone to use if this value has no time zone. + /// A zoned date time representing this value in its own time zone or the specified time zone. + public ZonedDateTime ToZonedOrDefault(DateTimeZone defaultZone) { - if (otherTzId is null) + if (_tzId is null) { - return new(_localDate, _localTime); + return ToLocalDateTime().InZoneLeniently(defaultZone); } - return new(ToZonedDateTime(otherTzId)); + return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } /// diff --git a/Ical.Net/DataTypes/FreeBusyEntry.cs b/Ical.Net/DataTypes/FreeBusyEntry.cs index 81a1dd45..7fb158c4 100644 --- a/Ical.Net/DataTypes/FreeBusyEntry.cs +++ b/Ical.Net/DataTypes/FreeBusyEntry.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -48,7 +48,7 @@ public override void CopyFrom(ICopyable obj) /// public bool Contains(CalDateTime? dt) { - return dt is not null && Contains(dt.ToInstant()); + return dt is not null && Contains(dt.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); } /// @@ -58,7 +58,7 @@ public bool Contains(CalDateTime? dt) /// public bool Contains(Instant value) { - var startInstant = StartTime.ToInstant(); + var startInstant = StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); if (startInstant > value) { return false; @@ -67,7 +67,7 @@ public bool Contains(Instant value) Instant end; if (EndTime is { } endTime) { - end = endTime.ToInstant(); + end = endTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); } else if (Duration is { } duration) { @@ -102,11 +102,11 @@ public bool CollidesWith(Period? period) throw new ArgumentException("Period start time must be in UTC"); } - var start = StartTime.ToInstant(); + var start = StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); Instant end; if (EndTime is { } endTime) { - end = endTime.ToInstant(); + end = endTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); } else if (Duration is { } duration) { @@ -120,7 +120,7 @@ public bool CollidesWith(Period? period) return false; } - var otherStart = period.StartTime.ToInstant(); + var otherStart = period.StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); Instant otherEnd; if (period.EndTime is { } periodEndTime) { @@ -129,7 +129,7 @@ public bool CollidesWith(Period? period) throw new ArgumentException("Period end time must be in UTC"); } - otherEnd = periodEndTime.ToInstant(); + otherEnd = periodEndTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); } else if (period.Duration is { } periodDuration) { diff --git a/Ical.Net/DataTypes/Occurrence.cs b/Ical.Net/DataTypes/Occurrence.cs index 4ccf36b1..2089198e 100644 --- a/Ical.Net/DataTypes/Occurrence.cs +++ b/Ical.Net/DataTypes/Occurrence.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -53,7 +53,7 @@ public CalDateTime? DtStart return dtStart.HasTime ? new(start.LocalDateTime, tzid) - : new(start.Date, tzid); + : new(start.Date); } } @@ -69,7 +69,7 @@ public bool Contains(CalDateTime? value) // Use floating time from the occurrence's time zone var valueInstant = value - .ToZonedDateTime(Start.Zone) + .ToZonedOrDefault(Start.Zone) .ToInstant(); return Start.ToInstant() <= valueInstant diff --git a/Ical.Net/DataTypes/Period.cs b/Ical.Net/DataTypes/Period.cs index 1c6913b9..0c894259 100644 --- a/Ical.Net/DataTypes/Period.cs +++ b/Ical.Net/DataTypes/Period.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -58,7 +58,7 @@ internal static Period Create(CalDateTime start, CalDateTime? end = null, Durati internal Period() { } internal Period(Instant start, Instant end) - : this(new CalDateTime(start), new CalDateTime(end)) { } + : this(start.InUtc().ToCalDateTime(), end.InUtc().ToCalDateTime()) { } /// /// Creates a new instance starting at the given time @@ -92,7 +92,7 @@ public Period(CalDateTime start, CalDateTime? end = null) else { // Both are zoned, so compare instants - isEndBeforeStart = end.ToInstant() < start.ToInstant(); + isEndBeforeStart = end.ToZonedOrDefault(DateTimeZone.Utc).ToInstant() < start.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); } if (isEndBeforeStart) diff --git a/Ical.Net/DataTypes/PeriodListWrapperBase.cs b/Ical.Net/DataTypes/PeriodListWrapperBase.cs index f7f2c1f9..33c4c5f9 100644 --- a/Ical.Net/DataTypes/PeriodListWrapperBase.cs +++ b/Ical.Net/DataTypes/PeriodListWrapperBase.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -105,4 +105,6 @@ private protected PeriodList GetOrCreatePeriodList(Period period) /// internal IEnumerable GetAllPeriodsByKind(params PeriodKind[] periodKinds) => ListOfPeriodList.SelectMany(pl => pl.Where(p => periodKinds.Contains(p.PeriodKind))).Distinct(); + + internal bool IsEmpty() => ListOfPeriodList.All(static pl => pl.Count == 0); } diff --git a/Ical.Net/DataTypes/RecurrenceIdentifier.cs b/Ical.Net/DataTypes/RecurrenceIdentifier.cs index 7eb9338d..f802ef24 100644 --- a/Ical.Net/DataTypes/RecurrenceIdentifier.cs +++ b/Ical.Net/DataTypes/RecurrenceIdentifier.cs @@ -1,9 +1,10 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // using System; +using NodaTime; namespace Ical.Net.DataTypes; @@ -52,7 +53,7 @@ public RecurrenceIdentifier(CalDateTime start, RecurrenceRange? range = null) ///
/// /// The comparison is performed first by the property. If the values are equal, the property is used as a tiebreaker. + /// cref="StartTime"/> values are equal, the property is used as a tiebreaker. /// /// /// The to compare with the current instance. @@ -65,7 +66,9 @@ public int CompareTo(RecurrenceIdentifier? other) return 1; } - var startComparison = StartTime.ToInstant().CompareTo(other.StartTime.ToInstant()); + var startComparison = StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant() + .CompareTo(other.StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); + if (startComparison != 0) { return startComparison; diff --git a/Ical.Net/DataTypes/Trigger.cs b/Ical.Net/DataTypes/Trigger.cs index d20cdbf0..853a1b8c 100644 --- a/Ical.Net/DataTypes/Trigger.cs +++ b/Ical.Net/DataTypes/Trigger.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -35,7 +35,7 @@ public virtual CalDateTime? DateTime // Ensure date/time has a time part if (!_mDateTime.HasTime) { - _mDateTime = new CalDateTime(_mDateTime.Date, new NodaTime.LocalTime(), _mDateTime.TzId); + _mDateTime = new CalDateTime(_mDateTime.Date.AtMidnight(), _mDateTime.TzId); } } } diff --git a/Ical.Net/Evaluation/EvaluationPeriod.cs b/Ical.Net/Evaluation/EvaluationPeriod.cs index 4afb8550..42791428 100644 --- a/Ical.Net/Evaluation/EvaluationPeriod.cs +++ b/Ical.Net/Evaluation/EvaluationPeriod.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -11,21 +11,18 @@ namespace Ical.Net.Evaluation; public sealed class EvaluationPeriod : IComparable, IEquatable { public ZonedDateTime Start { get; private set; } - public ZonedDateTime? End { get; private set; } + public ZonedDateTime End { get; private set; } - public EvaluationPeriod(ZonedDateTime start, ZonedDateTime? end = null) + public EvaluationPeriod(ZonedDateTime start, ZonedDateTime end) { - if (end != null) + if (start.Zone.Id != end.Zone.Id) { - if (start.Zone.Id != end.Value.Zone.Id) - { - throw new ArgumentException("End time zone must equal start time zone", nameof(end)); - } + throw new ArgumentException("End time zone must equal start time zone", nameof(end)); + } - if (end.Value.ToInstant() < start.ToInstant()) - { - throw new ArgumentException("End time must be greater or equal to start time", nameof(end)); - } + if (end.ToInstant() < start.ToInstant()) + { + throw new ArgumentException("End time must be greater or equal to start time", nameof(end)); } Start = start; @@ -34,7 +31,7 @@ public EvaluationPeriod(ZonedDateTime start, ZonedDateTime? end = null) public EvaluationPeriod WithZone(DateTimeZone zone) { - return new(Start.WithZone(zone), End?.WithZone(zone)); + return new(Start.WithZone(zone), End.WithZone(zone)); } public bool Equals(EvaluationPeriod? other) diff --git a/Ical.Net/Evaluation/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index bddf5340..0a02a5c4 100644 --- a/Ical.Net/Evaluation/EventEvaluator.cs +++ b/Ical.Net/Evaluation/EventEvaluator.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -24,9 +24,9 @@ public EventEvaluator(CalendarEvent evt) : base(evt) { } protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTimeZone referenceTimeZone) { - var start = rdate.StartTime.AsZonedOrDefault(referenceTimeZone); + var start = rdate.StartTime.ToZonedOrDefault(referenceTimeZone); - ZonedDateTime? end = null; + ZonedDateTime end; if (rdate.Duration is { } duration) { if (!rdate.StartTime.HasTime && duration.HasTime) @@ -41,7 +41,7 @@ protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTi } else if (rdate.EndTime is { } dtEnd) { - var exactDuration = dtEnd.ToInstant() - rdate.StartTime.ToInstant(); + var exactDuration = dtEnd.ToZonedOrDefault(referenceTimeZone).ToInstant() - start.ToInstant(); if (exactDuration < Duration.Zero) { @@ -85,32 +85,36 @@ protected override ZonedDateTime GetEnd(ZonedDateTime start) if (CalendarEvent.DtEnd is { } dtEnd) { - // The spec says DtEnd MUST be the same type as DtStart. - // Some cases can be reasonably handled though. - - // Assume a floating end is in the time zone of the event. - if (dtEnd.IsFloating && !dtStart.IsFloating) - { - dtEnd = dtEnd.ToTimeZone(dtStart.TimeZoneName); - } - // The spec says specifying DTEND results in exact time, // but tests say that all day events should be treated // as a nominal duration. if (!dtStart.HasTime && !dtEnd.HasTime) { // Calculate nominal duration between dates - var nominalDuration = dtEnd.ToTimeZone(dtStart.TimeZoneName) - .ToZonedDateTime() - .Date - .Minus(dtStart.ToZonedDateTime().Date); + var nominalDuration = dtEnd.Date.Minus(dtStart.Date); - return start.LocalDateTime + var end = start.LocalDateTime .Plus(nominalDuration) .InZoneRelativeTo(start); + + if (end.LocalDateTime < start.LocalDateTime) + { + throw new InvalidOperationException("DtEnd is before DtStart"); + } + + return end; + } + + // The spec says DtEnd MUST be the same type as DtStart. + // Some cases can be reasonably handled though. + + // Assume a floating end is in the time zone of the event. + if (dtStart.TzId != null && dtEnd.TzId == null && dtEnd.Time != null) + { + dtEnd = new(dtEnd.ToLocalDateTime(), dtStart.TzId); } - var exactDuration = dtEnd.ToInstant() - dtStart.ToInstant(); + var exactDuration = dtEnd.ToZonedOrDefault(start.Zone).ToInstant() - dtStart.ToZonedOrDefault(start.Zone).ToInstant(); if (exactDuration < Duration.Zero) { diff --git a/Ical.Net/Evaluation/RecurrenceRuleEvaluator.cs b/Ical.Net/Evaluation/RecurrenceRuleEvaluator.cs index d0678a82..a94c332f 100644 --- a/Ical.Net/Evaluation/RecurrenceRuleEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrenceRuleEvaluator.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -48,14 +48,14 @@ public RecurrenceRuleEvaluator( // Copy rule values _frequency = rule.Frequency; - _until = rule.Until?.ToZonedDateTime(timeZone).ToInstant(); + _until = rule.Until?.ToZonedOrDefault(timeZone).ToInstant(); _count = rule.Count; _interval = Math.Max(1, rule.Interval); _rule = ByRuleValues.From(rule); _firstDayOfWeek = rule.FirstDayOfWeek.ToIsoDayOfWeek(); _weekYearRule = WeekYearRules.ForMinDaysInFirstWeek(4, _firstDayOfWeek); - _zonedReferenceDate = referenceDate.AsZonedOrDefault(timeZone); + _zonedReferenceDate = referenceDate.ToZonedOrDefault(timeZone); _referenceWeekNo = _weekYearRule.GetWeekOfWeekYear(_zonedReferenceDate.Date); } @@ -66,7 +66,7 @@ public RecurrenceRuleEvaluator( EvaluationOptions? options) : this(rule, referenceDate, periodStart.Zone, periodStart.ToInstant(), options) { } - public IEnumerable Evaluate() + public IEnumerable Evaluate() { var evaluatedValuesCount = 0; @@ -96,7 +96,7 @@ public IEnumerable Evaluate() continue; } - yield return new(value); + yield return value; // Update threshold to prevent duplicate values // caused by daylight saving transitions. diff --git a/Ical.Net/Evaluation/RecurrenceUtil.cs b/Ical.Net/Evaluation/RecurrenceUtil.cs index 8554fe6b..68d53c47 100644 --- a/Ical.Net/Evaluation/RecurrenceUtil.cs +++ b/Ical.Net/Evaluation/RecurrenceUtil.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -30,18 +30,9 @@ public static IEnumerable GetOccurrences(IRecurrable recurrable, Dat var start = recurrable.Start; - var periods = evaluator.Evaluate(start, timeZone, periodStart, options); - if (periodStart != null) - { - periods = - from p in periods - where - p.Start.ToInstant() >= periodStart - || (p.End != null && p.End.Value.ToInstant() > periodStart.Value) - select p; - } - - return periods.Select(p => new Occurrence(recurrable, p.Start, p.End ?? p.Start)); + return evaluator + .Evaluate(start, timeZone, periodStart, options) + .Select(p => new Occurrence(recurrable, p.Start, p.End)); } public static IEnumerable HandleEvaluationExceptions(this IEnumerable sequence) diff --git a/Ical.Net/Evaluation/RecurringEvaluator.cs b/Ical.Net/Evaluation/RecurringEvaluator.cs index 3823c823..e3090ef8 100644 --- a/Ical.Net/Evaluation/RecurringEvaluator.cs +++ b/Ical.Net/Evaluation/RecurringEvaluator.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -27,7 +27,7 @@ protected RecurringEvaluator(IRecurrable obj) /// /// The beginning date of the range to evaluate. /// - protected IEnumerable EvaluateRRule(CalDateTime referenceDate, DateTimeZone timeZone, Instant? periodStart, EvaluationOptions? options) + protected IEnumerable EvaluateRRule(CalDateTime referenceDate, DateTimeZone timeZone, Instant? periodStart, EvaluationOptions? options) { if (Recurrable.RecurrenceRule is null) return []; @@ -77,59 +77,64 @@ public virtual IEnumerable Evaluate( Instant? periodStart, EvaluationOptions? options) { - IEnumerable rruleOccurrences; - // Evaluate recurrence in the reference zone - var zonedReference = referenceDate.AsZonedOrDefault(timeZone); + var zonedReference = referenceDate.ToZonedOrDefault(timeZone); // Only add referenceDate if there is no RecurrenceRule. This is in line // with RFC 5545 which requires DTSTART to match any RRULE. If it doesn't, the behaviour // is undefined. It seems to be good practice not to return the referenceDate in this case. - rruleOccurrences = Recurrable.RecurrenceRule is null - ? [new EvaluationPeriod(zonedReference)] + var rruleOccurrences = Recurrable.RecurrenceRule is null + ? [zonedReference] : EvaluateRRule(referenceDate, zonedReference.Zone, periodStart, options); - var rdateOccurrences = EvaluateRDate(zonedReference.Zone); + var periods = rruleOccurrences + .Select(start => new EvaluationPeriod(start, GetEnd(start))); - var periods = - rruleOccurrences - .OrderedMerge(rdateOccurrences) - .OrderedDistinct(); + // Merge with recurrence dates if there are any + if (!Recurrable.RecurrenceDates.IsEmpty()) + { + var rdateOccurrences = EvaluateRDate(zonedReference.Zone); - // Apply the default duration, if any. - periods = periods.Select(p => (p.End != null) ? p : new EvaluationPeriod(p.Start, GetEnd(p.Start))); + periods = periods + .OrderedMerge(rdateOccurrences) + .OrderedDistinct(); + } // Filter by periodStart if (periodStart is not null) { // Include occurrences that start before periodStart, but end after periodStart. periods = periods.Where(p => (p.Start.ToInstant() >= periodStart.Value) - || (p.End?.ToInstant() > periodStart.Value)); + || (p.End.ToInstant() > periodStart.Value)); } - // EXDATEs could contain date-only entries while DTSTART is date-time. This case isn't clearly defined - // by the RFC, but it seems to be used in the wild (see https://github.com/ical-org/ical.net/issues/829). - // Different systems handle this differently, e.g. Outlook excludes any occurrences where the date portion - // matches an date-only EXDATE, while Google Calendar ignores such EXDATEs completely, if DTSTART is date-time. - // In Ical.Net we follow the Outlook approach, which requires us to handle date-only EXDATEs separately. - var exDateExclusionsDateOnly = new HashSet(EvaluateExDate(PeriodKind.DateOnly) - .Select(x => x.StartTime.ToLocalDateTime().Date)); - - var exDateExclusionsDateTime = new SortedSet(EvaluateExDate(PeriodKind.DateTime) - .Select(x => new EvaluationPeriod(x.StartTime.ToZonedDateTime(zonedReference.Zone)))); - - // Exclude occurrences according to EXDATEs. - periods = periods - .OrderedExclude(exDateExclusionsDateTime) - - // We accept date-only EXDATEs to be used with date-time DTSTARTs. In such cases we exclude those occurrences - // that, in their respective time zone, have a date component that matches an EXDATE. - // See https://github.com/ical-org/ical.net/pull/830 for more information. - // - // The order of dates in the EXDATEs doesn't necessarily match the order of dates returned by RDATEs - // due to RDATEs could have different time zones. We therefore use a regular `.Where()` to look up - // the EXDATEs in the HashSet rather than using `.OrderedExclude()`, which would require correct ordering. - .Where(dt => !exDateExclusionsDateOnly.Contains(dt.Start.Date)); + // Filter out exception dates if there are any + if (!Recurrable.ExceptionDates.IsEmpty()) + { + // Exclude occurrences according to EXDATEs. + var exDateExclusionsDateTime = new HashSet(EvaluateExDate(PeriodKind.DateTime) + .Select(x => x.StartTime.ToZonedOrDefault(zonedReference.Zone).ToInstant())); + + if (exDateExclusionsDateTime.Count > 0) + { + periods = periods.Where(x => !exDateExclusionsDateTime.Contains(x.Start.ToInstant())); + } + + // EXDATEs could contain date-only entries while DTSTART is date-time. This case isn't clearly defined + // by the RFC, but it seems to be used in the wild (see https://github.com/ical-org/ical.net/issues/829). + // Different systems handle this differently, e.g. Outlook excludes any occurrences where the date portion + // matches an date-only EXDATE, while Google Calendar ignores such EXDATEs completely, if DTSTART is date-time. + // In Ical.Net we follow the Outlook approach, which requires us to handle date-only EXDATEs separately. + // In such cases we exclude those occurrences that, in their respective time zone, have a date component + // that matches an EXDATE. + var exDateExclusionsDateOnly = new HashSet(EvaluateExDate(PeriodKind.DateOnly) + .Select(x => x.StartTime.ToLocalDateTime().Date)); + + if (exDateExclusionsDateOnly.Count > 0) + { + periods = periods.Where(dt => !exDateExclusionsDateOnly.Contains(dt.Start.Date)); + } + } // Convert results to the requested time zone periods = periods.Select(x => x.WithZone(timeZone)); diff --git a/Ical.Net/Evaluation/TodoEvaluator.cs b/Ical.Net/Evaluation/TodoEvaluator.cs index 77e3eddc..5ef0f237 100644 --- a/Ical.Net/Evaluation/TodoEvaluator.cs +++ b/Ical.Net/Evaluation/TodoEvaluator.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -19,9 +19,9 @@ public TodoEvaluator(Todo todo) : base(todo) { } protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTimeZone referenceTimeZone) { - var start = rdate.StartTime.AsZonedOrDefault(referenceTimeZone); + var start = rdate.StartTime.ToZonedOrDefault(referenceTimeZone); - ZonedDateTime? end; + ZonedDateTime end; if (rdate.Duration is { } duration) { end = start.LocalDateTime @@ -31,7 +31,7 @@ protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTi } else if (rdate.EndTime is { } dtEnd) { - var exactDuration = dtEnd.ToInstant() - rdate.StartTime.ToInstant(); + var exactDuration = dtEnd.ToZonedOrDefault(referenceTimeZone).ToInstant() - start.ToInstant(); if (exactDuration < NodaTime.Duration.Zero) { @@ -70,7 +70,7 @@ protected override ZonedDateTime GetEnd(ZonedDateTime start) if (Todo.Due is { } due && dtStart is not null) { - var exactDuration = due.ToInstant() - dtStart.ToInstant(); + var exactDuration = due.ToZonedOrDefault(start.Zone).ToInstant() - dtStart.ToZonedOrDefault(start.Zone).ToInstant(); return start.Plus(exactDuration); } diff --git a/Ical.Net/NodaTimeExtensions.cs b/Ical.Net/NodaTimeExtensions.cs index 55e8baf5..cdf0a80c 100644 --- a/Ical.Net/NodaTimeExtensions.cs +++ b/Ical.Net/NodaTimeExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -12,13 +12,11 @@ namespace Ical.Net; public static class NodaTimeExtensions { - public static CalDateTime ToCalDateTime(this ZonedDateTime value) => new(value); + public static CalDateTime ToCalDateTime(this ZonedDateTime value) => CalDateTime.FromZonedDateTime(value); public static CalDateTime ToCalDateTime(this LocalDateTime value, string? timeZone = null) => new(value, timeZone); - public static CalDateTime ToCalDateTime(this LocalDate value, string? timeZone = null) => new(value, timeZone); - - public static CalDateTime ToCalDateTime(this Instant value) => new(value); + public static CalDateTime ToCalDateTime(this LocalDate value) => new(value); /// /// Returns a ZonedDateTime that matches the time zone and diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index 07e16bd0..f46d628a 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -49,12 +49,12 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } var value = new StringBuilder(512); // NOSONAR: netstandard2.x does not support string.Create(CultureInfo.InvariantCulture, $"{...}"); value.Append(FormattableString.Invariant($"{dt.Year:0000}{dt.Month:00}{dt.Day:00}")); // NOSONAR - if (dt.HasTime) + if (dt.Time is { } time) { - value.Append(FormattableString.Invariant($"T{dt.Hour:00}{dt.Minute:00}{dt.Second:00}")); // NOSONAR + value.Append(FormattableString.Invariant($"T{time.Hour:00}{time.Minute:00}{time.Second:00}")); // NOSONAR if (dt.IsUtc) { - value.Append("Z"); + value.Append('Z'); } } @@ -115,7 +115,7 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } if (isUtc) timeZoneId = "UTC"; var res = timePart.HasValue - ? new CalDateTime(datePart, timePart.Value, timeZoneId) + ? new CalDateTime(datePart.At(timePart.Value), timeZoneId) : new CalDateTime(datePart); return res; diff --git a/Ical.Net/Utility/CollectionHelpers.cs b/Ical.Net/Utility/CollectionHelpers.cs index 66fc369a..226bc8a9 100644 --- a/Ical.Net/Utility/CollectionHelpers.cs +++ b/Ical.Net/Utility/CollectionHelpers.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -169,52 +169,6 @@ private static IEnumerable OrderedMergeMany(this IList> seq return left.OrderedMerge(right, comparer); } - /// - /// Returns the elements of the first ordered sequence that are not present in the second ordered sequence. - /// - /// - /// Both sequences must be ordered according to the type's default comparer. - /// - /// The method operates in a streaming manner, meaning it only enumerates the input sequences while the - /// output sequence is being enumerated, and can therefore handle indefinite sequences. - /// - public static IEnumerable OrderedExclude(this IEnumerable items, IEnumerable exclude) - => items.OrderedExclude(exclude, Comparer.Default); - - /// - /// Returns the elements of the first ordered sequence that are not present in the second ordered sequence. - /// - /// - /// Both sequences must be ordered according to the specified comparer. - /// - /// The method operates in a streaming manner, meaning it only enumerates the input sequences while the - /// output sequence is being enumerated, and can therefore handle indefinite sequences. - /// - public static IEnumerable OrderedExclude(this IEnumerable items, IEnumerable exclude, IComparer comparer) - { - using var it = items.GetEnumerator(); - using var itEx = exclude.GetEnumerator(); - - var hasNextIt = it.MoveNext(); - var hasNextEx = itEx.MoveNext(); - - while (hasNextIt) - { - var cmp = hasNextEx ? comparer.Compare(it.Current, itEx.Current) : -1; - if (cmp <= 0) - { - if (cmp < 0) - yield return it.Current; - - hasNextIt = it.MoveNext(); - } - else - { - hasNextEx = itEx.MoveNext(); - } - } - } - /// /// Returns a sequence containing the items of the ordered input sequence, with duplicates removed. ///