Skip to content

Commit 32ca1c9

Browse files
committed
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
1 parent 4588209 commit 32ca1c9

File tree

2 files changed

+31
-10
lines changed

2 files changed

+31
-10
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: 10 additions & 9 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
@@ -201,9 +203,15 @@ public CalDateTime(string value, string? tzId = null)
201203
{
202204
var serializer = new DateTimeSerializer();
203205
CopyFrom(serializer.Deserialize(new StringReader(value)) as CalDateTime
204-
?? throw new InvalidOperationException($"$Failure for deserializing value '{value}'"));
206+
?? throw new InvalidOperationException($"$Failure when deserializing value '{value}'"));
207+
205208
// The string may contain a date only, meaning that the tzId should be ignored.
206-
_tzId = HasTime ? tzId : null;
209+
_tzId ??= HasTime ? tzId : null;
210+
211+
if (IsUtc && tzId != null && !string.Equals(tzId, UtcTzId, StringComparison.OrdinalIgnoreCase))
212+
{
213+
throw new ArgumentException($"The value '{value}' is a UTC date/time, but the specified timezone '{tzId}' is not '{UtcTzId}'.", nameof(tzId));
214+
}
207215
}
208216

209217
private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
@@ -218,7 +226,6 @@ private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
218226
};
219227
}
220228

221-
/// <inheritdoc/>
222229
private void CopyFrom(CalDateTime calDt)
223230
{
224231
// Maintain the private date/time backing fields
@@ -436,12 +443,6 @@ public DateTime Value
436443
return new TimeOnly(time.Value.Hour, time.Value.Minute, time.Value.Second);
437444
}
438445

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-
445446
/// <summary>
446447
/// Converts the <see cref="Value"/> to a date/time
447448
/// within the specified <see paramref="otherTzId"/> timezone.

0 commit comments

Comments
 (0)