Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TryFindSystemTimeZoneById #88071

Merged
merged 5 commits into from
Jun 29, 2023
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 @@ -271,59 +271,13 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out

/// <summary>
/// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order
///
/// This function will either return a valid TimeZoneInfo instance or
/// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'.
/// This function may return null.
///
/// assumes cachedData lock is taken
/// </summary>
public static TimeZoneInfo FindSystemTimeZoneById(string id)
{
// Special case for Utc as it will not exist in the dictionary with the rest
// of the system time zones. There is no need to do this check for Local.Id
// since Local is a real time zone that exists in the dictionary cache
if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
{
return Utc;
}

ArgumentNullException.ThrowIfNull(id);
if (id.Length == 0 || id.Contains('\0'))
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id));
}

TimeZoneInfo? value;
Exception? e;

TimeZoneInfoResult result;

CachedData cachedData = s_cachedData;

lock (cachedData)
{
result = TryGetTimeZone(id, false, out value, out e, cachedData, alwaysFallbackToLocalMachine: true);
}

if (result == TimeZoneInfoResult.Success)
{
return value!;
}
else if (result == TimeZoneInfoResult.InvalidTimeZoneException)
{
Debug.Assert(e is InvalidTimeZoneException,
"TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException");
throw e;
}
else if (result == TimeZoneInfoResult.SecurityException)
{
throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e);
}
else
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
}
}
private static TimeZoneInfoResult TryGetTimeZone(string id, out TimeZoneInfo? timeZone, out Exception? e, CachedData cachedData)
=> TryGetTimeZone(id, false, out timeZone, out e, cachedData, alwaysFallbackToLocalMachine: true);

// DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone
internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public sealed partial class TimeZoneInfo
private const string FirstEntryValue = "FirstEntry";
private const string LastEntryValue = "LastEntry";

private const int MaxKeyLength = 255;
private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time";

private sealed partial class CachedData
Expand Down Expand Up @@ -314,56 +313,13 @@ private static TimeZoneInfo GetLocalTimeZoneFromWin32Data(in TIME_ZONE_INFORMATI

/// <summary>
/// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order
///
/// This function will either return a valid TimeZoneInfo instance or
/// it will throw 'InvalidTimeZoneException' / 'TimeZoneNotFoundException'.
/// This function may return null.
///
/// assumes cachedData lock is taken
/// </summary>
public static TimeZoneInfo FindSystemTimeZoneById(string id)
{
ArgumentNullException.ThrowIfNull(id);

// Special case for Utc to avoid having TryGetTimeZone creating a new Utc object
if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
{
return Utc;
}

if (id.Length == 0 || id.Length > MaxKeyLength || id.Contains('\0'))
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id));
}

TimeZoneInfo? value;
Exception? e;

TimeZoneInfoResult result;

CachedData cachedData = s_cachedData;

lock (cachedData)
{
result = TryGetTimeZone(id, false, out value, out e, cachedData);
}

if (result == TimeZoneInfoResult.Success)
{
return value!;
}
else if (result == TimeZoneInfoResult.InvalidTimeZoneException)
{
throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_InvalidRegistryData, id), e);
}
else if (result == TimeZoneInfoResult.SecurityException)
{
throw new SecurityException(SR.Format(SR.Security_CannotReadRegistryData, id), e);
}
else
{
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
}
}
private static TimeZoneInfoResult TryGetTimeZone(string id, out TimeZoneInfo? timeZone, out Exception? e, CachedData cachedData)
=> TryGetTimeZone(id, false, out timeZone, out e, cachedData);

// DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone
internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst)
Expand Down
87 changes: 87 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Security;
using System.Threading;

namespace System
Expand Down Expand Up @@ -40,6 +41,8 @@ private enum TimeZoneInfoResult
SecurityException = 3
}

private const int MaxKeyLength = 255;

