Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Ical.Net.Tests/CalDateTimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Licensed under the MIT license.
//

#nullable enable
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using NUnit.Framework;
Expand Down Expand Up @@ -55,7 +56,7 @@ public void ToTimeZoneFloating()
[Test, TestCaseSource(nameof(ToTimeZoneTestCases))]
public void ToTimeZoneTests(CalendarEvent calendarEvent, string targetTimeZone)
{
var startAsUtc = calendarEvent.Start.AsUtc;
var startAsUtc = calendarEvent.Start!.AsUtc;

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

Assert.That(() => new CalDateTime(dt), constraint);
}

[TestCase("20250703T060000Z", null)]
[TestCase("20250703T060000Z", CalDateTime.UtcTzId)]
public void ConstructorWithIso8601UtcString_ShouldResultInUtc(string value, string? tzId)
{
var dt = new CalDateTime(value, tzId);
Assert.Multiple(() =>
{
Assert.That(dt.Value, Is.EqualTo(new DateTime(2025, 7, 3, 6, 0, 0, DateTimeKind.Utc)));
#pragma warning disable CA1305
Assert.That(dt.ToString("yyyy-MM-dd HH:mm:ss"), Is.EqualTo("2025-07-03 06:00:00 UTC"));
#pragma warning restore CA1305
Assert.That(dt.IsUtc, Is.True);
});
}

[Test]
public void ConstructorWithIso8601UtcString_ButDifferentTzId_ShouldThrow()
=> Assert.That(() => _ = new CalDateTime("20250703T060000Z", "CEST"), Throws.ArgumentException);
}
32 changes: 13 additions & 19 deletions Ical.Net/DataTypes/CalDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Ical.Net.Utility;
using NodaTime;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;

Expand Down Expand Up @@ -55,6 +56,7 @@ public sealed class CalDateTime : IComparable<CalDateTime>, IFormattable
/// <summary>
/// This constructor is required for the SerializerFactory to work.
/// </summary>
[ExcludeFromCodeCoverage]
private CalDateTime()
{
// required for the SerializerFactory to work
Expand Down Expand Up @@ -200,10 +202,17 @@ public CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null)
public CalDateTime(string value, string? tzId = null)
{
var serializer = new DateTimeSerializer();
CopyFrom(serializer.Deserialize(new StringReader(value)) as CalDateTime
?? throw new InvalidOperationException($"$Failure for deserializing value '{value}'"));
// The string may contain a date only, meaning that the tzId should be ignored.
_tzId = HasTime ? tzId : null;
var dt = serializer.Deserialize(new StringReader(value)) as CalDateTime
?? throw new InvalidOperationException($"Failure when deserializing value '{value}'");

Initialize(dt._dateOnly, dt._timeOnly, dt.IsUtc ? UtcTzId : tzId);

if (dt.IsUtc && tzId != null && !string.Equals(tzId, UtcTzId, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
$"The value '{value}' represents UTC date/time, but the specified timezone '{tzId}' is not '{UtcTzId}'.",
nameof(tzId));
}
}

private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
Expand All @@ -218,15 +227,6 @@ private void Initialize(DateOnly dateOnly, TimeOnly? timeOnly, string? tzId)
};
}

/// <inheritdoc/>
private void CopyFrom(CalDateTime calDt)
{
// Maintain the private date/time backing fields
_dateOnly = calDt._dateOnly;
_timeOnly = TruncateTimeToSeconds(calDt._timeOnly);
_tzId = calDt._tzId;
}

public bool Equals(CalDateTime? other) => this == other;

/// <inheritdoc/>
Expand Down Expand Up @@ -436,12 +436,6 @@ public DateTime Value
return new TimeOnly(time.Value.Hour, time.Value.Minute, time.Value.Second);
}

/// <summary>
/// Any <see cref="Time"/> values are truncated to seconds, because
/// RFC 5545, Section 3.3.5 does not allow for fractional seconds.
/// </summary>
private static TimeOnly? TruncateTimeToSeconds(DateTime dateTime) => new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second);

/// <summary>
/// Converts the <see cref="Value"/> to a date/time
/// within the specified <see paramref="otherTzId"/> timezone.
Expand Down
Loading