Skip to content

Commit 4b8d45a

Browse files
authored
fix: CalDateTime CTOR using ISO 8601 UTC string resolves to UTC (#833)
* fix: `CalDateTime` CTOR using ISO 8601 UTC string resolves to UTC Throws for controversion of ISO 8601 UTC string, while timezone ID is not UTC chore: `ExcludeFromCodeCoverage` for private constructor chore: Remove unused method `TruncateTimeToSeconds` Resolves #831 * Remove redundant `CalDateTime.CopyFrom` * Updated the `CalDateTime(string value, string? tzId = null)` constructor to directly initialize the object using the `Initialize` method. * Removed the unnecessary `CopyFrom` method.
1 parent 4588209 commit 4b8d45a

File tree

2 files changed

+34
-20
lines changed

2 files changed

+34
-20
lines changed

Ical.Net.Tests/CalDateTimeTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Licensed under the MIT license.
44
//
55

6+
#nullable enable
67
using Ical.Net.CalendarComponents;
78
using Ical.Net.DataTypes;
89
using NUnit.Framework;
@@ -55,7 +56,7 @@ public void ToTimeZoneFloating()
5556
[Test, TestCaseSource(nameof(ToTimeZoneTestCases))]
5657
public void ToTimeZoneTests(CalendarEvent calendarEvent, string targetTimeZone)
5758
{
58-
var startAsUtc = calendarEvent.Start.AsUtc;
59+
var startAsUtc = calendarEvent.Start!.AsUtc;
5960

6061
var convertedStart = calendarEvent.Start.ToTimeZone(targetTimeZone);
6162
var convertedAsUtc = convertedStart.AsUtc;
@@ -369,4 +370,23 @@ public void CalDateTime_FromDateTime_HandlesKindCorrectly(DateTimeKind kind, IRe
369370

370371
Assert.That(() => new CalDateTime(dt), constraint);
371372
}
373+
374+
[TestCase("20250703T060000Z", null)]
375+
[TestCase("20250703T060000Z", CalDateTime.UtcTzId)]
376+
public void ConstructorWithIso8601UtcString_ShouldResultInUtc(string value, string? tzId)
377+
{
378+
var dt = new CalDateTime(value, tzId);
379+
Assert.Multiple(() =>
380+
{
381+
Assert.That(dt.Value, Is.EqualTo(new DateTime(2025, 7, 3, 6, 0, 0, DateTimeKind.Utc)));
382+
#pragma warning disable CA1305
383+
Assert.That(dt.ToString("yyyy-MM-dd HH:mm:ss"), Is.EqualTo("2025-07-03 06:00:00 UTC"));
384+
#pragma warning restore CA1305
385+
Assert.That(dt.IsUtc, Is.True);
386+
});
387+
}
388+
389+
[Test]
390+
public void ConstructorWithIso8601UtcString_ButDifferentTzId_ShouldThrow()
391+
=> Assert.That(() => _ = new CalDateTime("20250703T060000Z", "CEST"), Throws.ArgumentException);
372392
}

Ical.Net/DataTypes/CalDateTime.cs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Ical.Net.Utility;
88
using NodaTime;
99
using System;
10+
using System.Diagnostics.CodeAnalysis;
1011
using System.Globalization;
1112
using System.IO;
1213

@@ -55,6 +56,7 @@ public sealed class CalDateTime : IComparable<CalDateTime>, IFormattable
5556
/// <summary>
5657
/// This constructor is required for the SerializerFactory to work.
5758
/// </summary>
59+
[ExcludeFromCodeCoverage]
5860
private CalDateTime()
5961
{
6062
// required for the SerializerFactory to work
@@ -200,10 +202,17 @@ public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null)
200202
public CalDateTime(string value, string? tzId = null)
201203
{
202204
var serializer = new DateTimeSerializer();
203-
CopyFrom(serializer.Deserialize(new StringReader(value)) as CalDateTime
204-
?? throw new InvalidOperationException($"$Failure for deserializing value '{value}'"));
205-
// The string may contain a date only, meaning that the tzId should be ignored.
206-
_tzId = HasTime ? tzId : null;
205+
var dt = serializer.Deserialize(new StringReader(value)) as CalDateTime
206+
?? throw new InvalidOperationException($"Failure when deserializing value '{value}'");
207+
208+
Initialize(dt._dateOnly, dt._timeOnly, dt.IsUtc ? UtcTzId : tzId);
209+
210+
if (dt.IsUtc && tzId != null && !string.Equals(tzId, UtcTzId, StringComparison.OrdinalIgnoreCase))
211+
{
212+
throw new ArgumentException(
213+
$"The value '{value}' represents UTC date/time, but the specified timezone '{tzId}' is not '{UtcTzId}'.",
214+
nameof(tzId));
215+
}
207216
}
208217

209218
private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
@@ -218,15 +227,6 @@ private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
218227
};
219228
}
220229

221-
/// <inheritdoc/>
222-
private void CopyFrom(CalDateTime calDt)
223-
{
224-
// Maintain the private date/time backing fields
225-
_dateOnly = calDt._dateOnly;
226-
_timeOnly = TruncateTimeToSeconds(calDt._timeOnly);
227-
_tzId = calDt._tzId;
228-
}
229-
230230
public bool Equals(CalDateTime? other) => this == other;
231231

232232
/// <inheritdoc/>
@@ -436,12 +436,6 @@ public DateTime Value
436436
return new TimeOnly(time.Value.Hour, time.Value.Minute, time.Value.Second);
437437
}
438438

439-
/// <summary>
440-
/// Any <see cref="Time"/> values are truncated to seconds, because
441-
/// RFC 5545, Section 3.3.5 does not allow for fractional seconds.
442-
/// </summary>
443-
private static TimeOnly? TruncateTimeToSeconds(DateTime dateTime) => new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second);
444-
445439
/// <summary>
446440
/// Converts the <see cref="Value"/> to a date/time
447441
/// within the specified <see paramref="otherTzId"/> timezone.

0 commit comments

Comments
 (0)