private readonly string _id;
private readonly string? _displayName;
private readonly string? _standardDisplayName;
Expand Down Expand Up @@ -556,6 +559,90 @@ public static DateTimeOffset ConvertTimeBySystemTimeZoneId(DateTimeOffset dateTi
public static DateTime ConvertTimeBySystemTimeZoneId(DateTime dateTime, string destinationTimeZoneId) =>
ConvertTime(dateTime, FindSystemTimeZoneById(destinationTimeZoneId));

/// <summary>
/// Helper function for retrieving a <see cref="TimeZoneInfo"/> object by time zone name.
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order.
///
/// This function will either return a valid <see cref="TimeZoneInfo"/> instance or
/// it will throw <see cref="InvalidTimeZoneException"/> / <see cref="TimeZoneNotFoundException"/> /
/// <see cref="SecurityException"/>
/// </summary>
AlexRadch marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="id">Time zone name.</param>
/// <returns>Valid <see cref="TimeZoneInfo"/> instance.</returns>
public static TimeZoneInfo FindSystemTimeZoneById(string id)
{
TimeZoneInfo? value;
Exception? e;

TimeZoneInfoResult result = TryFindSystemTimeZoneById(id, out value, out e);
switch (result)
{
case TimeZoneInfoResult.Success:
return value!;
case TimeZoneInfoResult.InvalidTimeZoneException:
Debug.Assert(e is InvalidTimeZoneException,
"TryGetTimeZone must create an InvalidTimeZoneException when it returns TimeZoneInfoResult.InvalidTimeZoneException");
throw e;
case TimeZoneInfoResult.SecurityException:
throw new SecurityException(SR.Format(SR.Security_CannotReadFileData, id), e);
default:
throw new TimeZoneNotFoundException(SR.Format(SR.TimeZoneNotFound_MissingData, id), e);
}
}

/// <summary>
/// Helper function for retrieving a <see cref="TimeZoneInfo"/> object by time zone name.
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order.
///
/// This function will either return <c>true</c> and a valid <see cref="TimeZoneInfo"/>
/// instance or return <c>false</c> and <c>null</c>.
/// </summary>
/// <param name="id">Time zone name.</param>
/// <param name="timeZoneInfo">A valid retrieved <see cref="TimeZoneInfo"/> or <c>null</c>.</param>
/// <returns><c>true</c> if the <see cref="TimeZoneInfo"/> object was successfully retrieved, <c>false</c> otherwise.</returns>
public static bool TryFindSystemTimeZoneById(string id, [NotNullWhenAttribute(true)] out TimeZoneInfo? timeZoneInfo)
=> TryFindSystemTimeZoneById(id, out timeZoneInfo, out _) == TimeZoneInfoResult.Success;

/// <summary>
/// Helper function for retrieving a TimeZoneInfo object by time_zone_name.
/// This function wraps the logic necessary to keep the private
/// SystemTimeZones cache in working order.
///
/// This function will either return:
/// <c>TimeZoneInfoResult.Success</c> and a valid <see cref="TimeZoneInfo"/>instance and <c>null</c> Exception or
/// <c>TimeZoneInfoResult.TimeZoneNotFoundException</c> and <c>null</c> <see cref="TimeZoneInfo"/> and Exception (can be null) or
/// other <c>TimeZoneInfoResult</c> and <c>null</c> <see cref="TimeZoneInfo"/> and valid Exception.
/// </summary>
private static TimeZoneInfoResult TryFindSystemTimeZoneById(string id, out TimeZoneInfo? timeZone, out Exception? e)
{
// Special case for Utc as it will not exist in the dictionary with the rest
// of the system time zones. There is no need to do this check for Local.Id
// since Local is a real time zone that exists in the dictionary cache
if (string.Equals(id, UtcId, StringComparison.OrdinalIgnoreCase))
{
timeZone = Utc;
e = default;
return TimeZoneInfoResult.Success;
}

ArgumentNullException.ThrowIfNull(id);
if (id.Length == 0 || id.Length > MaxKeyLength || id.Contains('\0'))
AlexRadch marked this conversation as resolved.
Show resolved Hide resolved
{
timeZone = default;
e = default;
return TimeZoneInfoResult.TimeZoneNotFoundException;
}

CachedData cachedData = s_cachedData;

lock (cachedData)
{
return TryGetTimeZone(id, out timeZone, out e, cachedData);
}
}

/// <summary>
/// Converts the value of a DateTime object from sourceTimeZone to destinationTimeZone.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5610,6 +5610,7 @@ public static void ClearCachedData() { }
public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; }
public bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] System.TimeZoneInfo? other) { throw null; }
public static System.TimeZoneInfo FindSystemTimeZoneById(string id) { throw null; }
public static bool TryFindSystemTimeZoneById(string id, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.TimeZoneInfo? timeZoneInfo) { throw null; }
public static System.TimeZoneInfo FromSerializedString(string source) { throw null; }
public System.TimeZoneInfo.AdjustmentRule[] GetAdjustmentRules() { throw null; }
public System.TimeSpan[] GetAmbiguousTimeOffsets(System.DateTime dateTime) { throw null; }
Expand Down
46 changes: 46 additions & 0 deletions src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,13 @@ public static void LibyaTimeZone()
try
{
tripoli = TimeZoneInfo.FindSystemTimeZoneById(s_strLibya);
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(s_strLibya, out _));
}
catch (Exception /* TimeZoneNotFoundException in netstandard1.7 test*/ )
{
// Libya time zone not found
Console.WriteLine("Warning: Libya time zone is not exist in this machine");
Assert.False(TimeZoneInfo.TryFindSystemTimeZoneById(s_strLibya, out _));
return;
}

