From 45d78b80c20d25d1119237114fe9cc8c6dfe1cb5 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Mon, 16 Mar 2026 20:38:05 -0400 Subject: [PATCH 01/20] Remove invalid CalDateTime constructor DATE values should not have a time zone. --- Ical.Net/DataTypes/CalDateTime.cs | 23 ++++++++++++++++++----- Ical.Net/DataTypes/Occurrence.cs | 2 +- Ical.Net/NodaTimeExtensions.cs | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index d76e0d766..b03affdd6 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -142,12 +142,25 @@ public CalDateTime(int year, int month, int day, int hour, int minute, int secon /// /// public CalDateTime(int year, int month, int day) - : this(new LocalDate(year, month, day), null, null) + : this(new LocalDate(year, month, day)) { } - public CalDateTime(LocalDate value, string? tzId = null) - : this(value, null, tzId) - { } + public CalDateTime(LocalDate date) + { + // NodaTime supports year values <1 (BCE). Make sure these + // years are considered invalid. + if (date.Year < 1) + { + throw new ArgumentOutOfRangeException(nameof(date), "Year must be a positive value"); + } + + _localDate = date; + _localTime = null; + + // RFC 5545, Section 3.2.19 + // The "TZID" property parameter MUST NOT be applied to DATE properties + _tzId = null; + } public CalDateTime(LocalDateTime value, string? tzId = null) : this(value.Date, value.TimeOfDay, tzId) @@ -229,7 +242,7 @@ public CalDateTime(string value, string? tzId = null) } #if NET6_0_OR_GREATER - public CalDateTime(DateOnly date) : this(date, null, null) { } + public CalDateTime(DateOnly date) : this(date.ToLocalDate()) { } public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null) : this(date.ToLocalDate(), time?.ToLocalTime(), tzId) { } diff --git a/Ical.Net/DataTypes/Occurrence.cs b/Ical.Net/DataTypes/Occurrence.cs index 4ccf36b15..210e1111a 100644 --- a/Ical.Net/DataTypes/Occurrence.cs +++ b/Ical.Net/DataTypes/Occurrence.cs @@ -53,7 +53,7 @@ public CalDateTime? DtStart return dtStart.HasTime ? new(start.LocalDateTime, tzid) - : new(start.Date, tzid); + : new(start.Date); } } diff --git a/Ical.Net/NodaTimeExtensions.cs b/Ical.Net/NodaTimeExtensions.cs index 55e8baf5b..9a7a51823 100644 --- a/Ical.Net/NodaTimeExtensions.cs +++ b/Ical.Net/NodaTimeExtensions.cs @@ -16,7 +16,7 @@ public static class NodaTimeExtensions 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 LocalDate value) => new(value); public static CalDateTime ToCalDateTime(this Instant value) => new(value); From 1ea7c06e7db811c8f18c325df299b2f45f9c4faa Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Tue, 17 Mar 2026 20:25:04 -0400 Subject: [PATCH 02/20] Unambiguous CalDateTime DateTime constructors Removes hasTime parameter from CalDateTime constructors and requires DATE values to be created with a static method. This prevents the time zone parameter from being silently ignored when creating DATE values. Fixes #933 --- Ical.Net.Tests/CalDateTimeTests.cs | 2 +- Ical.Net.Tests/RecurrenceTests.cs | 15 +++-- Ical.Net/CalendarComponents/VTimeZone.cs | 2 +- Ical.Net/DataTypes/CalDateTime.cs | 83 ++++++++---------------- 4 files changed, 39 insertions(+), 63 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index b98b8c386..036579e7a 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -246,7 +246,7 @@ 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))] diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 4f4e2b4b7..27785c585 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -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.FromDate(DateTime.MaxValue)).ToList(); Assert.That(occurrences, Has.Count.EqualTo(10), "There should be 10 occurrences of this event."); } @@ -2562,7 +2562,7 @@ 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.FromDate(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. @@ -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.Date, now.TimeOfDay, tzid), + DtEnd = new CalDateTime(later.Date, later.TimeOfDay, 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.Date.PlusDays(1), now.TimeOfDay, 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.Date.PlusDays(2), now.TimeOfDay, tzid)); serialized = SerializationHelpers.SerializeToString(e); Assert.That(Regex.Matches(serialized, expected), Has.Count.EqualTo(3)); } diff --git a/Ical.Net/CalendarComponents/VTimeZone.cs b/Ical.Net/CalendarComponents/VTimeZone.cs index d5eb7672a..a360d6285 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 = new CalDateTime(start); if (isRRule) { diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index b03affdd6..71e63c079 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -44,19 +44,26 @@ public sealed class CalDateTime : IFormattable, IEquatable /// Creates a new instance of the class /// with the current date/time and sets the to . /// - public static CalDateTime Now => new CalDateTime(DateTime.Now, null, true); + public static CalDateTime Now => new(DateTime.Now); /// /// Creates a new instance of the class /// with the current date and sets the to . /// - public static CalDateTime Today => new CalDateTime(DateTime.Today, null, false); + public static CalDateTime Today => FromDate(DateTime.Today); /// /// Creates a new instance of the class /// with the current date/time in the Coordinated Universal Time (UTC) timezone. /// - public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow, UtcTzId, true); + public static CalDateTime UtcNow => new(DateTime.UtcNow); + + /// + /// Creates a DATE value without time or time zone. + /// + /// The value to copy the DATE from. + /// A new with same date as the specified . + public static CalDateTime FromDate(DateTime value) => new(LocalDate.FromDateTime(value)); /// /// This constructor is required for the SerializerFactory to work. @@ -83,36 +90,13 @@ private CalDateTime() /// 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 + public CalDateTime(DateTime value, string? tzId = null) : this( + LocalDateTime.FromDateTime(value), + tzId ?? 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 . - /// - /// 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) + _ => null + }) { } /// @@ -189,7 +173,7 @@ public CalDateTime(Instant instant) : this(instant.InUtc()) /// /// /// - public CalDateTime(LocalDate date, LocalTime? time, string? tzId = null) + public CalDateTime(LocalDate date, LocalTime time, string? tzId = null) { // NodaTime supports year values <1 (BCE). Make sure these // years are considered invalid. @@ -199,13 +183,11 @@ 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 - }; + // RFC 5545, Section 3.3.5 does not allow for fractional seconds. + _localTime = TimeAdjusters.TruncateToSecond(time); + + _tzId = tzId; } /// @@ -244,8 +226,8 @@ public CalDateTime(string value, string? tzId = null) #if NET6_0_OR_GREATER public CalDateTime(DateOnly date) : this(date.ToLocalDate()) { } - public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null) - : this(date.ToLocalDate(), time?.ToLocalTime(), tzId) { } + public CalDateTime(DateOnly date, TimeOnly time, string? tzId = null) + : this(date.ToLocalDate(), time.ToLocalTime(), tzId) { } #endif public bool Equals(CalDateTime? other) => this == other; @@ -397,20 +379,6 @@ public override bool Equals(object? obj) public TimeOnly? ToTimeOnly() => _localTime?.ToTimeOnly(); #endif - /// - /// Any values are truncated to seconds, because - /// RFC 5545, Section 3.3.5 does not allow for fractional seconds. - /// - private static LocalTime? TruncateTimeToSeconds(LocalTime? time) - { - if (time is null) - { - return null; - } - - return TimeAdjusters.TruncateToSecond(time.Value); - } - public LocalDateTime ToLocalDateTime() => _localDate.At(_localTime ?? LocalTime.Midnight); @@ -471,7 +439,12 @@ public CalDateTime ToTimeZone(string? otherTzId) { if (otherTzId is null) { - return new(_localDate, _localTime); + if (_localTime.HasValue) + { + return new(_localDate, _localTime.Value); + } + + return new(_localDate); } return new(ToZonedDateTime(otherTzId)); From 9718a647ca153bf63a8dc5f1c0a85e559fff50c6 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Wed, 18 Mar 2026 20:58:19 -0400 Subject: [PATCH 03/20] Simplify occurrence end time calculation Time zone can be ignored for DATE values since they cannot have a time zone. This also verifies that end is not before start for DATE values. --- Ical.Net/Evaluation/EventEvaluator.cs | 32 +++++++++++++++------------ 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Ical.Net/Evaluation/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index bddf53401..fa18ca090 100644 --- a/Ical.Net/Evaluation/EventEvaluator.cs +++ b/Ical.Net/Evaluation/EventEvaluator.cs @@ -85,29 +85,33 @@ 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.Date, dtEnd.Time.Value, dtStart.TzId); } var exactDuration = dtEnd.ToInstant() - dtStart.ToInstant(); From 347bf29cae4e50844d7c1f2872a30cf27e6586c3 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Wed, 18 Mar 2026 21:02:32 -0400 Subject: [PATCH 04/20] Remove CalDateTime.ToTimeZone This was only being used in tests. --- Ical.Net.Benchmarks/CalDateTimePerfTests.cs | 10 --- Ical.Net.Tests/CalDateTimeTests.cs | 71 --------------------- Ical.Net.Tests/FreeBusyTest.cs | 8 +-- Ical.Net.Tests/RecurrenceTests.cs | 20 +++--- Ical.Net.Tests/SerializationTests.cs | 4 +- Ical.Net/DataTypes/CalDateTime.cs | 23 ------- 6 files changed, 16 insertions(+), 120 deletions(-) diff --git a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs index 9fcd21725..f9be6510f 100644 --- a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs +++ b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs @@ -12,7 +12,6 @@ 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; @@ -22,13 +21,4 @@ public class CalDateTimePerfTests [Benchmark] public CalDateTime UtcDateTime() => new CalDateTime(DateTime.UtcNow); - - [Benchmark] - public CalDateTime EmptyTzidToTzid() => EmptyTzid().ToTimeZone(_bTzid); - - [Benchmark] - public CalDateTime SpecifiedTzidToDifferentTzid() => SpecifiedTzid().ToTimeZone(_bTzid); - - [Benchmark] - public CalDateTime UtcToDifferentTzid() => UtcDateTime().ToTimeZone(_bTzid); } diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 036579e7a..016f0c65a 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -16,77 +16,6 @@ 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() { diff --git a/Ical.Net.Tests/FreeBusyTest.cs b/Ical.Net.Tests/FreeBusyTest.cs index 27bf2671c..79e2203cd 100644 --- a/Ical.Net.Tests/FreeBusyTest.cs +++ b/Ical.Net.Tests/FreeBusyTest.cs @@ -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)); } } diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 27785c585..083d67b5d 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -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)), @@ -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 { diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index aaa55a6bb..651c7cbf7 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -465,8 +465,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/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 71e63c079..5af0b2474 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -427,29 +427,6 @@ public ZonedDateTime AsZonedOrDefault(DateTimeZone timeZone) } } - /// - /// Converts the to a date/time - /// within the specified timezone. - /// - /// If == - /// it means that the is considered as local time for every timezone: - /// The returned is unchanged and the is set as . - /// - public CalDateTime ToTimeZone(string? otherTzId) - { - if (otherTzId is null) - { - if (_localTime.HasValue) - { - return new(_localDate, _localTime.Value); - } - - return new(_localDate); - } - - return new(ToZonedDateTime(otherTzId)); - } - /// public override string ToString() => ToString(null, null); From cb0b3702e5304c49eca405f58f9946a1338e8707 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Wed, 18 Mar 2026 21:09:11 -0400 Subject: [PATCH 05/20] Formatting --- Ical.Net/DataTypes/CalDateTime.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 5af0b2474..44cc040af 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -390,10 +390,8 @@ public ZonedDateTime ToZonedDateTime() { return ToLocalDateTime().InUtc(); } - else - { - return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); - } + + return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } public ZonedDateTime ToZonedDateTime(DateTimeZone timeZone) @@ -402,18 +400,14 @@ public ZonedDateTime ToZonedDateTime(DateTimeZone timeZone) { return ToLocalDateTime().InZoneLeniently(timeZone); } - else - { - return DateUtil.GetZone(_tzId) - .AtLeniently(ToLocalDateTime()) - .WithZone(timeZone); - } + + return DateUtil.GetZone(_tzId) + .AtLeniently(ToLocalDateTime()) + .WithZone(timeZone); } public ZonedDateTime ToZonedDateTime(string zoneId) - { - return ToZonedDateTime(DateUtil.GetZone(zoneId)); - } + => ToZonedDateTime(DateUtil.GetZone(zoneId)); public ZonedDateTime AsZonedOrDefault(DateTimeZone timeZone) { @@ -421,10 +415,8 @@ public ZonedDateTime AsZonedOrDefault(DateTimeZone timeZone) { return ToLocalDateTime().InZoneLeniently(timeZone); } - else - { - return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); - } + + return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } /// From bb91f799c52f4d77f7773f8b06f0aa3f429798e3 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Fri, 20 Mar 2026 08:43:45 -0400 Subject: [PATCH 06/20] Replace CalDateTime.Value with ToDateTime method --- Ical.Net.Tests/CalDateTimeTests.cs | 12 ++++++------ Ical.Net.Tests/EqualityAndHashingTests.cs | 2 +- Ical.Net.Tests/RecurrenceTests.cs | 4 ++-- Ical.Net/Calendar.cs | 6 +++--- Ical.Net/DataTypes/CalDateTime.cs | 11 ++++------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 016f0c65a..caab2ffaf 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -32,9 +32,9 @@ public void SameDateTimeWithDifferentTzIdShouldReturnSameUtc() [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; + => new CalDateTime(dateTime, tzId).ToDateTime().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); @@ -161,10 +161,10 @@ public void Simple_PropertyAndMethod_HasTime_Tests() using (Assert.EnterMultipleScope()) { - Assert.That(c2.Value, Is.EqualTo(c3.Value)); + Assert.That(c2.ToDateTime(), Is.EqualTo(c3.ToDateTime())); 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.ToDateTime().Kind, Is.EqualTo(DateTimeKind.Unspecified)); + Assert.That(CalDateTime.Today.ToDateTime().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")); @@ -195,7 +195,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.ToDateTime(), 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 diff --git a/Ical.Net.Tests/EqualityAndHashingTests.cs b/Ical.Net.Tests/EqualityAndHashingTests.cs index 16c02a231..6e3e58994 100644 --- a/Ical.Net.Tests/EqualityAndHashingTests.cs +++ b/Ical.Net.Tests/EqualityAndHashingTests.cs @@ -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.ToDateTime(), Is.EqualTo(incomingDt.ToDateTime())); Assert.That(expectedDt.GetHashCode(), Is.EqualTo(incomingDt.GetHashCode())); Assert.That(expectedDt.TzId, Is.EqualTo(incomingDt.TzId)); Assert.That(incomingDt.Equals(expectedDt), Is.True); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 083d67b5d..3186924e7 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -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 = new CalDateTime(endSearch.ToDateTime().AddSeconds(1), "UTC"); occurrences = firstEvent.GetOccurrences(startSearch.ToZonedDateTime()).TakeWhileBefore(endSearch.ToZonedDateTime().ToInstant()) .Select(o => o.Period) .ToList(); @@ -3395,7 +3395,7 @@ 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.ToDateTime() >= testCase.StartAt.ToDateTime()).ToList(); var startDates = occurrences.Select(x => x.Start).ToList(); Assert.That(startDates, Is.EqualTo(expectedInstances?.Select(x => x.ToZonedDateTime(timeZone)).ToList())); diff --git a/Ical.Net/Calendar.cs b/Ical.Net/Calendar.cs index dae957300..30e29c50d 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!.ToDateTime())) .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.ToDateTime()), 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?.ToDateTime() ?? default)), // If not a unique component, always keep _ => true diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 44cc040af..eea675573 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -271,14 +271,11 @@ public override bool Equals(object? obj) 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 the local date and time as a with . /// - /// Use along with and - /// to control how this date/time is handled. + /// For DATE values, time will be midnight. The value has no associated timezone. ///
- public DateTime Value => ToLocalDateTime().ToDateTimeUnspecified(); + public DateTime ToDateTime() => ToLocalDateTime().ToDateTimeUnspecified(); /// /// Returns , if the date/time value is floating. @@ -299,7 +296,7 @@ public override bool Equals(object? obj) public bool IsUtc => string.Equals(_tzId, UtcTzId, StringComparison.OrdinalIgnoreCase); /// - /// if the underlying has a 'time' part (hour, minute, second). + /// if is not null. /// public bool HasTime => _localTime.HasValue; From d05e510181f452ae5947e09946bf0fcdae8eb311 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Fri, 20 Mar 2026 08:59:11 -0400 Subject: [PATCH 07/20] Replace CalDateTime.AsUtc with ToDateTimeUtc method --- Ical.Net.Tests/FreeBusyTest.cs | 2 +- Ical.Net/DataTypes/CalDateTime.cs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Ical.Net.Tests/FreeBusyTest.cs b/Ical.Net.Tests/FreeBusyTest.cs index 79e2203cd..953b642c4 100644 --- a/Ical.Net.Tests/FreeBusyTest.cs +++ b/Ical.Net.Tests/FreeBusyTest.cs @@ -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/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index eea675573..9f7c44e27 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -262,13 +262,12 @@ 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 . + /// Converts this value to with . + /// + /// Values with a time zone will be converted to the UTC time zone. + /// Values without a time zone () will have the same local time. /// - /// - public DateTime AsUtc => ToInstant().ToDateTimeUtc(); + public DateTime ToDateTimeUtc() => ToInstant().ToDateTimeUtc(); /// /// Returns the local date and time as a with . From f391f59cc1a4fe34b627ef41079b1876d9507bf1 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sat, 21 Mar 2026 11:24:11 -0400 Subject: [PATCH 08/20] Add and update CalDateTime comments --- Ical.Net/DataTypes/CalDateTime.cs | 255 ++++++++++++++++++------------ 1 file changed, 153 insertions(+), 102 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 9f7c44e27..033fce391 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -16,52 +16,47 @@ 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(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 => FromDate(DateTime.Today); /// - /// 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(DateTime.UtcNow); /// - /// Creates a DATE value without time or time zone. + /// Creates a representing a DATE value. /// - /// The value to copy the DATE from. + /// The value to copy the local date from. /// A new with same date as the specified . public static CalDateTime FromDate(DateTime value) => new(LocalDate.FromDateTime(value)); @@ -75,21 +70,14 @@ 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 . + /// Creates a representing a DATE-TIME value + /// with an optional time zone. /// - /// 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 + /// Time zone will be UTC if is and + /// kind is . + /// + /// The value to copy the local date and time from. + /// The time zone ID. public CalDateTime(DateTime value, string? tzId = null) : this( LocalDateTime.FromDateTime(value), tzId ?? value.Kind switch @@ -100,27 +88,22 @@ public CalDateTime(DateTime value, string? tzId = null) : this( { } /// - /// 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-TIME value + /// with an optional time zone. /// - /// 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 time zone ID. public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null) //NOSONAR - must keep this signature : 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. /// /// /// @@ -129,51 +112,43 @@ public CalDateTime(int year, int month, int day) : this(new LocalDate(year, month, day)) { } - public CalDateTime(LocalDate date) - { - // NodaTime supports year values <1 (BCE). Make sure these - // years are considered invalid. - if (date.Year < 1) - { - throw new ArgumentOutOfRangeException(nameof(date), "Year must be a positive value"); - } - - _localDate = date; - _localTime = null; - - // RFC 5545, Section 3.2.19 - // The "TZID" property parameter MUST NOT be applied to DATE properties - _tzId = null; - } - + /// + /// Creates a representing a DATE-TIME value + /// with an optional time zone. + /// + /// The local date and time. + /// The time zone ID. public CalDateTime(LocalDateTime value, string? tzId = null) : this(value.Date, value.TimeOfDay, tzId) { } /// - /// 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 a time zone. + /// + /// The time zone offset from the is ignored, + /// so converting back to may produce a + /// different value. /// - /// - internal CalDateTime(ZonedDateTime value) + /// The value to copy the date, time, and time zone ID from. + public CalDateTime(ZonedDateTime value) : this(value.LocalDateTime, value.Zone.Id) { } + /// + /// Creates a representing a DATE-TIME value + /// in the UTC time zone. + /// + /// public CalDateTime(Instant instant) : this(instant.InUtc()) { } /// - /// 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. @@ -183,7 +158,18 @@ public CalDateTime(LocalDate date, LocalTime time, string? tzId = null) } _localDate = date; + } + /// + /// Creates a representing a DATE-TIME value + /// with an optional time zone. + /// + /// The local date. + /// The local time. + /// The time zone ID. + public 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); @@ -191,20 +177,17 @@ public CalDateTime(LocalDate date, LocalTime time, string? tzId = null) } /// - /// 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. - /// + /// The time zone ID. public CalDateTime(string value, string? tzId = null) { var serializer = new DateTimeSerializer(); @@ -224,10 +207,25 @@ public CalDateTime(string value, string? tzId = null) } #if NET6_0_OR_GREATER - public CalDateTime(DateOnly date) : this(date.ToLocalDate()) { } + /// + /// Creates a representing a DATE value. + /// + /// The local date. + public CalDateTime(DateOnly date) : this(date.ToLocalDate()) + { } + + /// + /// Creates a representing a DATE-TIME value + /// with an optional time zone. + /// + /// The local date. + /// The local time. + /// The time zone ID. public CalDateTime(DateOnly date, TimeOnly time, string? tzId = null) - : this(date.ToLocalDate(), time.ToLocalTime(), tzId) { } + : this(date.ToLocalDate(), time.ToLocalTime(), tzId) + { } + #endif public bool Equals(CalDateTime? other) => this == other; @@ -272,42 +270,41 @@ public override bool Equals(object? obj) /// /// Returns the local date and time as a with . /// - /// For DATE values, time will be midnight. The value has no associated timezone. + /// For DATE values, time will be midnight. The value has no associated time zone. /// public DateTime ToDateTime() => ToLocalDateTime().ToDateTimeUnspecified(); /// - /// Returns , if the date/time value is floating. + /// 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 is not null. + /// 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; @@ -328,17 +325,17 @@ public override bool Equals(object? obj) public int Day => _localDate.Day; /// - /// Gets the hour. + /// Gets the hour. Defaults to 0 for DATE values. /// public int Hour => _localTime?.Hour ?? 0; /// - /// Gets the minute. + /// Gets the minute. Defaults to 0 for DATE values. /// public int Minute => _localTime?.Minute ?? 0; /// - /// Gets the second. + /// Gets the second. Defaults to 0 for DATE values. /// public int Second => _localTime?.Second ?? 0; @@ -365,7 +362,7 @@ public override bool Equals(object? obj) #if NET6_0_OR_GREATER /// - /// Gets the date.. + /// Gets the date. /// public DateOnly ToDateOnly() => _localDate.ToDateOnly(); @@ -375,11 +372,35 @@ public override bool Equals(object? obj) public TimeOnly? ToTimeOnly() => _localTime?.ToTimeOnly(); #endif + /// + /// 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); + /// + /// Converts this value to . + /// + /// DATE values will default to . + /// + /// Values without a time zone will be treated as being in the UTC time zone. + /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// + /// An instant representing the point in time of this value. public Instant ToInstant() => ToZonedDateTime().ToInstant(); + /// + /// Converts this value to . + /// + /// DATE values will default to . + /// + /// Values without a time zone will be treated as being in the UTC time zone. + /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// + /// A zoned date time representing this value as close as possible. public ZonedDateTime ToZonedDateTime() { if (_tzId is null) @@ -390,26 +411,56 @@ public ZonedDateTime ToZonedDateTime() return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } - public ZonedDateTime ToZonedDateTime(DateTimeZone timeZone) + /// + /// Converts this value to in the specified time zone. + /// + /// DATE values will default to . + /// + /// Values without a time zone will be treated as being in the specified time zone. + /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// + /// The time zone to convert to. + /// A zoned date time representing this value as close as possible, but in the specified time zone. + public ZonedDateTime ToZonedDateTime(DateTimeZone targetZone) { if (_tzId is null) { - return ToLocalDateTime().InZoneLeniently(timeZone); + return ToLocalDateTime().InZoneLeniently(targetZone); } return DateUtil.GetZone(_tzId) .AtLeniently(ToLocalDateTime()) - .WithZone(timeZone); + .WithZone(targetZone); } + /// + /// Converts this value to in the specified time zone. + /// + /// DATE values will default to . + /// + /// Values without a time zone will be treated as being in the specified time zone. + /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// + /// The time zone ID to convert to. + /// A zoned date time representing this value as close as possible, but in the specified time zone. public ZonedDateTime ToZonedDateTime(string zoneId) => ToZonedDateTime(DateUtil.GetZone(zoneId)); - public ZonedDateTime AsZonedOrDefault(DateTimeZone timeZone) + /// + /// Converts this value to . + /// + /// DATE values will default to . + /// + /// Values without a time zone will be treated as being in the specified time zone. + /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// + /// The time zone to use if this value has no time zone. + /// A zoned date time representing this value in same time zone or the specified time zone. + public ZonedDateTime AsZonedOrDefault(DateTimeZone defaultZone) { if (_tzId is null) { - return ToLocalDateTime().InZoneLeniently(timeZone); + return ToLocalDateTime().InZoneLeniently(defaultZone); } return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); From 52f2c131b6f554ca7900cfed9a080b09e45218bd Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sat, 21 Mar 2026 11:54:13 -0400 Subject: [PATCH 09/20] Add kind to CalDateTime.ToDateTime Explicit names for ToDateTimeUtc and ToDateTimeUnspecified help clarify intention when using DateTime. --- Ical.Net.Tests/CalDateTimeTests.cs | 10 +++---- Ical.Net.Tests/EqualityAndHashingTests.cs | 2 +- Ical.Net.Tests/RecurrenceTests.cs | 4 +-- Ical.Net/Calendar.cs | 6 ++-- Ical.Net/DataTypes/CalDateTime.cs | 34 +++++++++++++---------- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index caab2ffaf..586b8723c 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -32,7 +32,7 @@ public void SameDateTimeWithDifferentTzIdShouldReturnSameUtc() [Test, TestCaseSource(nameof(DateTimeKindOverrideTestCases)), Description("DateTimeKind of values is always DateTimeKind.Unspecified")] public DateTimeKind DateTimeKindOverrideTests(DateTime dateTime, string tzId) - => new CalDateTime(dateTime, tzId).ToDateTime().Kind; + => new CalDateTime(dateTime, tzId).ToDateTimeUnspecified().Kind; private static IEnumerable DateTimeKindOverrideTestCases() { @@ -161,10 +161,10 @@ public void Simple_PropertyAndMethod_HasTime_Tests() using (Assert.EnterMultipleScope()) { - Assert.That(c2.ToDateTime(), Is.EqualTo(c3.ToDateTime())); + Assert.That(c2.ToDateTimeUnspecified(), Is.EqualTo(c3.ToDateTimeUnspecified())); Assert.That(c2.TzId, Is.EqualTo(c3.TzId)); - Assert.That(CalDateTime.UtcNow.ToDateTime().Kind, Is.EqualTo(DateTimeKind.Unspecified)); - Assert.That(CalDateTime.Today.ToDateTime().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")); @@ -195,7 +195,7 @@ public void ConstructorWithIso8601UtcString_ShouldResultInUtc(string value, stri var dt = new CalDateTime(value, tzId); using (Assert.EnterMultipleScope()) { - Assert.That(dt.ToDateTime(), 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 diff --git a/Ical.Net.Tests/EqualityAndHashingTests.cs b/Ical.Net.Tests/EqualityAndHashingTests.cs index 6e3e58994..7f0276964 100644 --- a/Ical.Net.Tests/EqualityAndHashingTests.cs +++ b/Ical.Net.Tests/EqualityAndHashingTests.cs @@ -28,7 +28,7 @@ public void CalDateTime_Tests(CalDateTime incomingDt, CalDateTime expectedDt) { using (Assert.EnterMultipleScope()) { - Assert.That(expectedDt.ToDateTime(), Is.EqualTo(incomingDt.ToDateTime())); + 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); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 3186924e7..a52413730 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -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.ToDateTime().AddSeconds(1), "UTC"); + endSearch = new CalDateTime(endSearch.ToDateTimeUnspecified().AddSeconds(1), "UTC"); occurrences = firstEvent.GetOccurrences(startSearch.ToZonedDateTime()).TakeWhileBefore(endSearch.ToZonedDateTime().ToInstant()) .Select(o => o.Period) .ToList(); @@ -3395,7 +3395,7 @@ RecurrenceRule GetRule() var expectedInstances = testCase.Instances?.Skip(instanceOffs).ToList(); if (testCase.StartAt != null) - expectedInstances = expectedInstances?.Where(x => x.ToDateTime() >= testCase.StartAt.ToDateTime()).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())); diff --git a/Ical.Net/Calendar.cs b/Ical.Net/Calendar.cs index 30e29c50d..a986935ec 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!.ToDateTime())) + .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.ToDateTime()), + 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?.ToDateTime() ?? default)), + !recurrenceIdsAndUids.ContainsKey((uc.Uid, r.DtStart?.ToDateTimeUnspecified() ?? default)), // If not a unique component, always keep _ => true diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 033fce391..f388d4dff 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -259,21 +259,6 @@ public override bool Equals(object? obj) return !(left == right); } - /// - /// Converts this value to with . - /// - /// Values with a time zone will be converted to the UTC time zone. - /// Values without a time zone () will have the same local time. - /// - public DateTime ToDateTimeUtc() => ToInstant().ToDateTimeUtc(); - - /// - /// Returns the local date and time as a with . - /// - /// For DATE values, time will be midnight. The value has no associated time zone. - /// - public DateTime ToDateTime() => ToLocalDateTime().ToDateTimeUnspecified(); - /// /// Returns if the date/time value is floating. /// @@ -372,6 +357,25 @@ public override bool Equals(object? obj) public TimeOnly? ToTimeOnly() => _localTime?.ToTimeOnly(); #endif + /// + /// 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() => ToInstant().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. /// From b796d6c6d40c51f5ba03139677145d68a44e2406 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Wed, 1 Apr 2026 20:14:52 -0400 Subject: [PATCH 10/20] Fix CalDateTime serializer doc --- Ical.Net/DataTypes/CalDateTime.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index f388d4dff..6ba8fdc33 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -183,9 +183,9 @@ public CalDateTime(LocalDate date, LocalTime time, string? tzId = null) /// 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 time zone, 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. + /// 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) From a06f6439a7ee19b882c4d51eecf317d4a65a67c9 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Wed, 1 Apr 2026 20:27:22 -0400 Subject: [PATCH 11/20] Remove nosonar --- Ical.Net/DataTypes/CalDateTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 6ba8fdc33..5022d1374 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -98,7 +98,7 @@ public CalDateTime(DateTime value, string? tzId = null) : this( /// /// /// The time zone ID. - public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null) //NOSONAR - must keep this signature + 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) { } From 5d266c9ad2acc31e642f896cacba6a27f0de3689 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Thu, 2 Apr 2026 14:31:52 -0400 Subject: [PATCH 12/20] Change some NodaTime constructors to static --- Ical.Net.Benchmarks/OccurencePerfTests.cs | 4 +- Ical.Net.Tests/CalDateTimeTests.cs | 5 +- Ical.Net.Tests/FreeBusyTest.cs | 4 +- Ical.Net.Tests/RecurrenceTests.cs | 32 +++---- .../WikiSamples/RecurrenceWikiTests.cs | 4 +- Ical.Net/DataTypes/CalDateTime.cs | 89 ++++++++----------- Ical.Net/DataTypes/Period.cs | 4 +- Ical.Net/DataTypes/Trigger.cs | 4 +- Ical.Net/Evaluation/EventEvaluator.cs | 4 +- Ical.Net/NodaTimeExtensions.cs | 6 +- .../DataTypes/DateTimeSerializer.cs | 2 +- 11 files changed, 71 insertions(+), 87 deletions(-) diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index 070175b2f..7f3122b7d 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. // @@ -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); diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 586b8723c..217ff0414 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // @@ -156,8 +156,7 @@ public void Simple_PropertyAndMethod_HasTime_Tests() var c = new CalDateTime(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()) { diff --git a/Ical.Net.Tests/FreeBusyTest.cs b/Ical.Net.Tests/FreeBusyTest.cs index 953b642c4..bf93301b9 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. // @@ -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(CalDateTime.FromInstant(start), Duration.FromHours(1)), FreeBusyStatus.Free); using (Assert.EnterMultipleScope()) { Assert.That(periodWithDuration.Contains(null), Is.False, "Contains should return false for null dt."); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index a52413730..daf60684a 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. // @@ -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()); } @@ -2343,7 +2343,7 @@ public void ReccurenceRule_MaxDate_StopsOnCount() }; var occurrences = evt.GetOccurrences(new CalDateTime(2018, 1, 1)) - .TakeWhileBefore(CalDateTime.FromDate(DateTime.MaxValue)).ToList(); + .TakeWhileBefore(CalDateTime.FromDateTimeDate(DateTime.MaxValue)).ToList(); Assert.That(occurrences, Has.Count.EqualTo(10), "There should be 10 occurrences of this event."); } @@ -2562,7 +2562,7 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) evt.Summary = "Event summary"; // Start at midnight, UTC time - evt.Start = CalDateTime.FromDate(DateTime.UtcNow); + 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. @@ -3080,21 +3080,21 @@ public void ExDateTimeZone_Tests() var e = new CalendarEvent { - DtStart = new CalDateTime(now.Date, now.TimeOfDay, tzid), - DtEnd = new CalDateTime(later.Date, later.TimeOfDay, 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.TimeOfDay, 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.TimeOfDay, tzid)); + e.ExceptionDates.Add(new CalDateTime(now.PlusDays(2), tzid)); serialized = SerializationHelpers.SerializeToString(e); Assert.That(Regex.Matches(serialized, expected), Has.Count.EqualTo(3)); } diff --git a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs index 38edc7164..898e8527f 100644 --- a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs +++ b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs @@ -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/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 5022d1374..153de24be 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -45,7 +45,7 @@ public sealed class CalDateTime : IFormattable, IEquatable /// Creates a /// with the current local date and no time or time zone. /// - public static CalDateTime Today => FromDate(DateTime.Today); + public static CalDateTime Today => FromDateTimeDate(DateTime.Today); /// /// Creates a @@ -53,13 +53,6 @@ public sealed class CalDateTime : IFormattable, IEquatable /// public static CalDateTime UtcNow => new(DateTime.UtcNow); - /// - /// Creates a representing a DATE value. - /// - /// The value to copy the local date from. - /// A new with same date as the specified . - public static CalDateTime FromDate(DateTime value) => new(LocalDate.FromDateTime(value)); - /// /// This constructor is required for the SerializerFactory to work. /// @@ -122,27 +115,6 @@ public CalDateTime(LocalDateTime value, string? tzId = null) : this(value.Date, value.TimeOfDay, tzId) { } - /// - /// Creates 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 CalDateTime(ZonedDateTime value) - : this(value.LocalDateTime, value.Zone.Id) - { } - - /// - /// Creates a representing a DATE-TIME value - /// in the UTC time zone. - /// - /// - public CalDateTime(Instant instant) : this(instant.InUtc()) - { } - /// /// Creates a representing a DATE value. /// @@ -167,7 +139,7 @@ public CalDateTime(LocalDate date) /// The local date. /// The local time. /// The time zone ID. - public CalDateTime(LocalDate date, LocalTime time, string? tzId = null) + private CalDateTime(LocalDate date, LocalTime time, string? tzId = null) : this(date) { // RFC 5545, Section 3.3.5 does not allow for fractional seconds. @@ -206,28 +178,6 @@ public CalDateTime(string value, string? tzId = null) } } -#if NET6_0_OR_GREATER - - /// - /// Creates a representing a DATE value. - /// - /// The local date. - public CalDateTime(DateOnly date) : this(date.ToLocalDate()) - { } - - /// - /// Creates a representing a DATE-TIME value - /// with an optional time zone. - /// - /// The local date. - /// The local time. - /// The time zone ID. - public CalDateTime(DateOnly date, TimeOnly time, string? tzId = null) - : this(date.ToLocalDate(), time.ToLocalTime(), tzId) - { } - -#endif - public bool Equals(CalDateTime? other) => this == other; /// @@ -346,6 +296,12 @@ public override bool Equals(object? obj) #if NET6_0_OR_GREATER + /// + /// Creates a representing a DATE value. + /// + /// The local date. + public static CalDateTime FromDateOnly(DateOnly date) => new(date.ToLocalDate()); + /// /// Gets the date. /// @@ -357,6 +313,13 @@ public override bool Equals(object? obj) public TimeOnly? ToTimeOnly() => _localTime?.ToTimeOnly(); #endif + /// + /// Creates 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 . /// @@ -385,6 +348,15 @@ public override bool Equals(object? obj) public LocalDateTime ToLocalDateTime() => _localDate.At(_localTime ?? LocalTime.Midnight); + + + /// + /// Creates a representing a DATE-TIME value + /// in the UTC time zone. + /// + /// + public static CalDateTime FromInstant(Instant instant) => FromZonedDateTime(instant.InUtc()); + /// /// Converts this value to . /// @@ -396,6 +368,19 @@ public LocalDateTime ToLocalDateTime() /// An instant representing the point in time of this value. public Instant ToInstant() => ToZonedDateTime().ToInstant(); + + + /// + /// Creates 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 . /// diff --git a/Ical.Net/DataTypes/Period.cs b/Ical.Net/DataTypes/Period.cs index 1c6913b97..4705b0802 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(CalDateTime.FromInstant(start), CalDateTime.FromInstant(end)) { } /// /// Creates a new instance starting at the given time diff --git a/Ical.Net/DataTypes/Trigger.cs b/Ical.Net/DataTypes/Trigger.cs index d20cdbf07..853a1b8c6 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/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index fa18ca090..a2110b5cf 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. // @@ -111,7 +111,7 @@ protected override ZonedDateTime GetEnd(ZonedDateTime start) // 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.Date, dtEnd.Time.Value, dtStart.TzId); + dtEnd = new(dtEnd.ToLocalDateTime(), dtStart.TzId); } var exactDuration = dtEnd.ToInstant() - dtStart.ToInstant(); diff --git a/Ical.Net/NodaTimeExtensions.cs b/Ical.Net/NodaTimeExtensions.cs index 9a7a51823..cca045784 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,13 @@ 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) => new(value); - public static CalDateTime ToCalDateTime(this Instant value) => new(value); + public static CalDateTime ToCalDateTime(this Instant value) => CalDateTime.FromInstant(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 07e16bd03..b6b55b339 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -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; From dc445919497c8a826ef5301dce02d9490fc0f701 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Fri, 3 Apr 2026 15:19:09 -0400 Subject: [PATCH 13/20] Remove Instant from CalDateTime --- Ical.Net.Tests/CalDateTimeTests.cs | 13 ++++--- Ical.Net.Tests/FreeBusyTest.cs | 2 +- Ical.Net.Tests/RecurrenceTests.cs | 10 +++--- Ical.Net.Tests/RecurrenceWithExDateTests.cs | 9 ++--- .../WikiSamples/RecurrenceWikiTests.cs | 2 +- Ical.Net/CalendarComponents/FreeBusy.cs | 17 +++++++--- Ical.Net/CollectionExtensions.cs | 4 +-- Ical.Net/DataTypes/CalDateTime.cs | 34 ++++++------------- Ical.Net/DataTypes/FreeBusyEntry.cs | 16 ++++----- Ical.Net/DataTypes/Period.cs | 4 +-- Ical.Net/DataTypes/RecurrenceIdentifier.cs | 9 +++-- Ical.Net/Evaluation/EventEvaluator.cs | 6 ++-- Ical.Net/Evaluation/TodoEvaluator.cs | 6 ++-- Ical.Net/NodaTimeExtensions.cs | 2 -- 14 files changed, 65 insertions(+), 69 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 217ff0414..c2c7fd736 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -3,14 +3,13 @@ // 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 NUnit.Framework; +using NUnit.Framework.Constraints; namespace Ical.Net.Tests; @@ -22,11 +21,11 @@ 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 firstUtc = someDt.AsZoned().ToDateTimeOffset(); + Assert.That(firstUtc, Is.EqualTo(someTime)); someDt = new CalDateTime(someTime.DateTime, "Europe/Berlin"); - var berlinUtc = someDt.ToInstant(); + var berlinUtc = someDt.AsZoned().ToDateTimeOffset(); Assert.That(berlinUtc, Is.Not.EqualTo(firstUtc)); } diff --git a/Ical.Net.Tests/FreeBusyTest.cs b/Ical.Net.Tests/FreeBusyTest.cs index bf93301b9..eb04a225e 100644 --- a/Ical.Net.Tests/FreeBusyTest.cs +++ b/Ical.Net.Tests/FreeBusyTest.cs @@ -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(CalDateTime.FromInstant(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."); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index daf60684a..527b8a558 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -4212,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()) @@ -4570,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.AsZoned().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4599,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.AsZoned().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4628,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.AsZoned().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4662,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.AsZoned().ToInstant()) .Select(x => x.Start) .ToList(); diff --git a/Ical.Net.Tests/RecurrenceWithExDateTests.cs b/Ical.Net.Tests/RecurrenceWithExDateTests.cs index e940be285..14f73da7c 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.AsZonedOrDefault(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.AsZonedOrDefault(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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant().Equals(o.Start.ToInstant()))), Is.True); } } } diff --git a/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs b/Ical.Net.Tests/WikiSamples/RecurrenceWikiTests.cs index 898e8527f..cea607a23 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() }; diff --git a/Ical.Net/CalendarComponents/FreeBusy.cs b/Ical.Net/CalendarComponents/FreeBusy.cs index 7f2eadab8..dec4b2c3d 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? + .AsZonedOrDefault(timeZone) + .ToInstant(); + + var endInstant = freeBusyRequest.End? + .AsZonedOrDefault(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?.AsZonedOrDefault(DateTimeZone.Utc).ToInstant()); } public virtual FreeBusyStatus GetFreeBusyStatus(Instant? dt) diff --git a/Ical.Net/CollectionExtensions.cs b/Ical.Net/CollectionExtensions.cs index 51aa2d7de..182a5e021 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.AsZonedOrDefault(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 153de24be..8aeda7f94 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -329,7 +329,7 @@ public override bool Equals(object? obj) /// 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() => ToInstant().ToDateTimeUtc(); + public DateTime ToDateTimeUtc() => AsZonedOrDefault(DateTimeZone.Utc).ToDateTimeUtc(); /// /// Returns the local date and time as a with . @@ -348,28 +348,6 @@ public override bool Equals(object? obj) public LocalDateTime ToLocalDateTime() => _localDate.At(_localTime ?? LocalTime.Midnight); - - - /// - /// Creates a representing a DATE-TIME value - /// in the UTC time zone. - /// - /// - public static CalDateTime FromInstant(Instant instant) => FromZonedDateTime(instant.InUtc()); - - /// - /// Converts this value to . - /// - /// DATE values will default to . - /// - /// Values without a time zone will be treated as being in the UTC time zone. - /// If the local date and time is ambiguous due to the time zone, it will be resolved using . - /// - /// An instant representing the point in time of this value. - public Instant ToInstant() => ToZonedDateTime().ToInstant(); - - - /// /// Creates a representing a DATE-TIME value /// with a time zone. @@ -455,6 +433,16 @@ public ZonedDateTime AsZonedOrDefault(DateTimeZone defaultZone) return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } + public ZonedDateTime AsZoned() + { + if (_tzId is null) + { + throw new InvalidOperationException("CalDateTime must have a time zone to convert to ZonedDateTime"); + } + + return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); + } + /// public override string ToString() => ToString(null, null); diff --git a/Ical.Net/DataTypes/FreeBusyEntry.cs b/Ical.Net/DataTypes/FreeBusyEntry.cs index 81a1dd459..cf2931fef 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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant()); } /// @@ -58,7 +58,7 @@ public bool Contains(CalDateTime? dt) /// public bool Contains(Instant value) { - var startInstant = StartTime.ToInstant(); + var startInstant = StartTime.AsZonedOrDefault(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.AsZonedOrDefault(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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant(); Instant end; if (EndTime is { } endTime) { - end = endTime.ToInstant(); + end = endTime.AsZonedOrDefault(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.AsZonedOrDefault(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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant(); } else if (period.Duration is { } periodDuration) { diff --git a/Ical.Net/DataTypes/Period.cs b/Ical.Net/DataTypes/Period.cs index 4705b0802..b42116cc0 100644 --- a/Ical.Net/DataTypes/Period.cs +++ b/Ical.Net/DataTypes/Period.cs @@ -58,7 +58,7 @@ internal static Period Create(CalDateTime start, CalDateTime? end = null, Durati internal Period() { } internal Period(Instant start, Instant end) - : this(CalDateTime.FromInstant(start), CalDateTime.FromInstant(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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant() < start.AsZonedOrDefault(DateTimeZone.Utc).ToInstant(); } if (isEndBeforeStart) diff --git a/Ical.Net/DataTypes/RecurrenceIdentifier.cs b/Ical.Net/DataTypes/RecurrenceIdentifier.cs index 7eb9338d8..d4c33ba07 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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant() + .CompareTo(other.StartTime.AsZonedOrDefault(DateTimeZone.Utc).ToInstant()); + if (startComparison != 0) { return startComparison; diff --git a/Ical.Net/Evaluation/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index a2110b5cf..4c8c8a01c 100644 --- a/Ical.Net/Evaluation/EventEvaluator.cs +++ b/Ical.Net/Evaluation/EventEvaluator.cs @@ -26,7 +26,7 @@ protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTi { var start = rdate.StartTime.AsZonedOrDefault(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.AsZonedOrDefault(referenceTimeZone).ToInstant() - start.ToInstant(); if (exactDuration < Duration.Zero) { @@ -114,7 +114,7 @@ protected override ZonedDateTime GetEnd(ZonedDateTime start) dtEnd = new(dtEnd.ToLocalDateTime(), dtStart.TzId); } - var exactDuration = dtEnd.ToInstant() - dtStart.ToInstant(); + var exactDuration = dtEnd.AsZonedOrDefault(start.Zone).ToInstant() - dtStart.AsZonedOrDefault(start.Zone).ToInstant(); if (exactDuration < Duration.Zero) { diff --git a/Ical.Net/Evaluation/TodoEvaluator.cs b/Ical.Net/Evaluation/TodoEvaluator.cs index 77e3eddc3..9ca4355ac 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. // @@ -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.AsZonedOrDefault(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.AsZonedOrDefault(start.Zone).ToInstant() - dtStart.AsZonedOrDefault(start.Zone).ToInstant(); return start.Plus(exactDuration); } diff --git a/Ical.Net/NodaTimeExtensions.cs b/Ical.Net/NodaTimeExtensions.cs index cca045784..cdf0a80cf 100644 --- a/Ical.Net/NodaTimeExtensions.cs +++ b/Ical.Net/NodaTimeExtensions.cs @@ -18,8 +18,6 @@ public static class NodaTimeExtensions public static CalDateTime ToCalDateTime(this LocalDate value) => new(value); - public static CalDateTime ToCalDateTime(this Instant value) => CalDateTime.FromInstant(value); - /// /// Returns a ZonedDateTime that matches the time zone and /// offset of the start value, or shifts forward if the local From 607a9598cdcdd07c2d1244f5543aec0e2bbfbe26 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Fri, 3 Apr 2026 16:41:48 -0400 Subject: [PATCH 14/20] Remove extra CalDateTime.ToZoneDateTime --- Ical.Net.Benchmarks/OccurencePerfTests.cs | 24 ++++---- Ical.Net.Tests/CalDateTimeTests.cs | 4 +- Ical.Net.Tests/DeserializationTests.cs | 2 +- Ical.Net.Tests/RecurrenceTests.cs | 44 +++++++------- Ical.Net.Tests/RecurrenceTests_From_Issues.cs | 4 +- Ical.Net.Tests/RecurrenceWithExDateTests.cs | 6 +- Ical.Net.Tests/TestExtensions.cs | 8 ++- .../TestHelpers/OccurrenceTester.cs | 10 ++-- Ical.Net/CalendarComponents/Alarm.cs | 6 +- Ical.Net/CalendarComponents/FreeBusy.cs | 6 +- Ical.Net/CollectionExtensions.cs | 2 +- Ical.Net/DataTypes/CalDateTime.cs | 58 +++---------------- Ical.Net/DataTypes/FreeBusyEntry.cs | 14 ++--- Ical.Net/DataTypes/Occurrence.cs | 4 +- Ical.Net/DataTypes/Period.cs | 2 +- Ical.Net/DataTypes/RecurrenceIdentifier.cs | 4 +- Ical.Net/Evaluation/EventEvaluator.cs | 6 +- .../Evaluation/RecurrenceRuleEvaluator.cs | 6 +- Ical.Net/Evaluation/RecurringEvaluator.cs | 6 +- Ical.Net/Evaluation/TodoEvaluator.cs | 6 +- 20 files changed, 92 insertions(+), 130 deletions(-) diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index 7f3122b7d..309cfcc18 100644 --- a/Ical.Net.Benchmarks/OccurencePerfTests.cs +++ b/Ical.Net.Benchmarks/OccurencePerfTests.cs @@ -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) @@ -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) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index c2c7fd736..b5c63ae7f 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -21,11 +21,11 @@ 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.AsZoned().ToDateTimeOffset(); + var firstUtc = someDt.ToZonedDateTime().ToDateTimeOffset(); Assert.That(firstUtc, Is.EqualTo(someTime)); someDt = new CalDateTime(someTime.DateTime, "Europe/Berlin"); - var berlinUtc = someDt.AsZoned().ToDateTimeOffset(); + var berlinUtc = someDt.ToZonedDateTime().ToDateTimeOffset(); Assert.That(berlinUtc, Is.Not.EqualTo(firstUtc)); } diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index d0a59a89d..78e56e621 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. // diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 527b8a558..b91747fa4 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -2568,7 +2568,7 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) // 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(); @@ -3371,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); @@ -3398,7 +3398,7 @@ RecurrenceRule GetRule() 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) @@ -3888,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); @@ -3937,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)); } @@ -3989,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")); } @@ -4043,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")); } @@ -4070,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) @@ -4111,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)); @@ -4125,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)); @@ -4193,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(); @@ -4252,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 @@ -4266,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()) { @@ -4570,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.AsZoned().ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4599,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.AsZoned().ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4628,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.AsZoned().ToInstant()) + var result = cal.GetOccurrences(tz, evt.Start.ToZonedDateTime().ToInstant()) .Select(x => x.Start) .ToList(); @@ -4662,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.AsZoned().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 754c08fc4..47a2d4484 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. // @@ -348,7 +348,7 @@ private static void CheckDates(DateTime startDate, DateTime endDate, CalDateTime var occurrences = calendarEvent.GetOccurrences(new CalDateTime(startDate)).TakeWhileBefore(new CalDateTime(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)); } diff --git a/Ical.Net.Tests/RecurrenceWithExDateTests.cs b/Ical.Net.Tests/RecurrenceWithExDateTests.cs index 14f73da7c..8ad1be709 100644 --- a/Ical.Net.Tests/RecurrenceWithExDateTests.cs +++ b/Ical.Net.Tests/RecurrenceWithExDateTests.cs @@ -119,7 +119,7 @@ public void ShouldNotOccurOnUtcExceptionDate() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.AsZonedOrDefault(DateTimeZone.Utc).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")); } } @@ -178,7 +178,7 @@ public void MultipleExclusionDatesSameTimeZoneShouldBeExcluded() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.AsZonedOrDefault(DateTimeZone.Utc).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")); } } @@ -231,7 +231,7 @@ public void MultipleExclusionDatesDifferentZoneShouldBeExcluded() o => !cal .Events[0]! .ExceptionDates.GetAllDates() - .Any(ex => ex.AsZonedOrDefault(DateTimeZone.Utc).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/TestExtensions.cs b/Ical.Net.Tests/TestExtensions.cs index 58472d3bb..bc8fad900 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. // @@ -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 2452e4f5c..90584b1b7 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/CalendarComponents/Alarm.cs b/Ical.Net/CalendarComponents/Alarm.cs index a8539d1f5..23c49ae14 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 dec4b2c3d..a365a457a 100644 --- a/Ical.Net/CalendarComponents/FreeBusy.cs +++ b/Ical.Net/CalendarComponents/FreeBusy.cs @@ -24,11 +24,11 @@ public class FreeBusy : UniqueComponent, IMergeable } var startInstant = freeBusyRequest.Start? - .AsZonedOrDefault(timeZone) + .ToZonedOrDefault(timeZone) .ToInstant(); var endInstant = freeBusyRequest.End? - .AsZonedOrDefault(timeZone) + .ToZonedOrDefault(timeZone) .ToInstant(); var occurrences = occ.GetOccurrences(timeZone, startInstant, options) @@ -199,7 +199,7 @@ public virtual FreeBusyStatus GetFreeBusyStatus(DataTypes.Period? period) /// public virtual FreeBusyStatus GetFreeBusyStatus(CalDateTime? dt) { - return GetFreeBusyStatus(dt?.AsZonedOrDefault(DateTimeZone.Utc).ToInstant()); + return GetFreeBusyStatus(dt?.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); } public virtual FreeBusyStatus GetFreeBusyStatus(Instant? dt) diff --git a/Ical.Net/CollectionExtensions.cs b/Ical.Net/CollectionExtensions.cs index 182a5e021..84ae23473 100644 --- a/Ical.Net/CollectionExtensions.cs +++ b/Ical.Net/CollectionExtensions.cs @@ -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.AsZonedOrDefault(DateTimeZone.Utc).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 8aeda7f94..8fae3a37c 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -329,7 +329,7 @@ public override bool Equals(object? obj) /// 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() => AsZonedOrDefault(DateTimeZone.Utc).ToDateTimeUtc(); + public DateTime ToDateTimeUtc() => ToZonedOrDefault(DateTimeZone.Utc).ToDateTimeUtc(); /// /// Returns the local date and time as a with . @@ -362,57 +362,23 @@ public LocalDateTime ToLocalDateTime() /// /// Converts this value to . /// - /// DATE values will default to . + /// Values without a time zone will throw an . + /// Use to handle floating values. /// - /// Values without a time zone will be treated as being in the UTC time zone. /// If the local date and time is ambiguous due to the time zone, it will be resolved using . /// /// 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(); + throw new InvalidOperationException("CalDateTime must have a time zone to convert to ZonedDateTime"); } return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } - /// - /// Converts this value to in the specified time zone. - /// - /// DATE values will default to . - /// - /// Values without a time zone will be treated as being in the specified time zone. - /// If the local date and time is ambiguous due to the time zone, it will be resolved using . - /// - /// The time zone to convert to. - /// A zoned date time representing this value as close as possible, but in the specified time zone. - public ZonedDateTime ToZonedDateTime(DateTimeZone targetZone) - { - if (_tzId is null) - { - return ToLocalDateTime().InZoneLeniently(targetZone); - } - - return DateUtil.GetZone(_tzId) - .AtLeniently(ToLocalDateTime()) - .WithZone(targetZone); - } - - /// - /// Converts this value to in the specified time zone. - /// - /// DATE values will default to . - /// - /// Values without a time zone will be treated as being in the specified time zone. - /// If the local date and time is ambiguous due to the time zone, it will be resolved using . - /// - /// The time zone ID to convert to. - /// A zoned date time representing this value as close as possible, but in the specified time zone. - public ZonedDateTime ToZonedDateTime(string zoneId) - => ToZonedDateTime(DateUtil.GetZone(zoneId)); - /// /// Converts this value to . /// @@ -422,8 +388,8 @@ public ZonedDateTime ToZonedDateTime(string zoneId) /// If the local date and time is ambiguous due to the time zone, it will be resolved using . /// /// The time zone to use if this value has no time zone. - /// A zoned date time representing this value in same time zone or the specified time zone. - public ZonedDateTime AsZonedOrDefault(DateTimeZone defaultZone) + /// A zoned date time representing this value in its own time zone or the specified time zone. + public ZonedDateTime ToZonedOrDefault(DateTimeZone defaultZone) { if (_tzId is null) { @@ -433,16 +399,6 @@ public ZonedDateTime AsZonedOrDefault(DateTimeZone defaultZone) return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); } - public ZonedDateTime AsZoned() - { - if (_tzId is null) - { - throw new InvalidOperationException("CalDateTime must have a time zone to convert to ZonedDateTime"); - } - - return DateUtil.GetZone(_tzId).AtLeniently(ToLocalDateTime()); - } - /// public override string ToString() => ToString(null, null); diff --git a/Ical.Net/DataTypes/FreeBusyEntry.cs b/Ical.Net/DataTypes/FreeBusyEntry.cs index cf2931fef..7fb158c43 100644 --- a/Ical.Net/DataTypes/FreeBusyEntry.cs +++ b/Ical.Net/DataTypes/FreeBusyEntry.cs @@ -48,7 +48,7 @@ public override void CopyFrom(ICopyable obj) /// public bool Contains(CalDateTime? dt) { - return dt is not null && Contains(dt.AsZonedOrDefault(DateTimeZone.Utc).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.AsZonedOrDefault(DateTimeZone.Utc).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.AsZonedOrDefault(DateTimeZone.Utc).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.AsZonedOrDefault(DateTimeZone.Utc).ToInstant(); + var start = StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); Instant end; if (EndTime is { } endTime) { - end = endTime.AsZonedOrDefault(DateTimeZone.Utc).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.AsZonedOrDefault(DateTimeZone.Utc).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.AsZonedOrDefault(DateTimeZone.Utc).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 210e1111a..2089198eb 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. // @@ -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 b42116cc0..0c894259f 100644 --- a/Ical.Net/DataTypes/Period.cs +++ b/Ical.Net/DataTypes/Period.cs @@ -92,7 +92,7 @@ public Period(CalDateTime start, CalDateTime? end = null) else { // Both are zoned, so compare instants - isEndBeforeStart = end.AsZonedOrDefault(DateTimeZone.Utc).ToInstant() < start.AsZonedOrDefault(DateTimeZone.Utc).ToInstant(); + isEndBeforeStart = end.ToZonedOrDefault(DateTimeZone.Utc).ToInstant() < start.ToZonedOrDefault(DateTimeZone.Utc).ToInstant(); } if (isEndBeforeStart) diff --git a/Ical.Net/DataTypes/RecurrenceIdentifier.cs b/Ical.Net/DataTypes/RecurrenceIdentifier.cs index d4c33ba07..f802ef24e 100644 --- a/Ical.Net/DataTypes/RecurrenceIdentifier.cs +++ b/Ical.Net/DataTypes/RecurrenceIdentifier.cs @@ -66,8 +66,8 @@ public int CompareTo(RecurrenceIdentifier? other) return 1; } - var startComparison = StartTime.AsZonedOrDefault(DateTimeZone.Utc).ToInstant() - .CompareTo(other.StartTime.AsZonedOrDefault(DateTimeZone.Utc).ToInstant()); + var startComparison = StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant() + .CompareTo(other.StartTime.ToZonedOrDefault(DateTimeZone.Utc).ToInstant()); if (startComparison != 0) { diff --git a/Ical.Net/Evaluation/EventEvaluator.cs b/Ical.Net/Evaluation/EventEvaluator.cs index 4c8c8a01c..5195c12a3 100644 --- a/Ical.Net/Evaluation/EventEvaluator.cs +++ b/Ical.Net/Evaluation/EventEvaluator.cs @@ -24,7 +24,7 @@ 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; if (rdate.Duration is { } duration) @@ -41,7 +41,7 @@ protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTi } else if (rdate.EndTime is { } dtEnd) { - var exactDuration = dtEnd.AsZonedOrDefault(referenceTimeZone).ToInstant() - start.ToInstant(); + var exactDuration = dtEnd.ToZonedOrDefault(referenceTimeZone).ToInstant() - start.ToInstant(); if (exactDuration < Duration.Zero) { @@ -114,7 +114,7 @@ protected override ZonedDateTime GetEnd(ZonedDateTime start) dtEnd = new(dtEnd.ToLocalDateTime(), dtStart.TzId); } - var exactDuration = dtEnd.AsZonedOrDefault(start.Zone).ToInstant() - dtStart.AsZonedOrDefault(start.Zone).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 d0678a825..050d2dc14 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); } diff --git a/Ical.Net/Evaluation/RecurringEvaluator.cs b/Ical.Net/Evaluation/RecurringEvaluator.cs index 3823c823f..efd05de06 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. // @@ -80,7 +80,7 @@ public virtual IEnumerable Evaluate( 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 @@ -116,7 +116,7 @@ public virtual IEnumerable Evaluate( .Select(x => x.StartTime.ToLocalDateTime().Date)); var exDateExclusionsDateTime = new SortedSet(EvaluateExDate(PeriodKind.DateTime) - .Select(x => new EvaluationPeriod(x.StartTime.ToZonedDateTime(zonedReference.Zone)))); + .Select(x => new EvaluationPeriod(x.StartTime.ToZonedOrDefault(zonedReference.Zone).WithZone(zonedReference.Zone)))); // Exclude occurrences according to EXDATEs. periods = periods diff --git a/Ical.Net/Evaluation/TodoEvaluator.cs b/Ical.Net/Evaluation/TodoEvaluator.cs index 9ca4355ac..ada907a4c 100644 --- a/Ical.Net/Evaluation/TodoEvaluator.cs +++ b/Ical.Net/Evaluation/TodoEvaluator.cs @@ -19,7 +19,7 @@ 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; if (rdate.Duration is { } duration) @@ -31,7 +31,7 @@ protected override EvaluationPeriod EvaluateRDate(DataTypes.Period rdate, DateTi } else if (rdate.EndTime is { } dtEnd) { - var exactDuration = dtEnd.AsZonedOrDefault(referenceTimeZone).ToInstant() - start.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.AsZonedOrDefault(start.Zone).ToInstant() - dtStart.AsZonedOrDefault(start.Zone).ToInstant(); + var exactDuration = due.ToZonedOrDefault(start.Zone).ToInstant() - dtStart.ToZonedOrDefault(start.Zone).ToInstant(); return start.Plus(exactDuration); } From 723c54eab5aab428181307dc341acca0cc8c0db2 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 19 Apr 2026 11:25:22 -0400 Subject: [PATCH 15/20] Change to CalDateTime.FromDateTime --- Ical.Net.Benchmarks/CalDateTimePerfTests.cs | 6 +- Ical.Net.Benchmarks/OccurencePerfTests.cs | 4 +- Ical.Net.Benchmarks/SerializationPerfTests.cs | 6 +- Ical.Net.Tests/CalDateTimeTests.cs | 10 +-- Ical.Net.Tests/CalendarEventTest.cs | 8 +-- Ical.Net.Tests/CalendarPropertiesTest.cs | 6 +- Ical.Net.Tests/CollectionHelpersTests.cs | 6 +- Ical.Net.Tests/CopyComponentTests.cs | 14 ++-- Ical.Net.Tests/DateTimeSerializerTests.cs | 6 +- Ical.Net.Tests/DeserializationTests.cs | 2 +- Ical.Net.Tests/DocumentationExamples.cs | 10 +-- Ical.Net.Tests/EqualityAndHashingTests.cs | 24 +++---- Ical.Net.Tests/MatchTimeZoneTests.cs | 4 +- Ical.Net.Tests/RecurrenceTests.cs | 66 +++++++++---------- Ical.Net.Tests/RecurrenceTests_From_Issues.cs | 18 ++--- Ical.Net.Tests/SymmetricSerializationTests.cs | 6 +- Ical.Net.Tests/VTimeZoneTest.cs | 10 +-- Ical.Net/CalendarComponents/VTimeZone.cs | 2 +- Ical.Net/DataTypes/CalDateTime.cs | 42 ++++++------ 19 files changed, 126 insertions(+), 124 deletions(-) diff --git a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs index f9be6510f..94c646bb0 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. // @@ -17,8 +17,8 @@ public class CalDateTimePerfTests public CalDateTime EmptyTzid() => CalDateTime.Now; [Benchmark] - public CalDateTime SpecifiedTzid() => new CalDateTime(DateTime.Now, _aTzid); + public CalDateTime SpecifiedTzid() => CalDateTime.FromDateTime(DateTime.Now, _aTzid); [Benchmark] - public CalDateTime UtcDateTime() => new CalDateTime(DateTime.UtcNow); + public CalDateTime UtcDateTime() => CalDateTime.FromDateTime(DateTime.UtcNow); } diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index 309cfcc18..d14d9634f 100644 --- a/Ical.Net.Benchmarks/OccurencePerfTests.cs +++ b/Ical.Net.Benchmarks/OccurencePerfTests.cs @@ -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 094046e4b..8081a1dee 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 b5c63ae7f..99d633ce7 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -20,18 +20,18 @@ 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 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"); + 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).ToDateTimeUnspecified().Kind; + => CalDateTime.FromDateTime(dateTime, tzId).ToDateTimeUnspecified().Kind; private static IEnumerable DateTimeKindOverrideTestCases() { @@ -152,7 +152,7 @@ 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.LocalDateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second), c.TzId); @@ -183,7 +183,7 @@ public void CalDateTime_FromDateTime_HandlesKindCorrectly(DateTimeKind kind, IRe { 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)] diff --git a/Ical.Net.Tests/CalendarEventTest.cs b/Ical.Net.Tests/CalendarEventTest.cs index d54b7b299..ebd912b63 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 c50c46e2b..a4d8b2a6c 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 e94e461c9..d06b3086b 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())); } diff --git a/Ical.Net.Tests/CopyComponentTests.cs b/Ical.Net.Tests/CopyComponentTests.cs index 7cac4384a..9da117ca9 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 888eea6a5..00a4b6a94 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 78e56e621..269b197ff 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -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 af79b061d..5cac0c5e7 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 7f0276964..6ee35e284 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. // @@ -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/MatchTimeZoneTests.cs b/Ical.Net.Tests/MatchTimeZoneTests.cs index 72c5d558a..06675726e 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/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index b91747fa4..a2182d94c 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -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)) @@ -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; @@ -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.ToDateTimeUnspecified().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)); @@ -3104,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"); @@ -3131,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) }; @@ -3201,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)); @@ -4053,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, @@ -4097,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(); diff --git a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs index 47a2d4484..a374dd449 100644 --- a/Ical.Net.Tests/RecurrenceTests_From_Issues.cs +++ b/Ical.Net.Tests/RecurrenceTests_From_Issues.cs @@ -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,7 +345,7 @@ 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.Date).ToList(); diff --git a/Ical.Net.Tests/SymmetricSerializationTests.cs b/Ical.Net.Tests/SymmetricSerializationTests.cs index f70509c61..a54132684 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/VTimeZoneTest.cs b/Ical.Net.Tests/VTimeZoneTest.cs index e7d34bb23..caa445edb 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/CalendarComponents/VTimeZone.cs b/Ical.Net/CalendarComponents/VTimeZone.cs index a360d6285..06b02325b 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); + timeZoneInfo.Start = CalDateTime.FromDateTime(start); if (isRRule) { diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 8fae3a37c..8e870953d 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -39,7 +39,7 @@ public sealed class CalDateTime : IFormattable, IEquatable /// Creates a /// with the current local date/time and no time zone. /// - public static CalDateTime Now => new(DateTime.Now); + public static CalDateTime Now => FromDateTime(DateTime.Now); /// /// Creates a @@ -51,7 +51,7 @@ public sealed class CalDateTime : IFormattable, IEquatable /// Creates a /// with the current date/time in the UTC time zone. /// - public static CalDateTime UtcNow => new(DateTime.UtcNow); + public static CalDateTime UtcNow => FromDateTime(DateTime.UtcNow); /// /// This constructor is required for the SerializerFactory to work. @@ -62,24 +62,6 @@ private CalDateTime() // required for the SerializerFactory to work } - /// - /// Creates a representing a DATE-TIME value - /// with an optional time zone. - /// - /// Time zone will be UTC if is and - /// kind is . - /// - /// The value to copy the local date and time from. - /// The time zone ID. - public CalDateTime(DateTime value, string? tzId = null) : this( - LocalDateTime.FromDateTime(value), - tzId ?? value.Kind switch - { - DateTimeKind.Utc => UtcTzId, - _ => null - }) - { } - /// /// Creates a representing a DATE-TIME value /// with an optional time zone. @@ -313,6 +295,26 @@ public override bool Equals(object? obj) public TimeOnly? ToTimeOnly() => _localTime?.ToTimeOnly(); #endif + /// + /// Creates a representing a DATE-TIME value + /// with an optional time zone. + /// + /// Time zone will be UTC if is and + /// kind is . + /// + /// The value to copy the local date and time from. + /// The time zone ID. + public static CalDateTime FromDateTime(DateTime value, string? tzId = null) + { + tzId ??= value.Kind switch + { + DateTimeKind.Utc => UtcTzId, + _ => null + }; + + return new CalDateTime(LocalDateTime.FromDateTime(value), tzId); + } + /// /// Creates a representing a DATE value. /// From 3ddabbb2e7362783a60905723b190b2b8aaa2584 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 19 Apr 2026 11:26:18 -0400 Subject: [PATCH 16/20] Get date only once for CalDateTime.Today DateTime.Today is calculated from DateTime.Now but NodaTime also does the same calculation, so just use DateTime.Now to get the same result. --- Ical.Net/DataTypes/CalDateTime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 8e870953d..c47313b6a 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -45,7 +45,7 @@ public sealed class CalDateTime : IFormattable, IEquatable /// Creates a /// with the current local date and no time or time zone. /// - public static CalDateTime Today => FromDateTimeDate(DateTime.Today); + public static CalDateTime Today => FromDateTimeDate(DateTime.Now); /// /// Creates a From 2f020eda4d0b4388895c7c0608fb3981487bcdc0 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 19 Apr 2026 11:56:40 -0400 Subject: [PATCH 17/20] Clarify CalDateTime converting --- Ical.Net/DataTypes/CalDateTime.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index c47313b6a..1cf8483ba 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -279,7 +279,7 @@ public override bool Equals(object? obj) #if NET6_0_OR_GREATER /// - /// Creates a representing a DATE value. + /// Converts a to a representing a DATE value. /// /// The local date. public static CalDateTime FromDateOnly(DateOnly date) => new(date.ToLocalDate()); @@ -296,8 +296,8 @@ public override bool Equals(object? obj) #endif /// - /// Creates a representing a DATE-TIME value - /// with an optional time zone. + /// 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 . @@ -316,7 +316,7 @@ public static CalDateTime FromDateTime(DateTime value, string? tzId = null) } /// - /// Creates a representing a DATE value. + /// 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 . @@ -351,7 +351,7 @@ public LocalDateTime ToLocalDateTime() => _localDate.At(_localTime ?? LocalTime.Midnight); /// - /// Creates a representing a DATE-TIME value + /// Converts a to a representing a DATE-TIME value /// with a time zone. /// /// The time zone offset from the is ignored, @@ -367,7 +367,7 @@ public LocalDateTime ToLocalDateTime() /// Values without a time zone will throw an . /// Use to handle floating values. /// - /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// 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 @@ -387,7 +387,7 @@ public ZonedDateTime ToZonedDateTime() /// DATE values will default to . /// /// Values without a time zone will be treated as being in the specified time zone. - /// If the local date and time is ambiguous due to the time zone, it will be resolved using . + /// Ambiguous local time will return the earlier time and skipped local time will be shifted foward. /// /// 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. From 613d5678b20055b4ca79e1bd346d9b1484cce37c Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 19 Apr 2026 14:29:20 -0400 Subject: [PATCH 18/20] Add CalDateTime tests --- Ical.Net.Tests/CalDateTimeTests.cs | 102 +++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 99d633ce7..55569aefd 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using Ical.Net.DataTypes; +using NodaTime; using NUnit.Framework; using NUnit.Framework.Constraints; @@ -204,4 +205,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 TimeValuesAreZeroForDateValues() + { + var c = new CalDateTime(2026, 4, 19); + + using (Assert.EnterMultipleScope()) + { + Assert.That(c.Hour, Is.Zero); + Assert.That(c.Minute, Is.Zero); + Assert.That(c.Second, Is.Zero); + } + } + + [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)); + } } From a5fa4994d3899533887fe023e912a5643f6c60a3 Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 19 Apr 2026 12:15:01 -0400 Subject: [PATCH 19/20] CalDateTime case insensitve hash code --- Ical.Net.Tests/CalDateTimeTests.cs | 15 +++++++++++++-- Ical.Net/DataTypes/CalDateTime.cs | 5 ++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 55569aefd..5db1c3637 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -178,8 +178,6 @@ public void Simple_PropertyAndMethod_HasTime_Tests() ]; [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); @@ -306,4 +304,17 @@ public void DateValueDoesNotEqualDateTimeAtMidnight() 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/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 1cf8483ba..c88e4f551 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -167,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) { From ccac7da267f5c321e41b4d4b9893d2b3ac1a601e Mon Sep 17 00:00:00 2001 From: Mark Knapp Date: Sun, 26 Apr 2026 10:40:09 -0400 Subject: [PATCH 20/20] Remove CalDateTime hour minute second --- Ical.Net.Tests/CalDateTimeTests.cs | 13 -------- Ical.Net.Tests/SerializationTests.cs | 32 +++++++------------ Ical.Net/DataTypes/CalDateTime.cs | 15 --------- .../DataTypes/DateTimeSerializer.cs | 6 ++-- 4 files changed, 14 insertions(+), 52 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 5db1c3637..6dc97fdec 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -273,19 +273,6 @@ public void DayOfWeekIsCorrect() Assert.That(c.DayOfWeek, Is.EqualTo(DayOfWeek.Sunday)); } - [Test] - public void TimeValuesAreZeroForDateValues() - { - var c = new CalDateTime(2026, 4, 19); - - using (Assert.EnterMultipleScope()) - { - Assert.That(c.Hour, Is.Zero); - Assert.That(c.Minute, Is.Zero); - Assert.That(c.Second, Is.Zero); - } - } - [Test] public void OtherTypeIsNotEqual() { diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index 651c7cbf7..a1f7af424 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() ]); diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index c88e4f551..a72364c42 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -244,21 +244,6 @@ public override int GetHashCode() => HashCode.Combine( /// public int Day => _localDate.Day; - /// - /// Gets the hour. Defaults to 0 for DATE values. - /// - public int Hour => _localTime?.Hour ?? 0; - - /// - /// Gets the minute. Defaults to 0 for DATE values. - /// - public int Minute => _localTime?.Minute ?? 0; - - /// - /// Gets the second. Defaults to 0 for DATE values. - /// - public int Second => _localTime?.Second ?? 0; - /// /// Gets the DayOfWeek. /// diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index b6b55b339..f46d628a6 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'); } }