From 92c3535194affff1deeda7fe55a3bcac1b926e14 Mon Sep 17 00:00:00 2001 From: axunonb Date: Sat, 9 Nov 2024 16:30:57 +0100 Subject: [PATCH 1/9] Enhance CalDateTime and DateTimeSerializer * Introduced new methods and test cases in `CalDateTimeTests.cs` to ensure correct behavior of `CalDateTime` properties and methods. * Updated various tests in `DeserializationTests.cs` and `RecurrenceTests.cs` to handle `CalDateTime` without time components and improve clarity. * Refined `IsAllDay` property in `CalendarEvent.cs` and updated methods in `UniqueComponent.cs` and `VTimeZone.cs` to use `CalDateTime` with time components. * Removed outdated comments and improved code formatting. * Enabled nullable reference types and updated `IDateTime` interface. * Added new methods and properties to `CalDateTime.cs` for better date and time management. * Refactored methods in `Period`, `RecurrencePatternEvaluator`, and `RecurringEvaluator` classes to handle `HasTime` property correctly. * Improved `DateTimeSerializer` class for better performance and handling of nullable types. * Added XML documentation and marked obsolete methods. Resolves #630 Resolves #633 Resolves #635 Resolves #636 Resolves #637 --- Ical.Net.Benchmarks/ApplicationWorkflows.cs | 4 +- Ical.Net.Benchmarks/CalDateTimePerfTests.cs | 2 +- Ical.Net.Benchmarks/OccurencePerfTests.cs | 6 +- Ical.Net.Benchmarks/Runner.cs | 4 +- Ical.Net.Benchmarks/SerializationPerfTests.cs | 6 +- Ical.Net.Benchmarks/ThroughputTests.cs | 2 +- Ical.Net.Tests/AlarmTest.cs | 200 ++---- Ical.Net.Tests/AttendeeTest.cs | 9 +- Ical.Net.Tests/CalDateTimeTests.cs | 176 ++++- Ical.Net.Tests/DeserializationTests.cs | 63 +- Ical.Net.Tests/RecurrenceTests.cs | 284 ++++---- Ical.Net.Tests/SerializationTests.cs | 108 ++-- Ical.Net.Tests/SimpleDeserializationTests.cs | 62 +- Ical.Net/CalendarComponents/CalendarEvent.cs | 4 +- .../CalendarComponents/UniqueComponent.cs | 6 +- Ical.Net/CalendarComponents/VTimeZone.cs | 31 +- Ical.Net/DataTypes/CalDateTime.cs | 604 ++++++++++++------ Ical.Net/DataTypes/IDateTime.cs | 9 +- Ical.Net/DataTypes/Period.cs | 15 +- Ical.Net/DataTypes/Trigger.cs | 6 +- .../Evaluation/RecurrencePatternEvaluator.cs | 223 +++---- Ical.Net/Evaluation/RecurrenceUtil.cs | 54 +- Ical.Net/Evaluation/RecurringEvaluator.cs | 8 +- Ical.Net/Ical.Net.csproj | 3 + .../DataTypes/DateTimeSerializer.cs | 96 ++- .../DataTypes/PeriodSerializer.cs | 6 +- .../DataTypes/RecurrencePatternSerializer.cs | 28 +- 27 files changed, 1125 insertions(+), 894 deletions(-) diff --git a/Ical.Net.Benchmarks/ApplicationWorkflows.cs b/Ical.Net.Benchmarks/ApplicationWorkflows.cs index 4358a662..82863cac 100644 --- a/Ical.Net.Benchmarks/ApplicationWorkflows.cs +++ b/Ical.Net.Benchmarks/ApplicationWorkflows.cs @@ -3,12 +3,12 @@ // Licensed under the MIT license. // +using BenchmarkDotNet.Attributes; +using Ical.Net.DataTypes; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using BenchmarkDotNet.Attributes; -using Ical.Net.DataTypes; namespace Ical.Net.Benchmarks; diff --git a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs index eabb0d38..8767cd62 100644 --- a/Ical.Net.Benchmarks/CalDateTimePerfTests.cs +++ b/Ical.Net.Benchmarks/CalDateTimePerfTests.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. // -using System; using BenchmarkDotNet.Attributes; using Ical.Net.DataTypes; +using System; namespace Ical.Net.Benchmarks; diff --git a/Ical.Net.Benchmarks/OccurencePerfTests.cs b/Ical.Net.Benchmarks/OccurencePerfTests.cs index 02096158..250b7099 100644 --- a/Ical.Net.Benchmarks/OccurencePerfTests.cs +++ b/Ical.Net.Benchmarks/OccurencePerfTests.cs @@ -3,12 +3,12 @@ // Licensed under the MIT license. // -using System; -using System.Collections.Generic; -using System.Linq; using BenchmarkDotNet.Attributes; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; +using System; +using System.Collections.Generic; +using System.Linq; namespace Ical.Net.Benchmarks; diff --git a/Ical.Net.Benchmarks/Runner.cs b/Ical.Net.Benchmarks/Runner.cs index 8383e2c1..fd4dbc1c 100644 --- a/Ical.Net.Benchmarks/Runner.cs +++ b/Ical.Net.Benchmarks/Runner.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. // -using System.IO; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; +using System.IO; namespace Ical.Net.Benchmarks; @@ -15,7 +15,7 @@ private static void Main(string[] args) { #if DEBUG BenchmarkSwitcher.FromAssembly(typeof(ApplicationWorkflows).Assembly).Run(args, new DebugInProcessConfig()); -#else +#else #region * ApplicationWorkflows results * /* // * Summary * diff --git a/Ical.Net.Benchmarks/SerializationPerfTests.cs b/Ical.Net.Benchmarks/SerializationPerfTests.cs index 552be24b..185e3256 100644 --- a/Ical.Net.Benchmarks/SerializationPerfTests.cs +++ b/Ical.Net.Benchmarks/SerializationPerfTests.cs @@ -3,13 +3,13 @@ // Licensed under the MIT license. // -using System; -using System.Collections.Generic; -using System.Linq; using BenchmarkDotNet.Attributes; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using Ical.Net.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; namespace Ical.Net.Benchmarks; diff --git a/Ical.Net.Benchmarks/ThroughputTests.cs b/Ical.Net.Benchmarks/ThroughputTests.cs index c1d557ce..6c7f22fb 100644 --- a/Ical.Net.Benchmarks/ThroughputTests.cs +++ b/Ical.Net.Benchmarks/ThroughputTests.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. // +using BenchmarkDotNet.Attributes; using System; using System.Linq; -using BenchmarkDotNet.Attributes; namespace Ical.Net.Benchmarks; diff --git a/Ical.Net.Tests/AlarmTest.cs b/Ical.Net.Tests/AlarmTest.cs index 896d2dca..122e8b8e 100644 --- a/Ical.Net.Tests/AlarmTest.cs +++ b/Ical.Net.Tests/AlarmTest.cs @@ -1,174 +1,94 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // -using System; -using System.Collections.Generic; -using System.Linq; +using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; namespace Ical.Net.Tests; [TestFixture] -public class AlarmTest +public class AttendeeTest { - private const string _tzid = "US-Eastern"; - - public void TestAlarm(string calendarString, List dates, CalDateTime start, CalDateTime end) + internal static CalendarEvent VEventFactory() => new CalendarEvent { - var iCal = Calendar.Load(calendarString); - ProgramTest.TestCal(iCal); - var evt = iCal.Events.First(); - - // Poll all alarms that occurred between Start and End - var alarms = evt.PollAlarms(start, end); + Summary = "Testing", + Start = new CalDateTime(2010, 3, 25), + End = new CalDateTime(2010, 3, 26) + }; - var utcDates = new HashSet(dates.Select(d => d.AsUtc)); - - //Only compare the UTC values here, since we care about the time coordinate when the alarm fires, and nothing else - foreach (var alarm in alarms.Select(a => a.DateTime.AsUtc)) - { - Assert.That(utcDates.Contains(alarm), Is.True, "Alarm triggers at " + alarm + ", but it should not."); - } - Assert.That(dates.Count == alarms.Count, Is.True, "There were " + alarms.Count + " alarm occurrences; there should have been " + dates.Count + "."); - } - - [Test, Category("Alarm")] - public void Alarm1() + private static readonly IList _attendees = new List { - var dateTimes = new List(); - dateTimes.AddRange(new[] + new Attendee("MAILTO:james@example.com") { - new CalDateTime(2006, 7, 18, 9, 30, 0, _tzid) - }); - - var content = IcsFiles.Alarm1; - TestAlarm(content, dateTimes, new CalDateTime(2006, 7, 1, _tzid), new CalDateTime(2006, 9, 1, _tzid)); - } - - [Test, Category("Alarm")] - public void Alarm2() - { - var dateTimes = new List(); - dateTimes.AddRange(new[] + CommonName = "James James", + Role = ParticipationRole.RequiredParticipant, + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Tentative + }, + new Attendee("MAILTO:mary@example.com") { - new CalDateTime(2006, 7, 18, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 20, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 22, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 24, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 26, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 28, 9, 30, 0, _tzid), - new CalDateTime(2006, 7, 30, 9, 30, 0, _tzid), - new CalDateTime(2006, 8, 1, 9, 30, 0, _tzid), - new CalDateTime(2006, 8, 3, 9, 30, 0, _tzid), - new CalDateTime(2006, 8, 5, 9, 30, 0, _tzid) - }); + CommonName = "Mary Mary", + Role = ParticipationRole.RequiredParticipant, + Rsvp = true, + ParticipationStatus = EventParticipationStatus.Accepted + } + }.AsReadOnly(); - var content = IcsFiles.Alarm2; - TestAlarm(content, dateTimes, new CalDateTime(2006, 7, 1, _tzid), new CalDateTime(2006, 9, 1, _tzid)); - } - [Test, Category("Alarm")] - public void Alarm3() + /// + /// Ensures that attendees can be properly added to an event. + /// + [Test, Category("Attendee")] + public void Add1Attendee() { - var dateTimes = new List(); - dateTimes.AddRange(new[] - { - new CalDateTime(1998, 2, 11, 9, 0, 0, _tzid), - new CalDateTime(1998, 3, 11, 9, 0, 0, _tzid), - new CalDateTime(1998, 11, 11, 9, 0, 0, _tzid), - new CalDateTime(1999, 8, 11, 9, 0, 0, _tzid), - new CalDateTime(2000, 10, 11, 9, 0, 0, _tzid) - }); + var evt = VEventFactory(); + Assert.That(evt.Attendees.Count, Is.EqualTo(0)); - var content = IcsFiles.Alarm3; - TestAlarm(content, dateTimes, new CalDateTime(1997, 1, 1, _tzid), new CalDateTime(2000, 12, 31, _tzid)); - } + evt.Attendees.Add(_attendees[0]); + Assert.That(evt.Attendees, Has.Count.EqualTo(1)); - [Test, Category("Alarm")] - public void Alarm4() - { - var dateTimes = new List(); - dateTimes.AddRange(new[] + Assert.Multiple(() => { - new CalDateTime(1998, 2, 11, 9, 0, 0, _tzid), - new CalDateTime(1998, 2, 11, 11, 0, 0, _tzid), - new CalDateTime(1998, 2, 11, 13, 0, 0, _tzid), - new CalDateTime(1998, 2, 11, 15, 0, 0, _tzid), - new CalDateTime(1998, 3, 11, 9, 0, 0, _tzid), - new CalDateTime(1998, 3, 11, 11, 0, 0, _tzid), - new CalDateTime(1998, 3, 11, 13, 0, 0, _tzid), - new CalDateTime(1998, 3, 11, 15, 0, 0, _tzid), - new CalDateTime(1998, 11, 11, 9, 0, 0, _tzid), - new CalDateTime(1998, 11, 11, 11, 0, 0, _tzid), - new CalDateTime(1998, 11, 11, 13, 0, 0, _tzid), - new CalDateTime(1998, 11, 11, 15, 0, 0, _tzid), - new CalDateTime(1999, 8, 11, 9, 0, 0, _tzid), - new CalDateTime(1999, 8, 11, 11, 0, 0, _tzid), - new CalDateTime(1999, 8, 11, 13, 0, 0, _tzid), - new CalDateTime(1999, 8, 11, 15, 0, 0, _tzid), - new CalDateTime(2000, 10, 11, 9, 0, 0, _tzid), - new CalDateTime(2000, 10, 11, 11, 0, 0, _tzid), - new CalDateTime(2000, 10, 11, 13, 0, 0, _tzid), - new CalDateTime(2000, 10, 11, 15, 0, 0, _tzid) + //the properties below had been set to null during the Attendees.Add operation in NuGet version 2.1.4 + Assert.That(evt.Attendees[0].Role, Is.EqualTo(ParticipationRole.RequiredParticipant)); + Assert.That(evt.Attendees[0].ParticipationStatus, Is.EqualTo(EventParticipationStatus.Tentative)); }); - - var content = IcsFiles.Alarm4; - TestAlarm(content, dateTimes, new CalDateTime(1997, 1, 1, _tzid), new CalDateTime(2000, 12, 31, _tzid)); } - [Test, Category("Alarm")] - public void Alarm5() + [Test, Category("Attendee")] + public void Add2Attendees() { - var dateTimes = new List(); - dateTimes.AddRange(new[] - { - new CalDateTime(1998, 1, 2, 8, 0, 0, _tzid) - }); + var evt = VEventFactory(); + Assert.That(evt.Attendees.Count, Is.EqualTo(0)); - var content = IcsFiles.Alarm5; - TestAlarm(content, dateTimes, new CalDateTime(1997, 7, 1, _tzid), new CalDateTime(2000, 12, 31, _tzid)); + evt.Attendees.Add(_attendees[0]); + evt.Attendees.Add(_attendees[1]); + Assert.That(evt.Attendees, Has.Count.EqualTo(2)); + Assert.That(evt.Attendees[1].Role, Is.EqualTo(ParticipationRole.RequiredParticipant)); } - [Test, Category("Alarm")] - public void Alarm6() + /// + /// Ensures that attendees can be properly removed from an event. + /// + [Test, Category("Attendee")] + public void Remove1Attendee() { - var dateTimes = new List(); - dateTimes.AddRange(new[] - { - new CalDateTime(1998, 1, 2, 8, 0, 0, _tzid), - new CalDateTime(1998, 1, 5, 8, 0, 0, _tzid), - new CalDateTime(1998, 1, 8, 8, 0, 0, _tzid), - new CalDateTime(1998, 1, 11, 8, 0, 0, _tzid), - new CalDateTime(1998, 1, 14, 8, 0, 0, _tzid), - new CalDateTime(1998, 1, 17, 8, 0, 0, _tzid) - }); + var evt = VEventFactory(); + Assert.That(evt.Attendees.Count, Is.EqualTo(0)); - var content = IcsFiles.Alarm6; - TestAlarm(content, dateTimes, new CalDateTime(1997, 7, 1, _tzid), new CalDateTime(2000, 12, 31, _tzid)); - } + var attendee = _attendees.First(); + evt.Attendees.Add(attendee); + Assert.That(evt.Attendees, Has.Count.EqualTo(1)); - [Test, Category("Alarm")] - public void Alarm7() - { - var dateTimes = new List(); - dateTimes.AddRange(new[] - { - new CalDateTime(2006, 7, 18, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 20, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 22, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 24, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 26, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 28, 10, 30, 0, _tzid), - new CalDateTime(2006, 7, 30, 10, 30, 0, _tzid), - new CalDateTime(2006, 8, 1, 10, 30, 0, _tzid), - new CalDateTime(2006, 8, 3, 10, 30, 0, _tzid), - new CalDateTime(2006, 8, 5, 10, 30, 0, _tzid) - }); + evt.Attendees.Remove(attendee); + Assert.That(evt.Attendees.Count, Is.EqualTo(0)); - var content = IcsFiles.Alarm7; - TestAlarm(content, dateTimes, new CalDateTime(2006, 7, 1, _tzid), new CalDateTime(2006, 9, 1, _tzid)); + evt.Attendees.Remove(_attendees.Last()); + Assert.That(evt.Attendees.Count, Is.EqualTo(0)); } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/AttendeeTest.cs b/Ical.Net.Tests/AttendeeTest.cs index d0be4a58..545346f7 100644 --- a/Ical.Net.Tests/AttendeeTest.cs +++ b/Ical.Net.Tests/AttendeeTest.cs @@ -3,13 +3,12 @@ // Licensed under the MIT license. // -using System.Collections.Generic; -using System.Linq; +using Ical.Net; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using NUnit.Framework; - -namespace Ical.Net.Tests; +using System.Collections.Generic; +using System.Linq; [TestFixture] public class AttendeeTest @@ -91,4 +90,4 @@ public void Remove1Attendee() evt.Attendees.Remove(_attendees.Last()); Assert.That(evt.Attendees.Count, Is.EqualTo(0)); } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 26f39eda..8620e0a7 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -3,12 +3,13 @@ // Licensed under the MIT license. // -using System; -using System.Collections; -using System.Collections.Generic; using Ical.Net.CalendarComponents; using Ical.Net.DataTypes; using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; namespace Ical.Net.Tests; @@ -16,6 +17,7 @@ public class CalDateTimeTests { private static readonly DateTime _now = DateTime.Now; private static readonly DateTime _later = _now.AddHours(1); + private static CalendarEvent GetEventWithRecurrenceRules(string tzId) { var dailyForFiveDays = new RecurrencePattern(FrequencyType.Daily, 1) @@ -94,13 +96,15 @@ public static IEnumerable AsDateTimeOffsetTestCases() var convertedToNySummer = new CalDateTime(summerDate, "UTC"); convertedToNySummer.TzId = nyTzId; yield return new TestCaseData(convertedToNySummer) - .SetName("Summer UTC DateTime converted to NY time zone by setting TzId returns a DateTimeOffset with UTC-4") + .SetName( + "Summer UTC DateTime converted to NY time zone by setting TzId returns a DateTimeOffset with UTC-4") .Returns(new DateTimeOffset(summerDate, nySummerOffset)); var noTz = new CalDateTime(summerDate); var currentSystemOffset = TimeZoneInfo.Local.GetUtcOffset(summerDate); yield return new TestCaseData(noTz) - .SetName($"Summer DateTime with no time zone information returns the system-local's UTC offset ({currentSystemOffset})") + .SetName( + $"Summer DateTime with no time zone information returns the system-local's UTC offset ({currentSystemOffset})") .Returns(new DateTimeOffset(summerDate, currentSystemOffset)); } @@ -155,4 +159,164 @@ public static IEnumerable DateTimeKindOverrideTestCases() .Returns(DateTimeKind.Unspecified) .SetName("DateTime with Kind = Unspecified and null tzid returns DateTimeKind.Unspecified"); } -} \ No newline at end of file + + [Test, TestCaseSource(nameof(ToStringTestCases))] + public string ToStringTests(CalDateTime calDateTime, string format, IFormatProvider formatProvider) + => calDateTime.ToString(format, formatProvider); + + public static IEnumerable ToStringTestCases() + { + yield return new TestCaseData(new CalDateTime(2024, 8, 30, 10, 30, 0, tzId: "Pacific/Auckland"), "O", null) + .Returns("2024-08-30T10:30:00.0000000+12:00 Pacific/Auckland") + .SetName("Date and time with 'O' format arg, default culture"); + + yield return new TestCaseData(new CalDateTime(2024, 8, 30, tzId: "Pacific/Auckland"), "O", null) + .Returns("08/30/2024 Pacific/Auckland") + .SetName("Date only with 'O' format arg, default culture"); + + yield return new TestCaseData(new CalDateTime(2024, 8, 30, 10, 30, 0, tzId: "Pacific/Auckland"), "O", + CultureInfo.GetCultureInfo("fr-FR")) + .Returns("2024-08-30T10:30:00.0000000+12:00 Pacific/Auckland") + .SetName("Date and time with 'O' format arg, French culture"); + + yield return new TestCaseData(new CalDateTime(2024, 8, 30, 10, 30, 0, tzId: "Pacific/Auckland"), + "yyyy-MM-dd", CultureInfo.InvariantCulture) + .Returns("2024-08-30 Pacific/Auckland") + .SetName("Date and time with custom format, default culture"); + + yield return new TestCaseData(new CalDateTime(2024, 8, 30, 10, 30, 0, tzId: "Pacific/Auckland"), + "MM/dd/yyyy HH:mm:ss", CultureInfo.GetCultureInfo("FR")) + .Returns("08/30/2024 10:30:00 Pacific/Auckland") + .SetName("Date and time with format and 'FR' CultureInfo"); + + yield return new TestCaseData(new CalDateTime(2024, 8, 30, tzId: "Pacific/Auckland"), null, + CultureInfo.GetCultureInfo("IT")) + .Returns("30/08/2024 Pacific/Auckland") + .SetName("Date only with 'IT' CultureInfo and default format arg"); + } + + [Test] + public void SetValue_AppliesSameRulesAsWith_CTOR() + { + var dateTime = new DateTime(2024, 8, 30, 10, 30, 0, DateTimeKind.Unspecified); + var tzId = "Europe/Berlin"; + + var dt1 = new CalDateTime(dateTime, tzId); + var dt2 = new CalDateTime(DateTime.Now, tzId); + dt2.Value = dateTime; + + Assert.Multiple(() => + { + // TzId changes the DateTimeKind to Local + Assert.That(dt1.Value.Kind, Is.Not.EqualTo(dateTime.Kind)); + Assert.That(dt1.Value.Kind, Is.EqualTo(dt2.Value.Kind)); + Assert.That(dt1.TzId, Is.EqualTo(dt2.TzId)); + }); + } + + [Test] + public void SetValue_LeavesExistingPropertiesUnchanged() + { + var cal = new Calendar(); + var dateTime = new DateTime(2024, 8, 30, 10, 30, 0, DateTimeKind.Unspecified); + var tzId = "Europe/Berlin"; + + var dt = new CalDateTime(dateTime, tzId, false) + { + AssociatedObject = cal + }; + var hasTimeInitial = dt.HasTime; + + dt.Value = DateTime.MinValue; + + // Properties should remain unchanged + Assert.Multiple(() => + { + Assert.That(dt.HasTime, Is.EqualTo(hasTimeInitial)); + Assert.That(dt.TzId, Is.EqualTo(tzId)); + Assert.That(dt.Calendar, Is.SameAs(cal)); + }); + } + + [Test] + public void Simple_PropertyAndMethod_HasTime_Tests() + { + var dt = new DateTime(2025, 1, 2, 10, 20, 30, DateTimeKind.Utc); + 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, null); + var c3 = new CalDateTime(new DateOnly(dt.Year, dt.Month, dt.Day), + new TimeOnly(dt.Hour, dt.Minute, dt.Second), dt.Kind, c.TzId); + + Assert.Multiple(() => + { + Assert.That(c2.Ticks, Is.EqualTo(c3.Ticks)); + Assert.That(c2.TzId, Is.EqualTo(c3.TzId)); + Assert.That(CalDateTime.UtcNow.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + Assert.That(c.Millisecond, Is.EqualTo(0)); + Assert.That(c.Ticks, Is.EqualTo(dt.Ticks)); + Assert.That(c.DayOfYear, Is.EqualTo(dt.DayOfYear)); + Assert.That(c.TimeOfDay, Is.EqualTo(dt.TimeOfDay)); + Assert.That(c.Subtract(TimeSpan.FromSeconds(dt.Second)).Value.Second, Is.EqualTo(0)); + Assert.That(c.AddYears(1).Value, Is.EqualTo(dt.AddYears(1))); + Assert.That(c.AddMonths(1).Value, Is.EqualTo(dt.AddMonths(1))); + Assert.That(c.AddDays(1).Value, Is.EqualTo(dt.AddDays(1))); + Assert.That(c.AddHours(1).Value, Is.EqualTo(dt.AddHours(1))); + Assert.That(c.AddMinutes(1).Value, Is.EqualTo(dt.AddMinutes(1))); + Assert.That(c.AddSeconds(15).Value, Is.EqualTo(dt.AddSeconds(15))); + Assert.That(c.AddMilliseconds(100).Value, Is.EqualTo(dt.AddMilliseconds(0))); // truncated + Assert.That(c.AddMilliseconds(1000).Value, Is.EqualTo(dt.AddMilliseconds(1000))); + Assert.That(c.AddTicks(1).Value, Is.EqualTo(dt.AddTicks(0))); // truncated + Assert.That(c.AddTicks(TimeSpan.FromMinutes(1).Ticks).Value, Is.EqualTo(dt.AddTicks(TimeSpan.FromMinutes(1).Ticks))); + Assert.That(c.DateOnlyValue, Is.EqualTo(new DateOnly(dt.Year, dt.Month, dt.Day))); + Assert.That(c.TimeOnlyValue, Is.EqualTo(new TimeOnly(dt.Hour, dt.Minute, dt.Second))); + Assert.That(c.ToString("dd.MM.yyyy"), Is.EqualTo("02.01.2025 Europe/Berlin")); + }); + } + + [Test] + public void Simple_PropertyAndMethod_NotHasTime_Tests() + { + var dt = new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc); + var c = new CalDateTime(dt, tzId: "Europe/Berlin", hasTime: false); + + // Adding time to a date-only value should not change the HasTime property + Assert.Multiple(() => + { + var result = c.AddHours(1); + Assert.That(result.HasTime, Is.EqualTo(true)); + + result = c.AddMinutes(1); + Assert.That(result.HasTime, Is.EqualTo(true)); + + result = c.AddSeconds(1); + Assert.That(result.HasTime, Is.EqualTo(true)); + + result = c.AddMilliseconds(1000); + Assert.That(result.HasTime, Is.EqualTo(true)); + + result = c.AddTicks(TimeSpan.FromMinutes(1).Ticks); + Assert.That(result.HasTime, Is.EqualTo(true)); + }); + } + + [Test] + public void Toggling_HasDate_ShouldSucceed() + { + var dateTime = new DateTime(2025, 1, 2, 10, 20, 30, DateTimeKind.Utc); + var dt = new CalDateTime(dateTime); + Assert.Multiple(() => + { + Assert.That(dt.HasTime, Is.True); + Assert.That(dt.HasDate, Is.True); + + dt.HasDate = false; + Assert.That(dt.HasDate, Is.False); + Assert.That(dt.DateOnlyValue.HasValue, Is.False); + Assert.That(() => dt.Value, Throws.InstanceOf()); + + dt.HasDate = true; + Assert.That(dt.HasDate, Is.True); + }); + } +} diff --git a/Ical.Net.Tests/DeserializationTests.cs b/Ical.Net.Tests/DeserializationTests.cs index 57ca53aa..79cf2d17 100644 --- a/Ical.Net.Tests/DeserializationTests.cs +++ b/Ical.Net.Tests/DeserializationTests.cs @@ -234,20 +234,20 @@ public void Encoding2() var evt = iCal.Events.First(); Assert.That( - evt.Attachments[0].ToString(), - Is.EqualTo("This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large."), - "Attached value does not match."); + evt.Attachments[0].ToString(), + Is.EqualTo("This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large.\r\n" + + "This is a test to try out base64 encoding without being too large."), + "Attached value does not match."); } [Test] @@ -356,9 +356,9 @@ public void RecurrenceDates1() Assert.Multiple(() => { - Assert.That(iCal.Events.First().RecurrenceDates[0][0].StartTime, Is.EqualTo((CalDateTime) new DateTime(1997, 7, 14, 12, 30, 0, DateTimeKind.Utc))); - Assert.That(iCal.Events.First().RecurrenceDates[1][0].StartTime, Is.EqualTo((CalDateTime) new DateTime(1996, 4, 3, 2, 0, 0, DateTimeKind.Utc))); - Assert.That(iCal.Events.First().RecurrenceDates[1][0].EndTime, Is.EqualTo((CalDateTime) new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[0][0].StartTime, Is.EqualTo((CalDateTime)new DateTime(1997, 7, 14, 12, 30, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[1][0].StartTime, Is.EqualTo((CalDateTime)new DateTime(1996, 4, 3, 2, 0, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[1][0].EndTime, Is.EqualTo((CalDateTime)new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc))); Assert.That(iCal.Events.First().RecurrenceDates[2][0].StartTime, Is.EqualTo(new CalDateTime(1997, 1, 1))); Assert.That(iCal.Events.First().RecurrenceDates[2][1].StartTime, Is.EqualTo(new CalDateTime(1997, 1, 20))); Assert.That(iCal.Events.First().RecurrenceDates[2][2].StartTime, Is.EqualTo(new CalDateTime(1997, 2, 17))); @@ -428,16 +428,16 @@ public void String2() { var serializer = new StringSerializer(); var value = @"test\with\;characters"; - var unescaped = (string) serializer.Deserialize(new StringReader(value)); + var unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo(@"test\with;characters"), "String unescaping was incorrect."); value = @"C:\Path\To\My\New\Information"; - unescaped = (string) serializer.Deserialize(new StringReader(value)); + unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo("C:\\Path\\To\\My\new\\Information"), "String unescaping was incorrect."); value = @"\""This\r\nis\Na\, test\""\;\\;,"; - unescaped = (string) serializer.Deserialize(new StringReader(value)); + unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo("\"This\\r\nis\na, test\";\\;,"), "String unescaping was incorrect."); } @@ -453,22 +453,13 @@ public void Transparency2() Assert.That(evt.Transparency, Is.EqualTo(TransparencyType.Transparent)); } - /// - /// Tests that DateTime values that are out-of-range are still parsed correctly - /// and set to the closest representable date/time in .NET. - /// [Test] - public void DateTime1() + public void DateTime1_Unrepresentable_DateTimeArgs_ShouldThrow() { - var iCal = Calendar.Load(IcsFiles.DateTime1); - Assert.That(iCal.Events, Has.Count.EqualTo(6)); - - var evt = iCal.Events["nc2o66s0u36iesitl2l0b8inn8@google.com"]; - Assert.That(evt, Is.Not.Null); - - // The "Created" date is out-of-bounds. It should be coerced to the - // closest representable date/time. - Assert.That(evt.Created.Value, Is.EqualTo(DateTime.MinValue)); + Assert.That(() => + { + _ = Calendar.Load(IcsFiles.DateTime1); + }, Throws.Exception.TypeOf()); } [Test] @@ -492,7 +483,7 @@ public void Outlook2007_LineFolds2() var longName = "The Exceptionally Long Named Meeting Room Whose Name Wraps Over Several Lines When Exported From Leading Calendar and Office Software Application Microsoft Office 2007"; var iCal = Calendar.Load(IcsFiles.Outlook2007LineFolds); var events = iCal.GetOccurrences(new CalDateTime(2009, 06, 20), new CalDateTime(2009, 06, 22)).OrderBy(o => o.Period.StartTime).ToList(); - Assert.That(((CalendarEvent) events[0].Source).Location, Is.EqualTo(longName)); + Assert.That(((CalendarEvent)events[0].Source).Location, Is.EqualTo(longName)); } /// @@ -552,4 +543,4 @@ public void Property1() Assert.That(props[i].Value, Is.EqualTo("2." + i)); } } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index bac40fba..7086759c 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -1906,7 +1906,7 @@ public void Minutely_DefinedNumberOfOccurrences_ShouldSucceed() var end = new CalDateTime(2007, 6, 21, 12, 0, 1, _tzid); // End period is exclusive, not inclusive. var dateTimes = new List(); - for (var dt = start; dt.LessThan(end); dt = (CalDateTime) dt.AddMinutes(1)) + for (var dt = start; dt.LessThan(end); dt = (CalDateTime)dt.AddMinutes(1)) { dateTimes.Add(new CalDateTime(dt)); } @@ -1923,7 +1923,7 @@ public void Hourly_DefinedNumberOfOccurrences_ShouldSucceed() var end = new CalDateTime(2007, 6, 25, 8, 0, 1, _tzid); // End period is exclusive, not inclusive. var dateTimes = new List(); - for (var dt = start; dt.LessThan(end); dt = (CalDateTime) dt.AddHours(1)) + for (var dt = start; dt.LessThan(end); dt = (CalDateTime)dt.AddHours(1)) { dateTimes.Add(new CalDateTime(dt)); } @@ -2476,20 +2476,22 @@ public void Bug3007244() { var iCal = Calendar.Load(IcsFiles.Bug3007244); + // CalDateTimes.HasTime = false EventOccurrenceTest( cal: iCal, - fromDate: new CalDateTime(2010, 7, 18, 0, 0, 0), - toDate: new CalDateTime(2010, 7, 26, 0, 0, 0), - dateTimes: new[] { new CalDateTime(2010, 05, 23, 0, 0, 0), }, + fromDate: new CalDateTime(2010, 7, 18), + toDate: new CalDateTime(2010, 7, 26), + dateTimes: new[] { new CalDateTime(2010, 05, 23) }, timeZones: null, eventIndex: 0 ); + // CalDateTimes.HasTime = false EventOccurrenceTest( cal: iCal, - fromDate: new CalDateTime(2011, 7, 18, 0, 0, 0), - toDate: new CalDateTime(2011, 7, 26, 0, 0, 0), - dateTimes: new[] { new CalDateTime(2011, 05, 23, 0, 0, 0), }, + fromDate: new CalDateTime(2011, 7, 18), + toDate: new CalDateTime(2011, 7, 26), + dateTimes: new[] { new CalDateTime(2011, 05, 23) }, timeZones: null, eventIndex: 0 ); @@ -2505,7 +2507,7 @@ public void BugByWeekNoNotWorking() var end = new DateTime(2019, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYWEEKNO=2")); - var recurringPeriods = rpe.Evaluate(new CalDateTime(start), start, end, false); + var recurringPeriods = rpe.Evaluate(new CalDateTime(start, false), start, end, false); Assert.That(recurringPeriods, Has.Count.EqualTo(1)); Assert.That(recurringPeriods.First().StartTime, Is.EqualTo(new CalDateTime(2019, 1, 7))); @@ -2521,7 +2523,7 @@ public void BugByMonthWhileFreqIsWeekly() var end = new DateTime(2020, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=WEEKLY;BYDAY=MO;BYMONTH=1")); - var recurringPeriods = rpe.Evaluate(new CalDateTime(start), start, end, false).OrderBy(x => x).ToList(); + var recurringPeriods = rpe.Evaluate(new CalDateTime(start, false), start, end, false).OrderBy(x => x).ToList(); Assert.That(recurringPeriods, Has.Count.EqualTo(4)); Assert.Multiple(() => @@ -2564,7 +2566,7 @@ public void BugByMonthWhileFreqIsMonthly() var end = new DateTime(2020, 12, 31); var rpe = new RecurrencePatternEvaluator(new RecurrencePattern("FREQ=MONTHLY;BYDAY=MO;BYMONTH=1")); - var recurringPeriods = rpe.Evaluate(new CalDateTime(start), start, end, false).OrderBy(x => x).ToList(); + var recurringPeriods = rpe.Evaluate(new CalDateTime(start, false), start, end, false).OrderBy(x => x).ToList(); Assert.That(recurringPeriods, Has.Count.EqualTo(4)); Assert.Multiple(() => @@ -2587,7 +2589,7 @@ public void Bug3119920() { var start = DateTime.Parse("2010-11-27 9:00:00"); var serializer = new RecurrencePatternSerializer(); - var rp = (RecurrencePattern) serializer.Deserialize(sr); + var rp = (RecurrencePattern)serializer.Deserialize(sr); var rpe = new RecurrencePatternEvaluator(rp); var recurringPeriods = rpe.Evaluate(new CalDateTime(start), start, rp.Until, false); @@ -2635,7 +2637,7 @@ public void Bug3292737() using (var sr = new StringReader("FREQ=WEEKLY;UNTIL=20251126")) { var serializer = new RecurrencePatternSerializer(); - var rp = (RecurrencePattern) serializer.Deserialize(sr); + var rp = (RecurrencePattern)serializer.Deserialize(sr); Assert.That(rp, Is.Not.Null); Assert.That(rp.Until, Is.EqualTo(new DateTime(2025, 11, 26))); @@ -2772,7 +2774,7 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) evt.Summary = "Event summary"; // Start at midnight, UTC time - evt.Start = new CalDateTime(DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc)) { HasTime = false }; + evt.Start = new CalDateTime(DateTime.UtcNow.Date, false); // 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. @@ -2818,10 +2820,10 @@ public void RecurrencePattern1() Assert.That(evaluator, Is.Not.Null); var occurrences = evaluator.Evaluate( - startDate, - DateUtil.SimpleDateTimeToMatch(fromDate, startDate), - DateUtil.SimpleDateTimeToMatch(toDate, startDate), - false) + startDate, + DateUtil.SimpleDateTimeToMatch(fromDate, startDate), + DateUtil.SimpleDateTimeToMatch(toDate, startDate), + false) .OrderBy(o => o.StartTime) .ToList(); Assert.That(occurrences, Has.Count.EqualTo(4)); @@ -2962,10 +2964,10 @@ public void Test4() // Add the exception dates var periods = evaluator.Evaluate( - evtStart, - DateUtil.GetSimpleDateTimeData(evtStart), - DateUtil.SimpleDateTimeToMatch(evtEnd, evtStart), - false) + evtStart, + DateUtil.GetSimpleDateTimeData(evtStart), + DateUtil.SimpleDateTimeToMatch(evtEnd, evtStart), + false) .OrderBy(p => p.StartTime) .ToList(); Assert.That(periods, Has.Count.EqualTo(10)); @@ -3016,7 +3018,7 @@ public void RDateShouldBeUnionedWithRecurrenceSet() { //Issues #118 and #107 on Github const string ical = - @"BEGIN:VCALENDAR +@"BEGIN:VCALENDAR PRODID:-//ddaysoftware.com//NONSGML DDay.iCal 1.0//EN VERSION:2.0 BEGIN:VEVENT @@ -3098,9 +3100,9 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() var lastExpected = new CalDateTime(DateTime.Parse("2016-08-31T07:00:00"), "UTC"); var occurrences = firstEvent.GetOccurrences(startSearch, endSearch) - .Select(o => o.Period) - .OrderBy(p => p.StartTime) - .ToList(); + .Select(o => o.Period) + .OrderBy(p => p.StartTime) + .ToList(); Assert.That(occurrences.Last().StartTime.Equals(lastExpected), Is.False); @@ -3118,12 +3120,12 @@ public void OccurrenceMustBeCompletelyContainedWithinSearchRange() /// Evaluate relevancy and validity of the request. /// Find a solution for issue #120 or close forever /// - [Test, Ignore("Turn on in v3", Until = "2024-12-31")] + [Test, Ignore("No solution for issue #120 yet", Until = "2024-12-31")] public void EventsWithShareUidsShouldGenerateASingleRecurrenceSet() { //https://github.com/rianjs/ical.net/issues/120 dated Sep 5, 2016 const string ical = - @"BEGIN:VCALENDAR +@"BEGIN:VCALENDAR PRODID:-//Google Inc//Google Calendar 70.9054//EN VERSION:2.0 CALSCALE:GREGORIAN @@ -3217,21 +3219,21 @@ public void AddExDateToEventAfterGetOccurrencesShouldRecomputeResult() var searchEnd = _now.AddDays(7); var e = GetEventWithRecurrenceRules(); var occurrences = e.GetOccurrences(searchStart, searchEnd); - Assert.That(occurrences.Count == 5, Is.True); + Assert.That(occurrences, Has.Count.EqualTo(5)); var exDate = _now.AddDays(1); - var period = new Period(new CalDateTime(exDate)); + var period = new Period(new CalDateTime(exDate, false)); var periodList = new PeriodList { period }; e.ExceptionDates.Add(periodList); occurrences = e.GetOccurrences(searchStart, searchEnd); - Assert.That(occurrences.Count == 4, Is.True); + Assert.That(occurrences, Has.Count.EqualTo(4)); //Specifying just a date should "black out" that date var excludeTwoDaysFromNow = _now.AddDays(2).Date; - period = new Period(new CalDateTime(excludeTwoDaysFromNow)); + period = new Period(new CalDateTime(excludeTwoDaysFromNow, false)); periodList.Add(period); occurrences = e.GetOccurrences(searchStart, searchEnd); - Assert.That(occurrences.Count == 3, Is.True); + Assert.That(occurrences, Has.Count.EqualTo(3)); } private static readonly DateTime _now = DateTime.Now; @@ -3388,46 +3390,52 @@ public void RecurrenceRuleTests() Assert.That(eventB, Is.EqualTo(eventA)); - const string aString = @"BEGIN:VCALENDAR -PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN -VERSION:2.0 -BEGIN:VEVENT -DTEND;TZID=UTC:20170228T140000 -DTSTAMP;TZID=UTC:20170413T135927 -DTSTART;TZID=UTC:20170228T060000 -EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 - 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 - 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 - 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 - 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 - 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 - 2T060000,20170413T060000,20170417T060000 -IMPORTANCE:None -RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR -UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 -END:VEVENT -END:VCALENDAR"; - - const string bString = @"BEGIN:VCALENDAR -PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN -VERSION:2.0 -BEGIN:VEVENT -DTEND;TZID=UTC:20170228T140000 -DTSTAMP;TZID=UTC:20170428T171444 -DTSTART;TZID=UTC:20170228T060000 -EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 - 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 - 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 - 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 - 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 - 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 - 2T060000 -EXDATE;TZID=UTC:20170417T060000,20170413T060000 -IMPORTANCE:None -RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR -UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 -END:VEVENT -END:VCALENDAR"; + const string aString = + """ + BEGIN:VCALENDAR + PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN + VERSION:2.0 + BEGIN:VEVENT + DTEND;TZID=UTC:20170228T140000 + DTSTAMP;TZID=UTC:20170413T135927 + DTSTART;TZID=UTC:20170228T060000 + EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 + 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 + 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 + 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 + 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 + 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 + 2T060000,20170413T060000,20170417T060000 + IMPORTANCE:None + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR + UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 + END:VEVENT + END:VCALENDAR + """; + + const string bString = + """ + BEGIN:VCALENDAR + PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN + VERSION:2.0 + BEGIN:VEVENT + DTEND;TZID=UTC:20170228T140000 + DTSTAMP;TZID=UTC:20170428T171444 + DTSTART;TZID=UTC:20170228T060000 + EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 + 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 + 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 + 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 + 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 + 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 + 2T060000 + EXDATE;TZID=UTC:20170417T060000,20170413T060000 + IMPORTANCE:None + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR + UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 + END:VEVENT + END:VCALENDAR + """; var simpleA = Calendar.Load(aString); var normalA = Calendar.Load(aString); @@ -3457,70 +3465,67 @@ public void RecurrenceRuleTests() [Test] public void ManyExclusionDatesEqualityTesting() { - const string icalA = @"BEGIN:VCALENDAR -PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN -VERSION:2.0 -BEGIN:VEVENT -DTEND;TZID=UTC:20170228T140000 -DTSTAMP;TZID=UTC:20170428T145334 -DTSTART;TZID=UTC:20170228T060000 -EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 - 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 - 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 - 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 - 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 - 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 - 2T060000,20170413T060000,20170417T060000,20170418T060000,20170419T060000, - 20170420T060000,20170421T060000,20170424T060000,20170425T060000,20170427T - 060000,20170428T060000,20170501T060000 -IMPORTANCE:None -RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR -UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 -END:VEVENT -END:VCALENDAR"; + const string icalA = + """ + BEGIN:VCALENDAR + PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN + VERSION:2.0 + BEGIN:VEVENT + DTEND;TZID=UTC:20170228T140000 + DTSTAMP;TZID=UTC:20170428T145334 + DTSTART;TZID=UTC:20170228T060000 + EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 + 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 + 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 + 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 + 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 + 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 + 2T060000,20170413T060000,20170417T060000,20170418T060000,20170419T060000, + 20170420T060000,20170421T060000,20170424T060000,20170425T060000,20170427T + 060000,20170428T060000,20170501T060000 + IMPORTANCE:None + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR + UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 + END:VEVENT + END:VCALENDAR + """; + + const string icalB = + """ + BEGIN:VCALENDAR + PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN + VERSION:2.0 + BEGIN:VEVENT + DTEND;TZID=UTC:20170228T140000 + DTSTAMP;TZID=UTC:20170501T131355 + DTSTART;TZID=UTC:20170228T060000 + EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 + 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 + 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 + 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 + 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 + 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 + 2T060000,20170413T060000,20170417T060000,20170418T060000,20170419T060000, + 20170420T060000,20170421T060000,20170424T060000,20170425T060000,20170427T + 060000,20170428T060000,20170501T060000 + IMPORTANCE:None + RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR + UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 + END:VEVENT + END:VCALENDAR + """; + + // The only textual difference between A and B + // is a different DTSTAMP, which is not considered significant for equality or hashing - const string icalB = @"BEGIN:VCALENDAR -PRODID:-//github.com/rianjs/ical.net//NONSGML ical.net 2.2//EN -VERSION:2.0 -BEGIN:VEVENT -DTEND;TZID=UTC:20170228T140000 -DTSTAMP;TZID=UTC:20170501T131355 -DTSTART;TZID=UTC:20170228T060000 -EXDATE;TZID=UTC:20170302T060000,20170303T060000,20170306T060000,20170307T0 - 60000,20170308T060000,20170309T060000,20170310T060000,20170313T060000,201 - 70314T060000,20170317T060000,20170320T060000,20170321T060000,20170322T060 - 000,20170323T060000,20170324T060000,20170327T060000,20170328T060000,20170 - 329T060000,20170330T060000,20170331T060000,20170403T060000,20170405T06000 - 0,20170406T060000,20170407T060000,20170410T060000,20170411T060000,2017041 - 2T060000,20170413T060000,20170417T060000,20170418T060000,20170419T060000, - 20170420T060000,20170421T060000,20170424T060000,20170425T060000,20170427T - 060000,20170428T060000,20170501T060000 -IMPORTANCE:None -RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR -UID:001b7e43-98df-4fcc-b9ec-345a28a4fc14 -END:VEVENT -END:VCALENDAR"; - - //The only textual difference between A and B is a different DTSTAMP, which is not considered significant for equality or hashing - - //Tautologies... var collectionA = CalendarCollection.Load(icalA); - Assert.That(collectionA, Is.EqualTo(collectionA)); - Assert.That(collectionA.GetHashCode(), Is.EqualTo(collectionA.GetHashCode())); - var calendarA = collectionA.First(); - Assert.That(calendarA, Is.EqualTo(calendarA)); - Assert.That(calendarA.GetHashCode(), Is.EqualTo(calendarA.GetHashCode())); - var eventA = calendarA.Events.First(); - Assert.That(eventA, Is.EqualTo(eventA)); - Assert.That(eventA.GetHashCode(), Is.EqualTo(eventA.GetHashCode())); - var collectionB = CalendarCollection.Load(icalB); - Assert.That(collectionB, Is.EqualTo(collectionB)); - Assert.That(collectionB.GetHashCode(), Is.EqualTo(collectionB.GetHashCode())); + var calendarA = collectionA.First(); var calendarB = collectionB.First(); - Assert.That(calendarB, Is.EqualTo(calendarB)); - Assert.That(calendarB.GetHashCode(), Is.EqualTo(calendarB.GetHashCode())); + var eventA = calendarA.Events.First(); var eventB = calendarB.Events.First(); + var exDatesA = eventA.ExceptionDates; + var exDatesB = eventB.ExceptionDates; Assert.Multiple(() => { @@ -3531,17 +3536,12 @@ public void ManyExclusionDatesEqualityTesting() Assert.That(calendarB.GetHashCode(), Is.EqualTo(calendarA.GetHashCode())); Assert.That(eventB, Is.EqualTo(eventA)); Assert.That(eventB.GetHashCode(), Is.EqualTo(eventA.GetHashCode())); + Assert.That(exDatesB, Is.EqualTo(exDatesA)); }); - - - var exDatesA = eventA.ExceptionDates; - var exDatesB = eventB.ExceptionDates; - Assert.That(exDatesB, Is.EqualTo(exDatesA)); - } [Test, TestCaseSource(nameof(UntilTimeZoneSerializationTestCases))] - public void UntilTimeZoneSerializationTests(string tzid, DateTimeKind expectedKind) + public void UntilTimeZoneSerializationTests(string tzId, DateTimeKind expectedKind) { var now = DateTime.SpecifyKind(DateTime.Parse("2017-11-08 10:30:00"), expectedKind); var later = now.AddHours(1); @@ -3554,8 +3554,8 @@ public void UntilTimeZoneSerializationTests(string tzid, DateTimeKind expectedKi }; var e = new CalendarEvent { - Start = new CalDateTime(now, tzid), - End = new CalDateTime(later, tzid) + Start = new CalDateTime(now, tzId), + End = new CalDateTime(later, tzId) }; e.RecurrenceRules.Add(rrule); var calendar = new Calendar @@ -3728,4 +3728,4 @@ public void ExecuteRecurrenceTestCase(RecurrenceTestCase testCase) Assert.That(startDates, Is.EqualTo(testCase.Instances)); } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index 06996246..cd3321e5 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -57,12 +57,12 @@ internal static void CompareComponents(ICalendarComponent cb1, ICalendarComponen Assert.That(p2, Is.EqualTo(p1), "The properties '" + p1.Name + "' are not equal."); if (p1.Value is IComparable) { - if (((IComparable) p1.Value).CompareTo(p2.Value) != 0) + if (((IComparable)p1.Value).CompareTo(p2.Value) != 0) continue; } else if (p1.Value is IEnumerable) { - CompareEnumerables((IEnumerable) p1.Value, (IEnumerable) p2.Value, p1.Name); + CompareEnumerables((IEnumerable)p1.Value, (IEnumerable)p2.Value, p1.Name); } else { @@ -115,11 +115,11 @@ public static string InspectSerializedSection(string serialized, string sectionN { const string notFound = "expected '{0}' not found"; var searchFor = "BEGIN:" + sectionName; - var begin = serialized.IndexOf(searchFor); + var begin = serialized.IndexOf(searchFor, StringComparison.Ordinal); Assert.That(begin, Is.Not.EqualTo(-1), () => string.Format(notFound, searchFor)); searchFor = "END:" + sectionName; - var end = serialized.IndexOf(searchFor, begin); + var end = serialized.IndexOf(searchFor, begin, StringComparison.Ordinal); Assert.That(end, Is.Not.EqualTo(-1), () => string.Format(notFound, searchFor)); var searchRegion = serialized.Substring(begin, end - begin + searchFor.Length); @@ -163,7 +163,6 @@ private static Dictionary GetValues(string serialized, string na [Test, Category("Serialization"), Ignore("TODO: standard time, for NZ standard time (current example)")] public void TimeZoneSerialize() { - //ToDo: This test is broken as of 2016-07-13 var cal = new Calendar { Method = "PUBLISH", @@ -197,10 +196,10 @@ public void TimeZoneSerialize() InspectSerializedSection(vTimezone, "DAYLIGHT", new[] { "TZNAME:" + tzi.DaylightName, "TZOFFSETFROM:" + o }); } + [Test, Category("Serialization")] public void SerializeDeserialize() { - //ToDo: This test is broken as of 2016-07-13 var cal1 = new Calendar { Method = "PUBLISH", @@ -232,7 +231,6 @@ public void SerializeDeserialize() [Test, Category("Serialization")] public void EventPropertiesSerialized() { - //ToDo: This test is broken as of 2016-07-13 var cal = new Calendar { Method = "PUBLISH", @@ -333,13 +331,13 @@ public void AttendeesSerialized() { var vals = GetValues(vEvt, "ATTENDEE", a.Value.OriginalString.ToString()); foreach (var v in new Dictionary - { - ["CN"] = a.CommonName, - ["ROLE"] = a.Role, - ["RSVP"] = a.Rsvp.ToString() - .ToUpperInvariant(), - ["PARTSTAT"] = a.ParticipationStatus - }) + { + ["CN"] = a.CommonName, + ["ROLE"] = a.Role, + ["RSVP"] = a.Rsvp.ToString() + .ToUpperInvariant(), + ["PARTSTAT"] = a.ParticipationStatus + }) { Assert.Multiple(() => { @@ -426,28 +424,31 @@ public void JournalStatusAllCaps() [Test] public void UnicodeDescription() { - const string ics = @"BEGIN:VEVENT -DTSTAMP:20171120T124856Z -DTSTART;TZID=Europe/Helsinki:20160707T110000 -DTEND;TZID=Europe/Helsinki:20160707T140000 -SUMMARY:Some summary -UID:20160627T123608Z-182847102@atlassian.net -DESCRIPTION:Key points:\n� Some text (text, - , text\, text\, TP) some text\;\n� some tex - t Some text (Text\, Text)\;\n� Some tex - t some text\, some text\, text.\;\n\nsome te - xt some tex�t some text. -ORGANIZER;X-CONFLUENCE-USER-KEY=ff801df01547101c6720006;CN=Some - user;CUTYPE=INDIVIDUAL:mailto:some.mail@domain.com -CREATED:20160627T123608Z -LAST-MODIFIED:20160627T123608Z -ATTENDEE;X-CONFLUENCE-USER-KEY=ff8080ef1df01547101c6720006;CN=Some - text;CUTYPE=INDIVIDUAL:mailto:some.mail@domain.com -SEQUENCE:1 -X-CONFLUENCE-SUBCALENDAR-TYPE:other -TRANSP:TRANSPARENT -STATUS:CONFIRMED -END:VEVENT"; + const string ics = + """ + BEGIN:VEVENT + DTSTAMP:20171120T124856Z + DTSTART;TZID=Europe/Helsinki:20160707T110000 + DTEND;TZID=Europe/Helsinki:20160707T140000 + SUMMARY:Some summary + UID:20160627T123608Z-182847102@atlassian.net + DESCRIPTION:Key points:\n� Some text (text, + , text\, text\, TP) some text\;\n� some tex + t Some text (Text\, Text)\;\n� Some tex + t some text\, some text\, text.\;\n\nsome te + xt some tex�t some text. + ORGANIZER;X-CONFLUENCE-USER-KEY=ff801df01547101c6720006;CN=Some + user;CUTYPE=INDIVIDUAL:mailto:some.mail@domain.com + CREATED:20160627T123608Z + LAST-MODIFIED:20160627T123608Z + ATTENDEE;X-CONFLUENCE-USER-KEY=ff8080ef1df01547101c6720006;CN=Some + text;CUTYPE=INDIVIDUAL:mailto:some.mail@domain.com + SEQUENCE:1 + X-CONFLUENCE-SUBCALENDAR-TYPE:other + TRANSP:TRANSPARENT + STATUS:CONFIRMED + END:VEVENT + """; var deserializedEvent = Calendar.Load(ics).Single(); Assert.Multiple(() => @@ -462,21 +463,24 @@ xt some tex�t some text. public void TestStandardDaylightTimeZoneInfoDeserialization() { - const string ics = @"BEGIN:VTIMEZONE -TZID: -BEGIN:STANDARD -DTSTART:16010101T030000 -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:16010101T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 -END:DAYLIGHT -END:VTIMEZONE"; + const string ics = + """ + BEGIN:VTIMEZONE + TZID: + BEGIN:STANDARD + DTSTART:16010101T030000 + TZOFFSETFROM:+0200 + TZOFFSETTO:+0100 + RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 + END:STANDARD + BEGIN:DAYLIGHT + DTSTART:16010101T020000 + TZOFFSETFROM:+0100 + TZOFFSETTO:+0200 + RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 + END:DAYLIGHT + END:VTIMEZONE + """; var timeZone = Calendar.Load(ics).Single(); Assert.That(timeZone, Is.Not.Null, "Expected the TimeZone to be successfully deserialized"); var timeZoneInfos = timeZone.TimeZoneInfos; @@ -580,4 +584,4 @@ public void SerializeSubcomponent() Assert.That(!serialized.Contains("VCALENDAR", StringComparison.Ordinal), Is.True); }); } -} \ No newline at end of file +} diff --git a/Ical.Net.Tests/SimpleDeserializationTests.cs b/Ical.Net.Tests/SimpleDeserializationTests.cs index f0c73aaf..ea20f11b 100644 --- a/Ical.Net.Tests/SimpleDeserializationTests.cs +++ b/Ical.Net.Tests/SimpleDeserializationTests.cs @@ -20,7 +20,6 @@ namespace Ical.Net.Tests; [TestFixture] public class SimpleDeserializationTests { - [Test, Category("Deserialization")] public void Attendee1() { @@ -237,19 +236,19 @@ public void Encoding2() var evt = iCal.Events.First(); Assert.That( - evt.Attachments[0].ToString(), +evt.Attachments[0].ToString(), Is.EqualTo("This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large.\r\n" + - "This is a test to try out base64 encoding without being too large."), +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large.\r\n" + +"This is a test to try out base64 encoding without being too large."), "Attached value does not match."); } @@ -359,9 +358,9 @@ public void RecurrenceDates1() Assert.Multiple(() => { - Assert.That(iCal.Events.First().RecurrenceDates[0][0].StartTime, Is.EqualTo((CalDateTime) new DateTime(1997, 7, 14, 12, 30, 0, DateTimeKind.Utc))); - Assert.That(iCal.Events.First().RecurrenceDates[1][0].StartTime, Is.EqualTo((CalDateTime) new DateTime(1996, 4, 3, 2, 0, 0, DateTimeKind.Utc))); - Assert.That(iCal.Events.First().RecurrenceDates[1][0].EndTime, Is.EqualTo((CalDateTime) new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[0][0].StartTime, Is.EqualTo((CalDateTime)new DateTime(1997, 7, 14, 12, 30, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[1][0].StartTime, Is.EqualTo((CalDateTime)new DateTime(1996, 4, 3, 2, 0, 0, DateTimeKind.Utc))); + Assert.That(iCal.Events.First().RecurrenceDates[1][0].EndTime, Is.EqualTo((CalDateTime)new DateTime(1996, 4, 3, 4, 0, 0, DateTimeKind.Utc))); Assert.That(iCal.Events.First().RecurrenceDates[2][0].StartTime, Is.EqualTo(new CalDateTime(1997, 1, 1))); Assert.That(iCal.Events.First().RecurrenceDates[2][1].StartTime, Is.EqualTo(new CalDateTime(1997, 1, 20))); Assert.That(iCal.Events.First().RecurrenceDates[2][2].StartTime, Is.EqualTo(new CalDateTime(1997, 2, 17))); @@ -431,16 +430,16 @@ public void String2() { var serializer = new StringSerializer(); var value = @"test\with\;characters"; - var unescaped = (string) serializer.Deserialize(new StringReader(value)); + var unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo(@"test\with;characters"), "String unescaping was incorrect."); value = @"C:\Path\To\My\New\Information"; - unescaped = (string) serializer.Deserialize(new StringReader(value)); + unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo("C:\\Path\\To\\My\new\\Information"), "String unescaping was incorrect."); value = @"\""This\r\nis\Na\, test\""\;\\;,"; - unescaped = (string) serializer.Deserialize(new StringReader(value)); + unescaped = (string)serializer.Deserialize(new StringReader(value)); Assert.That(unescaped, Is.EqualTo("\"This\\r\nis\na, test\";\\;,"), "String unescaping was incorrect."); } @@ -456,22 +455,15 @@ public void Transparency2() Assert.That(evt.Transparency, Is.EqualTo(TransparencyType.Transparent)); } - /// - /// Tests that DateTime values that are out-of-range are still parsed correctly - /// and set to the closest representable date/time in .NET. - /// [Test, Category("Deserialization")] - public void DateTime1() + public void DateTime1_Unrepresentable_DateTimeArgs_ShouldThrow() { - var iCal = SimpleDeserializer.Default.Deserialize(new StringReader(IcsFiles.DateTime1)).Cast().Single(); - Assert.That(iCal.Events, Has.Count.EqualTo(6)); - - var evt = iCal.Events["nc2o66s0u36iesitl2l0b8inn8@google.com"]; - Assert.That(evt, Is.Not.Null); - - // The "Created" date is out-of-bounds. It should be coerced to the - // closest representable date/time. - Assert.That(evt.Created.Value, Is.EqualTo(DateTime.MinValue)); + Assert.That(() => + { + _ = SimpleDeserializer.Default.Deserialize(new StringReader(IcsFiles.DateTime1)) + .Cast() + .Single(); + }, Throws.Exception.TypeOf()); } [Test, Category("Deserialization"), Ignore("Ignore until @thoemy commits the EventStatus.ics file")] @@ -524,7 +516,7 @@ public void Outlook2007_LineFolds2() var longName = "The Exceptionally Long Named Meeting Room Whose Name Wraps Over Several Lines When Exported From Leading Calendar and Office Software Application Microsoft Office 2007"; var iCal = SimpleDeserializer.Default.Deserialize(new StringReader(IcsFiles.Outlook2007LineFolds)).Cast().Single(); var events = iCal.GetOccurrences(new CalDateTime(2009, 06, 20), new CalDateTime(2009, 06, 22)).OrderBy(o => o.Period.StartTime).ToList(); - Assert.That(((CalendarEvent) events[0].Source).Location, Is.EqualTo(longName)); + Assert.That(((CalendarEvent)events[0].Source).Location, Is.EqualTo(longName)); } /// @@ -582,4 +574,4 @@ public void Property1() for (var i = 0; i < props.Count; i++) Assert.That(props[i].Value, Is.EqualTo("2." + i)); } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/CalendarEvent.cs b/Ical.Net/CalendarComponents/CalendarEvent.cs index 2f13549f..ba4173a5 100644 --- a/Ical.Net/CalendarComponents/CalendarEvent.cs +++ b/Ical.Net/CalendarComponents/CalendarEvent.cs @@ -350,7 +350,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((CalendarEvent) obj); + return obj.GetType() == GetType() && Equals((CalendarEvent)obj); } public override int GetHashCode() @@ -391,4 +391,4 @@ public int CompareTo(CalendarEvent other) } throw new Exception("An error occurred while comparing two CalDateTimes."); } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/UniqueComponent.cs b/Ical.Net/CalendarComponents/UniqueComponent.cs index ea355bd9..9fbec22e 100644 --- a/Ical.Net/CalendarComponents/UniqueComponent.cs +++ b/Ical.Net/CalendarComponents/UniqueComponent.cs @@ -47,7 +47,7 @@ private void EnsureProperties() { // icalendar RFC doesn't care about sub-second time resolution, so shave off everything smaller than seconds. var utcNow = DateTime.UtcNow.Truncate(TimeSpan.FromSeconds(1)); - DtStamp = new CalDateTime(utcNow, "UTC"); + DtStamp = CalDateTime.UtcNow; } } @@ -101,7 +101,7 @@ public override bool Equals(object obj) { if (obj is RecurringComponent && obj != this) { - var r = (RecurringComponent) obj; + var r = (RecurringComponent)obj; if (Uid != null) { return Uid.Equals(r.Uid); @@ -118,4 +118,4 @@ public virtual string Uid get => Properties.Get("UID"); set => Properties.Set("UID", value); } -} \ No newline at end of file +} diff --git a/Ical.Net/CalendarComponents/VTimeZone.cs b/Ical.Net/CalendarComponents/VTimeZone.cs index 3ca05b2c..d87597f4 100644 --- a/Ical.Net/CalendarComponents/VTimeZone.cs +++ b/Ical.Net/CalendarComponents/VTimeZone.cs @@ -1,16 +1,16 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // -using System; -using System.Collections.Generic; -using System.Linq; using Ical.Net.DataTypes; using Ical.Net.Proxies; using Ical.Net.Utility; using NodaTime; using NodaTime.TimeZones; +using System; +using System.Collections.Generic; +using System.Linq; namespace Ical.Net.CalendarComponents; @@ -182,7 +182,7 @@ private static VTimeZoneInfo CreateTimeZoneInfo(List matchedInterv timeZoneInfo.TimeZoneName = oldestInterval.Name; var start = oldestInterval.IsoLocalStart.ToDateTimeUnspecified() + delta; - timeZoneInfo.Start = new CalDateTime(start) { HasTime = true }; + timeZoneInfo.Start = new CalDateTime(start, true); if (isRRule) { @@ -201,11 +201,11 @@ private static List GetMatchingIntervals(List interv var matchedIntervals = intervals .Where(x => x.Start != Instant.MinValue) .Where(x => x.IsoLocalStart.Month == intervalToMatch.IsoLocalStart.Month - && x.IsoLocalStart.Hour == intervalToMatch.IsoLocalStart.Hour - && x.IsoLocalStart.Minute == intervalToMatch.IsoLocalStart.Minute - && x.IsoLocalStart.ToDateTimeUnspecified().DayOfWeek == intervalToMatch.IsoLocalStart.ToDateTimeUnspecified().DayOfWeek - && x.WallOffset == intervalToMatch.WallOffset - && x.Name == intervalToMatch.Name) + && x.IsoLocalStart.Hour == intervalToMatch.IsoLocalStart.Hour + && x.IsoLocalStart.Minute == intervalToMatch.IsoLocalStart.Minute + && x.IsoLocalStart.ToDateTimeUnspecified().DayOfWeek == intervalToMatch.IsoLocalStart.ToDateTimeUnspecified().DayOfWeek + && x.WallOffset == intervalToMatch.WallOffset + && x.Name == intervalToMatch.Name) .ToList(); if (!consecutiveOnly) @@ -243,13 +243,12 @@ private static void PopulateTimeZoneInfoRecurrenceDates(VTimeZoneInfo tzi, List< { var periodList = new PeriodList(); var time = interval.IsoLocalStart.ToDateTimeUnspecified(); - var date = new CalDateTime(time).Add(delta) as CalDateTime; + var date = new CalDateTime(time, true).Add(delta) as CalDateTime; if (date == null) { continue; } - date.HasTime = true; periodList.Add(date); tzi.RecurrenceDates.Add(periodList); } @@ -365,15 +364,15 @@ public string Location protected bool Equals(VTimeZone other) => string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(TzId, other.TzId, StringComparison.OrdinalIgnoreCase) - && Equals(Url, other.Url); + && string.Equals(TzId, other.TzId, StringComparison.OrdinalIgnoreCase) + && Equals(Url, other.Url); public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((VTimeZone) obj); + return Equals((VTimeZone)obj); } public override int GetHashCode() @@ -386,4 +385,4 @@ public override int GetHashCode() return hashCode; } } -} \ No newline at end of file +} diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 76941924..cf5ea07d 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -1,13 +1,15 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // -using System; -using System.IO; +#nullable enable using Ical.Net.Serialization.DataTypes; using Ical.Net.Utility; using NodaTime; +using System; +using System.Globalization; +using System.IO; namespace Ical.Net.DataTypes; @@ -15,116 +17,169 @@ namespace Ical.Net.DataTypes; /// The iCalendar equivalent of the .NET class. /// /// In addition to the features of the class, the -/// class handles time zone differences, and integrates seamlessly into the iCalendar framework. +/// class handles time zones, and integrates seamlessly into the iCalendar framework. /// /// public sealed class CalDateTime : EncodableDataType, IDateTime { + // The date and time parts that were used to initialize the instance + // or by the Value setter. + private DateTime _value; + // The date part that is used to return the Value property. + private DateOnly? _dateOnly; + // The time part that is used to return the Value property. + private TimeOnly? _timeOnly; + + const double AlmostZeroEpsilon = 1e-10; + public static CalDateTime Now => new CalDateTime(DateTime.Now); public static CalDateTime Today => new CalDateTime(DateTime.Today); - private bool _hasDate; - private bool _hasTime; + public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow); + /// + /// This constructor is required for the SerializerFactory to work. + /// public CalDateTime() { } + /// + /// Creates a new instance of the class + /// respecting the setting. + /// + /// public CalDateTime(IDateTime value) { - Initialize(value.Value, value.TzId, null); + Initialize(value.Value, value.HasTime, value.TzId, value.Calendar); } - public CalDateTime(DateTime value) : this(value, null) { } + /// + /// Creates a new instance of the class + /// and sets the to "UTC" if the + /// has a of . + /// + /// + /// Set to (default), if the must be included. + public CalDateTime(DateTime value, bool hasTime = true) : this(value, value.Kind == DateTimeKind.Utc ? "UTC" : null, hasTime) + { } /// - /// Specifying a `tzId` value will override `value`'s `DateTimeKind` property. If the time zone specified is UTC, the underlying `DateTimeKind` will be - /// `Utc`. If a non-UTC time zone is specified, the underlying `DateTimeKind` property will be `Local`. If no time zone is specified, the `DateTimeKind` - /// property will be left untouched. + /// Creates a new instance of the class using the specified time zone. /// - public CalDateTime(DateTime value, string tzId) + /// + /// The specified value will override value's property. + /// If the time zone specified is UTC, the underlying will be + /// . If a non-UTC time zone is specified, the underlying + /// property will be . + /// If no time zone is specified, the property will be left untouched. + /// Set to (default), if the must be included. + public CalDateTime(DateTime value, string? tzId, bool hasTime = true) { - Initialize(value, tzId, null); + Initialize(value, hasTime, tzId, null); } - public CalDateTime(int year, int month, int day, int hour, int minute, int second) - { - Initialize(year, month, day, hour, minute, second, null, null); - HasTime = true; + /// + /// Creates a new instance of the class using the specified time zone. + /// Sets for the property. + /// + /// + /// The specified value will override value's property. + /// If a non-UTC time zone is specified, the underlying property will be . + /// If no time zone is specified, the property will be left untouched. + /// + /// + /// + /// + /// + /// + /// + public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null, Calendar? cal = null) //NOSONAR - must keep this signature + { + Initialize(new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified), true, tzId, cal); } - public CalDateTime(int year, int month, int day, int hour, int minute, int second, string tzId) + /// + /// Creates a new instance of the class using the specified time zone. + /// Sets for the property. + /// + /// The specified value will override value's property. + /// If a non-UTC time zone is specified, the underlying property will be . + /// If no time zone is specified, the property will be left untouched. + /// + /// + /// + /// + public CalDateTime(int year, int month, int day, string? tzId = null) { - Initialize(year, month, day, hour, minute, second, tzId, null); - HasTime = true; + Initialize(new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified), false, tzId, null); } - public CalDateTime(int year, int month, int day, int hour, int minute, int second, string tzId, Calendar cal) - { - Initialize(year, month, day, hour, minute, second, tzId, cal); - HasTime = true; + /// + /// Creates a new instance of the class using the specified time zone. + /// + /// If , is used. + /// The specified value will override value's property. + /// If a non-UTC time zone is specified, the underlying property will be . + /// If no time zone is specified, the property will be left untouched. + /// + /// + /// + /// + public CalDateTime(DateOnly date, TimeOnly? time, DateTimeKind kind, string? tzId = null, Calendar? cal = null) + { + if (time.HasValue) + Initialize(new DateTime(date.Year, date.Month, date.Day, time.Value.Hour, time.Value.Minute, time.Value.Second, kind), true, tzId, cal); + else + Initialize(new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, kind), false, tzId, cal); } - public CalDateTime(int year, int month, int day) : this(year, month, day, 0, 0, 0) { } - public CalDateTime(int year, int month, int day, string tzId) : this(year, month, day, 0, 0, 0, tzId) { } - + /// + /// Creates a new instance of the class by parsing + /// using the . + /// + /// An iCalendar-compatible date or date-time string. public CalDateTime(string value) { var serializer = new DateTimeSerializer(); - CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable); + CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable + ?? throw new InvalidOperationException("Failure deserializing value")); } - private void Initialize(int year, int month, int day, int hour, int minute, int second, string tzId, Calendar cal) + private void Initialize(DateTime dateTime, bool hasTime, string? tzId, Calendar? cal) { - Initialize(CoerceDateTime(year, month, day, hour, minute, second, DateTimeKind.Local), tzId, cal); - } + DateTime initialValue; - private void Initialize(DateTime value, string tzId, Calendar cal) - { - if (!string.IsNullOrWhiteSpace(tzId) && !tzId.Equals("UTC", StringComparison.OrdinalIgnoreCase)) + if ((tzId != null && !string.IsNullOrWhiteSpace(tzId) && !tzId.Equals("UTC", StringComparison.OrdinalIgnoreCase)) + || (string.IsNullOrWhiteSpace(tzId) && dateTime.Kind == DateTimeKind.Local)) { // Definitely local - value = DateTime.SpecifyKind(value, DateTimeKind.Local); - TzId = tzId; + _tzId = tzId; + + initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Local); + } - else if (string.Equals("UTC", tzId, StringComparison.OrdinalIgnoreCase) || value.Kind == DateTimeKind.Utc) + else if (string.Equals("UTC", tzId, StringComparison.OrdinalIgnoreCase) || dateTime.Kind == DateTimeKind.Utc) { - // Probably UTC - value = DateTime.SpecifyKind(value, DateTimeKind.Utc); - TzId = "UTC"; - } - - Value = new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Kind); - HasDate = true; - HasTime = value.Second != 0 || value.Minute != 0 || value.Hour != 0; - AssociatedObject = cal; - } + // It is UTC + _tzId = "UTC"; - private DateTime CoerceDateTime(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind) - { - var dt = DateTime.MinValue; - - // NOTE: determine if a date/time value exceeds the representable date/time values in .NET. - // If so, let's automatically adjust the date/time to compensate. - // FIXME: should we have a parsing setting that will throw an exception - // instead of automatically adjusting the date/time value to the - // closest representable date/time? - try + initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + else { - if (year > 9999) - { - dt = DateTime.MaxValue; - } - else if (year > 0) - { - dt = new DateTime(year, month, day, hour, minute, second, kind); - } + // Unspecified + _tzId = null; + + initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); } - catch { } - return dt; + SynchronizeDateTimeFields(initialValue, hasTime); + + AssociatedObject = cal; } - public override ICalendarObject AssociatedObject + /// + public override ICalendarObject? AssociatedObject { get => base.AssociatedObject; set @@ -141,17 +196,21 @@ public override void CopyFrom(ICopyable obj) { base.CopyFrom(obj); - var dt = obj as IDateTime; - if (dt == null) + if (obj is not IDateTime dt) { return; } - _value = dt.Value; - _hasDate = dt.HasDate; - _hasTime = dt.HasTime; - // String assignments create new instances - TzId = dt.TzId; + if (dt is CalDateTime calDt) + { + // Maintain the private date/time backing fields + _dateOnly = calDt._dateOnly; + _timeOnly = calDt._timeOnly; + + // Copy the underlying DateTime value and time zone + _value = calDt._value; + _tzId = calDt._tzId; + } AssociateWith(dt); } @@ -159,9 +218,11 @@ public override void CopyFrom(ICopyable obj) public bool Equals(CalDateTime other) => this == other; - public override bool Equals(object other) - => other is IDateTime && (CalDateTime) other == this; + /// + public override bool Equals(object? obj) + => obj is IDateTime && (CalDateTime)obj == this; + /// public override int GetHashCode() { unchecked @@ -174,146 +235,174 @@ public override int GetHashCode() } } - public static bool operator <(CalDateTime left, IDateTime right) + public static bool operator <(CalDateTime? left, IDateTime? right) => left != null && right != null && left.AsUtc < right.AsUtc; - public static bool operator >(CalDateTime left, IDateTime right) + public static bool operator >(CalDateTime? left, IDateTime? right) => left != null && right != null && left.AsUtc > right.AsUtc; - public static bool operator <=(CalDateTime left, IDateTime right) + public static bool operator <=(CalDateTime? left, IDateTime? right) => left != null && right != null && left.AsUtc <= right.AsUtc; - public static bool operator >=(CalDateTime left, IDateTime right) + public static bool operator >=(CalDateTime? left, IDateTime? right) => left != null && right != null && left.AsUtc >= right.AsUtc; - public static bool operator ==(CalDateTime left, IDateTime right) + public static bool operator ==(CalDateTime? left, IDateTime? right) { return ReferenceEquals(left, null) || ReferenceEquals(right, null) ? ReferenceEquals(left, right) - : right is CalDateTime - && left.Value.Equals(right.Value) - && left.HasDate == right.HasDate - && left.AsUtc.Equals(right.AsUtc) - && string.Equals(left.TzId, right.TzId, StringComparison.OrdinalIgnoreCase); + : right is CalDateTime calDateTime + && left.Value.Equals(calDateTime.Value) + && left.HasDate == calDateTime.HasDate + && left.HasTime == calDateTime.HasTime + && left.AsUtc.Equals(calDateTime.AsUtc) + && string.Equals(left.TzId, calDateTime.TzId, StringComparison.OrdinalIgnoreCase); } - public static bool operator !=(CalDateTime left, IDateTime right) + public static bool operator !=(CalDateTime? left, IDateTime? right) => !(left == right); - public static TimeSpan operator -(CalDateTime left, IDateTime right) - { - left.AssociateWith(right); - return left.AsUtc - right.AsUtc; - } - + /// + /// Subtracts a from the . + /// + /// + /// This will also set to , + /// if the is not a multiple of 24 hours. + /// public static IDateTime operator -(CalDateTime left, TimeSpan right) { - var copy = left.Copy(); + var copy = left.Copy(); + if (Math.Abs(right.TotalDays % 1) > AlmostZeroEpsilon) + { + copy.HasTime = true; + } copy.Value -= right; return copy; } + /// + /// Adds a to the . + /// + /// + /// This will also set to , + /// if the is not a multiple of 24 hours. + /// public static IDateTime operator +(CalDateTime left, TimeSpan right) { - var copy = left.Copy(); + var copy = left.Copy(); + if (Math.Abs(right.TotalDays % 1) > AlmostZeroEpsilon) + { + copy.HasTime = true; + } copy.Value += right; return copy; } + /// + /// Creates a new instance of with for + /// public static implicit operator CalDateTime(DateTime left) => new CalDateTime(left); /// - /// Converts the date/time to the date/time of the computer running the program. If the DateTimeKind is Unspecified, it's assumed that the underlying + /// Converts the date/time to the date/time of the computer running the program. + /// If the DateTimeKind is Unspecified, it's assumed that the underlying /// Value already represents the system's datetime. /// - public DateTime AsSystemLocal - { - get - { - if (Value.Kind == DateTimeKind.Unspecified) - { - return HasTime - ? Value - : Value.Date; - } + public DateTime AsSystemLocal => AsDateTimeOffset.LocalDateTime; - return HasTime - ? Value.ToLocalTime() - : Value.ToLocalTime().Date; - } - } + /// + /// Returns a representation of the in UTC. + /// + public DateTime AsUtc => AsDateTimeOffset.UtcDateTime; + + /// + /// Gets the underlying of . + /// + public DateOnly? DateOnlyValue => _dateOnly; + + /// + /// Gets the underlying of . + /// + public TimeOnly? TimeOnlyValue => _timeOnly; - private DateTime _asUtc = DateTime.MinValue; /// - /// Returns a representation of the DateTime in Coordinated Universal Time (UTC) + /// Gets the underlying . + /// Depending on setting, + /// the returned has + /// set to midnight or the time from initialization. The precision of the time part is up to seconds. + /// + /// See also and for the date and time parts. /// - public DateTime AsUtc + public DateTime Value { get { - if (_asUtc == DateTime.MinValue) + // HasDate and HasTime both have setters, so they can be changed. + if (_dateOnly.HasValue && _timeOnly.HasValue) { - // In order of weighting: - // 1) Specified TzId - // 2) Value having a DateTimeKind.Utc - // 3) Use the OS's time zone - - if (!string.IsNullOrWhiteSpace(TzId)) - { - var asLocal = DateUtil.ToZonedDateTimeLeniently(Value, TzId); - _asUtc = asLocal.ToDateTimeUtc(); - } - else if (IsUtc || Value.Kind == DateTimeKind.Utc) - { - _asUtc = DateTime.SpecifyKind(Value, DateTimeKind.Utc); - } - else - { - _asUtc = DateTime.SpecifyKind(Value, DateTimeKind.Local).ToUniversalTime(); - } + return new DateTime(_dateOnly.Value.Year, _dateOnly.Value.Month, + _dateOnly.Value.Day, _timeOnly.Value.Hour, _timeOnly.Value.Minute, _timeOnly.Value.Second, + _value.Kind); } - return _asUtc; + + if (_dateOnly.HasValue) // _timeOnly is null here + return new DateTime(_dateOnly.Value.Year, _dateOnly.Value.Month, _dateOnly.Value.Day, + 0, 0, 0, + _value.Kind); + + throw new InvalidOperationException($"Cannot create DateTime when {nameof(HasDate)} is false."); } - } - private DateTime _value; - public DateTime Value - { - get => _value; set { + // Kind must be checked in addition to the value, + // as the value can be the same but the Kind different. if (_value == value && _value.Kind == value.Kind) { return; } - _asUtc = DateTime.MinValue; - _value = value; + // Initialize with the new value, keeping current 'HasTime' setting + Initialize(value, _timeOnly.HasValue, TzId, Calendar); } } + /// + /// Returns true if the underlying is in UTC. + /// public bool IsUtc => _value.Kind == DateTimeKind.Utc; + /// + /// Toggles the part of the underlying . + /// if the underlying has a 'date' part (year, month, day). + /// public bool HasDate { - get => _hasDate; - set => _hasDate = value; + get => _dateOnly.HasValue; + set => _dateOnly = value ? DateOnly.FromDateTime(_value) : null; } + /// + /// Toggles the part of the underlying . + /// if the underlying has a 'time' part (hour, minute, second). + /// public bool HasTime { - get => _hasTime; - set => _hasTime = value; + get => _timeOnly.HasValue; + set => _timeOnly = value ? TimeOnly.FromDateTime(_value) : null; } - private string _tzId = string.Empty; + private string? _tzId = string.Empty; /// - /// Setting the TzId to a local time zone will set Value.Kind to Local. Setting TzId to UTC will set Value.Kind to Utc. If the incoming value is null - /// or whitespace, Value.Kind will be set to Unspecified. Setting the TzId will NOT incur a UTC offset conversion under any circumstances. To convert - /// to another time zone, use the ToTimeZone() method. + /// Setting the to a local time zone will set to . + /// Setting to UTC will set to . + /// If the value is set to or whitespace, will be . + /// + /// Setting the will initialize in the same way aw with the .
+ /// To convert to another time zone, use . ///
- public string TzId + public string? TzId { get { @@ -330,63 +419,65 @@ public string TzId return; } - _asUtc = DateTime.MinValue; - - var isEmpty = string.IsNullOrWhiteSpace(value); - if (isEmpty) + if (string.IsNullOrWhiteSpace(value)) { + Initialize(_value, _timeOnly.HasValue, value, Calendar); Parameters.Remove("TZID"); - _tzId = null; - Value = DateTime.SpecifyKind(Value, DateTimeKind.Local); return; } - var kind = string.Equals(value, "UTC", StringComparison.OrdinalIgnoreCase) - ? DateTimeKind.Utc - : DateTimeKind.Local; - - Value = DateTime.SpecifyKind(Value, kind); - Parameters.Set("TZID", value); - _tzId = value; + Initialize(_value, _timeOnly.HasValue, value, Calendar); + Parameters.Set("TZID", _tzId); // Use the value after the initialization } } - public string TimeZoneName => TzId; + /// + /// Gets the time zone name, if it references a time zone. + /// This is an alias for . + /// + public string? TimeZoneName => TzId; + /// public int Year => Value.Year; + /// public int Month => Value.Month; + /// public int Day => Value.Day; + /// public int Hour => Value.Hour; + /// public int Minute => Value.Minute; + /// public int Second => Value.Second; + /// public int Millisecond => Value.Millisecond; + /// public long Ticks => Value.Ticks; + /// public DayOfWeek DayOfWeek => Value.DayOfWeek; + /// public int DayOfYear => Value.DayOfYear; + /// public DateTime Date => Value.Date; + /// public TimeSpan TimeOfDay => Value.TimeOfDay; /// - /// Returns a representation of the IDateTime in the specified time zone + /// Returns a representation of the in the time zone /// - public IDateTime ToTimeZone(string tzId) + public IDateTime ToTimeZone(string? tzId) { - if (string.IsNullOrWhiteSpace(tzId)) - { - throw new ArgumentException("You must provide a valid time zone id", nameof(tzId)); - } - // If TzId is empty, it's a system-local datetime, so we should use the system time zone as the starting point. var originalTzId = string.IsNullOrWhiteSpace(TzId) ? TimeZoneInfo.Local.Id @@ -401,44 +492,74 @@ public IDateTime ToTimeZone(string tzId) } /// - /// Returns a DateTimeOffset representation of the Value. If a TzId is specified, it will use that time zone's UTC offset, otherwise it will use the + /// Returns a representation of the . + /// If a TzId is specified, it will use that time zone's UTC offset, otherwise it will use the /// system-local time zone. /// public DateTimeOffset AsDateTimeOffset => string.IsNullOrWhiteSpace(TzId) - ? new DateTimeOffset(AsSystemLocal) + ? new DateTimeOffset(Value) : DateUtil.ToZonedDateTimeLeniently(Value, TzId).ToDateTimeOffset(); + /// + /// + /// This will also set to , + /// if the hours are not a multiple of 24. + /// public IDateTime Add(TimeSpan ts) => this + ts; + /// Returns a new from subtracting the specified from to the value of this instance. + /// + public TimeSpan Subtract(IDateTime dt) => (AsUtc - dt.AsUtc)!; + + /// Returns a new by subtracting the specified from the value of this instance. + /// A interval. + /// An object whose value is the difference of the date and time represented by this instance and the time interval represented by . + /// + /// This will also set to , + /// if the hours are not a multiple of 24. + /// public IDateTime Subtract(TimeSpan ts) => this - ts; - public TimeSpan Subtract(IDateTime dt) => this - dt; + [Obsolete("This operator will be removed in a future version.", true)] + public static TimeSpan? operator -(CalDateTime? left, IDateTime? right) + { + left?.AssociateWith(right); // Should not be done in operator overloads + return left?.AsUtc - right?.AsUtc; + } + /// public IDateTime AddYears(int years) { - var dt = Copy(); + var dt = Copy(); dt.Value = Value.AddYears(years); return dt; } + /// public IDateTime AddMonths(int months) { - var dt = Copy(); + var dt = Copy(); dt.Value = Value.AddMonths(months); return dt; } + /// public IDateTime AddDays(int days) { - var dt = Copy(); + var dt = Copy(); dt.Value = Value.AddDays(days); return dt; } + /// + /// + /// This will also set to , + /// if the hours are not a multiple of 24. + /// public IDateTime AddHours(int hours) { - var dt = Copy(); + var dt = Copy(); if (!dt.HasTime && hours % 24 > 0) { dt.HasTime = true; @@ -447,9 +568,14 @@ public IDateTime AddHours(int hours) return dt; } + /// + /// + /// This will also set to , + /// if the minutes are not a multiple of 1440. + /// public IDateTime AddMinutes(int minutes) { - var dt = Copy(); + var dt = Copy(); if (!dt.HasTime && minutes % 1440 > 0) { dt.HasTime = true; @@ -458,9 +584,14 @@ public IDateTime AddMinutes(int minutes) return dt; } + /// + /// + /// This will also set to , + /// if the seconds are not a multiple of 86400. + /// public IDateTime AddSeconds(int seconds) { - var dt = Copy(); + var dt = Copy(); if (!dt.HasTime && seconds % 86400 > 0) { dt.HasTime = true; @@ -469,9 +600,16 @@ public IDateTime AddSeconds(int seconds) return dt; } + /// + /// + /// This will also set to + /// if the milliseconds are not a multiple of 86400000. + /// + /// Milliseconds less than full seconds get truncated. + /// public IDateTime AddMilliseconds(int milliseconds) { - var dt = Copy(); + var dt = Copy(); if (!dt.HasTime && milliseconds % 86400000 > 0) { dt.HasTime = true; @@ -480,75 +618,123 @@ public IDateTime AddMilliseconds(int milliseconds) return dt; } + /// + /// + /// This will also set to . + /// if ticks do not result in multiple of full days. + /// + /// Ticks less than full seconds get truncated. + /// public IDateTime AddTicks(long ticks) { - var dt = Copy(); - dt.HasTime = true; + var dt = Copy(); + if (!dt.HasTime && Math.Abs(TimeSpan.FromTicks(ticks).TotalDays % 1) > AlmostZeroEpsilon) + { + dt.HasTime = true; + } + dt.Value = Value.AddTicks(ticks); return dt; } + /// + /// Returns if the current instance is less than . + /// public bool LessThan(IDateTime dt) => this < dt; + /// + /// Returns if the current instance is greater than . + /// public bool GreaterThan(IDateTime dt) => this > dt; + /// + /// Returns if the current instance is less than or equal to . + /// public bool LessThanOrEqual(IDateTime dt) => this <= dt; + /// + /// Returns if the current instance is greater than or equal to . + /// public bool GreaterThanOrEqual(IDateTime dt) => this >= dt; - public void AssociateWith(IDateTime dt) + + /// + /// Associates the current instance with the specified object. + /// + /// The object to associate with. + public void AssociateWith(IDateTime? dt) { - if (AssociatedObject == null && dt.AssociatedObject != null) + if (AssociatedObject == null && dt?.AssociatedObject != null) { AssociatedObject = dt.AssociatedObject; } - else if (AssociatedObject != null && dt.AssociatedObject == null) + else if (AssociatedObject != null && dt?.AssociatedObject == null && dt != null) { dt.AssociatedObject = AssociatedObject; } } - public int CompareTo(IDateTime dt) + /// + /// Compares the current instance with another object and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other IDateTime. + /// + /// The object to compare with this instance. + /// A value that indicates the relative order of the objects being compared. The return value has these meanings: + /// Less than zero: This instance is less than . + /// Zero: This instance is equal to . + /// Greater than zero: This instance is greater than . + /// + public int CompareTo(IDateTime? dt) { if (Equals(dt)) { return 0; } - if (this < dt) + + if (dt == null) { - return -1; + return 1; } - if (this > dt) + + if (this < dt) { - return 1; + return -1; } - throw new Exception("An error occurred while comparing two IDateTime values."); + + // Meaning "this > dt" + return 1; } + /// public override string ToString() => ToString(null, null); - public string ToString(string format) => ToString(format, null); + /// + public string ToString(string? format) => ToString(format, null); - public string ToString(string format, IFormatProvider formatProvider) + /// + public string ToString(string? format, IFormatProvider? formatProvider) { - var tz = TimeZoneName; - if (!string.IsNullOrEmpty(tz)) - { - tz = " " + tz; - } + formatProvider ??= CultureInfo.InvariantCulture; + var dateTimeOffset = AsDateTimeOffset; - if (format != null) + // Use the .NET format options to format the DateTimeOffset + + if (HasTime && !HasDate) { - return Value.ToString(format, formatProvider) + tz; + return $"{dateTimeOffset.TimeOfDay.ToString(format, formatProvider)} {_tzId}"; } + if (HasTime && HasDate) { - return Value + tz; - } - if (HasTime) - { - return Value.TimeOfDay + tz; + return $"{dateTimeOffset.ToString(format, formatProvider)} {_tzId}"; } - return Value.ToString("d") + tz; + + return $"{dateTimeOffset.ToString("d", formatProvider)} {_tzId}"; + } + + private void SynchronizeDateTimeFields(DateTime dateTime, bool hasTime) + { + _value = dateTime; + _dateOnly = DateOnly.FromDateTime(_value); + _timeOnly = hasTime ? TimeOnly.FromDateTime(_value) : null; } -} \ No newline at end of file +} diff --git a/Ical.Net/DataTypes/IDateTime.cs b/Ical.Net/DataTypes/IDateTime.cs index 155d374f..ef791d5b 100644 --- a/Ical.Net/DataTypes/IDateTime.cs +++ b/Ical.Net/DataTypes/IDateTime.cs @@ -1,8 +1,9 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // +#nullable enable using System; namespace Ical.Net.DataTypes; @@ -34,7 +35,7 @@ public interface IDateTime : IEncodableDataType, IComparable, IFormat /// /// Gets the time zone name this time is in, if it references a time zone. /// - string TimeZoneName { get; } + string? TimeZoneName { get; } /// /// Gets/sets the underlying DateTime value stored. This should always @@ -57,7 +58,7 @@ public interface IDateTime : IEncodableDataType, IComparable, IFormat /// /// Gets/sets the time zone ID for this date/time value. /// - string TzId { get; set; } + string? TzId { get; set; } /// /// Gets the year for this date/time value. @@ -134,4 +135,4 @@ public interface IDateTime : IEncodableDataType, IComparable, IFormat bool GreaterThanOrEqual(IDateTime dt); void AssociateWith(IDateTime dt); -} \ No newline at end of file +} diff --git a/Ical.Net/DataTypes/Period.cs b/Ical.Net/DataTypes/Period.cs index c64bcd11..c89b2759 100644 --- a/Ical.Net/DataTypes/Period.cs +++ b/Ical.Net/DataTypes/Period.cs @@ -67,7 +67,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - return obj.GetType() == GetType() && Equals((Period) obj); + return obj.GetType() == GetType() && Equals((Period)obj); } public override int GetHashCode() @@ -87,6 +87,9 @@ public override string ToString() return periodSerializer.SerializeToString(this); } + /// + /// Infers or calculates values based on existing time data. + /// private void ExtrapolateTimes() { if (EndTime == null && StartTime != null && Duration != default(TimeSpan)) @@ -106,9 +109,7 @@ private void ExtrapolateTimes() private IDateTime _startTime; public virtual IDateTime StartTime { - get => _startTime.HasTime - ? _startTime - : new CalDateTime(new DateTime(_startTime.Value.Year, _startTime.Value.Month, _startTime.Value.Day, 0, 0, 0), _startTime.TzId); + get => _startTime; set { if (Equals(_startTime, value)) @@ -142,7 +143,7 @@ public virtual TimeSpan Duration { if (StartTime != null && EndTime == null - && StartTime.Value.TimeOfDay == TimeSpan.Zero) + && !StartTime.HasTime) { return TimeSpan.FromDays(1); } @@ -172,7 +173,7 @@ public virtual bool Contains(IDateTime dt) } public virtual bool CollidesWith(Period period) => period != null - && ((period.StartTime != null && Contains(period.StartTime)) || (period.EndTime != null && Contains(period.EndTime))); + && ((period.StartTime != null && Contains(period.StartTime)) || (period.EndTime != null && Contains(period.EndTime))); public int CompareTo(Period other) { @@ -190,4 +191,4 @@ public int CompareTo(Period other) } throw new Exception("An error occurred while comparing two Periods."); } -} \ No newline at end of file +} diff --git a/Ical.Net/DataTypes/Trigger.cs b/Ical.Net/DataTypes/Trigger.cs index 904aceb8..35df092f 100644 --- a/Ical.Net/DataTypes/Trigger.cs +++ b/Ical.Net/DataTypes/Trigger.cs @@ -36,7 +36,7 @@ public virtual IDateTime DateTime // DateTime and Duration are mutually exclusive Duration = null; - // Do not allow timeless date/time values + // Ensure date/time has a time part _mDateTime.HasTime = true; } } @@ -108,7 +108,7 @@ public override bool Equals(object obj) { return false; } - return Equals((Trigger) obj); + return Equals((Trigger)obj); } public override int GetHashCode() @@ -121,4 +121,4 @@ public override int GetHashCode() return hashCode; } } -} \ No newline at end of file +} diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index 32b2967b..57a7a375 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -12,45 +12,6 @@ namespace Ical.Net.Evaluation; -/// -/// Much of this code comes from iCal4j, as Ben Fortuna has done an -/// excellent job with the recurrence pattern evaluation there. -/// -/// Here's the iCal4j license: -/// ================== -/// iCal4j - License -/// ================== -/// -/// Copyright (c) 2009, Ben Fortuna -/// All rights reserved. -/// -/// Redistribution and use in source and binary forms, with or without -/// modification, are permitted provided that the following conditions -/// are met: -/// -/// o Redistributions of source code must retain the above copyright -/// notice, this list of conditions and the following disclaimer. -/// -/// o Redistributions in binary form must reproduce the above copyright -/// notice, this list of conditions and the following disclaimer in the -/// documentation and/or other materials provided with the distribution. -/// -/// o Neither the name of Ben Fortuna nor the names of any other contributors -/// may be used to endorse or promote products derived from this software -/// without specific prior written permission. -/// -/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -/// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -/// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -/// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -/// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -/// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -/// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -/// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -/// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -/// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/// public class RecurrencePatternEvaluator : Evaluator { private const int _maxIncrementCount = 1000; @@ -154,44 +115,44 @@ private void EnforceEvaluationRestrictions(RecurrencePattern pattern) switch (pattern.Frequency) { case FrequencyType.Secondly: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.Default: - case RecurrenceRestrictionType.RestrictSecondly: - pattern.Frequency = FrequencyType.Minutely; - break; - case RecurrenceRestrictionType.RestrictMinutely: - pattern.Frequency = FrequencyType.Hourly; - break; - case RecurrenceRestrictionType.RestrictHourly: - pattern.Frequency = FrequencyType.Daily; - break; + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.Default: + case RecurrenceRestrictionType.RestrictSecondly: + pattern.Frequency = FrequencyType.Minutely; + break; + case RecurrenceRestrictionType.RestrictMinutely: + pattern.Frequency = FrequencyType.Hourly; + break; + case RecurrenceRestrictionType.RestrictHourly: + pattern.Frequency = FrequencyType.Daily; + break; + } } - } break; case FrequencyType.Minutely: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.RestrictMinutely: - pattern.Frequency = FrequencyType.Hourly; - break; - case RecurrenceRestrictionType.RestrictHourly: - pattern.Frequency = FrequencyType.Daily; - break; + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.RestrictMinutely: + pattern.Frequency = FrequencyType.Hourly; + break; + case RecurrenceRestrictionType.RestrictHourly: + pattern.Frequency = FrequencyType.Daily; + break; + } } - } break; case FrequencyType.Hourly: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.RestrictHourly: - pattern.Frequency = FrequencyType.Daily; - break; + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.RestrictHourly: + pattern.Frequency = FrequencyType.Daily; + break; + } } - } break; } break; @@ -200,42 +161,42 @@ private void EnforceEvaluationRestrictions(RecurrencePattern pattern) switch (pattern.Frequency) { case FrequencyType.Secondly: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.Default: - case RecurrenceRestrictionType.RestrictSecondly: - case RecurrenceRestrictionType.RestrictMinutely: - case RecurrenceRestrictionType.RestrictHourly: - throw new ArgumentException(); + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.Default: + case RecurrenceRestrictionType.RestrictSecondly: + case RecurrenceRestrictionType.RestrictMinutely: + case RecurrenceRestrictionType.RestrictHourly: + throw new ArgumentException(); + } } - } break; case FrequencyType.Minutely: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.RestrictMinutely: - case RecurrenceRestrictionType.RestrictHourly: - throw new ArgumentException(); + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.RestrictMinutely: + case RecurrenceRestrictionType.RestrictHourly: + throw new ArgumentException(); + } } - } break; case FrequencyType.Hourly: - { - switch (evaluationRestriction) { - case RecurrenceRestrictionType.RestrictHourly: - throw new ArgumentException(); + switch (evaluationRestriction) + { + case RecurrenceRestrictionType.RestrictHourly: + throw new ArgumentException(); + } } - } break; } break; } } } -#pragma warning 0618 restore +#pragma warning restore 0618 /// /// Returns a list of start dates in the specified period represented by this recurrence pattern. /// This method includes a base date argument, which indicates the start of the first occurrence of this recurrence. @@ -244,9 +205,10 @@ private void EnforceEvaluationRestrictions(RecurrencePattern pattern) /// the start dates returned should all be at 9:00AM, and not 12:19PM. /// private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTime periodEnd, int maxCount, RecurrencePattern pattern, - bool includeReferenceDateInResults) + bool includeReferenceDateInResults) { var dates = new HashSet(); + // In the first step, we work with DateTime values, so we need to convert the IDateTime to DateTime var originalDate = DateUtil.GetSimpleDateTimeData(seed); var seedCopy = DateUtil.GetSimpleDateTimeData(seed); @@ -488,8 +450,8 @@ private List GetYearDayVariants(List dates, RecurrencePatter { var date1 = date; yearDayDates.AddRange(pattern.ByYearDay.Select(yearDay => yearDay > 0 - ? date1.AddDays(-date1.DayOfYear + yearDay) - : date1.AddDays(-date1.DayOfYear + 1).AddYears(1).AddDays(yearDay)) + ? date1.AddDays(-date1.DayOfYear + yearDay) + : date1.AddDays(-date1.DayOfYear + 1).AddYears(1).AddDays(yearDay)) // Ignore the BY values that don't fit into the current year (i.e. +-366 in non-leap-years). .Where(d => d.Year == date1.Year)); } @@ -499,6 +461,7 @@ private List GetYearDayVariants(List dates, RecurrencePatter for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = false; for (var j = 0; j < pattern.ByYearDay.Count; j++) { var yearDay = pattern.ByYearDay[j]; @@ -509,13 +472,15 @@ private List GetYearDayVariants(List dates, RecurrencePatter if (newDate.Date == date.Date) { - goto Next; + keepDate = true; + break; } } - dates.RemoveAt(i); - Next: - ; + if (!keepDate) + { + dates.RemoveAt(i); + } } return dates; @@ -554,6 +519,7 @@ select monthDay > 0 for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = true; for (var j = 0; j < pattern.ByMonthDay.Count; j++) { var monthDay = pattern.ByMonthDay[j]; @@ -571,12 +537,15 @@ select monthDay > 0 if (newDate.Day.Equals(date.Day)) { - goto Next; + keepDate = false; + break; } } - Next: - dates.RemoveAt(i); + if (!keepDate) + { + dates.RemoveAt(i); + } } return dates; @@ -614,6 +583,7 @@ private List GetDayVariants(List dates, RecurrencePattern pa for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = false; for (var j = 0; j < pattern.ByDay.Count; j++) { var weekDay = pattern.ByDay[j]; @@ -623,13 +593,16 @@ private List GetDayVariants(List dates, RecurrencePattern pa // FIXME: test with offset... if (date.DayOfWeek.Equals(weekDay.DayOfWeek)) { - goto Next; + keepDate = true; + break; } } } - dates.RemoveAt(i); - Next: - ; + + if (!keepDate) + { + dates.RemoveAt(i); + } } return dates; @@ -787,18 +760,21 @@ private List GetHourVariants(List dates, RecurrencePattern p for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = false; for (var j = 0; j < pattern.ByHour.Count; j++) { var hour = pattern.ByHour[j]; if (date.Hour == hour) { - goto Next; + keepDate = true; + break; } } // Remove unmatched dates - dates.RemoveAt(i); - Next: - ; + if (!keepDate) + { + dates.RemoveAt(i); + } } return dates; } @@ -838,18 +814,20 @@ private List GetMinuteVariants(List dates, RecurrencePattern for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = false; for (var j = 0; j < pattern.ByMinute.Count; j++) { var minute = pattern.ByMinute[j]; if (date.Minute == minute) { - goto Next; + keepDate = true; } } // Remove unmatched dates - dates.RemoveAt(i); - Next: - ; + if (!keepDate) + { + dates.RemoveAt(i); + } } return dates; } @@ -889,30 +867,37 @@ private List GetSecondVariants(List dates, RecurrencePattern for (var i = dates.Count - 1; i >= 0; i--) { var date = dates[i]; + var keepDate = false; for (var j = 0; j < pattern.BySecond.Count; j++) { var second = pattern.BySecond[j]; if (date.Second == second) { - goto Next; + keepDate = true; + break; } } + // Remove unmatched dates - dates.RemoveAt(i); - Next: - ; + if (!keepDate) + { + dates.RemoveAt(i); + } } + return dates; } - private Period CreatePeriod(DateTime dt, IDateTime referenceDate) + /// + /// Creates a new period from the specified date/time, + /// where the is taken into account. + /// when initializing the new period with a new . + /// + private static Period CreatePeriod(DateTime dateTime, IDateTime referenceDate) { // Turn each resulting date/time into an IDateTime and associate it // with the reference date. - IDateTime newDt = new CalDateTime(dt, referenceDate.TzId); - - // NOTE: fixes bug #2938007 - hasTime missing - newDt.HasTime = referenceDate.HasTime; + IDateTime newDt = new CalDateTime(dateTime, referenceDate.TzId, referenceDate.HasTime); newDt.AssociateWith(referenceDate); @@ -953,4 +938,4 @@ public override HashSet Evaluate(IDateTime referenceDate, DateTime perio return Periods; } -} \ No newline at end of file +} diff --git a/Ical.Net/Evaluation/RecurrenceUtil.cs b/Ical.Net/Evaluation/RecurrenceUtil.cs index 2423389b..9fb3fc17 100644 --- a/Ical.Net/Evaluation/RecurrenceUtil.cs +++ b/Ical.Net/Evaluation/RecurrenceUtil.cs @@ -44,12 +44,12 @@ public static HashSet GetOccurrences(IRecurrable recurrable, IDateTi includeReferenceDateInResults); var otherOccurrences = from p in periods - let endTime = p.EndTime ?? p.StartTime - where - (endTime.GreaterThan(periodStart) && p.StartTime.LessThan(periodEnd) || - (periodStart.Equals(periodEnd) && p.StartTime.LessThanOrEqual(periodStart) && endTime.GreaterThan(periodEnd))) || //A period that starts at the same time it ends - (p.StartTime.Equals(endTime) && periodStart.Equals(p.StartTime)) //An event that starts at the same time it ends - select new Occurrence(recurrable, p); + let endTime = p.EndTime ?? p.StartTime + where + (endTime.GreaterThan(periodStart) && p.StartTime.LessThan(periodEnd) || + (periodStart.Equals(periodEnd) && p.StartTime.LessThanOrEqual(periodStart) && endTime.GreaterThan(periodEnd))) || //A period that starts at the same time it ends + (p.StartTime.Equals(endTime) && periodStart.Equals(p.StartTime)) //An event that starts at the same time it ends + select new Occurrence(recurrable, p); var occurrences = new HashSet(otherOccurrences); return occurrences; @@ -69,32 +69,32 @@ public static HashSet GetOccurrences(IRecurrable recurrable, IDateTi case FrequencyType.Weekly: return new bool?[] { false, null, null, null, true, true, true, true, false }; case FrequencyType.Monthly: - { - var row = new bool?[] { false, null, null, true, true, true, true, true, false }; - - // Limit if BYMONTHDAY is present; otherwise, special expand for MONTHLY. - if (p.ByMonthDay.Count > 0) { - row[4] = false; - } + var row = new bool?[] { false, null, null, true, true, true, true, true, false }; + + // Limit if BYMONTHDAY is present; otherwise, special expand for MONTHLY. + if (p.ByMonthDay.Count > 0) + { + row[4] = false; + } - return row; - } + return row; + } case FrequencyType.Yearly: - { - var row = new bool?[] { true, true, true, true, true, true, true, true, false }; - - // Limit if BYYEARDAY or BYMONTHDAY is present; otherwise, - // special expand for WEEKLY if BYWEEKNO present; otherwise, - // special expand for MONTHLY if BYMONTH present; otherwise, - // special expand for YEARLY. - if (p.ByYearDay.Count > 0 || p.ByMonthDay.Count > 0) { - row[4] = false; + var row = new bool?[] { true, true, true, true, true, true, true, true, false }; + + // Limit if BYYEARDAY or BYMONTHDAY is present; otherwise, + // special expand for WEEKLY if BYWEEKNO present; otherwise, + // special expand for MONTHLY if BYMONTH present; otherwise, + // special expand for YEARLY. + if (p.ByYearDay.Count > 0 || p.ByMonthDay.Count > 0) + { + row[4] = false; + } + + return row; } - - return row; - } default: return new bool?[] { false, null, false, false, false, false, false, false, false }; } diff --git a/Ical.Net/Evaluation/RecurringEvaluator.cs b/Ical.Net/Evaluation/RecurringEvaluator.cs index 0604346a..ce2b8827 100644 --- a/Ical.Net/Evaluation/RecurringEvaluator.cs +++ b/Ical.Net/Evaluation/RecurringEvaluator.cs @@ -24,11 +24,11 @@ public RecurringEvaluator(IRecurrable obj) // the associated object manually if (obj is ICalendarObject) { - AssociatedObject = (ICalendarObject) obj; + AssociatedObject = (ICalendarObject)obj; } if (obj is ICalendarDataType) { - var dt = (ICalendarDataType) obj; + var dt = (ICalendarDataType)obj; AssociatedObject = dt.AssociatedObject; } } @@ -162,8 +162,8 @@ public override HashSet Evaluate(IDateTime referenceDate, DateTime perio private HashSet FindDateOverlaps(HashSet dates) { - var datesWithoutTimes = new HashSet(dates.Where(d => d.StartTime.Value.TimeOfDay == TimeSpan.Zero).Select(d => d.StartTime.Value)); + var datesWithoutTimes = new HashSet(dates.Where(d => !d.StartTime.HasTime).Select(d => d.StartTime.Value)); var overlaps = new HashSet(Periods.Where(p => datesWithoutTimes.Contains(p.StartTime.Value.Date))); return overlaps; } -} \ No newline at end of file +} diff --git a/Ical.Net/Ical.Net.csproj b/Ical.Net/Ical.Net.csproj index 335d33f4..0a514633 100644 --- a/Ical.Net/Ical.Net.csproj +++ b/Ical.Net/Ical.Net.csproj @@ -8,6 +8,9 @@ + + + <_Parameter1>Ical.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a1f790f70176d52efbd248577bdb292be2d0acc62f3227c523e267d64767f207f81536c77bb91d17031a5afbc2d69cd3b5b3b9c98fa8df2cd363ec90a08639a1213ad70079eff666bcc14cf6574b899f4ad0eac672c8f763291cb1e0a2304d371053158cb398b2e6f9eeb45db7d1b4d2bbba1f985676c5ca4602fab3671d34bf diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index 20e67dc2..f57476fb 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -1,53 +1,38 @@ -// +// // Copyright ical.net project maintainers and contributors. // Licensed under the MIT license. // +#nullable enable +using Ical.Net.DataTypes; using System; using System.IO; using System.Text; using System.Text.RegularExpressions; -using Ical.Net.DataTypes; namespace Ical.Net.Serialization.DataTypes; +/// +/// A serializer for the data type. +/// public class DateTimeSerializer : EncodableDataTypeSerializer { + /// + /// This constructor is required for the SerializerFactory to work. + /// public DateTimeSerializer() { } + /// + /// Creates a new instance of the class. + /// + /// public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } - private DateTime CoerceDateTime(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind) - { - var dt = DateTime.MinValue; - - // NOTE: determine if a date/time value exceeds the representable date/time values in .NET. - // If so, let's automatically adjust the date/time to compensate. - // FIXME: should we have a parsing setting that will throw an exception - // instead of automatically adjusting the date/time value to the - // closest representable date/time? - try - { - if (year > 9999) - { - dt = DateTime.MaxValue; - } - else if (year > 0) - { - dt = new DateTime(year, month, day, hour, minute, second, kind); - } - } - catch { } - - return dt; - } - public override Type TargetType => typeof(CalDateTime); - public override string SerializeToString(object obj) + public override string? SerializeToString(object obj) { - var dt = obj as IDateTime; - if (dt == null) + if (obj is not IDateTime dt) { return null; } @@ -71,7 +56,11 @@ public override string SerializeToString(object obj) dt.Parameters.Set("TZID", dt.TzId); } - DateTime.SpecifyKind(dt.Value, kind); + var dateWithNewKind = DateTime.SpecifyKind(dt.Value, kind); + // We can't use 'Copy' because we need to change the value + dt = dt.HasTime + ? new CalDateTime(dateWithNewKind, dt.TzId, true) { AssociatedObject = dt.AssociatedObject } + : new CalDateTime(dateWithNewKind, dt.TzId, false) { AssociatedObject = dt.AssociatedObject }; // FIXME: what if DATE is the default value type for this? // Also, what if the DATE-TIME value type is specified on something @@ -82,7 +71,7 @@ public override string SerializeToString(object obj) dt.SetValueType("DATE"); } - var value = new StringBuilder(); + var value = new StringBuilder(512); value.Append($"{dt.Year:0000}{dt.Month:00}{dt.Day:00}"); if (dt.HasTime) { @@ -97,16 +86,16 @@ public override string SerializeToString(object obj) return Encode(dt, value.ToString()); } - private const RegexOptions _ciCompiled = RegexOptions.Compiled | RegexOptions.IgnoreCase; - internal static readonly Regex DateOnlyMatch = new Regex(@"^((\d{4})(\d{2})(\d{2}))?$", _ciCompiled, RegexDefaults.Timeout); - internal static readonly Regex FullDateTimePatternMatch = new Regex(@"^((\d{4})(\d{2})(\d{2}))T((\d{2})(\d{2})(\d{2})(Z)?)$", _ciCompiled, RegexDefaults.Timeout); + private const RegexOptions Options = RegexOptions.Compiled | RegexOptions.IgnoreCase; + internal static readonly Regex DateOnlyMatch = new Regex(@"^((\d{4})(\d{2})(\d{2}))?$", Options, RegexDefaults.Timeout); + internal static readonly Regex FullDateTimePatternMatch = new Regex(@"^((\d{4})(\d{2})(\d{2}))T((\d{2})(\d{2})(\d{2})(Z)?)$", Options, RegexDefaults.Timeout); - public override object Deserialize(TextReader tr) + public override object? Deserialize(TextReader tr) { var value = tr.ReadToEnd(); - var dt = CreateAndAssociate() as IDateTime; - if (dt == null) + // CalDateTime is defined as the Target type + if (CreateAndAssociate() is not CalDateTime dt) { return null; } @@ -124,28 +113,21 @@ public override object Deserialize(TextReader tr) { return null; } - var now = DateTime.Now; - var year = now.Year; - var month = now.Month; - var date = now.Day; - var hour = 0; - var minute = 0; - var second = 0; + var datePart = new DateOnly(); // Initialize. At this point, we know that the date part is present + TimeOnly? timePart = null; if (match.Groups[1].Success) { - dt.HasDate = true; - year = Convert.ToInt32(match.Groups[2].Value); - month = Convert.ToInt32(match.Groups[3].Value); - date = Convert.ToInt32(match.Groups[4].Value); + datePart = new DateOnly(Convert.ToInt32(match.Groups[2].Value), + Convert.ToInt32(match.Groups[3].Value), + Convert.ToInt32(match.Groups[4].Value)); } if (match.Groups.Count >= 6 && match.Groups[5].Success) { - dt.HasTime = true; - hour = Convert.ToInt32(match.Groups[6].Value); - minute = Convert.ToInt32(match.Groups[7].Value); - second = Convert.ToInt32(match.Groups[8].Value); + timePart = new TimeOnly(Convert.ToInt32(match.Groups[6].Value), + Convert.ToInt32(match.Groups[7].Value), + Convert.ToInt32(match.Groups[8].Value)); } var isUtc = match.Groups[9].Success; @@ -158,7 +140,11 @@ public override object Deserialize(TextReader tr) dt.TzId = "UTC"; } - dt.Value = CoerceDateTime(year, month, date, hour, minute, second, kind); + dt.Value = timePart.HasValue + ? new DateTime(datePart.Year, datePart.Month, datePart.Day, timePart.Value.Hour, timePart.Value.Minute, timePart.Value.Second, kind) + : new DateTime(datePart.Year, datePart.Month, datePart.Day, 0, 0, 0, kind); + dt.HasTime = timePart.HasValue; + return dt; } -} \ No newline at end of file +} diff --git a/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs b/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs index a0d828b7..9e402f55 100644 --- a/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/PeriodSerializer.cs @@ -99,15 +99,15 @@ public override object Deserialize(TextReader tr) p.EndTime = dtSerializer.Deserialize(new StringReader(values[1])) as IDateTime; if (p.EndTime == null) { - p.Duration = (TimeSpan) durationSerializer.Deserialize(new StringReader(values[1])); + p.Duration = (TimeSpan)durationSerializer.Deserialize(new StringReader(values[1])); } // Only return an object if it has been deserialized correctly. - if (p.StartTime != null && p.Duration != null) + if (p.StartTime != null) { return p; } return null; } -} \ No newline at end of file +} diff --git a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs index 0d7e9597..73c6500e 100644 --- a/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/RecurrencePatternSerializer.cs @@ -251,14 +251,14 @@ public override object Deserialize(TextReader tr) switch (keyword.ToUpper()) { case "UNTIL": - { - var serializer = factory.Build(typeof(IDateTime), SerializationContext) as IStringSerializer; - var dt = serializer?.Deserialize(new StringReader(keyValue)) as IDateTime; - if (dt != null) { - r.Until = dt.Value; + var serializer = factory.Build(typeof(IDateTime), SerializationContext) as IStringSerializer; + var dt = serializer?.Deserialize(new StringReader(keyValue)) as IDateTime; + if (dt != null) + { + r.Until = dt.Value; + } } - } break; case "COUNT": r.Count = Convert.ToInt32(keyValue); @@ -276,13 +276,13 @@ public override object Deserialize(TextReader tr) AddInt32Values(r.ByHour, keyValue); break; case "BYDAY": - { - var days = keyValue.Split(','); - foreach (var day in days) { - r.ByDay.Add(new WeekDay(day)); + var days = keyValue.Split(','); + foreach (var day in days) + { + r.ByDay.Add(new WeekDay(day)); + } } - } break; case "BYMONTHDAY": AddInt32Values(r.ByMonthDay, keyValue); @@ -417,8 +417,8 @@ public override object Deserialize(TextReader tr) } var dayOfWeekQuery = from Capture capture in match.Groups["Day"].Captures - select (DayOfWeek) Enum.Parse(typeof(DayOfWeek), capture.Value, true) into dayOfWeek - select new WeekDay(dayOfWeek) { Offset = num }; + select (DayOfWeek) Enum.Parse(typeof(DayOfWeek), capture.Value, true) into dayOfWeek + select new WeekDay(dayOfWeek) { Offset = num }; r.ByDay.AddRange(dayOfWeekQuery); } @@ -493,4 +493,4 @@ public override object Deserialize(TextReader tr) return r; } -} \ No newline at end of file +} From e7f772f6fa64c2b518276d4c310aa2ed941baa40 Mon Sep 17 00:00:00 2001 From: axunonb Date: Wed, 13 Nov 2024 22:28:24 +0100 Subject: [PATCH 2/9] Fix occasual failing unit test Depending on the time offset from local time to UTC the tests failed. Changed all date kinds to UTC. --- Ical.Net.Tests/RecurrenceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 7086759c..efed389c 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -2783,14 +2783,14 @@ public void Evaluate1(string freq, int secsPerInterval, bool hasTime) evt.RecurrenceRules[0].RestrictionType = RecurrenceRestrictionType.NoRestriction; #pragma warning restore 0618 - var occurrences = evt.GetOccurrences(CalDateTime.Today.AddDays(-1), CalDateTime.Today.AddDays(100)) + var occurrences = evt.GetOccurrences(evt.Start.AddDays(-1), evt.Start.AddDays(100)) .OrderBy(x => x) .ToList(); var startDates = occurrences.Select(x => x.Period.StartTime.Value).ToList(); var expectedStartDates = Enumerable.Range(0, 5) - .Select(i => DateTime.SpecifyKind(DateTime.Today, DateTimeKind.Utc).AddSeconds(i * secsPerInterval * 10)) + .Select(i => DateTime.UtcNow.Date.AddSeconds(i * secsPerInterval * 10)) .ToList(); Assert.Multiple(() => From b638e2299f21ad55ce80efc91e3ade798c0dbc56 Mon Sep 17 00:00:00 2001 From: axunonb Date: Sat, 16 Nov 2024 18:14:43 +0100 Subject: [PATCH 3/9] Make AlmostZeroEpsilon private const --- .editorconfig | 7 ++++--- Ical.Net/DataTypes/CalDateTime.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 52000afa..1ee1b1ee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -85,12 +85,13 @@ csharp_prefer_braces = false:suggestion # Expression-bodied members csharp_style_expression_bodied_methods = false:suggestion -csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_properties = true csharp_style_expression_bodied_properties = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = false:warning #prefer block bodies for constructors -csharp_style_expression_bodied_constructors = false:suggestion +csharp_style_expression_bodied_constructors = true #prefer block bodies for accessors -csharp_style_expression_bodied_accessors = false:suggestion +csharp_style_expression_bodied_accessors = true #Style - expression level options diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index cf5ea07d..184d103d 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -30,7 +30,7 @@ public sealed class CalDateTime : EncodableDataType, IDateTime // The time part that is used to return the Value property. private TimeOnly? _timeOnly; - const double AlmostZeroEpsilon = 1e-10; + private const double AlmostZeroEpsilon = 1e-10; public static CalDateTime Now => new CalDateTime(DateTime.Now); From bfaa18983fefadc3b2e6c20e00d8d8152167fea6 Mon Sep 17 00:00:00 2001 From: axunonb Date: Sun, 17 Nov 2024 22:15:43 +0100 Subject: [PATCH 4/9] Replace DateTimeKind.Local with DateTimeKind.Unspecified Affected classes: * `CalDateTime` * `DateUtil` * `DateTimeSerializer` --- Ical.Net.Tests/CalDateTimeTests.cs | 20 +++++++++---------- Ical.Net.Tests/RecurrenceTests.cs | 6 +++--- Ical.Net/DataTypes/CalDateTime.cs | 16 +++++++-------- .../DataTypes/DateTimeSerializer.cs | 4 ++-- Ical.Net/Utility/DateUtil.cs | 4 ++-- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 8620e0a7..578d039b 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -140,20 +140,20 @@ public static IEnumerable DateTimeKindOverrideTestCases() .SetName("DateTime with Kind = Utc and no tzid returns DateTimeKind.Utc"); yield return new TestCaseData(localDt, localTz) - .Returns(DateTimeKind.Local) - .SetName("Local datetime with local tzid returns DateTimeKind.Local"); + .Returns(DateTimeKind.Unspecified) + .SetName("Datetime with kind Local and local tzid returns DateTimeKind.Unspecified"); yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Utc), localTz) - .Returns(DateTimeKind.Local) - .SetName("DateTime with Kind = Utc with explicit local tzid returns DateTimeKind.Local"); + .Returns(DateTimeKind.Unspecified) + .SetName("DateTime with Kind = Utc with explicit local tzid returns DateTimeKind.Unspecified"); yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Unspecified), localTz) - .Returns(DateTimeKind.Local) - .SetName("DateTime with Kind = Unspecified with explicit local tzid returns DateTimeKind.Local"); + .Returns(DateTimeKind.Unspecified) + .SetName("DateTime with Kind = Unspecified with explicit local tzid returns DateTimeKind.Unspecified"); yield return new TestCaseData(localDt, null) - .Returns(DateTimeKind.Local) - .SetName("DateTime with Kind = Local with null tzid returns DateTimeKind.Local"); + .Returns(DateTimeKind.Unspecified) + .SetName("DateTime with Kind = Local with null tzid returns DateTimeKind.Unspecified"); yield return new TestCaseData(DateTime.SpecifyKind(localDt, DateTimeKind.Unspecified), null) .Returns(DateTimeKind.Unspecified) @@ -207,8 +207,8 @@ public void SetValue_AppliesSameRulesAsWith_CTOR() Assert.Multiple(() => { - // TzId changes the DateTimeKind to Local - Assert.That(dt1.Value.Kind, Is.Not.EqualTo(dateTime.Kind)); + // TzId changes the DateTimeKind to Unspecified + Assert.That(dt1.Value.Kind, Is.EqualTo(dateTime.Kind)); Assert.That(dt1.Value.Kind, Is.EqualTo(dt2.Value.Kind)); Assert.That(dt1.TzId, Is.EqualTo(dt2.TzId)); }); diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index efed389c..5d7719ff 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -3567,7 +3567,7 @@ public void UntilTimeZoneSerializationTests(string tzId, DateTimeKind expectedKi var serialized = serializer.SerializeToString(calendar); const string contains = "20171108T103000"; - var expectedContains = expectedKind == DateTimeKind.Local + var expectedContains = expectedKind == DateTimeKind.Unspecified ? $"{contains}{SerializationConstants.LineBreak}" : $"{contains}Z{SerializationConstants.LineBreak}"; @@ -3580,9 +3580,9 @@ public void UntilTimeZoneSerializationTests(string tzId, DateTimeKind expectedKi public static IEnumerable UntilTimeZoneSerializationTestCases() { - yield return new TestCaseData("America/New_York", DateTimeKind.Local) + yield return new TestCaseData("America/New_York", DateTimeKind.Unspecified) .SetName("IANA time time zone results in a local DateTimeKind"); - yield return new TestCaseData("Eastern Standard Time", DateTimeKind.Local) + yield return new TestCaseData("Eastern Standard Time", DateTimeKind.Unspecified) .SetName("BCL time zone results in a Local DateTimeKind"); yield return new TestCaseData("UTC", DateTimeKind.Utc) .SetName("UTC results in DateTimeKind.Utc"); diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 184d103d..7f8d8cbb 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -70,7 +70,7 @@ public CalDateTime(DateTime value, bool hasTime = true) : this(value, value.Kind /// The specified value will override value's property. /// If the time zone specified is UTC, the underlying will be /// . If a non-UTC time zone is specified, the underlying - /// property will be . + /// property will be . /// If no time zone is specified, the property will be left untouched. /// Set to (default), if the must be included. public CalDateTime(DateTime value, string? tzId, bool hasTime = true) @@ -84,7 +84,7 @@ public CalDateTime(DateTime value, string? tzId, bool hasTime = true) /// /// /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . + /// If a non-UTC time zone is specified, the underlying property will be . /// If no time zone is specified, the property will be left untouched. /// /// @@ -103,7 +103,7 @@ public CalDateTime(int year, int month, int day, int hour, int minute, int secon /// Sets for the property. /// /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . + /// If a non-UTC time zone is specified, the underlying property will be . /// If no time zone is specified, the property will be left untouched. /// /// @@ -119,7 +119,7 @@ public CalDateTime(int year, int month, int day, string? tzId = null) /// /// If , is used. /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . + /// If a non-UTC time zone is specified, the underlying property will be . /// If no time zone is specified, the property will be left untouched. /// /// @@ -150,12 +150,12 @@ private void Initialize(DateTime dateTime, bool hasTime, string? tzId, Calendar? DateTime initialValue; if ((tzId != null && !string.IsNullOrWhiteSpace(tzId) && !tzId.Equals("UTC", StringComparison.OrdinalIgnoreCase)) - || (string.IsNullOrWhiteSpace(tzId) && dateTime.Kind == DateTimeKind.Local)) + || (string.IsNullOrWhiteSpace(tzId) && dateTime.Kind == DateTimeKind.Unspecified)) { // Definitely local _tzId = tzId; - initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Local); + initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); } else if (string.Equals("UTC", tzId, StringComparison.OrdinalIgnoreCase) || dateTime.Kind == DateTimeKind.Utc) @@ -395,7 +395,7 @@ public bool HasTime private string? _tzId = string.Empty; /// - /// Setting the to a local time zone will set to . + /// Setting the to a local time zone will set to . /// Setting to UTC will set to . /// If the value is set to or whitespace, will be . /// @@ -488,7 +488,7 @@ public IDateTime ToTimeZone(string? tzId) return converted.Zone == DateTimeZone.Utc ? new CalDateTime(converted.ToDateTimeUtc(), tzId) - : new CalDateTime(DateTime.SpecifyKind(converted.ToDateTimeUnspecified(), DateTimeKind.Local), tzId); + : new CalDateTime(DateTime.SpecifyKind(converted.ToDateTimeUnspecified(), DateTimeKind.Unspecified), tzId); } /// diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index f57476fb..bf30bf0d 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -45,7 +45,7 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } var kind = dt.IsUtc ? DateTimeKind.Utc - : DateTimeKind.Local; + : DateTimeKind.Unspecified; if (dt.IsUtc) { @@ -133,7 +133,7 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } var isUtc = match.Groups[9].Success; var kind = isUtc ? DateTimeKind.Utc - : DateTimeKind.Local; + : DateTimeKind.Unspecified; if (isUtc) { diff --git a/Ical.Net/Utility/DateUtil.cs b/Ical.Net/Utility/DateUtil.cs index ef9b433d..b5ee9501 100644 --- a/Ical.Net/Utility/DateUtil.cs +++ b/Ical.Net/Utility/DateUtil.cs @@ -22,7 +22,7 @@ public static IDateTime EndOfDay(IDateTime dt) => StartOfDay(dt).AddDays(1).AddTicks(-1); public static DateTime GetSimpleDateTimeData(IDateTime dt) - => DateTime.SpecifyKind(dt.Value, dt.IsUtc ? DateTimeKind.Utc : DateTimeKind.Local); + => DateTime.SpecifyKind(dt.Value, dt.IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); public static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch) { @@ -226,4 +226,4 @@ public static int WeekOfMonth(DateTime d) : 1; return (int) Math.Floor(d.Day / 7.0) + offset; } -} \ No newline at end of file +} From 5864b878692a014e1bba6846d63531dc49b5d616 Mon Sep 17 00:00:00 2001 From: axunonb Date: Mon, 18 Nov 2024 10:17:07 +0100 Subject: [PATCH 5/9] SOC: Move getting "TZID" from `IParameterCollection` to `DateTimeSerializer` `CalDateTime.TzId` does no more read or write `CalDateTime.Parameters` --- Ical.Net/DataTypes/CalDateTime.cs | 12 ------------ .../Serialization/DataTypes/DateTimeSerializer.cs | 14 ++++++-------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 7f8d8cbb..92c6d36a 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -406,10 +406,6 @@ public string? TzId { get { - if (string.IsNullOrWhiteSpace(_tzId)) - { - _tzId = Parameters.Get("TZID"); - } return _tzId; } set @@ -419,15 +415,7 @@ public string? TzId return; } - if (string.IsNullOrWhiteSpace(value)) - { - Initialize(_value, _timeOnly.HasValue, value, Calendar); - Parameters.Remove("TZID"); - return; - } - Initialize(_value, _timeOnly.HasValue, value, Calendar); - Parameters.Set("TZID", _tzId); // Use the value after the initialization } } diff --git a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs index bf30bf0d..4e0ed1f1 100644 --- a/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs +++ b/Ical.Net/Serialization/DataTypes/DateTimeSerializer.cs @@ -95,10 +95,11 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } var value = tr.ReadToEnd(); // CalDateTime is defined as the Target type - if (CreateAndAssociate() is not CalDateTime dt) - { - return null; - } + var dt = (CalDateTime) CreateAndAssociate(); + + // The associated object is an ICalendarObject of type CalendarProperty + // that contains any timezone ("TZID" property) deserialized in a prior step + dt.TzId = dt.Parameters.Get("TZID"); // Decode the value as necessary value = Decode(dt, value); @@ -135,10 +136,7 @@ public DateTimeSerializer(SerializationContext ctx) : base(ctx) { } ? DateTimeKind.Utc : DateTimeKind.Unspecified; - if (isUtc) - { - dt.TzId = "UTC"; - } + if (isUtc) dt.TzId = "UTC"; dt.Value = timePart.HasValue ? new DateTime(datePart.Year, datePart.Month, datePart.Day, timePart.Value.Hour, timePart.Value.Minute, timePart.Value.Second, kind) From 82f5dbab1390992b688fcb4fb79493b58c5eb94a Mon Sep 17 00:00:00 2001 From: axunonb Date: Mon, 18 Nov 2024 11:57:40 +0100 Subject: [PATCH 6/9] Use backing fields `DateOnly` and `TimeOnly` to create the `CalDateTime.Value` * Depending on `HasTime`, `Value` contains only the `DateTime.Date`, or the value including `DateTime.TimeOfDay`. * The timezone dermines whether `DateTimeKind.Utc`or `DateTimeKind.Unspecified`will be used. --- Ical.Net.Tests/CalDateTimeTests.cs | 14 +- Ical.Net/DataTypes/CalDateTime.cs | 235 +++++++++++++---------------- 2 files changed, 116 insertions(+), 133 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 578d039b..4c1cc5e9 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -129,7 +129,7 @@ public DateTimeKind DateTimeKindOverrideTests(DateTime dateTime, string tzId) public static IEnumerable DateTimeKindOverrideTestCases() { const string localTz = "America/New_York"; - var localDt = DateTime.SpecifyKind(DateTime.Parse("2018-05-21T11:35:33"), DateTimeKind.Local); + var localDt = DateTime.SpecifyKind(DateTime.Parse("2018-05-21T11:35:33"), DateTimeKind.Unspecified); yield return new TestCaseData(localDt, "UTC") .Returns(DateTimeKind.Utc) @@ -250,9 +250,11 @@ public void Simple_PropertyAndMethod_HasTime_Tests() Assert.Multiple(() => { + Assert.That(c2.Value, Is.EqualTo(c3.Value)); Assert.That(c2.Ticks, Is.EqualTo(c3.Ticks)); Assert.That(c2.TzId, Is.EqualTo(c3.TzId)); Assert.That(CalDateTime.UtcNow.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + Assert.That(CalDateTime.Today.Value.Kind, Is.EqualTo(DateTimeKind.Unspecified)); Assert.That(c.Millisecond, Is.EqualTo(0)); Assert.That(c.Ticks, Is.EqualTo(dt.Ticks)); Assert.That(c.DayOfYear, Is.EqualTo(dt.DayOfYear)); @@ -284,19 +286,19 @@ public void Simple_PropertyAndMethod_NotHasTime_Tests() Assert.Multiple(() => { var result = c.AddHours(1); - Assert.That(result.HasTime, Is.EqualTo(true)); + Assert.That(result.HasTime, Is.True); result = c.AddMinutes(1); - Assert.That(result.HasTime, Is.EqualTo(true)); + Assert.That(result.HasTime, Is.True); result = c.AddSeconds(1); - Assert.That(result.HasTime, Is.EqualTo(true)); + Assert.That(result.HasTime, Is.True); result = c.AddMilliseconds(1000); - Assert.That(result.HasTime, Is.EqualTo(true)); + Assert.That(result.HasTime, Is.True); result = c.AddTicks(TimeSpan.FromMinutes(1).Ticks); - Assert.That(result.HasTime, Is.EqualTo(true)); + Assert.That(result.HasTime, Is.True); }); } diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 92c6d36a..628bbf4a 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -17,26 +17,33 @@ namespace Ical.Net.DataTypes; /// The iCalendar equivalent of the .NET class. /// /// In addition to the features of the class, the -/// class handles time zones, and integrates seamlessly into the iCalendar framework. +/// class handles timezones, and integrates seamlessly into the iCalendar framework. /// /// public sealed class CalDateTime : EncodableDataType, IDateTime { - // The date and time parts that were used to initialize the instance - // or by the Value setter. - private DateTime _value; // The date part that is used to return the Value property. - private DateOnly? _dateOnly; + private DateOnly _dateOnly; // The time part that is used to return the Value property. - private TimeOnly? _timeOnly; + private TimeOnly _timeOnly; private const double AlmostZeroEpsilon = 1e-10; + private const string UtcTzId = "UTC"; - public static CalDateTime Now => new CalDateTime(DateTime.Now); + /// + /// Gets the current date/time in the local timezone. + /// + public static CalDateTime Now => new CalDateTime(DateTime.Now, null, true); - public static CalDateTime Today => new CalDateTime(DateTime.Today); + /// + /// Gets the current date in the local timezone. + /// + public static CalDateTime Today => new CalDateTime(DateTime.Today, null, false); - public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow); + /// + /// Gets the current date/time in the Coordinated Universal Time (UTC) timezone. + /// + public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow, UtcTzId, true); /// /// This constructor is required for the SerializerFactory to work. @@ -44,93 +51,107 @@ public sealed class CalDateTime : EncodableDataType, IDateTime public CalDateTime() { } /// - /// Creates a new instance of the class - /// respecting the setting. + /// Creates a new instance of the class. /// /// public CalDateTime(IDateTime value) { - Initialize(value.Value, value.HasTime, value.TzId, value.Calendar); + if (value.HasTime) + Initialize(DateOnly.FromDateTime(value.Value), TimeOnly.FromDateTime(value.Value), value.TzId, value.Calendar); + else + Initialize(DateOnly.FromDateTime(value.Value), null, value.TzId, value.Calendar); } /// /// Creates a new instance of the class /// and sets the to "UTC" if the - /// has a of . + /// has , otherwise it will be left as . + /// + /// The timezone will be set to UTC if the has . + /// Else, the timezone will be . /// /// /// Set to (default), if the must be included. - public CalDateTime(DateTime value, bool hasTime = true) : this(value, value.Kind == DateTimeKind.Utc ? "UTC" : null, hasTime) + public CalDateTime(DateTime value, bool hasTime = true) : this(value, value.Kind == DateTimeKind.Utc ? UtcTzId : null, hasTime) { } /// - /// Creates a new instance of the class using the specified time zone. + /// Creates a new instance of the class using the specified timezone. /// /// /// The specified value will override value's property. - /// If the time zone specified is UTC, the underlying will be - /// . If a non-UTC time zone is specified, the underlying - /// property will be . - /// If no time zone is specified, the property will be left untouched. + /// If the timezone specified is UTC, the underlying will be + /// . If a non-UTC timezone or no timezone is specified, the + /// will be used. A timezone of represents + /// the system's local timezone. + /// /// Set to (default), if the must be included. public CalDateTime(DateTime value, string? tzId, bool hasTime = true) { - Initialize(value, hasTime, tzId, null); + if (value.Kind == DateTimeKind.Utc && tzId is null or UtcTzId) + tzId = UtcTzId; + + if (hasTime) + Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), tzId, null); + else + Initialize(DateOnly.FromDateTime(value), null, tzId, null); } /// - /// Creates a new instance of the class using the specified time zone. - /// Sets for the property. + /// Creates a new instance of the class using the specified timezone. /// - /// - /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . - /// If no time zone is specified, the property will be left untouched. + /// The specified value will determine the property. + /// If the timezone specified is UTC, the underlying will be + /// . If a non-UTC timezone or no timezone is specified, the underlying + /// will be used. A timezone of represents + /// the system's local timezone. /// /// /// /// /// /// + /// /// public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null, Calendar? cal = null) //NOSONAR - must keep this signature { - Initialize(new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified), true, tzId, cal); + Initialize(new DateOnly(year, month, day), new TimeOnly(hour, minute, second), tzId, cal); } /// - /// Creates a new instance of the class using the specified time zone. + /// Creates a new instance of the class using the specified timezone. /// Sets for the property. /// - /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . - /// If no time zone is specified, the property will be left untouched. + /// The specified value will determine the property. + /// If the timezone specified is UTC, the underlying will be + /// . If a non-UTC timezone or no timezone is specified, the underlying + /// will be used. A timezone of represents + /// the system's local timezone. /// /// /// /// public CalDateTime(int year, int month, int day, string? tzId = null) { - Initialize(new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Unspecified), false, tzId, null); + Initialize(new DateOnly(year, month, day), null, tzId, null); } /// - /// Creates a new instance of the class using the specified time zone. + /// Creates a new instance of the class using the specified timezone. /// /// If , is used. - /// The specified value will override value's property. - /// If a non-UTC time zone is specified, the underlying property will be . - /// If no time zone is specified, the property will be left untouched. + /// The specified value will determine the property. + /// If the timezone specified is UTC, the underlying will be + /// . If a non-UTC timezone or no timezone is specified, the underlying + /// will be used. A timezone of represents + /// the system's local timezone. /// /// /// /// public CalDateTime(DateOnly date, TimeOnly? time, DateTimeKind kind, string? tzId = null, Calendar? cal = null) { - if (time.HasValue) - Initialize(new DateTime(date.Year, date.Month, date.Day, time.Value.Hour, time.Value.Minute, time.Value.Second, kind), true, tzId, cal); - else - Initialize(new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, kind), false, tzId, cal); + Initialize(date, time, tzId, cal); } /// @@ -138,42 +159,28 @@ public CalDateTime(DateOnly date, TimeOnly? time, DateTimeKind kind, string? tzI /// using the . /// /// An iCalendar-compatible date or date-time string. - public CalDateTime(string value) + /// The specified value will determine the property. + /// If the timezone specified is UTC, the underlying will be + /// . If a non-UTC timezone or no timezone is specified, the underlying + /// will be used. A timezone of represents + /// the system's local timezone. + /// + public CalDateTime(string value, string? tzId = null) { var serializer = new DateTimeSerializer(); CopyFrom(serializer.Deserialize(new StringReader(value)) as ICopyable ?? throw new InvalidOperationException("Failure deserializing value")); + TzId = tzId; } - private void Initialize(DateTime dateTime, bool hasTime, string? tzId, Calendar? cal) + private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId, Calendar? cal) { - DateTime initialValue; - - if ((tzId != null && !string.IsNullOrWhiteSpace(tzId) && !tzId.Equals("UTC", StringComparison.OrdinalIgnoreCase)) - || (string.IsNullOrWhiteSpace(tzId) && dateTime.Kind == DateTimeKind.Unspecified)) - { - // Definitely local - _tzId = tzId; + HasTime = timeOnly.HasValue; + HasDate = true; + _dateOnly = dateOnly; + _timeOnly = timeOnly ?? new TimeOnly(); - initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); - - } - else if (string.Equals("UTC", tzId, StringComparison.OrdinalIgnoreCase) || dateTime.Kind == DateTimeKind.Utc) - { - // It is UTC - _tzId = "UTC"; - - initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); - } - else - { - // Unspecified - _tzId = null; - - initialValue = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified); - } - - SynchronizeDateTimeFields(initialValue, hasTime); + _tzId = string.Equals(UtcTzId, tzId, StringComparison.OrdinalIgnoreCase) ? UtcTzId : tzId; AssociatedObject = cal; } @@ -206,10 +213,9 @@ public override void CopyFrom(ICopyable obj) // Maintain the private date/time backing fields _dateOnly = calDt._dateOnly; _timeOnly = calDt._timeOnly; - - // Copy the underlying DateTime value and time zone - _value = calDt._value; _tzId = calDt._tzId; + HasTime = calDt.HasTime; + HasDate = calDt.HasDate; } AssociateWith(dt); @@ -318,12 +324,12 @@ public override int GetHashCode() /// /// Gets the underlying of . /// - public DateOnly? DateOnlyValue => _dateOnly; + public DateOnly? DateOnlyValue => HasDate ? _dateOnly : null; /// /// Gets the underlying of . /// - public TimeOnly? TimeOnlyValue => _timeOnly; + public TimeOnly? TimeOnlyValue => HasTime ? _timeOnly : null; /// /// Gets the underlying . @@ -338,76 +344,60 @@ public DateTime Value get { // HasDate and HasTime both have setters, so they can be changed. - if (_dateOnly.HasValue && _timeOnly.HasValue) + if (HasDate && HasTime) { - return new DateTime(_dateOnly.Value.Year, _dateOnly.Value.Month, - _dateOnly.Value.Day, _timeOnly.Value.Hour, _timeOnly.Value.Minute, _timeOnly.Value.Second, - _value.Kind); + return new DateTime(_dateOnly.Year, _dateOnly.Month, + _dateOnly.Day, _timeOnly.Hour, _timeOnly.Minute, _timeOnly.Second, + IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); } - if (_dateOnly.HasValue) // _timeOnly is null here - return new DateTime(_dateOnly.Value.Year, _dateOnly.Value.Month, _dateOnly.Value.Day, + if (HasDate) // but no time + return new DateTime(_dateOnly.Year, _dateOnly.Month, _dateOnly.Day, 0, 0, 0, - _value.Kind); + IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); throw new InvalidOperationException($"Cannot create DateTime when {nameof(HasDate)} is false."); } set { - // Kind must be checked in addition to the value, - // as the value can be the same but the Kind different. - if (_value == value && _value.Kind == value.Kind) - { - return; - } - - // Initialize with the new value, keeping current 'HasTime' setting - Initialize(value, _timeOnly.HasValue, TzId, Calendar); + // Initialize, keeping the HasTime setting + if (HasTime) + Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), _tzId, Calendar); + else + Initialize(DateOnly.FromDateTime(value), null, _tzId, Calendar); } } /// /// Returns true if the underlying is in UTC. /// - public bool IsUtc => _value.Kind == DateTimeKind.Utc; + public bool IsUtc => string.Equals(_tzId, UtcTzId, StringComparison.OrdinalIgnoreCase); /// - /// Toggles the part of the underlying . /// if the underlying has a 'date' part (year, month, day). /// - public bool HasDate - { - get => _dateOnly.HasValue; - set => _dateOnly = value ? DateOnly.FromDateTime(_value) : null; - } + public bool HasDate { get; set; } = true; /// - /// Toggles the part of the underlying . /// if the underlying has a 'time' part (hour, minute, second). /// - public bool HasTime - { - get => _timeOnly.HasValue; - set => _timeOnly = value ? TimeOnly.FromDateTime(_value) : null; - } + public bool HasTime { get; set; } = true; private string? _tzId = string.Empty; /// - /// Setting the to a local time zone will set to . + /// Setting the to a local timezone will set to . /// Setting to UTC will set to . - /// If the value is set to or whitespace, will be . + /// If the value is set to , will be , + /// and the system's local timezone will be used. /// /// Setting the will initialize in the same way aw with the .
- /// To convert to another time zone, use . + /// To convert to another timezone, use . ///
public string? TzId { - get - { - return _tzId; - } + get => _tzId; set { if (string.Equals(_tzId, value, StringComparison.OrdinalIgnoreCase)) @@ -415,12 +405,12 @@ public string? TzId return; } - Initialize(_value, _timeOnly.HasValue, value, Calendar); + Initialize(_dateOnly, HasTime ? _timeOnly : null, value, Calendar); } } /// - /// Gets the time zone name, if it references a time zone. + /// Gets the timezone name, if it references a timezone. /// This is an alias for . /// public string? TimeZoneName => TzId; @@ -462,14 +452,12 @@ public string? TzId public TimeSpan TimeOfDay => Value.TimeOfDay; /// - /// Returns a representation of the in the time zone + /// Returns a representation of the in the timezone /// public IDateTime ToTimeZone(string? tzId) { - // If TzId is empty, it's a system-local datetime, so we should use the system time zone as the starting point. - var originalTzId = string.IsNullOrWhiteSpace(TzId) - ? TimeZoneInfo.Local.Id - : TzId; + // If TzId is empty, it's a system-local datetime, so we should use the system timezone as the starting point. + var originalTzId = TzId ?? TimeZoneInfo.Local.Id; var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, originalTzId); var converted = zonedOriginal.WithZone(DateUtil.GetZone(tzId)); @@ -481,11 +469,11 @@ public IDateTime ToTimeZone(string? tzId) /// /// Returns a representation of the . - /// If a TzId is specified, it will use that time zone's UTC offset, otherwise it will use the - /// system-local time zone. + /// If a TzId is specified, it will use that timezone's UTC offset, otherwise it will use the + /// system-local timezone. /// public DateTimeOffset AsDateTimeOffset => - string.IsNullOrWhiteSpace(TzId) + TzId is null ? new DateTimeOffset(Value) : DateUtil.ToZonedDateTimeLeniently(Value, TzId).ToDateTimeOffset(); @@ -501,7 +489,7 @@ public IDateTime ToTimeZone(string? tzId) public TimeSpan Subtract(IDateTime dt) => (AsUtc - dt.AsUtc)!; /// Returns a new by subtracting the specified from the value of this instance. - /// A interval. + /// An interval. /// An object whose value is the difference of the date and time represented by this instance and the time interval represented by . /// /// This will also set to , @@ -718,11 +706,4 @@ public string ToString(string? format, IFormatProvider? formatProvider) return $"{dateTimeOffset.ToString("d", formatProvider)} {_tzId}"; } - - private void SynchronizeDateTimeFields(DateTime dateTime, bool hasTime) - { - _value = dateTime; - _dateOnly = DateOnly.FromDateTime(_value); - _timeOnly = hasTime ? TimeOnly.FromDateTime(_value) : null; - } } From 80c2578836d80dcd5e6529034745c6965a5805aa Mon Sep 17 00:00:00 2001 From: axunonb Date: Mon, 18 Nov 2024 12:21:38 +0100 Subject: [PATCH 7/9] Remove the redundant `Calendar` argument to CTORs --- Ical.Net.Tests/CalDateTimeTests.cs | 4 ++-- Ical.Net/DataTypes/CalDateTime.cs | 30 +++++++++++++----------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Ical.Net.Tests/CalDateTimeTests.cs b/Ical.Net.Tests/CalDateTimeTests.cs index 4c1cc5e9..b3ca3191 100644 --- a/Ical.Net.Tests/CalDateTimeTests.cs +++ b/Ical.Net.Tests/CalDateTimeTests.cs @@ -244,9 +244,9 @@ public void Simple_PropertyAndMethod_HasTime_Tests() var dt = new DateTime(2025, 1, 2, 10, 20, 30, DateTimeKind.Utc); 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, null); + var c2 = new CalDateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, c.TzId); var c3 = new CalDateTime(new DateOnly(dt.Year, dt.Month, dt.Day), - new TimeOnly(dt.Hour, dt.Minute, dt.Second), dt.Kind, c.TzId); + new TimeOnly(dt.Hour, dt.Minute, dt.Second), c.TzId); Assert.Multiple(() => { diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 628bbf4a..a6fa5946 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -57,9 +57,9 @@ public CalDateTime() { } public CalDateTime(IDateTime value) { if (value.HasTime) - Initialize(DateOnly.FromDateTime(value.Value), TimeOnly.FromDateTime(value.Value), value.TzId, value.Calendar); + Initialize(DateOnly.FromDateTime(value.Value), TimeOnly.FromDateTime(value.Value), value.TzId); else - Initialize(DateOnly.FromDateTime(value.Value), null, value.TzId, value.Calendar); + Initialize(DateOnly.FromDateTime(value.Value), null, value.TzId); } /// @@ -92,9 +92,9 @@ public CalDateTime(DateTime value, string? tzId, bool hasTime = true) tzId = UtcTzId; if (hasTime) - Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), tzId, null); + Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), tzId); else - Initialize(DateOnly.FromDateTime(value), null, tzId, null); + Initialize(DateOnly.FromDateTime(value), null, tzId); } /// @@ -112,10 +112,9 @@ public CalDateTime(DateTime value, string? tzId, bool hasTime = true) /// /// /// - /// - public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null, Calendar? cal = null) //NOSONAR - must keep this signature + public CalDateTime(int year, int month, int day, int hour, int minute, int second, string? tzId = null) //NOSONAR - must keep this signature { - Initialize(new DateOnly(year, month, day), new TimeOnly(hour, minute, second), tzId, cal); + Initialize(new DateOnly(year, month, day), new TimeOnly(hour, minute, second), tzId); } /// @@ -133,7 +132,7 @@ public CalDateTime(int year, int month, int day, int hour, int minute, int secon /// public CalDateTime(int year, int month, int day, string? tzId = null) { - Initialize(new DateOnly(year, month, day), null, tzId, null); + Initialize(new DateOnly(year, month, day), null, tzId); } /// @@ -148,10 +147,9 @@ public CalDateTime(int year, int month, int day, string? tzId = null) /// /// /// - /// - public CalDateTime(DateOnly date, TimeOnly? time, DateTimeKind kind, string? tzId = null, Calendar? cal = null) + public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null) { - Initialize(date, time, tzId, cal); + Initialize(date, time, tzId); } /// @@ -173,7 +171,7 @@ public CalDateTime(string value, string? tzId = null) TzId = tzId; } - private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId, Calendar? cal) + private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId) { HasTime = timeOnly.HasValue; HasDate = true; @@ -181,8 +179,6 @@ private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId, Cal _timeOnly = timeOnly ?? new TimeOnly(); _tzId = string.Equals(UtcTzId, tzId, StringComparison.OrdinalIgnoreCase) ? UtcTzId : tzId; - - AssociatedObject = cal; } /// @@ -363,9 +359,9 @@ public DateTime Value { // Initialize, keeping the HasTime setting if (HasTime) - Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), _tzId, Calendar); + Initialize(DateOnly.FromDateTime(value), TimeOnly.FromDateTime(value), _tzId); else - Initialize(DateOnly.FromDateTime(value), null, _tzId, Calendar); + Initialize(DateOnly.FromDateTime(value), null, _tzId); } } @@ -405,7 +401,7 @@ public string? TzId return; } - Initialize(_dateOnly, HasTime ? _timeOnly : null, value, Calendar); + Initialize(_dateOnly, HasTime ? _timeOnly : null, value); } } From 320d624d56da177397948dff4dc87db182dfd5de Mon Sep 17 00:00:00 2001 From: axunonb Date: Thu, 21 Nov 2024 21:44:15 +0100 Subject: [PATCH 8/9] Implement review comments * `if (Math.Abs(right.TotalDays % 1) > AlmostZeroEpsilon)` is now `if ((right.Ticks % TimeSpan.TicksPerDay) != 0)` * `DateUtil.DateTime GetSimpleDateTimeData(IDateTime dt)` returns `dt.Value` * Move internal `DateUtil.SimpleDateTimeToMatch` as private to `RecurrenceTests` * Move internal `DateUtil.MatchTimeZone` as private to `RecurrencePatternEvaluator` * Create tasks in #646 and #647 for new PRs --- Ical.Net.Tests/RecurrenceTests.cs | 27 ++++++++++--- Ical.Net/DataTypes/CalDateTime.cs | 7 ++-- .../Evaluation/RecurrencePatternEvaluator.cs | 23 ++++++++++- Ical.Net/Utility/DateUtil.cs | 40 +------------------ 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/Ical.Net.Tests/RecurrenceTests.cs b/Ical.Net.Tests/RecurrenceTests.cs index 50405497..16cfba8b 100644 --- a/Ical.Net.Tests/RecurrenceTests.cs +++ b/Ical.Net.Tests/RecurrenceTests.cs @@ -2821,8 +2821,8 @@ public void RecurrencePattern1() var occurrences = evaluator.Evaluate( startDate, - DateUtil.SimpleDateTimeToMatch(fromDate, startDate), - DateUtil.SimpleDateTimeToMatch(toDate, startDate), + SimpleDateTimeToMatch(fromDate, startDate), + SimpleDateTimeToMatch(toDate, startDate), false) .OrderBy(o => o.StartTime) .ToList(); @@ -2854,8 +2854,8 @@ public void RecurrencePattern2() var occurrences = evaluator.Evaluate( startDate, - DateUtil.SimpleDateTimeToMatch(fromDate, startDate), - DateUtil.SimpleDateTimeToMatch(toDate, startDate), + SimpleDateTimeToMatch(fromDate, startDate), + SimpleDateTimeToMatch(toDate, startDate), false); Assert.That(occurrences.Count, Is.Not.EqualTo(0)); } @@ -2966,7 +2966,7 @@ public void Test4() var periods = evaluator.Evaluate( evtStart, DateUtil.GetSimpleDateTimeData(evtStart), - DateUtil.SimpleDateTimeToMatch(evtEnd, evtStart), + SimpleDateTimeToMatch(evtEnd, evtStart), false) .OrderBy(p => p.StartTime) .ToList(); @@ -3758,5 +3758,22 @@ public void ShouldCreateARecurringYearlyEvent() //occurences is 26 here, omitting 4/16/2024 Assert.That(occurrences.Count, Is.EqualTo(27)); } + + private static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch) + { + if (toMatch.IsUtc && dt.IsUtc) + { + return dt.Value; + } + if (toMatch.IsUtc) + { + return dt.Value.ToUniversalTime(); + } + if (dt.IsUtc) + { + return dt.Value.ToLocalTime(); + } + return dt.Value; + } } diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index a6fa5946..5f2c29a6 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -27,7 +27,6 @@ public sealed class CalDateTime : EncodableDataType, IDateTime // The time part that is used to return the Value property. private TimeOnly _timeOnly; - private const double AlmostZeroEpsilon = 1e-10; private const string UtcTzId = "UTC"; /// @@ -274,7 +273,7 @@ public override int GetHashCode() public static IDateTime operator -(CalDateTime left, TimeSpan right) { var copy = left.Copy(); - if (Math.Abs(right.TotalDays % 1) > AlmostZeroEpsilon) + if ((right.Ticks % TimeSpan.TicksPerDay) != 0) { copy.HasTime = true; } @@ -292,7 +291,7 @@ public override int GetHashCode() public static IDateTime operator +(CalDateTime left, TimeSpan right) { var copy = left.Copy(); - if (Math.Abs(right.TotalDays % 1) > AlmostZeroEpsilon) + if ((right.Ticks % TimeSpan.TicksPerDay) != 0) { copy.HasTime = true; } @@ -600,7 +599,7 @@ public IDateTime AddMilliseconds(int milliseconds) public IDateTime AddTicks(long ticks) { var dt = Copy(); - if (!dt.HasTime && Math.Abs(TimeSpan.FromTicks(ticks).TotalDays % 1) > AlmostZeroEpsilon) + if (!dt.HasTime && (ticks % TimeSpan.TicksPerDay) != 0) { dt.HasTime = true; } diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index 57a7a375..35286c89 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -31,7 +31,7 @@ private RecurrencePattern ProcessRecurrencePattern(IDateTime referenceDate) // Convert the UNTIL value to one that matches the same time information as the reference date if (r.Until != DateTime.MinValue) { - r.Until = DateUtil.MatchTimeZone(referenceDate, new CalDateTime(r.Until, referenceDate.TzId)).Value; + r.Until = MatchTimeZone(referenceDate, new CalDateTime(r.Until, referenceDate.TzId)).Value; } if (referenceDate.HasTime) @@ -938,4 +938,25 @@ public override HashSet Evaluate(IDateTime referenceDate, DateTime perio return Periods; } + + private static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) + { + // Associate the date/time with the first. + var copy = dt2; + copy.AssociateWith(dt1); + + // If the dt1 time does not occur in the same time zone as the + // dt2 time, then let's convert it so they can be used in the + // same context (i.e. evaluation). + if (dt1.TzId != null) + { + return string.Equals(dt1.TzId, copy.TzId, StringComparison.OrdinalIgnoreCase) + ? copy + : copy.ToTimeZone(dt1.TzId); + } + + return dt1.IsUtc + ? new CalDateTime(copy.AsUtc) + : new CalDateTime(copy.AsSystemLocal); + } } diff --git a/Ical.Net/Utility/DateUtil.cs b/Ical.Net/Utility/DateUtil.cs index b5ee9501..941c9ca2 100644 --- a/Ical.Net/Utility/DateUtil.cs +++ b/Ical.Net/Utility/DateUtil.cs @@ -22,45 +22,7 @@ public static IDateTime EndOfDay(IDateTime dt) => StartOfDay(dt).AddDays(1).AddTicks(-1); public static DateTime GetSimpleDateTimeData(IDateTime dt) - => DateTime.SpecifyKind(dt.Value, dt.IsUtc ? DateTimeKind.Utc : DateTimeKind.Unspecified); - - public static DateTime SimpleDateTimeToMatch(IDateTime dt, IDateTime toMatch) - { - if (toMatch.IsUtc && dt.IsUtc) - { - return dt.Value; - } - if (toMatch.IsUtc) - { - return dt.Value.ToUniversalTime(); - } - if (dt.IsUtc) - { - return dt.Value.ToLocalTime(); - } - return dt.Value; - } - - public static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) - { - // Associate the date/time with the first. - var copy = dt2; - copy.AssociateWith(dt1); - - // If the dt1 time does not occur in the same time zone as the - // dt2 time, then let's convert it so they can be used in the - // same context (i.e. evaluation). - if (dt1.TzId != null) - { - return string.Equals(dt1.TzId, copy.TzId, StringComparison.OrdinalIgnoreCase) - ? copy - : copy.ToTimeZone(dt1.TzId); - } - - return dt1.IsUtc - ? new CalDateTime(copy.AsUtc) - : new CalDateTime(copy.AsSystemLocal); - } + => dt.Value; public static DateTime AddWeeks(DateTime dt, int interval, DayOfWeek firstDayOfWeek) { From 7bf9c3b8871345cdc8e8c3a4cd97e773ddd93e56 Mon Sep 17 00:00:00 2001 From: axunonb Date: Fri, 22 Nov 2024 19:20:20 +0100 Subject: [PATCH 9/9] Remove xmldoc for non-existing parameter "kind" Also refactor for SonarCloud complaints --- Ical.Net/CalendarComponents/UniqueComponent.cs | 2 -- Ical.Net/DataTypes/CalDateTime.cs | 4 ++-- Ical.Net/Evaluation/RecurrencePatternEvaluator.cs | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Ical.Net/CalendarComponents/UniqueComponent.cs b/Ical.Net/CalendarComponents/UniqueComponent.cs index 9fbec22e..eb7d7a0d 100644 --- a/Ical.Net/CalendarComponents/UniqueComponent.cs +++ b/Ical.Net/CalendarComponents/UniqueComponent.cs @@ -45,8 +45,6 @@ private void EnsureProperties() // See https://sourceforge.net/projects/dday-ical/forums/forum/656447/topic/3754354 if (DtStamp == null) { - // icalendar RFC doesn't care about sub-second time resolution, so shave off everything smaller than seconds. - var utcNow = DateTime.UtcNow.Truncate(TimeSpan.FromSeconds(1)); DtStamp = CalDateTime.UtcNow; } } diff --git a/Ical.Net/DataTypes/CalDateTime.cs b/Ical.Net/DataTypes/CalDateTime.cs index 5f2c29a6..a0d6b666 100644 --- a/Ical.Net/DataTypes/CalDateTime.cs +++ b/Ical.Net/DataTypes/CalDateTime.cs @@ -137,7 +137,6 @@ public CalDateTime(int year, int month, int day, string? tzId = null) /// /// Creates a new instance of the class using the specified timezone. /// - /// If , is used. /// The specified value will determine the property. /// If the timezone specified is UTC, the underlying will be /// . If a non-UTC timezone or no timezone is specified, the underlying @@ -400,6 +399,7 @@ public string? TzId return; } + _tzId = value; Initialize(_dateOnly, HasTime ? _timeOnly : null, value); } } @@ -694,7 +694,7 @@ public string ToString(string? format, IFormatProvider? formatProvider) return $"{dateTimeOffset.TimeOfDay.ToString(format, formatProvider)} {_tzId}"; } - if (HasTime && HasDate) + if (HasTime) { return $"{dateTimeOffset.ToString(format, formatProvider)} {_tzId}"; } diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index 80ce78b4..e5edcd30 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -14,7 +14,7 @@ namespace Ical.Net.Evaluation; public class RecurrencePatternEvaluator : Evaluator { - private const int _maxIncrementCount = 1000; + private const int MaxIncrementCount = 1000; protected RecurrencePattern Pattern { get; set; } @@ -284,7 +284,7 @@ private HashSet GetDates(IDateTime seed, DateTime periodStart, DateTim else { noCandidateIncrementCount++; - if (_maxIncrementCount > 0 && noCandidateIncrementCount > _maxIncrementCount) + if (noCandidateIncrementCount > MaxIncrementCount) { break; }