Expand All @@ -189,6 +191,7 @@ public static void TestYukonTZ()
try
{
TimeZoneInfo yukon = TimeZoneInfo.FindSystemTimeZoneById("Yukon Standard Time");
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById("Yukon Standard Time", out _));

// First, ensure we have the updated data
TimeZoneInfo.AdjustmentRule[] rules = yukon.GetAdjustmentRules();
Expand All @@ -214,13 +217,16 @@ public static void TestYukonTZ()
catch (TimeZoneNotFoundException)
{
// Some Windows versions don't carry the complete TZ data. Ignore the tests on such versions.
Assert.False(TimeZoneInfo.TryFindSystemTimeZoneById("Yukon Standard Time", out _));
return;
}
}

[Fact]
public static void RussianTimeZone()
{
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById(s_strRussian);
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(s_strRussian, out _));
var inputUtcDate = new DateTime(2013, 6, 1, 0, 0, 0, DateTimeKind.Utc);

DateTime russiaTime = TimeZoneInfo.ConvertTime(inputUtcDate, tz);
Expand Down Expand Up @@ -2060,6 +2066,15 @@ public static void ClearCachedData()
{
TimeZoneInfo.ConvertTime(DateTime.Now, local, cst);
});

Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(s_strSydney, out cst));
local = TimeZoneInfo.Local;

TimeZoneInfo.ClearCachedData();
Assert.ThrowsAny<ArgumentException>(() =>
{
TimeZoneInfo.ConvertTime(DateTime.Now, local, cst);
});
}

[Fact]
Expand Down Expand Up @@ -2721,6 +2736,9 @@ public static void EnsureUtcObjectSingleton()
TimeZoneInfo utcObject = TimeZoneInfo.GetSystemTimeZones().Single(x => x.Id.Equals("UTC", StringComparison.OrdinalIgnoreCase));
Assert.True(ReferenceEquals(utcObject, TimeZoneInfo.Utc));
Assert.True(ReferenceEquals(TimeZoneInfo.FindSystemTimeZoneById("UTC"), TimeZoneInfo.Utc));

Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById("UTC", out TimeZoneInfo tz));
Assert.True(ReferenceEquals(tz, TimeZoneInfo.Utc));
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
Expand Down Expand Up @@ -2748,11 +2766,20 @@ public static void UsingAlternativeTimeZoneIdsTest(string windowsId, string iana

Assert.Equal(tzi1.BaseUtcOffset, tzi2.BaseUtcOffset);
Assert.NotEqual(tzi1.Id, tzi2.Id);

Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(ianaId, out tzi1));
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(windowsId, out tzi2));

Assert.Equal(tzi1.BaseUtcOffset, tzi2.BaseUtcOffset);
Assert.NotEqual(tzi1.Id, tzi2.Id);
}
else
{
Assert.Throws<TimeZoneNotFoundException>(() => TimeZoneInfo.FindSystemTimeZoneById(s_isWindows ? ianaId : windowsId));
TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(s_isWindows ? windowsId : ianaId);

Assert.False(TimeZoneInfo.TryFindSystemTimeZoneById(s_isWindows ? ianaId : windowsId, out _));
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(s_isWindows ? windowsId : ianaId, out _));
}
}

Expand Down Expand Up @@ -2800,6 +2827,8 @@ public static void UnsupportedImplicitConversionTest()
string nonNativeTzName = s_isWindows ? "America/Los_Angeles" : "Pacific Standard Time";

Assert.Throws<TimeZoneNotFoundException>(() => TimeZoneInfo.FindSystemTimeZoneById(nonNativeTzName));

Assert.False(TimeZoneInfo.TryFindSystemTimeZoneById(nonNativeTzName, out _));
}

[ConditionalTheory(nameof(SupportIanaNamesConversion))]
Expand Down Expand Up @@ -2948,6 +2977,23 @@ public static void ChangeLocalTimeZone(string id)
TimeZoneInfo.ClearCachedData();
Environment.SetEnvironmentVariable("TZ", originalTZ);
}

try
{
TimeZoneInfo.ClearCachedData();
Environment.SetEnvironmentVariable("TZ", id);

TimeZoneInfo localtz = TimeZoneInfo.Local;
Assert.True(TimeZoneInfo.TryFindSystemTimeZoneById(id, out TimeZoneInfo tz));

Assert.Equal(tz.StandardName, localtz.StandardName);
Assert.Equal(tz.DisplayName, localtz.DisplayName);
}
finally
{
TimeZoneInfo.ClearCachedData();
Environment.SetEnvironmentVariable("TZ", originalTZ);
}
}

[Fact]
Expand Down