diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 4c74d5a254a93..87a03dcdfa218 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -271,59 +271,13 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out /// /// 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 /// - 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) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs index c748b9afa609a..3344e9ef82b77 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -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 @@ -314,56 +313,13 @@ private static TimeZoneInfo GetLocalTimeZoneFromWin32Data(in TIME_ZONE_INFORMATI /// /// 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 /// - 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) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 2d354edaa1620..97c6d0a10f7ae 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.Serialization; +using System.Security; using System.Threading; namespace System @@ -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; @@ -556,6 +559,90 @@ public static DateTimeOffset ConvertTimeBySystemTimeZoneId(DateTimeOffset dateTi public static DateTime ConvertTimeBySystemTimeZoneId(DateTime dateTime, string destinationTimeZoneId) => ConvertTime(dateTime, FindSystemTimeZoneById(destinationTimeZoneId)); + /// + /// Helper function for retrieving a 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 instance or + /// it will throw / / + /// + /// + /// Time zone name. + /// Valid instance. + 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); + } + } + + /// + /// Helper function for retrieving a 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 true and a valid + /// instance or return false and null. + /// + /// Time zone name. + /// A valid retrieved or null. + /// true if the object was successfully retrieved, false otherwise. + public static bool TryFindSystemTimeZoneById(string id, [NotNullWhenAttribute(true)] out TimeZoneInfo? timeZoneInfo) + => TryFindSystemTimeZoneById(id, out timeZoneInfo, out _) == TimeZoneInfoResult.Success; + + /// + /// 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: + /// TimeZoneInfoResult.Success and a valid instance and null Exception or + /// TimeZoneInfoResult.TimeZoneNotFoundException and null and Exception (can be null) or + /// other TimeZoneInfoResult and null and valid Exception. + /// + 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')) + { + timeZone = default; + e = default; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + + CachedData cachedData = s_cachedData; + + lock (cachedData) + { + return TryGetTimeZone(id, out timeZone, out e, cachedData); + } + } + /// /// Converts the value of a DateTime object from sourceTimeZone to destinationTimeZone. /// diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index e5421cf35e93a..9de97af5b7c50 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -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; } diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index 46dfb23a45877..18c033afd2ade 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -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; } @@ -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(); @@ -214,6 +217,8 @@ 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; } } @@ -221,6 +226,7 @@ public static void TestYukonTZ() 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); @@ -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(() => + { + TimeZoneInfo.ConvertTime(DateTime.Now, local, cst); + }); } [Fact] @@ -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))] @@ -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(() => 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 _)); } } @@ -2800,6 +2827,8 @@ public static void UnsupportedImplicitConversionTest() string nonNativeTzName = s_isWindows ? "America/Los_Angeles" : "Pacific Standard Time"; Assert.Throws(() => TimeZoneInfo.FindSystemTimeZoneById(nonNativeTzName)); + + Assert.False(TimeZoneInfo.TryFindSystemTimeZoneById(nonNativeTzName, out _)); } [ConditionalTheory(nameof(SupportIanaNamesConversion))] @@ -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]