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
Original file line number Diff line number Diff line change
Expand Up @@ -288,22 +288,21 @@ private bool IsDaylightSavingOn(DateTime localDateTime)
/// Tries to convert a local date and time to Coordinated Universal Time (UTC).
/// </summary>
/// <param name="localDateTime">The local date and time to convert.</param>
/// <param name="utcDateTime">When this method returns, contains the UTC date and time if the conversion succeeded.</param>
/// <param name="utcTicks">When this method returns, contains the UTC ticks if the conversion succeeded. The value may be negative or exceed DateTime range.</param>
/// <returns>True if the conversion was successful; otherwise, false.</returns>
/// <remarks>
/// This method attempts to convert a local time to UTC. It returns false if the local time is invalid,
/// such as during a daylight saving time transition when the local time does not exist.
/// </remarks>
private bool TryLocalToUtc(DateTime localDateTime, out DateTime utcDateTime)
private bool TryLocalToUtc(DateTime localDateTime, out long utcTicks)
{
if (TryGetUtcOffset(localDateTime, out TimeSpan offset))
{
long ticks = localDateTime.Ticks - offset.Ticks;
utcDateTime = SafeCreateDateTimeFromTicks(ticks, DateTimeKind.Utc);
utcTicks = localDateTime.Ticks - offset.Ticks;
return true;
}

utcDateTime = default;
utcTicks = 0;
return false;
}

Expand Down
17 changes: 14 additions & 3 deletions src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo
throw new ArgumentException(SR.Argument_ConvertMismatch, nameof(sourceTimeZone));
}

bool isInvalidTime = !sourceTimeZone.TryLocalToUtc(dateTime, out DateTime utcDateTime);
bool isInvalidTime = !sourceTimeZone.TryLocalToUtc(dateTime, out long utcTicks);

if (((flags & TimeZoneInfoOptions.NoThrowOnInvalidTime) == 0) && isInvalidTime)
{
Expand All @@ -660,7 +660,10 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo
{
// This is not logical to do but we are keeping it for app compatibility reason.
// We get here if the dateTime is invalid in the source time zone.
utcDateTime = new DateTime(dateTime.Ticks + sourceTimeZone.BaseUtcOffset.Ticks, DateTimeKind.Utc);
// Preserve the historical behavior of throwing if the computed UTC time is
// outside the DateTime range, rather than silently clamping it later.
DateTime invalidTimeUtc = new DateTime(dateTime.Ticks + sourceTimeZone.BaseUtcOffset.Ticks, DateTimeKind.Utc);
utcTicks = invalidTimeUtc.Ticks;
}

DateTimeKind targetKind = cachedData.GetCorrespondingKind(destinationTimeZone);
Expand All @@ -671,7 +674,15 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo
return dateTime;
}

DateTime targetConverted = destinationTimeZone.UtcToLocal(utcDateTime, out bool isDaylightSaving);
// Use a clamped DateTime for destination offset lookup because transition-table lookups require
// an in-range DateTime. This preserves the existing offset-selection behavior for the lookup,
// while the final local ticks are still computed from the raw utcTicks to avoid double-clamping.
DateTime utcForLookup = SafeCreateDateTimeFromTicks(utcTicks, DateTimeKind.Utc);
TimeSpan destOffset = destinationTimeZone.GetOffsetForUtcDate(utcForLookup, out bool isDaylightSaving);

// Compute the final result from raw ticks to avoid precision loss from double-clamping.
// The intermediate UTC ticks may be outside DateTime range, but the final local ticks may be valid.
DateTime targetConverted = SafeCreateDateTimeFromTicks(utcTicks + destOffset.Ticks);

if (targetKind == DateTimeKind.Local)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,45 @@ public static void ConvertTime_DateTimeOffset_NearMinMaxValue()
VerifyConvert(DateTime.MinValue.AddHours(0.5) + earlyTimesDifference, s_strSydney, s_strPacific, DateTime.MinValue.AddHours(0.5));
}

[Theory]
[InlineData(4, 3, 1, 13, 15, 0, 13, 15)] // UTC+4 to UTC+3: subtract 1 hour
[InlineData(5, 3, 2, 30, 0, 0, 30, 0)] // UTC+5 to UTC+3: subtract 2 hours
[InlineData(3, 4, 0, 30, 0, 1, 30, 0)] // UTC+3 to UTC+4: add 1 hour
[InlineData(8, 3, 4, 0, 0, 0, 0, 0)] // UTC+8 to UTC+3: subtract 5 hours, result is MinValue boundary
public static void ConvertTime_DateTime_NearMinValue_PositiveOffsetZones(
int sourceOffsetHours, int destOffsetHours,
int inputHour, int inputMinute, int inputSecond,
int expectedHour, int expectedMinute, int expectedSecond)
{
TimeZoneInfo sourceTimeZone = TimeZoneInfo.CreateCustomTimeZone($"UTC+{sourceOffsetHours}", TimeSpan.FromHours(sourceOffsetHours), $"UTC+{sourceOffsetHours}", $"UTC+{sourceOffsetHours}");
TimeZoneInfo destTimeZone = TimeZoneInfo.CreateCustomTimeZone($"UTC+{destOffsetHours}", TimeSpan.FromHours(destOffsetHours), $"UTC+{destOffsetHours}", $"UTC+{destOffsetHours}");

DateTime earlyDate = new DateTime(0001, 01, 01, inputHour, inputMinute, inputSecond);
DateTime converted = TimeZoneInfo.ConvertTime(earlyDate, sourceTimeZone, destTimeZone);

DateTime expected = new DateTime(0001, 01, 01, expectedHour, expectedMinute, expectedSecond);
Assert.Equal(expected, converted);
}

[Theory]
[InlineData(-4, -3, 22, 46, 45, 23, 46, 45)] // UTC-4 to UTC-3: add 1 hour
[InlineData(-3, -4, 23, 46, 45, 22, 46, 45)] // UTC-3 to UTC-4: subtract 1 hour
[InlineData(-3, -8, 23, 0, 0, 18, 0, 0)] // UTC-3 to UTC-8: subtract 5 hours
public static void ConvertTime_DateTime_NearMaxValue_NegativeOffsetZones(
int sourceOffsetHours, int destOffsetHours,
int inputHour, int inputMinute, int inputSecond,
int expectedHour, int expectedMinute, int expectedSecond)
{
TimeZoneInfo sourceTimeZone = TimeZoneInfo.CreateCustomTimeZone($"UTC{sourceOffsetHours}", TimeSpan.FromHours(sourceOffsetHours), $"UTC{sourceOffsetHours}", $"UTC{sourceOffsetHours}");
TimeZoneInfo destTimeZone = TimeZoneInfo.CreateCustomTimeZone($"UTC{destOffsetHours}", TimeSpan.FromHours(destOffsetHours), $"UTC{destOffsetHours}", $"UTC{destOffsetHours}");

DateTime lateDate = new DateTime(9999, 12, 31, inputHour, inputMinute, inputSecond);
DateTime converted = TimeZoneInfo.ConvertTime(lateDate, sourceTimeZone, destTimeZone);

DateTime expected = new DateTime(9999, 12, 31, expectedHour, expectedMinute, expectedSecond);
Assert.Equal(expected, converted);
}

[Fact]
public static void ConvertTime_DateTimeOffset_VariousSystemTimeZones()
{
Expand Down
Loading