Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -284,7 +284,7 @@ public AdjustmentRule[] GetAdjustmentRules()
return daylightDisplayName;
}

private static void PopulateAllSystemTimeZones(CachedData cachedData)
private static Dictionary<string, TimeZoneInfo> PopulateAllSystemTimeZones(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));

Expand All @@ -295,13 +295,25 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)

if (Invariant)
{
return;
return cachedData._systemTimeZones;
}

// The filtered list that shouldn't have any duplicates.
Dictionary<string, TimeZoneInfo> filteredTimeZones = new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
{
{ UtcId, s_utcTimeZone }
};

foreach (string timeZoneId in GetTimeZoneIds())
{
TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache
if (TryGetTimeZone(timeZoneId, false, out TimeZoneInfo? timeZone, out _, cachedData, alwaysFallbackToLocalMachine: true) == TimeZoneInfoResult.Success &&
timeZone is not null)
{
filteredTimeZones[timeZoneId] = timeZone;
}
}

return filteredTimeZones;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,19 @@ public AdjustmentRule[] GetAdjustmentRules()
return null;
}

private static void PopulateAllSystemTimeZones(CachedData cachedData)
private static Dictionary<string, TimeZoneInfo> PopulateAllSystemTimeZones(CachedData cachedData)
{
Debug.Assert(Monitor.IsEntered(cachedData));

// Ensure _systemTimeZones is initialized. TryGetTimeZone with Invariant mode depend on that.
cachedData._systemTimeZones ??= new Dictionary<string, TimeZoneInfo>(StringComparer.OrdinalIgnoreCase)
{
{ UtcId, s_utcTimeZone }
};

if (Invariant)
{
return;
return cachedData._systemTimeZones;
}

using (RegistryKey? reg = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false))
Expand All @@ -89,10 +90,13 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData)
{
foreach (string keyName in reg.GetSubKeyNames())
{
TryGetTimeZone(keyName, false, out _, out _, cachedData); // populate the cache
TryGetTimeZone(keyName, false, out _, out _, cachedData); // should update cache._systemTimeZones
}
}
}

// On Windows, there is no filtered list as _systemTimeZones always not having any duplicates.
return cachedData._systemTimeZones;
}

private static string? GetAlternativeId(string id, out bool idIsIana)
Expand Down
41 changes: 23 additions & 18 deletions src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,20 @@ internal long GetLocalDateTimeNowTicks(DateTime utcNow, out bool isAmbiguous)
return localTicks;
}


// System time zones list. This could be a superset of the list we return from TimeZoneInfo.GetSystemTimeZones on Linux and macOS.
// This list can contain duplicate time zones with different legacy IDs.
public Dictionary<string, TimeZoneInfo>? _systemTimeZones;

// The sorted readonly collection created after populating _systemTimeZones.
// This collection returned to the callers of TimeZoneInfo.GetSystemTimeZones(skipSorting: false).
public ReadOnlyCollection<TimeZoneInfo>? _readOnlySystemTimeZones;

// The unsorted readonly collection created after populating _systemTimeZones.
// This collection returned to the callers of TimeZoneInfo.GetSystemTimeZones(skipSorting: true).
public ReadOnlyCollection<TimeZoneInfo>? _readOnlyUnsortedSystemTimeZones;

// Alternative IDs usually be IANA names when running on Windows and will be Windows TZ IDs on Linux/macOS.
public Dictionary<string, TimeZoneInfo>? _timeZonesUsingAlternativeIds;
public bool _allSystemTimeZonesRead;
public volatile DateTimeNowCache? _dateTimeNowCache;
Expand Down Expand Up @@ -757,17 +768,14 @@ public static ReadOnlyCollection<TimeZoneInfo> GetSystemTimeZones(bool skipSorti
{
if ((skipSorting ? cachedData._readOnlyUnsortedSystemTimeZones : cachedData._readOnlySystemTimeZones) is null)
{
if (!cachedData._allSystemTimeZonesRead)
{
PopulateAllSystemTimeZones(cachedData);
cachedData._allSystemTimeZonesRead = true;
}
Dictionary<string, TimeZoneInfo>? filteredTimeZones = PopulateAllSystemTimeZones(cachedData);
cachedData._allSystemTimeZonesRead = true;

if (cachedData._systemTimeZones != null)
if (filteredTimeZones is not null)
{
// return a collection of the cached system time zones
TimeZoneInfo[] array = new TimeZoneInfo[cachedData._systemTimeZones.Count];
cachedData._systemTimeZones.Values.CopyTo(array, 0);
// return a collection of the filtered cached system time zones
TimeZoneInfo[] array = new TimeZoneInfo[filteredTimeZones.Count];
filteredTimeZones.Values.CopyTo(array, 0);

if (!skipSorting)
{
Expand Down Expand Up @@ -1205,18 +1213,15 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab
}

// check the cache
if (cachedData._systemTimeZones != null)
if (cachedData._systemTimeZones?.TryGetValue(id, out value) is true)
{
if (cachedData._systemTimeZones.TryGetValue(id, out value))
if (dstDisabled && value._supportsDaylightSavingTime)
{
if (dstDisabled && value._supportsDaylightSavingTime)
{
// we found a cache hit but we want a time zone without DST and this one has DST data
value = CreateCustomTimeZone(value._id, value._baseUtcOffset, value._displayName, value._standardDisplayName);
}

return result;
// we found a cache hit but we want a time zone without DST and this one has DST data
value = CreateCustomTimeZone(value._id, value._baseUtcOffset, value._displayName, value._standardDisplayName);
}

return result;
}

if (Invariant)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2766,8 +2766,7 @@ public static void FijiTimeZoneTest()
}
}

[ConditionalFact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/64111", TestPlatforms.Linux)]
[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/117731", TestPlatforms.Android)]
public static void NoBackwardTimeZones()
{
Expand All @@ -2776,6 +2775,15 @@ public static void NoBackwardTimeZones()
throw new SkipTestException("This test won't work on API level < 26");
}

// Clear cached data to always ensure predictable results
TimeZoneInfo.ClearCachedData();
if (SupportLegacyTimeZoneNames)
{
// Get legacy time zone "UCT" and ensure is not included in list returned by TimeZoneInfo.GetSystemTimeZones
// to ensure no duplicates display names as we should have "UTC" in the list.
TimeZoneInfo uct = TimeZoneInfo.FindSystemTimeZoneById("UCT");
}

ReadOnlyCollection<TimeZoneInfo> tzCollection = TimeZoneInfo.GetSystemTimeZones();
HashSet<String> tzDisplayNames = new HashSet<String>();

Expand All @@ -2786,6 +2794,47 @@ public static void NoBackwardTimeZones()
Assert.Equal(tzCollection.Count, tzDisplayNames.Count);
}

[Fact]
public static void TestGetSystemTimeZones()
{
TimeZoneInfo.ClearCachedData(); // Start clean
ReadOnlyCollection<TimeZoneInfo> tzCollection1 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true);
ReadOnlyCollection<TimeZoneInfo> tzCollection2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false);
Assert.Equal(tzCollection1.Count, tzCollection2.Count);
foreach (TimeZoneInfo timezone in tzCollection1)
{
Assert.Contains(timezone, tzCollection2);
}

TimeZoneInfo.ClearCachedData(); // Start again as clean
tzCollection1 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false);
tzCollection2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true);
Assert.Equal(tzCollection1.Count, tzCollection2.Count);
foreach (TimeZoneInfo timezone in tzCollection1)
{
Assert.Contains(timezone, tzCollection2);
}

TimeZoneInfo.ClearCachedData(); // Start clean
if (SupportLegacyTimeZoneNames)
{
// Get legacy time zone "UCT" and ensure is not included in list returned by TimeZoneInfo.GetSystemTimeZones
// to ensure no duplicates display names as we should have "UTC" in the list.
TimeZoneInfo uct = TimeZoneInfo.FindSystemTimeZoneById("UCT");
}

ReadOnlyCollection<TimeZoneInfo> tzCollection3 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true);
ReadOnlyCollection<TimeZoneInfo> tzCollection4 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false);
Assert.Equal(tzCollection3.Count, tzCollection4.Count);
Assert.Equal(tzCollection1.Count, tzCollection4.Count);
foreach (TimeZoneInfo timezone in tzCollection3)
{
Assert.Contains(timezone, tzCollection4);
}

Assert.DoesNotContain(tzCollection4, t => t.Id == "UCT");
}

[Fact]
[PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS)]
[Trait(XunitConstants.Category, "AdditionalTimezoneChecks")]
Expand Down
Loading