diff --git a/docs/design/features/timezone-invariant-mode.md b/docs/design/features/timezone-invariant-mode.md index 4fc3069602d1d..50951af8206b3 100644 --- a/docs/design/features/timezone-invariant-mode.md +++ b/docs/design/features/timezone-invariant-mode.md @@ -8,9 +8,28 @@ Therefore dotnet bundles the timezone database as binary as part of the runtime. That makes download size larger and application startup slower. If your application doesn't need to work with time zone information, you could use this feature to make the runtime about 200KB smaller. -You enable it in project file: +## Enabling the invariant mode + +Applications can enable the invariant mode by either of the following: + +1. in project file: + ```xml true ``` + +2. in `runtimeconfig.json` file: + + ```json + { + "runtimeOptions": { + "configProperties": { + "System.TimeZoneInfo.Invariant": true + } + } + } + ``` + +3. setting environment variable value `DOTNET_SYSTEM_TIMEZONE_INVARIANT` to `true` or `1`. diff --git a/eng/testing/linker/project.csproj.template b/eng/testing/linker/project.csproj.template index bef96938a55b8..003999b221b7c 100644 --- a/eng/testing/linker/project.csproj.template +++ b/eng/testing/linker/project.csproj.template @@ -76,6 +76,7 @@ <_PropertiesThatTriggerRelinking Remove="InvariantGlobalization" /> + <_PropertiesThatTriggerRelinking Remove="InvariantTimezone" /> diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index b0984084685b2..46014bbf3a7cd 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -253,7 +253,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs index 4a7e696f05797..cec0af6245a84 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs @@ -13,7 +13,8 @@ private static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, bool alloc windowsId = null; return false; #else - if (GlobalizationMode.Invariant || + if (Invariant || + GlobalizationMode.Invariant || GlobalizationMode.UseNls || ianaId is null || ianaId.AsSpan().ContainsAny('\\', '\n', '\r')) // ICU uses these characters as a separator diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs index 7a06c5ed21064..bd095ff47c354 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs @@ -128,6 +128,11 @@ private static int ParseGMTNumericZone(string name) // Defaults to Utc if local time zone cannot be found private static TimeZoneInfo GetLocalTimeZoneCore() { + if (Invariant) + { + return Utc; + } + string? id = Interop.Sys.GetDefaultTimeZone(); if (!string.IsNullOrEmpty(id)) { @@ -144,6 +149,12 @@ private static TimeZoneInfo GetLocalTimeZoneCore() private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e) { + if (Invariant) + { + value = null; + e = new TimeZoneNotFoundException(SR.Format(SR.InvalidTimeZone_InvalidId, id)); + return TimeZoneInfoResult.TimeZoneNotFoundException; + } value = id == LocalId ? GetLocalTimeZoneCore() : GetTimeZone(id, id); @@ -441,6 +452,11 @@ private void LoadEntryAt(Stream fs, long position, out string id, out int byteOf public string[] GetTimeZoneIds() { + if (Invariant) + { + return new string[] { "UTC" }; + } + int numTimeZoneIDs = _isBackwards.AsSpan(0, _ids.Length).Count(false); string[] nonBackwardsTZIDs = new string[numTimeZoneIDs]; var index = 0; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs index a47fabdae9847..c89c2455f5f08 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.NonAndroid.cs @@ -72,6 +72,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, value = null; e = null; + if (Invariant) + { + e = new TimeZoneNotFoundException(SR.Format(SR.InvalidTimeZone_InvalidId, id)); + return TimeZoneInfoResult.TimeZoneNotFoundException; + } if (Path.IsPathRooted(id) || IdContainsAnyDisallowedChars(id)) { e = new TimeZoneNotFoundException(SR.Format(SR.InvalidTimeZone_InvalidId, id)); @@ -147,6 +152,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, /// private static IEnumerable GetTimeZoneIds() { + if (Invariant) + { + return new string[] { "UTC" }; + } + try { var fileName = Path.Combine(GetTimeZoneDirectory(), TimeZoneFileName); @@ -403,6 +413,11 @@ private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] r /// private static string FindTimeZoneId(byte[] rawData) { + if (Invariant) + { + return LocalId; + } + // default to "Local" if we can't find the right tzfile string id = LocalId; string timeZoneDirectory = GetTimeZoneDirectory(); @@ -469,6 +484,11 @@ private static bool TryLoadTzFile(string tzFilePath, [NotNullWhen(true)] ref byt #if TARGET_WASI || TARGET_BROWSER private static bool TryLoadEmbeddedTzFile(string name, [NotNullWhen(true)] out byte[]? rawData) { + if (Invariant) + { + rawData = null; + return false; + } IntPtr bytes = Interop.Sys.GetTimeZoneData(name, out int length); if (bytes == IntPtr.Zero) { @@ -504,6 +524,10 @@ private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [ { rawData = null; id = null; + if (Invariant) + { + return false; + } string? tzVariable = GetTzEnvironmentVariable(); // If the env var is null, on iOS/tvOS, grab the default tz from the device. @@ -573,6 +597,11 @@ private static bool TryGetLocalTzFile([NotNullWhen(true)] out byte[]? rawData, [ /// private static TimeZoneInfo GetLocalTimeZoneFromTzFile() { + if (Invariant) + { + return Utc; + } + byte[]? rawData; string? id; if (TryGetLocalTzFile(out rawData, out id)) 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 a5a3412b26b65..89ce9d7beebe0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -288,6 +288,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); + cachedData._systemTimeZones ??= new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { UtcId, s_utcTimeZone } + }; + + if (Invariant) + { + return; + } + foreach (string timeZoneId in GetTimeZoneIds()) { TryGetTimeZone(timeZoneId, false, out _, out _, cachedData, alwaysFallbackToLocalMachine: true); // populate the cache 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 6a3fa2803af2d..82047c682c7a2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -114,6 +114,16 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); + cachedData._systemTimeZones ??= new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { UtcId, s_utcTimeZone } + }; + + if (Invariant) + { + return; + } + using (RegistryKey? reg = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false)) { if (reg != null) @@ -145,7 +155,7 @@ private TimeZoneInfo(in TIME_ZONE_INFORMATION zone, bool dstDisabled) } _baseUtcOffset = new TimeSpan(0, -(zone.Bias), 0); - if (!dstDisabled) + if (!Invariant && !dstDisabled) { // only create the adjustment rule if DST is enabled REG_TZI_FORMAT regZone = new REG_TZI_FORMAT(zone); @@ -232,6 +242,11 @@ private static bool CheckDaylightSavingTimeNotSupported(in TIME_ZONE_INFORMATION { dstDisabled = false; + if (Invariant) + { + return null; + } + using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive, writable: false)) { if (key == null) @@ -261,6 +276,11 @@ private static TimeZoneInfo GetLocalTimeZone(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); + if (Invariant) + { + return Utc; + } + // // Try using the "kernel32!GetDynamicTimeZoneInformation" API to get the "id" // @@ -347,6 +367,12 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, out TimeZoneInfo? ti internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) { isAmbiguousLocalDst = false; + + if (Invariant) + { + return TimeSpan.Zero; + } + int timeYear = time.Year; OffsetAndRule match = s_cachedData.GetOneYearLocalFromUtc(timeYear); @@ -493,6 +519,11 @@ private static bool TryCreateAdjustmentRules(string id, in REG_TZI_FORMAT defaul rules = null; e = null; + if (Invariant) + { + return false; + } + try { // Optional, Dynamic Time Zone Registry Data @@ -720,7 +751,7 @@ private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMA /// private static string GetLocalizedNameByMuiNativeResource(string resource) { - if (string.IsNullOrEmpty(resource) || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly)) + if (string.IsNullOrEmpty(resource) || Invariant || (GlobalizationMode.Invariant && GlobalizationMode.PredefinedCulturesOnly)) { return string.Empty; } @@ -785,6 +816,11 @@ private static string GetLocalizedNameByMuiNativeResource(string resource) /// private static unsafe string GetLocalizedNameByNativeResource(string filePath, int resource) { + if (Invariant) + { + return string.Empty; + } + IntPtr handle = IntPtr.Zero; try { @@ -868,6 +904,11 @@ private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string? private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) { e = null; + if (Invariant) + { + value = null; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } // Standard Time Zone Registry Data // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -950,6 +991,11 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out // Helper function to get the standard display name for the UTC static time zone instance private static string GetUtcStandardDisplayName() { + if (Invariant) + { + return InvariantUtcStandardDisplayName; + } + // Don't bother looking up the name for invariant or English cultures CultureInfo uiCulture = CultureInfo.CurrentUICulture; if (uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en") diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 8c28866238387..bd37b695689f2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -60,6 +60,9 @@ private enum TimeZoneInfoResult private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); private static CachedData s_cachedData = new CachedData(); + [FeatureSwitchDefinition("System.TimeZoneInfo.Invariant")] + internal static bool Invariant { get; } = AppContextConfigHelper.GetBooleanConfig("System.TimeZoneInfo.Invariant", "DOTNET_SYSTEM_TIMEZONE_INVARIANT"); + // // All cached data are encapsulated in a helper class to allow consistent view even when the data are refreshed using ClearCachedData() // @@ -2054,6 +2057,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab TimeZoneInfoResult result = TimeZoneInfoResult.Success; e = null; + if (Invariant && !cachedData._allSystemTimeZonesRead) + { + PopulateAllSystemTimeZones(cachedData); + cachedData._allSystemTimeZonesRead = true; + } + // check the cache if (cachedData._systemTimeZones != null) { @@ -2069,6 +2078,12 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab } } + if (Invariant) + { + value = null; + return TimeZoneInfoResult.TimeZoneNotFoundException; + } + if (cachedData._timeZonesUsingAlternativeIds != null) { if (cachedData._timeZonesUsingAlternativeIds.TryGetValue(id, out value)) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/Hybrid/System.Runtime.IOS.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/Hybrid/System.Runtime.IOS.Tests.csproj index 0eb9e1d6a4942..1686280b3a97b 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/Hybrid/System.Runtime.IOS.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/Hybrid/System.Runtime.IOS.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj new file mode 100644 index 0000000000000..0f7bb969766d7 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/InvariantTimezone/System.Runtime.InvariantTimezone.Tests.csproj @@ -0,0 +1,29 @@ + + + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-unix;$(NetCoreAppCurrent)-android;$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos + true + true + true + true + true + true + + + + $(WithoutCategories);AdditionalTimezoneChecks + + + + + + + + + + + + + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj index fdb5ae06eba71..5e2e8de1cf9e7 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System.Runtime.Tests.csproj @@ -168,6 +168,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Common.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Common.cs new file mode 100644 index 0000000000000..fe3fd057e5cc1 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Common.cs @@ -0,0 +1,714 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text.RegularExpressions; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace System.Tests +{ + public static partial class TimeZoneInfoTests + { + private static readonly bool s_isWindows = OperatingSystem.IsWindows(); + private static readonly bool s_isOSX = OperatingSystem.IsMacOS(); + + private static string s_strUtc = "UTC"; + private static string s_strPacific = s_isWindows ? "Pacific Standard Time" : "America/Los_Angeles"; + private static string s_strSydney = s_isWindows ? "AUS Eastern Standard Time" : "Australia/Sydney"; + private static string s_strGMT = s_isWindows ? "GMT Standard Time" : "Europe/London"; + private static string s_strTonga = s_isWindows ? "Tonga Standard Time" : "Pacific/Tongatapu"; + private static string s_strBrasil = s_isWindows ? "E. South America Standard Time" : "America/Sao_Paulo"; + private static string s_strPerth = s_isWindows ? "W. Australia Standard Time" : "Australia/Perth"; + private static string s_strBrasilia = s_isWindows ? "E. South America Standard Time" : "America/Sao_Paulo"; + private static string s_strNairobi = s_isWindows ? "E. Africa Standard Time" : "Africa/Nairobi"; + private static string s_strAmsterdam = s_isWindows ? "W. Europe Standard Time" : "Europe/Berlin"; + private static string s_strRussian = s_isWindows ? "Russian Standard Time" : "Europe/Moscow"; + private static string s_strLibya = s_isWindows ? "Libya Standard Time" : "Africa/Tripoli"; + private static string s_strJohannesburg = s_isWindows ? "South Africa Standard Time" : "Africa/Johannesburg"; + private static string s_strCasablanca = s_isWindows ? "Morocco Standard Time" : "Africa/Casablanca"; + private static string s_strCatamarca = s_isWindows ? "Argentina Standard Time" : "America/Argentina/Catamarca"; + private static string s_strLisbon = s_isWindows ? "GMT Standard Time" : "Europe/Lisbon"; + private static string s_strNewfoundland = s_isWindows ? "Newfoundland Standard Time" : "America/St_Johns"; + private static string s_strIran = s_isWindows ? "Iran Standard Time" : "Asia/Tehran"; + private static string s_strFiji = s_isWindows ? "Fiji Standard Time" : "Pacific/Fiji"; + + private static TimeZoneInfo s_myUtc = TimeZoneInfo.Utc; + private static TimeZoneInfo s_myLocal = TimeZoneInfo.Local; + + [Fact] + public static void Kind() + { + TimeZoneInfo tzi = TimeZoneInfo.Local; + Assert.Equal(tzi, TimeZoneInfo.Local); + tzi = TimeZoneInfo.Utc; + Assert.Equal(tzi, TimeZoneInfo.Utc); + } + + [Fact] + public static void Names() + { + TimeZoneInfo local = TimeZoneInfo.Local; + TimeZoneInfo utc = TimeZoneInfo.Utc; + + Assert.NotNull(local.DaylightName); + Assert.NotNull(local.DisplayName); + Assert.NotNull(local.StandardName); + Assert.NotNull(local.ToString()); + + Assert.NotNull(utc.DaylightName); + Assert.NotNull(utc.DisplayName); + Assert.NotNull(utc.StandardName); + Assert.NotNull(utc.ToString()); + } + + [Fact] + public static void ConvertTime() + { + TimeZoneInfo local = TimeZoneInfo.Local; + TimeZoneInfo utc = TimeZoneInfo.Utc; + + DateTime dt = TimeZoneInfo.ConvertTime(DateTime.Today, utc); + Assert.Equal(DateTime.Today, TimeZoneInfo.ConvertTime(dt, local)); + + DateTime today = new DateTime(DateTime.Today.Ticks, DateTimeKind.Utc); + dt = TimeZoneInfo.ConvertTime(today, local); + Assert.Equal(today, TimeZoneInfo.ConvertTime(dt, utc)); + } + + [Fact] + public static void CaseInsensitiveLookupUtc() + { + Assert.Equal(TimeZoneInfo.FindSystemTimeZoneById(s_strUtc), TimeZoneInfo.FindSystemTimeZoneById(s_strUtc.ToLowerInvariant())); + + // Populate internal cache with all timezones. The implementation takes different path for lookup by id + // when all timezones are populated. + TimeZoneInfo.GetSystemTimeZones(); + + // The timezones used for the tests after GetSystemTimeZones calls have to be different from the ones used before GetSystemTimeZones to + // exercise the rare path. + Assert.Equal(TimeZoneInfo.FindSystemTimeZoneById(s_strUtc), TimeZoneInfo.FindSystemTimeZoneById(s_strUtc.ToLowerInvariant())); + } + + [Fact] + public static void ConvertTime_DateTime_UtcToUtc() + { + var time1utc = new DateTime(2003, 3, 30, 0, 0, 23, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + time1utc = new DateTime(2003, 3, 30, 2, 0, 24, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + time1utc = new DateTime(2003, 3, 30, 5, 19, 20, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + time1utc = new DateTime(2003, 10, 26, 2, 0, 0, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + time1utc = new DateTime(2003, 10, 26, 2, 20, 0, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + time1utc = new DateTime(2003, 10, 26, 3, 0, 1, DateTimeKind.Utc); + VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); + } + + [Fact] + public static void ConvertTime_DateTime_MiscUtc_Utc() + { + VerifyConvert(new DateTime(2003, 4, 6, 1, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 4, 6, 1, 30, 0), DateTimeKind.Utc)); + VerifyConvert(new DateTime(2003, 4, 6, 2, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 4, 6, 2, 30, 0), DateTimeKind.Utc)); + VerifyConvert(new DateTime(2003, 10, 26, 1, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 10, 26, 1, 30, 0), DateTimeKind.Utc)); + VerifyConvert(new DateTime(2003, 10, 26, 2, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 10, 26, 2, 30, 0), DateTimeKind.Utc)); + VerifyConvert(new DateTime(2003, 8, 4, 12, 0, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 8, 4, 12, 0, 0), DateTimeKind.Utc)); + + // Round trip + + VerifyRoundTrip(new DateTime(2003, 8, 4, 12, 0, 0, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); + VerifyRoundTrip(new DateTime(1929, 3, 9, 23, 59, 59, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); + VerifyRoundTrip(new DateTime(2000, 2, 28, 23, 59, 59, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); + + // DateTime(2016, 11, 6, 8, 1, 17, DateTimeKind.Utc) is ambiguous time for Pacific Time Zone + VerifyRoundTrip(new DateTime(2016, 11, 6, 8, 1, 17, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); + + VerifyRoundTrip(DateTime.UtcNow, "UTC", TimeZoneInfo.Local.Id); + + var time1 = new DateTime(2006, 5, 12, 7, 34, 59); + VerifyConvert(time1, "UTC", DateTime.SpecifyKind(time1.Subtract(TimeZoneInfo.Local.GetUtcOffset(time1)), DateTimeKind.Utc)); + VerifyConvert(DateTime.SpecifyKind(time1, DateTimeKind.Local), "UTC", DateTime.SpecifyKind(time1.Subtract(TimeZoneInfo.Local.GetUtcOffset(time1)), DateTimeKind.Utc)); + } + + [Fact] + public static void ConvertTime_NullTimeZone_ThrowsArgumentNullException_Utc() + { + AssertExtensions.Throws("destinationTimeZone", () => TimeZoneInfo.ConvertTime(new DateTime(), null)); + AssertExtensions.Throws("destinationTimeZone", () => TimeZoneInfo.ConvertTime(new DateTimeOffset(), null)); + } + + [Fact] + public static void GetAmbiguousTimeOffsets_Invalid() + { + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32)); + + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31, DateTimeKind.Utc)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32, DateTimeKind.Utc)); + + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31, DateTimeKind.Local)); + VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32, DateTimeKind.Local)); + } + + [Fact] + public static void IsDaylightSavingTime_Utc() + { + VerifyDST(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32), false); + VerifyDST(TimeZoneInfo.Utc, new DateTime(2004, 4, 4, 2, 30, 0, DateTimeKind.Local), false); + } + + [Fact] + public static void IsInvalidTime_Utc() + { + VerifyInv(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31), false); + VerifyInv(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32), false); + } + + + private static void ValidateTimeZonesSorting(ReadOnlyCollection zones) + { + // validate sorting: first by base offset, then by display name + for (int i = 1; i < zones.Count; i++) + { + TimeZoneInfo previous = zones[i - 1]; + TimeZoneInfo current = zones[i]; + + int baseOffsetsCompared = current.BaseUtcOffset.CompareTo(previous.BaseUtcOffset); + Assert.True(baseOffsetsCompared >= 0, + string.Format($"TimeZoneInfos are out of order. {previous.Id}:{previous.BaseUtcOffset} should be before {current.Id}:{current.BaseUtcOffset}")); + + if (baseOffsetsCompared == 0) + { + Assert.True(string.CompareOrdinal(current.DisplayName, previous.DisplayName) >= 0, + string.Format($"TimeZoneInfos are out of order. {previous.DisplayName} should be before {current.DisplayName}")); + } + } + } + + private static void ValidateDifferentTimeZoneLists(ReadOnlyCollection defaultList, ReadOnlyCollection nonSortedList, ReadOnlyCollection sortedList) + { + Assert.Equal(defaultList.Count, nonSortedList.Count); + Assert.Equal(defaultList.Count, sortedList.Count); + + Assert.Equal(defaultList.Count, nonSortedList.Count); + Assert.True(object.ReferenceEquals(defaultList, sortedList)); + Dictionary zones1Dict = defaultList.ToDictionary(t => t.Id); + foreach (TimeZoneInfo zone in nonSortedList) + { + Assert.True(zones1Dict.TryGetValue(zone.Id, out TimeZoneInfo zone1)); + } + + ValidateTimeZonesSorting(defaultList); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void TestGetSystemTimeZonesCollectionsCallsOrder() + { + RemoteExecutor.Invoke(() => + { + // + // Get sorted list first and then the unsorted list + // + var zones1 = TimeZoneInfo.GetSystemTimeZones(); + var zones2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); + var zones3 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); + + ValidateDifferentTimeZoneLists(zones1, zones2, zones3); + + // + // Clear our caches so zone enumeration is forced to re-read the data + // + TimeZoneInfo.ClearCachedData(); + + // + // Get unsorted list first and then the sorted list + // + zones2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); + zones3 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); + zones1 = TimeZoneInfo.GetSystemTimeZones(); + ValidateDifferentTimeZoneLists(zones1, zones2, zones3); + + }).Dispose(); + } + + [Fact] + public static void TestGetSystemTimeZonesCollections() + { + // This test doing similar checks as TestGetSystemTimeZonesCollectionsCallsOrder does except we need to + // run this test without the RemoteExecutor to ensure testing on platforms like Android. + + ReadOnlyCollection unsortedList = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); + ReadOnlyCollection sortedList = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); + ReadOnlyCollection defaultList = TimeZoneInfo.GetSystemTimeZones(); + ValidateDifferentTimeZoneLists(defaultList, unsortedList, sortedList); + } + + [Fact] + public static void ConvertTime_DateTimeOffset_NullDestination_ArgumentNullException() + { + DateTimeOffset time1 = new DateTimeOffset(2006, 5, 12, 0, 0, 0, TimeSpan.Zero); + VerifyConvertException(time1, null); + } + + [Fact] + public static void ConvertTimeFromUtc() + { + // destination timezone is null + Assert.Throws(() => + { + DateTime dt = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2007, 5, 3, 11, 8, 0), null); + }); + + // destination timezone is UTC + DateTime now = DateTime.UtcNow; + DateTime convertedNow = TimeZoneInfo.ConvertTimeFromUtc(now, TimeZoneInfo.Utc); + Assert.Equal(now, convertedNow); + } + + [Fact] + public static void ConvertTimeToUtc() + { + // null source + VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 16, 0), null); + + TimeZoneInfo london = CreateCustomLondonTimeZone(); + + // invalid DateTime + DateTime invalidDate = new DateTime(2007, 3, 25, 1, 30, 0); + VerifyConvertToUtcException(invalidDate, london); + + // DateTimeKind and source types don't match + VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 8, 0, DateTimeKind.Utc), london); + + // correct UTC conversion + DateTime date = new DateTime(2007, 01, 01, 0, 0, 0); + Assert.Equal(date.ToUniversalTime(), TimeZoneInfo.ConvertTimeToUtc(date)); + } + + [Fact] + public static void ConvertTimeFromToUtc() + { + TimeZoneInfo london = CreateCustomLondonTimeZone(); + + DateTime utc = DateTime.UtcNow; + Assert.Equal(DateTimeKind.Utc, utc.Kind); + + DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Utc); + Assert.Equal(DateTimeKind.Utc, converted.Kind); + DateTime back = TimeZoneInfo.ConvertTimeToUtc(converted, TimeZoneInfo.Utc); + Assert.Equal(DateTimeKind.Utc, back.Kind); + Assert.Equal(utc, back); + + converted = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Local); + DateTimeKind expectedKind = (TimeZoneInfo.Local == TimeZoneInfo.Utc) ? DateTimeKind.Utc : DateTimeKind.Local; + Assert.Equal(expectedKind, converted.Kind); + back = TimeZoneInfo.ConvertTimeToUtc(converted, TimeZoneInfo.Local); + Assert.Equal(DateTimeKind.Utc, back.Kind); + Assert.Equal(utc, back); + } + + [Fact] + public static void ConvertTimeFromToUtcUsingCustomZone() + { + // DateTime Kind is Local + Assert.ThrowsAny(() => + { + DateTime dt = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2007, 5, 3, 11, 8, 0, DateTimeKind.Local), TimeZoneInfo.Local); + }); + + TimeZoneInfo london = CreateCustomLondonTimeZone(); + + // winter (no DST) + DateTime winter = new DateTime(2007, 12, 25, 12, 0, 0); + DateTime convertedWinter = TimeZoneInfo.ConvertTimeFromUtc(winter, london); + Assert.Equal(winter, convertedWinter); + + // summer (DST) + DateTime summer = new DateTime(2007, 06, 01, 12, 0, 0); + DateTime convertedSummer = TimeZoneInfo.ConvertTimeFromUtc(summer, london); + Assert.Equal(summer + new TimeSpan(1, 0, 0), convertedSummer); + + // Kind and source types don't match + VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 8, 0, DateTimeKind.Local), london); + + // Test the ambiguous date + DateTime utcAmbiguous = new DateTime(2016, 10, 30, 0, 14, 49, DateTimeKind.Utc); + DateTime convertedAmbiguous = TimeZoneInfo.ConvertTimeFromUtc(utcAmbiguous, london); + Assert.Equal(DateTimeKind.Unspecified, convertedAmbiguous.Kind); + Assert.True(london.IsAmbiguousTime(convertedAmbiguous), $"Expected to have {convertedAmbiguous} is ambiguous"); + + // roundtrip check using ambiguous time. + DateTime utc = new DateTime(2022, 10, 30, 1, 47, 13, DateTimeKind.Utc); + DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, london); + Assert.Equal(DateTimeKind.Unspecified, converted.Kind); + DateTime back = TimeZoneInfo.ConvertTimeToUtc(converted, london); + Assert.Equal(DateTimeKind.Utc, back.Kind); + Assert.True(london.IsAmbiguousTime(converted)); + Assert.Equal(utc, back); + } + + [Fact] + public static void CreateCustomTimeZone() + { + TimeZoneInfo.TransitionTime s1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 3, 2, DayOfWeek.Sunday); + TimeZoneInfo.TransitionTime e1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 10, 2, DayOfWeek.Sunday); + TimeZoneInfo.AdjustmentRule r1 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(1, 0, 0), s1, e1); + + // supports DST + TimeZoneInfo tz1 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1 }); + Assert.True(tz1.SupportsDaylightSavingTime); + + // doesn't support DST + TimeZoneInfo tz2 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(4, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1 }, true); + Assert.False(tz2.SupportsDaylightSavingTime); + + TimeZoneInfo tz3 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(6, 0, 0), null, null, null, null); + Assert.False(tz3.SupportsDaylightSavingTime); + } + + [Fact] + public static void CreateCustomTimeZone_Invalid() + { + VerifyCustomTimeZoneException(null, new TimeSpan(0), null, null); // null Id + VerifyCustomTimeZoneException("", new TimeSpan(0), null, null); // empty string Id + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(0, 0, 55), null, null); // offset not minutes + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(14, 1, 0), null, null); // offset too big + VerifyCustomTimeZoneException("mytimezone", -new TimeSpan(14, 1, 0), null, null); // offset too small + } + + [Fact] + public static void CreateCustomTimeZone_InvalidTimeZone() + { + TimeZoneInfo.TransitionTime s1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 3, 2, DayOfWeek.Sunday); + TimeZoneInfo.TransitionTime e1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 10, 2, DayOfWeek.Sunday); + TimeZoneInfo.TransitionTime s2 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 2, 2, DayOfWeek.Sunday); + TimeZoneInfo.TransitionTime e2 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 11, 2, DayOfWeek.Sunday); + + TimeZoneInfo.AdjustmentRule r1 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(1, 0, 0), s1, e1); + + // AdjustmentRules overlap + TimeZoneInfo.AdjustmentRule r2 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2004, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1, r2 }); + + // AdjustmentRules not ordered + TimeZoneInfo.AdjustmentRule r3 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2006, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r3, r1 }); + + // Offset out of range + TimeZoneInfo.AdjustmentRule r4 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(3, 0, 0), s1, e1); + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(12, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r4 }); + + // overlapping AdjustmentRules for a date + TimeZoneInfo.AdjustmentRule r5 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2005, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1, r5 }); + + // null AdjustmentRule + VerifyCustomTimeZoneException("mytimezone", new TimeSpan(12, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { null }); + } + + [Fact] + public static void HasSameRules_NullAdjustmentRules() + { + TimeZoneInfo utc = TimeZoneInfo.Utc; + TimeZoneInfo custom = TimeZoneInfo.CreateCustomTimeZone("Custom", new TimeSpan(0), "Custom", "Custom"); + Assert.True(utc.HasSameRules(custom)); + } + + [Fact] + public static void ConvertTimeBySystemTimeZoneIdTests() + { + DateTime now = DateTime.Now; + DateTime utcNow = TimeZoneInfo.ConvertTimeToUtc(now); + + Assert.Equal(now, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcNow, TimeZoneInfo.Local.Id)); + Assert.Equal(utcNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(now, TimeZoneInfo.Utc.Id)); + + Assert.Equal(now, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcNow, TimeZoneInfo.Utc.Id, TimeZoneInfo.Local.Id)); + Assert.Equal(utcNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(now, TimeZoneInfo.Local.Id, TimeZoneInfo.Utc.Id)); + + DateTimeOffset offsetNow = new DateTimeOffset(now); + DateTimeOffset utcOffsetNow = new DateTimeOffset(utcNow); + + Assert.Equal(offsetNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcOffsetNow, TimeZoneInfo.Local.Id)); + Assert.Equal(utcOffsetNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(offsetNow, TimeZoneInfo.Utc.Id)); + } + + // In recent Linux distros like Ubuntu 24.04, removed the legacy Time Zone names and not mapping it any more. User can still have a way to install it if they need to. + // UCT is one of the legacy aliases for UTC which we use here to detect if the legacy names is support at the runtime. + // https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#p-99950-tzdata-package-split + private static bool SupportLegacyTimeZoneNames { get; } = IsSupportedLegacyTimeZones(); + private static bool IsSupportedLegacyTimeZones() + { + try + { + TimeZoneInfo.FindSystemTimeZoneById("UCT"); + } + catch (TimeZoneNotFoundException) + { + return false; + } + + return true; + } + + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // (This list is not likely to change.) + private static readonly string[] s_UtcAliases = SupportLegacyTimeZoneNames ? + [ + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UCT", + "UTC", + "Universal", + "Zulu" + ] : [ + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UTC" + ]; + + [Fact] + public static void TimeZoneInfo_DoesNotCreateAdjustmentRulesWithOffsetOutsideOfRange() + { + // On some OSes with some time zones setting + // time zone may contain old adjustment rule which have offset higher than 14h + // Assert.DoesNotThrow + DateTimeOffset.FromFileTime(0); + } + + [Fact] + 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)); + } + + [Fact] + public static void AdjustmentRuleBaseUtcOffsetDeltaTest() + { + TimeZoneInfo.TransitionTime start = TimeZoneInfo.TransitionTime.CreateFixedDateRule(timeOfDay: new DateTime(1, 1, 1, 2, 0, 0), month: 3, day: 7); + TimeZoneInfo.TransitionTime end = TimeZoneInfo.TransitionTime.CreateFixedDateRule(timeOfDay: new DateTime(1, 1, 1, 1, 0, 0), month: 11, day: 7); + TimeZoneInfo.AdjustmentRule rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(DateTime.MinValue.Date, DateTime.MaxValue.Date, new TimeSpan(1, 0, 0), start, end, baseUtcOffsetDelta: new TimeSpan(1, 0, 0)); + TimeZoneInfo customTimeZone = TimeZoneInfo.CreateCustomTimeZone( + id: "Fake Time Zone", + baseUtcOffset: new TimeSpan(0), + displayName: "Fake Time Zone", + standardDisplayName: "Standard Fake Time Zone", + daylightDisplayName: "British Summer Time", + new TimeZoneInfo.AdjustmentRule[] { rule }); + + TimeZoneInfo.AdjustmentRule[] rules = customTimeZone.GetAdjustmentRules(); + + Assert.Equal(1, rules.Length); + Assert.Equal(new TimeSpan(1, 0, 0), rules[0].BaseUtcOffsetDelta); + + // BaseUtcOffsetDelta should be counted to the returned offset during the standard time. + Assert.Equal(new TimeSpan(1, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 1, 1, 2, 0, 0))); + + // BaseUtcOffsetDelta should be counted to the returned offset during the daylight time. + Assert.Equal(new TimeSpan(2, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 3, 10, 2, 0, 0))); + } + + [Fact] + public static void TestCustomTimeZonesWithNullNames() + { + TimeZoneInfo custom = TimeZoneInfo.CreateCustomTimeZone("Custom Time Zone With Null Names", TimeSpan.FromHours(-8), null, null); + Assert.Equal("Custom Time Zone With Null Names", custom.Id); + Assert.Equal(string.Empty, custom.StandardName); + Assert.Equal(string.Empty, custom.DaylightName); + Assert.Equal(string.Empty, custom.DisplayName); + } + + private static void VerifyConvertException(DateTimeOffset inputTime, string destinationTimeZoneId) where TException : Exception + { + Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); + } + + private static void VerifyConvertException(DateTime inputTime, string destinationTimeZoneId) where TException : Exception + { + Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); + } + + private static void VerifyConvertException(DateTime inputTime, string sourceTimeZoneId, string destinationTimeZoneId) where TException : Exception + { + Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId), TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); + } + + private static void VerifyConvert(DateTimeOffset inputTime, string destinationTimeZoneId, DateTimeOffset expectedTime) + { + DateTimeOffset returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); + Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); + } + + private static void VerifyConvert(DateTime inputTime, string destinationTimeZoneId, DateTime expectedTime) + { + DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); + Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); + Assert.True(expectedTime.Kind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime.Kind, returnedTime.Kind, inputTime, destinationTimeZoneId)); + } + + private static void VerifyConvert(DateTime inputTime, string destinationTimeZoneId, DateTime expectedTime, DateTimeKind expectedKind) + { + DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); + Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); + Assert.True(expectedKind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime.Kind, returnedTime.Kind, inputTime, destinationTimeZoneId)); + } + + private static void VerifyConvert(DateTime inputTime, string sourceTimeZoneId, string destinationTimeZoneId, DateTime expectedTime) + { + DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId), TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); + Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', Source TimeZone: {3}, Dest. Time Zone: {4}", expectedTime, returnedTime, inputTime, sourceTimeZoneId, destinationTimeZoneId)); + Assert.True(expectedTime.Kind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', Source TimeZone: {3}, Dest. Time Zone: {4}", expectedTime.Kind, returnedTime.Kind, inputTime, sourceTimeZoneId, destinationTimeZoneId)); + } + + private static void VerifyRoundTrip(DateTime dt1, string sourceTimeZoneId, string destinationTimeZoneId) + { + TimeZoneInfo sourceTzi = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId); + TimeZoneInfo destTzi = TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId); + + DateTime dt2 = TimeZoneInfo.ConvertTime(dt1, sourceTzi, destTzi); + DateTime dt3 = TimeZoneInfo.ConvertTime(dt2, destTzi, sourceTzi); + + if (!destTzi.IsAmbiguousTime(dt2)) + { + // the ambiguous time can be mapped to 2 UTC times so it is not guaranteed to round trip + Assert.True(dt1.Equals(dt3), string.Format("{0} failed to round trip using source '{1}' and '{2}' zones. wrong result {3}", dt1, sourceTimeZoneId, destinationTimeZoneId, dt3)); + } + + if (sourceTimeZoneId == TimeZoneInfo.Utc.Id) + { + Assert.True(dt3.Kind == DateTimeKind.Utc, string.Format("failed to get the right DT Kind after round trip {0} using source TZ {1} and dest TZi {2}", dt1, sourceTimeZoneId, destinationTimeZoneId)); + } + } + + private static void VerifyAmbiguousOffsetsException(TimeZoneInfo tz, DateTime dt) where TException : Exception + { + Assert.Throws(() => tz.GetAmbiguousTimeOffsets(dt)); + } + + private static void VerifyOffsets(TimeZoneInfo tz, DateTime dt, TimeSpan[] expectedOffsets) + { + TimeSpan[] ret = tz.GetAmbiguousTimeOffsets(dt); + VerifyTimeSpanArray(ret, expectedOffsets, string.Format("Wrong offsets when used {0} with the zone {1}", dt, tz.Id)); + } + + private static void VerifyTimeSpanArray(TimeSpan[] actual, TimeSpan[] expected, string errorMsg) + { + Assert.True(actual != null); + Assert.True(expected != null); + Assert.True(actual.Length == expected.Length); + + Array.Sort(expected); // TimeZoneInfo is expected to always return sorted TimeSpan arrays + + for (int i = 0; i < actual.Length; i++) + { + Assert.True(actual[i].Equals(expected[i]), errorMsg); + } + } + + private static void VerifyDST(TimeZoneInfo tz, DateTime dt, bool expectedDST) + { + bool ret = tz.IsDaylightSavingTime(dt); + Assert.True(ret == expectedDST, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); + } + + private static void VerifyInv(TimeZoneInfo tz, DateTime dt, bool expectedInvalid) + { + bool ret = tz.IsInvalidTime(dt); + Assert.True(expectedInvalid == ret, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); + } + + private static void VerifyAmbiguous(TimeZoneInfo tz, DateTime dt, bool expectedAmbiguous) + { + bool ret = tz.IsAmbiguousTime(dt); + Assert.True(expectedAmbiguous == ret, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); + } + + private static TimeZoneInfo CreateCustomLondonTimeZone() + { + TimeZoneInfo.TransitionTime start = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 1, 0, 0), 3, 5, DayOfWeek.Sunday); + TimeZoneInfo.TransitionTime end = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 5, DayOfWeek.Sunday); + TimeZoneInfo.AdjustmentRule rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(DateTime.MinValue.Date, DateTime.MaxValue.Date, new TimeSpan(1, 0, 0), start, end); + return TimeZoneInfo.CreateCustomTimeZone("Europe/London", new TimeSpan(0), "Europe/London", "British Standard Time", "British Summer Time", new TimeZoneInfo.AdjustmentRule[] { rule }); + } + + private static void VerifyConvertToUtcException(DateTime dateTime, TimeZoneInfo sourceTimeZone) where TException : Exception + { + Assert.ThrowsAny(() => TimeZoneInfo.ConvertTimeToUtc(dateTime, sourceTimeZone)); + } + + private static void VerifyCustomTimeZoneException(string id, TimeSpan baseUtcOffset, string displayName, string standardDisplayName, string daylightDisplayName = null, TimeZoneInfo.AdjustmentRule[] adjustmentRules = null) where TException : Exception + { + Assert.ThrowsAny(() => + { + if (daylightDisplayName == null && adjustmentRules == null) + { + TimeZoneInfo.CreateCustomTimeZone(id, baseUtcOffset, displayName, standardDisplayName); + } + else + { + TimeZoneInfo.CreateCustomTimeZone(id, baseUtcOffset, displayName, standardDisplayName, daylightDisplayName, adjustmentRules); + } + }); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Invariant.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Invariant.cs new file mode 100644 index 0000000000000..0533a081b738b --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.Invariant.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Tests +{ + public static partial class TimeZoneInfoTests + { + [Fact] + public static void IsInvariant() + { + Assert.True(GetInvariant(null)); + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_Invariant")] + static extern bool GetInvariant(TimeZoneInfo t); + } + + [Fact] + public static void JustUtcInvariant() + { + Assert.Equal(TimeZoneInfo.Local, TimeZoneInfo.Local); + + var tzs = TimeZoneInfo.GetSystemTimeZones(); + Assert.Equal(1, tzs.Count); + Assert.Equal(TimeZoneInfo.Utc, tzs[0]); + } + + [Fact] + public static void OnlyUtcWhenInvariant() + { + Assert.Throws(() => TimeZoneInfo.FindSystemTimeZoneById(s_strPacific)); + Assert.Throws(() => TimeZoneInfo.FindSystemTimeZoneById(s_strSydney)); + Assert.Throws(() => TimeZoneInfo.FindSystemTimeZoneById(s_strGMT)); + } + + [Fact] + public static void NoAlternateUTCIdsInvariant() + { + foreach (string alias in s_UtcAliases) + { + if (alias == s_strUtc) + { + continue; + } + Assert.Throws(() => + { + TimeZoneInfo.FindSystemTimeZoneById(alias); + }); + } + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.cs index 359f007861d3e..10a63e0e2bf53 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeZoneInfoTests.cs @@ -18,71 +18,39 @@ namespace System.Tests { public static partial class TimeZoneInfoTests { - private static readonly bool s_isWindows = OperatingSystem.IsWindows(); - private static readonly bool s_isOSX = OperatingSystem.IsMacOS(); - - private static string s_strPacific = s_isWindows ? "Pacific Standard Time" : "America/Los_Angeles"; - private static string s_strSydney = s_isWindows ? "AUS Eastern Standard Time" : "Australia/Sydney"; - private static string s_strGMT = s_isWindows ? "GMT Standard Time" : "Europe/London"; - private static string s_strTonga = s_isWindows ? "Tonga Standard Time" : "Pacific/Tongatapu"; - private static string s_strBrasil = s_isWindows ? "E. South America Standard Time" : "America/Sao_Paulo"; - private static string s_strPerth = s_isWindows ? "W. Australia Standard Time" : "Australia/Perth"; - private static string s_strBrasilia = s_isWindows ? "E. South America Standard Time" : "America/Sao_Paulo"; - private static string s_strNairobi = s_isWindows ? "E. Africa Standard Time" : "Africa/Nairobi"; - private static string s_strAmsterdam = s_isWindows ? "W. Europe Standard Time" : "Europe/Berlin"; - private static string s_strRussian = s_isWindows ? "Russian Standard Time" : "Europe/Moscow"; - private static string s_strLibya = s_isWindows ? "Libya Standard Time" : "Africa/Tripoli"; - private static string s_strJohannesburg = s_isWindows ? "South Africa Standard Time" : "Africa/Johannesburg"; - private static string s_strCasablanca = s_isWindows ? "Morocco Standard Time" : "Africa/Casablanca"; - private static string s_strCatamarca = s_isWindows ? "Argentina Standard Time" : "America/Argentina/Catamarca"; - private static string s_strLisbon = s_isWindows ? "GMT Standard Time" : "Europe/Lisbon"; - private static string s_strNewfoundland = s_isWindows ? "Newfoundland Standard Time" : "America/St_Johns"; - private static string s_strIran = s_isWindows ? "Iran Standard Time" : "Asia/Tehran"; - private static string s_strFiji = s_isWindows ? "Fiji Standard Time" : "Pacific/Fiji"; - - private static TimeZoneInfo s_myUtc = TimeZoneInfo.Utc; - private static TimeZoneInfo s_myLocal = TimeZoneInfo.Local; - private static TimeZoneInfo s_regLocal = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneInfo.Local.Id); // in case DST is disabled on Local - private static TimeZoneInfo s_GMTLondon = TimeZoneInfo.FindSystemTimeZoneById(s_strGMT); - private static TimeZoneInfo s_nairobiTz = TimeZoneInfo.FindSystemTimeZoneById(s_strNairobi); - private static TimeZoneInfo s_amsterdamTz = TimeZoneInfo.FindSystemTimeZoneById(s_strAmsterdam); - private static TimeZoneInfo s_johannesburgTz = TimeZoneInfo.FindSystemTimeZoneById(s_strJohannesburg); - private static TimeZoneInfo s_casablancaTz = TimeZoneInfo.FindSystemTimeZoneById(s_strCasablanca); - private static TimeZoneInfo s_catamarcaTz = TimeZoneInfo.FindSystemTimeZoneById(s_strCatamarca); - private static TimeZoneInfo s_LisbonTz = TimeZoneInfo.FindSystemTimeZoneById(s_strLisbon); - private static TimeZoneInfo s_NewfoundlandTz = TimeZoneInfo.FindSystemTimeZoneById(s_strNewfoundland); - - private static bool s_localIsPST = TimeZoneInfo.Local.Id == s_strPacific; - private static bool s_regLocalSupportsDST = s_regLocal.SupportsDaylightSavingTime; - private static bool s_localSupportsDST = TimeZoneInfo.Local.SupportsDaylightSavingTime; - - // In 2006, Australia delayed ending DST by a week. However, Windows says it still ended the last week of March. - private static readonly int s_sydneyOffsetLastWeekOfMarch2006 = s_isWindows ? 10 : 11; + private static TimeZoneInfo s_regLocal; + private static TimeZoneInfo s_GMTLondon; + private static TimeZoneInfo s_nairobiTz; + private static TimeZoneInfo s_amsterdamTz; + private static TimeZoneInfo s_johannesburgTz; + private static TimeZoneInfo s_casablancaTz; + private static TimeZoneInfo s_catamarcaTz; + private static TimeZoneInfo s_LisbonTz; + private static TimeZoneInfo s_NewfoundlandTz; - [Fact] - public static void Kind() - { - TimeZoneInfo tzi = TimeZoneInfo.Local; - Assert.Equal(tzi, TimeZoneInfo.Local); - tzi = TimeZoneInfo.Utc; - Assert.Equal(tzi, TimeZoneInfo.Utc); - } + private static bool s_localIsPST; + private static bool s_regLocalSupportsDST; + private static bool s_localSupportsDST; - [Fact] - public static void Names() + private static readonly int s_sydneyOffsetLastWeekOfMarch2006; + + static TimeZoneInfoTests() { - TimeZoneInfo local = TimeZoneInfo.Local; - TimeZoneInfo utc = TimeZoneInfo.Utc; + s_regLocal = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneInfo.Local.Id); // in case DST is disabled on Local + s_GMTLondon = TimeZoneInfo.FindSystemTimeZoneById(s_strGMT); + s_nairobiTz = TimeZoneInfo.FindSystemTimeZoneById(s_strNairobi); + s_amsterdamTz = TimeZoneInfo.FindSystemTimeZoneById(s_strAmsterdam); + s_johannesburgTz = TimeZoneInfo.FindSystemTimeZoneById(s_strJohannesburg); + s_casablancaTz = TimeZoneInfo.FindSystemTimeZoneById(s_strCasablanca); + s_catamarcaTz = TimeZoneInfo.FindSystemTimeZoneById(s_strCatamarca); + s_LisbonTz = TimeZoneInfo.FindSystemTimeZoneById(s_strLisbon); + s_NewfoundlandTz = TimeZoneInfo.FindSystemTimeZoneById(s_strNewfoundland); - Assert.NotNull(local.DaylightName); - Assert.NotNull(local.DisplayName); - Assert.NotNull(local.StandardName); - Assert.NotNull(local.ToString()); + s_localIsPST = TimeZoneInfo.Local.Id == s_strPacific; + s_regLocalSupportsDST = s_regLocal.SupportsDaylightSavingTime; + s_localSupportsDST = TimeZoneInfo.Local.SupportsDaylightSavingTime; - Assert.NotNull(utc.DaylightName); - Assert.NotNull(utc.DisplayName); - Assert.NotNull(utc.StandardName); - Assert.NotNull(utc.ToString()); + s_sydneyOffsetLastWeekOfMarch2006 = s_isWindows ? 10 : 11; } // Due to ICU size limitations, full daylight/standard names are not included for the browser. @@ -155,20 +123,6 @@ public static void Platform_TimeZoneNames(TimeZoneInfo tzi, string displayName, $"Daylight Name: Neither '{daylightName}' nor '{alternativeDaylightName}' equal to '{tzi.DaylightName}'"); } - [Fact] - public static void ConvertTime() - { - TimeZoneInfo local = TimeZoneInfo.Local; - TimeZoneInfo utc = TimeZoneInfo.Utc; - - DateTime dt = TimeZoneInfo.ConvertTime(DateTime.Today, utc); - Assert.Equal(DateTime.Today, TimeZoneInfo.ConvertTime(dt, local)); - - DateTime today = new DateTime(DateTime.Today.Ticks, DateTimeKind.Utc); - dt = TimeZoneInfo.ConvertTime(today, local); - Assert.Equal(today, TimeZoneInfo.ConvertTime(dt, utc)); - } - [Fact] public static void LibyaTimeZone() { @@ -614,23 +568,6 @@ public static void ConvertTime_DateTime_PerthRules() VerifyConvert(time1utc, s_strPerth, time1.AddHours(9)); } - [Fact] - public static void ConvertTime_DateTime_UtcToUtc() - { - var time1utc = new DateTime(2003, 3, 30, 0, 0, 23, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - time1utc = new DateTime(2003, 3, 30, 2, 0, 24, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - time1utc = new DateTime(2003, 3, 30, 5, 19, 20, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - time1utc = new DateTime(2003, 10, 26, 2, 0, 0, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - time1utc = new DateTime(2003, 10, 26, 2, 20, 0, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - time1utc = new DateTime(2003, 10, 26, 3, 0, 1, DateTimeKind.Utc); - VerifyConvert(time1utc, TimeZoneInfo.Utc.Id, time1utc); - } - [Fact] public static void ConvertTime_DateTime_UtcToLocal() { @@ -916,31 +853,10 @@ public static void ConvertTime_DateTime_VariousDateTimeKinds() [Fact] public static void ConvertTime_DateTime_MiscUtc() { - VerifyConvert(new DateTime(2003, 4, 6, 1, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 4, 6, 1, 30, 0), DateTimeKind.Utc)); - VerifyConvert(new DateTime(2003, 4, 6, 2, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 4, 6, 2, 30, 0), DateTimeKind.Utc)); - VerifyConvert(new DateTime(2003, 10, 26, 1, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 10, 26, 1, 30, 0), DateTimeKind.Utc)); - VerifyConvert(new DateTime(2003, 10, 26, 2, 30, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 10, 26, 2, 30, 0), DateTimeKind.Utc)); - VerifyConvert(new DateTime(2003, 8, 4, 12, 0, 0, DateTimeKind.Utc), "UTC", DateTime.SpecifyKind(new DateTime(2003, 8, 4, 12, 0, 0), DateTimeKind.Utc)); - - // Round trip - - VerifyRoundTrip(new DateTime(2003, 8, 4, 12, 0, 0, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); - VerifyRoundTrip(new DateTime(1929, 3, 9, 23, 59, 59, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); - VerifyRoundTrip(new DateTime(2000, 2, 28, 23, 59, 59, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); - - // DateTime(2016, 11, 6, 8, 1, 17, DateTimeKind.Utc) is ambiguous time for Pacific Time Zone - VerifyRoundTrip(new DateTime(2016, 11, 6, 8, 1, 17, DateTimeKind.Utc), "UTC", TimeZoneInfo.Local.Id); - - VerifyRoundTrip(DateTime.UtcNow, "UTC", TimeZoneInfo.Local.Id); - - var time1 = new DateTime(2006, 5, 12, 7, 34, 59); - VerifyConvert(time1, "UTC", DateTime.SpecifyKind(time1.Subtract(TimeZoneInfo.Local.GetUtcOffset(time1)), DateTimeKind.Utc)); - VerifyConvert(DateTime.SpecifyKind(time1, DateTimeKind.Local), "UTC", DateTime.SpecifyKind(time1.Subtract(TimeZoneInfo.Local.GetUtcOffset(time1)), DateTimeKind.Utc)); - if (s_localIsPST) { int delta = s_localSupportsDST ? 1 : 0; - time1 = new DateTime(2006, 4, 2, 1, 30, 0); // no DST (U.S. Pacific) + var time1 = new DateTime(2006, 4, 2, 1, 30, 0); // no DST (U.S. Pacific) VerifyConvert(time1, "UTC", DateTime.SpecifyKind(time1.AddHours(8), DateTimeKind.Utc)); VerifyConvert(DateTime.SpecifyKind(time1, DateTimeKind.Local), "UTC", DateTime.SpecifyKind(time1.AddHours(8), DateTimeKind.Utc)); time1 = new DateTime(2006, 4, 2, 2, 30, 0); // invalid hour (U.S. Pacific) @@ -1045,49 +961,10 @@ public static void ConvertTime_Tonga() [Fact] public static void ConvertTime_NullTimeZone_ThrowsArgumentNullException() { - AssertExtensions.Throws("destinationTimeZone", () => TimeZoneInfo.ConvertTime(new DateTime(), null)); - AssertExtensions.Throws("destinationTimeZone", () => TimeZoneInfo.ConvertTime(new DateTimeOffset(), null)); - AssertExtensions.Throws("sourceTimeZone", () => TimeZoneInfo.ConvertTime(new DateTime(), null, s_casablancaTz)); AssertExtensions.Throws("destinationTimeZone", () => TimeZoneInfo.ConvertTime(new DateTime(), s_casablancaTz, null)); } - [Fact] - public static void GetAmbiguousTimeOffsets_Invalid() - { - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32)); - - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31, DateTimeKind.Utc)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32, DateTimeKind.Utc)); - - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31, DateTimeKind.Local)); - VerifyAmbiguousOffsetsException(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32, DateTimeKind.Local)); - } [Fact] public static void GetAmbiguousTimeOffsets_Nairobi_Invalid() @@ -1389,18 +1266,6 @@ public static void GetAmbiguousTimeOffsets_LocalAmbiguousOffsets() [Fact] public static void IsDaylightSavingTime() { - VerifyDST(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32), false); - VerifyDST(TimeZoneInfo.Utc, new DateTime(2004, 4, 4, 2, 30, 0, DateTimeKind.Local), false); - VerifyDST(s_nairobiTz, new DateTime(2007, 3, 11, 2, 30, 0, DateTimeKind.Local), false); VerifyDST(s_nairobiTz, new DateTime(2006, 1, 15, 7, 15, 23), false); VerifyDST(s_nairobiTz, new DateTime(2050, 2, 15, 8, 30, 24), false); @@ -1591,17 +1456,6 @@ public static void IsDaylightSavingTime() [Fact] public static void IsInvalidTime() { - VerifyInv(TimeZoneInfo.Utc, new DateTime(2006, 1, 15, 7, 15, 23), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(2050, 2, 15, 8, 30, 24), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(1800, 3, 15, 9, 45, 25), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(1400, 4, 15, 10, 00, 26), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(1234, 5, 15, 11, 15, 27), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(4321, 6, 15, 12, 30, 28), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(1111, 7, 15, 13, 45, 29), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(2222, 8, 15, 14, 00, 30), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(9998, 9, 15, 15, 15, 31), false); - VerifyInv(TimeZoneInfo.Utc, new DateTime(9997, 10, 15, 16, 30, 32), false); - VerifyInv(s_nairobiTz, new DateTime(2006, 1, 15, 7, 15, 23), false); VerifyInv(s_nairobiTz, new DateTime(2050, 2, 15, 8, 30, 24), false); VerifyInv(s_nairobiTz, new DateTime(1800, 3, 15, 9, 45, 25), false); @@ -2010,84 +1864,6 @@ public static void GetSystemTimeZones() } } - private static void ValidateTimeZonesSorting(ReadOnlyCollection zones) - { - // validate sorting: first by base offset, then by display name - for (int i = 1; i < zones.Count; i++) - { - TimeZoneInfo previous = zones[i - 1]; - TimeZoneInfo current = zones[i]; - - int baseOffsetsCompared = current.BaseUtcOffset.CompareTo(previous.BaseUtcOffset); - Assert.True(baseOffsetsCompared >= 0, - string.Format($"TimeZoneInfos are out of order. {previous.Id}:{previous.BaseUtcOffset} should be before {current.Id}:{current.BaseUtcOffset}")); - - if (baseOffsetsCompared == 0) - { - Assert.True(string.CompareOrdinal(current.DisplayName, previous.DisplayName) >= 0, - string.Format($"TimeZoneInfos are out of order. {previous.DisplayName} should be before {current.DisplayName}")); - } - } - } - - private static void ValidateDifferentTimeZoneLists(ReadOnlyCollection defaultList, ReadOnlyCollection nonSortedList, ReadOnlyCollection sortedList) - { - Assert.Equal(defaultList.Count, nonSortedList.Count); - Assert.Equal(defaultList.Count, sortedList.Count); - - Assert.Equal(defaultList.Count, nonSortedList.Count); - Assert.True(object.ReferenceEquals(defaultList, sortedList)); - Dictionary zones1Dict = defaultList.ToDictionary(t => t.Id); - foreach (TimeZoneInfo zone in nonSortedList) - { - Assert.True(zones1Dict.TryGetValue(zone.Id, out TimeZoneInfo zone1)); - } - - ValidateTimeZonesSorting(defaultList); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public static void TestGetSystemTimeZonesCollectionsCallsOrder() - { - RemoteExecutor.Invoke(() => - { - // - // Get sorted list first and then the unsorted list - // - var zones1 = TimeZoneInfo.GetSystemTimeZones(); - var zones2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); - var zones3 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); - - ValidateDifferentTimeZoneLists(zones1, zones2, zones3); - - // - // Clear our caches so zone enumeration is forced to re-read the data - // - TimeZoneInfo.ClearCachedData(); - - // - // Get unsorted list first and then the sorted list - // - zones2 = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); - zones3 = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); - zones1 = TimeZoneInfo.GetSystemTimeZones(); - ValidateDifferentTimeZoneLists(zones1, zones2, zones3); - - }).Dispose(); - } - - [Fact] - public static void TestGetSystemTimeZonesCollections() - { - // This test doing similar checks as TestGetSystemTimeZonesCollectionsCallsOrder does except we need to - // run this test without the RemoteExecutor to ensure testing on platforms like Android. - - ReadOnlyCollection unsortedList = TimeZoneInfo.GetSystemTimeZones(skipSorting: true); - ReadOnlyCollection sortedList = TimeZoneInfo.GetSystemTimeZones(skipSorting: false); - ReadOnlyCollection defaultList = TimeZoneInfo.GetSystemTimeZones(); - ValidateDifferentTimeZoneLists(defaultList, unsortedList, sortedList); - } - [Fact] public static void DaylightTransitionsExactTime() { @@ -2166,13 +1942,6 @@ public static void ClearCachedData() }); } - [Fact] - public static void ConvertTime_DateTimeOffset_NullDestination_ArgumentNullException() - { - DateTimeOffset time1 = new DateTimeOffset(2006, 5, 12, 0, 0, 0, TimeSpan.Zero); - VerifyConvertException(time1, null); - } - public static IEnumerable ConvertTime_DateTimeOffset_InvalidDestination_TimeZoneNotFoundException_MemberData() { yield return new object[] { string.Empty }; @@ -2203,162 +1972,6 @@ public static void ConvertTime_DateTimeOffset_InvalidDestination_TimeZoneNotFoun VerifyConvertException(time1, destinationId); } - [Fact] - public static void ConvertTimeFromUtc() - { - // destination timezone is null - Assert.Throws(() => - { - DateTime dt = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2007, 5, 3, 11, 8, 0), null); - }); - - // destination timezone is UTC - DateTime now = DateTime.UtcNow; - DateTime convertedNow = TimeZoneInfo.ConvertTimeFromUtc(now, TimeZoneInfo.Utc); - Assert.Equal(now, convertedNow); - } - - [Fact] - public static void ConvertTimeToUtc() - { - // null source - VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 16, 0), null); - - TimeZoneInfo london = CreateCustomLondonTimeZone(); - - // invalid DateTime - DateTime invalidDate = new DateTime(2007, 3, 25, 1, 30, 0); - VerifyConvertToUtcException(invalidDate, london); - - // DateTimeKind and source types don't match - VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 8, 0, DateTimeKind.Utc), london); - - // correct UTC conversion - DateTime date = new DateTime(2007, 01, 01, 0, 0, 0); - Assert.Equal(date.ToUniversalTime(), TimeZoneInfo.ConvertTimeToUtc(date)); - } - - [Fact] - public static void ConvertTimeFromToUtc() - { - TimeZoneInfo london = CreateCustomLondonTimeZone(); - - DateTime utc = DateTime.UtcNow; - Assert.Equal(DateTimeKind.Utc, utc.Kind); - - DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Utc); - Assert.Equal(DateTimeKind.Utc, converted.Kind); - DateTime back = TimeZoneInfo.ConvertTimeToUtc(converted, TimeZoneInfo.Utc); - Assert.Equal(DateTimeKind.Utc, back.Kind); - Assert.Equal(utc, back); - - converted = TimeZoneInfo.ConvertTimeFromUtc(utc, TimeZoneInfo.Local); - DateTimeKind expectedKind = (TimeZoneInfo.Local == TimeZoneInfo.Utc) ? DateTimeKind.Utc : DateTimeKind.Local; - Assert.Equal(expectedKind, converted.Kind); - back = TimeZoneInfo.ConvertTimeToUtc(converted, TimeZoneInfo.Local); - Assert.Equal(DateTimeKind.Utc, back.Kind); - Assert.Equal(utc, back); - } - - [Fact] - public static void ConvertTimeFromToUtcUsingCustomZone() - { - // DateTime Kind is Local - Assert.ThrowsAny(() => - { - DateTime dt = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2007, 5, 3, 11, 8, 0, DateTimeKind.Local), TimeZoneInfo.Local); - }); - - TimeZoneInfo london = CreateCustomLondonTimeZone(); - - // winter (no DST) - DateTime winter = new DateTime(2007, 12, 25, 12, 0, 0); - DateTime convertedWinter = TimeZoneInfo.ConvertTimeFromUtc(winter, london); - Assert.Equal(winter, convertedWinter); - - // summer (DST) - DateTime summer = new DateTime(2007, 06, 01, 12, 0, 0); - DateTime convertedSummer = TimeZoneInfo.ConvertTimeFromUtc(summer, london); - Assert.Equal(summer + new TimeSpan(1, 0, 0), convertedSummer); - - // Kind and source types don't match - VerifyConvertToUtcException(new DateTime(2007, 5, 3, 12, 8, 0, DateTimeKind.Local), london); - - // Test the ambiguous date - DateTime utcAmbiguous = new DateTime(2016, 10, 30, 0, 14, 49, DateTimeKind.Utc); - DateTime convertedAmbiguous = TimeZoneInfo.ConvertTimeFromUtc(utcAmbiguous, london); - Assert.Equal(DateTimeKind.Unspecified, convertedAmbiguous.Kind); - Assert.True(london.IsAmbiguousTime(convertedAmbiguous), $"Expected to have {convertedAmbiguous} is ambiguous"); - - // roundtrip check using ambiguous time. - DateTime utc = new DateTime(2022, 10, 30, 1, 47, 13, DateTimeKind.Utc); - DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, london); - Assert.Equal(DateTimeKind.Unspecified, converted.Kind); - DateTime back = TimeZoneInfo.ConvertTimeToUtc(converted, london); - Assert.Equal(DateTimeKind.Utc, back.Kind); - Assert.True(london.IsAmbiguousTime(converted)); - Assert.Equal(utc, back); - } - - [Fact] - public static void CreateCustomTimeZone() - { - TimeZoneInfo.TransitionTime s1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 3, 2, DayOfWeek.Sunday); - TimeZoneInfo.TransitionTime e1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 10, 2, DayOfWeek.Sunday); - TimeZoneInfo.AdjustmentRule r1 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(1, 0, 0), s1, e1); - - // supports DST - TimeZoneInfo tz1 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1 }); - Assert.True(tz1.SupportsDaylightSavingTime); - - // doesn't support DST - TimeZoneInfo tz2 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(4, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1 }, true); - Assert.False(tz2.SupportsDaylightSavingTime); - - TimeZoneInfo tz3 = TimeZoneInfo.CreateCustomTimeZone("mytimezone", new TimeSpan(6, 0, 0), null, null, null, null); - Assert.False(tz3.SupportsDaylightSavingTime); - } - - [Fact] - public static void CreateCustomTimeZone_Invalid() - { - VerifyCustomTimeZoneException(null, new TimeSpan(0), null, null); // null Id - VerifyCustomTimeZoneException("", new TimeSpan(0), null, null); // empty string Id - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(0, 0, 55), null, null); // offset not minutes - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(14, 1, 0), null, null); // offset too big - VerifyCustomTimeZoneException("mytimezone", -new TimeSpan(14, 1, 0), null, null); // offset too small - } - - [Fact] - public static void CreateCustomTimeZone_InvalidTimeZone() - { - TimeZoneInfo.TransitionTime s1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 3, 2, DayOfWeek.Sunday); - TimeZoneInfo.TransitionTime e1 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 10, 2, DayOfWeek.Sunday); - TimeZoneInfo.TransitionTime s2 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 2, 2, DayOfWeek.Sunday); - TimeZoneInfo.TransitionTime e2 = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 4, 0, 0), 11, 2, DayOfWeek.Sunday); - - TimeZoneInfo.AdjustmentRule r1 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(1, 0, 0), s1, e1); - - // AdjustmentRules overlap - TimeZoneInfo.AdjustmentRule r2 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2004, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1, r2 }); - - // AdjustmentRules not ordered - TimeZoneInfo.AdjustmentRule r3 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2006, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r3, r1 }); - - // Offset out of range - TimeZoneInfo.AdjustmentRule r4 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2000, 1, 1), new DateTime(2005, 1, 1), new TimeSpan(3, 0, 0), s1, e1); - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(12, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r4 }); - - // overlapping AdjustmentRules for a date - TimeZoneInfo.AdjustmentRule r5 = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(new DateTime(2005, 1, 1), new DateTime(2007, 1, 1), new TimeSpan(1, 0, 0), s2, e2); - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(6, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { r1, r5 }); - - // null AdjustmentRule - VerifyCustomTimeZoneException("mytimezone", new TimeSpan(12, 0, 0), null, null, null, new TimeZoneInfo.AdjustmentRule[] { null }); - } - [Fact] [PlatformSpecific(TestPlatforms.AnyUnix)] // TimeZone not found on Windows public static void HasSameRules_RomeAndVatican() @@ -2368,33 +1981,6 @@ public static void HasSameRules_RomeAndVatican() Assert.True(rome.HasSameRules(vatican)); } - [Fact] - public static void HasSameRules_NullAdjustmentRules() - { - TimeZoneInfo utc = TimeZoneInfo.Utc; - TimeZoneInfo custom = TimeZoneInfo.CreateCustomTimeZone("Custom", new TimeSpan(0), "Custom", "Custom"); - Assert.True(utc.HasSameRules(custom)); - } - - [Fact] - public static void ConvertTimeBySystemTimeZoneIdTests() - { - DateTime now = DateTime.Now; - DateTime utcNow = TimeZoneInfo.ConvertTimeToUtc(now); - - Assert.Equal(now, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcNow, TimeZoneInfo.Local.Id)); - Assert.Equal(utcNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(now, TimeZoneInfo.Utc.Id)); - - Assert.Equal(now, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcNow, TimeZoneInfo.Utc.Id, TimeZoneInfo.Local.Id)); - Assert.Equal(utcNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(now, TimeZoneInfo.Local.Id, TimeZoneInfo.Utc.Id)); - - DateTimeOffset offsetNow = new DateTimeOffset(now); - DateTimeOffset utcOffsetNow = new DateTimeOffset(utcNow); - - Assert.Equal(offsetNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utcOffsetNow, TimeZoneInfo.Local.Id)); - Assert.Equal(utcOffsetNow, TimeZoneInfo.ConvertTimeBySystemTimeZoneId(offsetNow, TimeZoneInfo.Utc.Id)); - } - public static IEnumerable SystemTimeZonesTestData() { foreach (TimeZoneInfo tz in TimeZoneInfo.GetSystemTimeZones()) @@ -2434,47 +2020,9 @@ public static IEnumerable SystemTimeZonesTestData() } } - // In recent Linux distros like Ubuntu 24.04, removed the legacy Time Zone names and not mapping it any more. User can still have a way to install it if they need to. - // UCT is one of the legacy aliases for UTC which we use here to detect if the legacy names is support at the runtime. - // https://discourse.ubuntu.com/t/ubuntu-24-04-lts-noble-numbat-release-notes/39890#p-99950-tzdata-package-split - private static bool SupportLegacyTimeZoneNames { get; } = IsSupportedLegacyTimeZones(); - private static bool IsSupportedLegacyTimeZones() - { - try - { - TimeZoneInfo.FindSystemTimeZoneById("UCT"); - } - catch (TimeZoneNotFoundException) - { - return false; - } - - return true; - } - [GeneratedRegex(@"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$")] private static partial Regex IanaAbbreviationRegex { get; } - // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml - // (This list is not likely to change.) - private static readonly string[] s_UtcAliases = SupportLegacyTimeZoneNames ? - [ - "Etc/UTC", - "Etc/UCT", - "Etc/Universal", - "Etc/Zulu", - "UCT", - "UTC", - "Universal", - "Zulu" - ] : [ - "Etc/UTC", - "Etc/UCT", - "Etc/Universal", - "Etc/Zulu", - "UTC" - ]; - // On Android GMT, GMT+0, and GMT-0 are values private static readonly string[] s_GMTAliases = new[] { "GMT", @@ -2483,6 +2031,19 @@ private static bool IsSupportedLegacyTimeZones() "GMT-0" }; + private static bool SupportICUWithUtcAlias => PlatformDetection.IsIcuGlobalization && PlatformDetection.IsNotAppleMobile && PlatformDetection.IsNotBrowser; + + [ConditionalFact(nameof(SupportICUWithUtcAlias))] + public static void UtcAliases_MapToUtc() + { + foreach (string alias in s_UtcAliases) + { + TimeZoneInfo actualUtc = TimeZoneInfo.FindSystemTimeZoneById(alias); + Assert.True(TimeZoneInfo.Utc.HasSameRules(actualUtc)); + Assert.True(actualUtc.HasSameRules(TimeZoneInfo.Utc)); + } + } + [Theory] [MemberData(nameof(SystemTimeZonesTestData))] [PlatformSpecific(TestPlatforms.AnyUnix)] @@ -2544,19 +2105,6 @@ public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) } } - private static bool SupportICUWithUtcAlias => PlatformDetection.IsIcuGlobalization && PlatformDetection.IsNotAppleMobile && PlatformDetection.IsNotBrowser; - - [ConditionalFact(nameof(SupportICUWithUtcAlias))] - public static void UtcAliases_MapToUtc() - { - foreach (string alias in s_UtcAliases) - { - TimeZoneInfo actualUtc = TimeZoneInfo.FindSystemTimeZoneById(alias); - Assert.True(TimeZoneInfo.Utc.HasSameRules(actualUtc)); - Assert.True(actualUtc.HasSameRules(TimeZoneInfo.Utc)); - } - } - [ActiveIssue("https://github.com/dotnet/runtime/issues/19794", TestPlatforms.AnyUnix)] [Theory] [MemberData(nameof(SystemTimeZonesTestData))] @@ -2584,15 +2132,6 @@ public static void BinaryFormatter_RoundTrips(TimeZoneInfo timeZone) } } - [Fact] - public static void TimeZoneInfo_DoesNotCreateAdjustmentRulesWithOffsetOutsideOfRange() - { - // On some OSes with some time zones setting - // time zone may contain old adjustment rule which have offset higher than 14h - // Assert.DoesNotThrow - DateTimeOffset.FromFileTime(0); - } - [Fact] public static void TimeZoneInfo_DoesConvertTimeForOldDatesOfTimeZonesWithExceedingMaxRange() { @@ -2863,17 +2402,6 @@ public static void TimeZoneInfo_DisplayNameStartsWithOffset(TimeZoneInfo tzi) } } - [Fact] - 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)); - } - public static IEnumerable AlternativeName_TestData() { yield return new object[] { "Pacific Standard Time", "America/Los_Angeles" }; @@ -3175,32 +2703,6 @@ public static void FijiTimeZoneTest() } } - [Fact] - public static void AdjustmentRuleBaseUtcOffsetDeltaTest() - { - TimeZoneInfo.TransitionTime start = TimeZoneInfo.TransitionTime.CreateFixedDateRule(timeOfDay: new DateTime(1, 1, 1, 2, 0, 0), month: 3, day: 7); - TimeZoneInfo.TransitionTime end = TimeZoneInfo.TransitionTime.CreateFixedDateRule(timeOfDay: new DateTime(1, 1, 1, 1, 0, 0), month: 11, day: 7); - TimeZoneInfo.AdjustmentRule rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(DateTime.MinValue.Date, DateTime.MaxValue.Date, new TimeSpan(1, 0, 0), start, end, baseUtcOffsetDelta: new TimeSpan(1, 0, 0)); - TimeZoneInfo customTimeZone = TimeZoneInfo.CreateCustomTimeZone( - id: "Fake Time Zone", - baseUtcOffset: new TimeSpan(0), - displayName: "Fake Time Zone", - standardDisplayName: "Standard Fake Time Zone", - daylightDisplayName: "British Summer Time", - new TimeZoneInfo.AdjustmentRule[] { rule }); - - TimeZoneInfo.AdjustmentRule[] rules = customTimeZone.GetAdjustmentRules(); - - Assert.Equal(1, rules.Length); - Assert.Equal(new TimeSpan(1, 0, 0), rules[0].BaseUtcOffsetDelta); - - // BaseUtcOffsetDelta should be counted to the returned offset during the standard time. - Assert.Equal(new TimeSpan(1, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 1, 1, 2, 0, 0))); - - // BaseUtcOffsetDelta should be counted to the returned offset during the daylight time. - Assert.Equal(new TimeSpan(2, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 3, 10, 2, 0, 0))); - } - [ConditionalFact] [ActiveIssue("https://github.com/dotnet/runtime/issues/64111", TestPlatforms.Linux)] public static void NoBackwardTimeZones() @@ -3244,16 +2746,6 @@ public static void TestZoneNamesUsingAlternativeId(string zoneId) }, zoneId).Dispose(); } - [Fact] - public static void TestCustomTimeZonesWithNullNames() - { - TimeZoneInfo custom = TimeZoneInfo.CreateCustomTimeZone("Custom Time Zone With Null Names", TimeSpan.FromHours(-8), null, null); - Assert.Equal("Custom Time Zone With Null Names", custom.Id); - Assert.Equal(string.Empty, custom.StandardName); - Assert.Equal(string.Empty, custom.DaylightName); - Assert.Equal(string.Empty, custom.DisplayName); - } - [InlineData("Eastern Standard Time", "America/New_York")] [InlineData("Central Standard Time", "America/Chicago")] [InlineData("Mountain Standard Time", "America/Denver")] @@ -3284,112 +2776,6 @@ public static void TestTimeZoneNames(string windowsId, string ianaId) private static bool IsEnglishUILanguage => CultureInfo.CurrentUICulture.Name.Length == 0 || CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "en"; private static bool IsEnglishUILanguageAndRemoteExecutorSupported => IsEnglishUILanguage && RemoteExecutor.IsSupported; - - private static void VerifyConvertException(DateTimeOffset inputTime, string destinationTimeZoneId) where TException : Exception - { - Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); - } - - private static void VerifyConvertException(DateTime inputTime, string destinationTimeZoneId) where TException : Exception - { - Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); - } - - private static void VerifyConvertException(DateTime inputTime, string sourceTimeZoneId, string destinationTimeZoneId) where TException : Exception - { - Assert.ThrowsAny(() => TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId), TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId))); - } - - private static void VerifyConvert(DateTimeOffset inputTime, string destinationTimeZoneId, DateTimeOffset expectedTime) - { - DateTimeOffset returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); - Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); - } - - private static void VerifyConvert(DateTime inputTime, string destinationTimeZoneId, DateTime expectedTime) - { - DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); - Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); - Assert.True(expectedTime.Kind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime.Kind, returnedTime.Kind, inputTime, destinationTimeZoneId)); - } - - private static void VerifyConvert(DateTime inputTime, string destinationTimeZoneId, DateTime expectedTime, DateTimeKind expectedKind) - { - DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); - Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime, returnedTime, inputTime, destinationTimeZoneId)); - Assert.True(expectedKind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', TimeZone: {3}", expectedTime.Kind, returnedTime.Kind, inputTime, destinationTimeZoneId)); - } - - private static void VerifyConvert(DateTime inputTime, string sourceTimeZoneId, string destinationTimeZoneId, DateTime expectedTime) - { - DateTime returnedTime = TimeZoneInfo.ConvertTime(inputTime, TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId), TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId)); - Assert.True(returnedTime.Equals(expectedTime), string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', Source TimeZone: {3}, Dest. Time Zone: {4}", expectedTime, returnedTime, inputTime, sourceTimeZoneId, destinationTimeZoneId)); - Assert.True(expectedTime.Kind == returnedTime.Kind, string.Format("Error: Expected value '{0}' but got '{1}', input value is '{2}', Source TimeZone: {3}, Dest. Time Zone: {4}", expectedTime.Kind, returnedTime.Kind, inputTime, sourceTimeZoneId, destinationTimeZoneId)); - } - - private static void VerifyRoundTrip(DateTime dt1, string sourceTimeZoneId, string destinationTimeZoneId) - { - TimeZoneInfo sourceTzi = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId); - TimeZoneInfo destTzi = TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId); - - DateTime dt2 = TimeZoneInfo.ConvertTime(dt1, sourceTzi, destTzi); - DateTime dt3 = TimeZoneInfo.ConvertTime(dt2, destTzi, sourceTzi); - - if (!destTzi.IsAmbiguousTime(dt2)) - { - // the ambiguous time can be mapped to 2 UTC times so it is not guaranteed to round trip - Assert.True(dt1.Equals(dt3), string.Format("{0} failed to round trip using source '{1}' and '{2}' zones. wrong result {3}", dt1, sourceTimeZoneId, destinationTimeZoneId, dt3)); - } - - if (sourceTimeZoneId == TimeZoneInfo.Utc.Id) - { - Assert.True(dt3.Kind == DateTimeKind.Utc, string.Format("failed to get the right DT Kind after round trip {0} using source TZ {1} and dest TZi {2}", dt1, sourceTimeZoneId, destinationTimeZoneId)); - } - } - - private static void VerifyAmbiguousOffsetsException(TimeZoneInfo tz, DateTime dt) where TException : Exception - { - Assert.Throws(() => tz.GetAmbiguousTimeOffsets(dt)); - } - - private static void VerifyOffsets(TimeZoneInfo tz, DateTime dt, TimeSpan[] expectedOffsets) - { - TimeSpan[] ret = tz.GetAmbiguousTimeOffsets(dt); - VerifyTimeSpanArray(ret, expectedOffsets, string.Format("Wrong offsets when used {0} with the zone {1}", dt, tz.Id)); - } - - private static void VerifyTimeSpanArray(TimeSpan[] actual, TimeSpan[] expected, string errorMsg) - { - Assert.True(actual != null); - Assert.True(expected != null); - Assert.True(actual.Length == expected.Length); - - Array.Sort(expected); // TimeZoneInfo is expected to always return sorted TimeSpan arrays - - for (int i = 0; i < actual.Length; i++) - { - Assert.True(actual[i].Equals(expected[i]), errorMsg); - } - } - - private static void VerifyDST(TimeZoneInfo tz, DateTime dt, bool expectedDST) - { - bool ret = tz.IsDaylightSavingTime(dt); - Assert.True(ret == expectedDST, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); - } - - private static void VerifyInv(TimeZoneInfo tz, DateTime dt, bool expectedInvalid) - { - bool ret = tz.IsInvalidTime(dt); - Assert.True(expectedInvalid == ret, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); - } - - private static void VerifyAmbiguous(TimeZoneInfo tz, DateTime dt, bool expectedAmbiguous) - { - bool ret = tz.IsAmbiguousTime(dt); - Assert.True(expectedAmbiguous == ret, string.Format("Test with the zone {0} and date {1} failed", tz.Id, dt)); - } - /// /// Gets the offset for the time zone for early times (close to DateTime.MinValue). /// @@ -3441,33 +2827,6 @@ private static TimeZoneInfo TryGetSystemTimeZone(string id) } } - private static TimeZoneInfo CreateCustomLondonTimeZone() - { - TimeZoneInfo.TransitionTime start = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 1, 0, 0), 3, 5, DayOfWeek.Sunday); - TimeZoneInfo.TransitionTime end = TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), 10, 5, DayOfWeek.Sunday); - TimeZoneInfo.AdjustmentRule rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(DateTime.MinValue.Date, DateTime.MaxValue.Date, new TimeSpan(1, 0, 0), start, end); - return TimeZoneInfo.CreateCustomTimeZone("Europe/London", new TimeSpan(0), "Europe/London", "British Standard Time", "British Summer Time", new TimeZoneInfo.AdjustmentRule[] { rule }); - } - - private static void VerifyConvertToUtcException(DateTime dateTime, TimeZoneInfo sourceTimeZone) where TException : Exception - { - Assert.ThrowsAny(() => TimeZoneInfo.ConvertTimeToUtc(dateTime, sourceTimeZone)); - } - - private static void VerifyCustomTimeZoneException(string id, TimeSpan baseUtcOffset, string displayName, string standardDisplayName, string daylightDisplayName = null, TimeZoneInfo.AdjustmentRule[] adjustmentRules = null) where TException : Exception - { - Assert.ThrowsAny(() => - { - if (daylightDisplayName == null && adjustmentRules == null) - { - TimeZoneInfo.CreateCustomTimeZone(id, baseUtcOffset, displayName, standardDisplayName); - } - else - { - TimeZoneInfo.CreateCustomTimeZone(id, baseUtcOffset, displayName, standardDisplayName, daylightDisplayName, adjustmentRules); - } - }); - } // This helper class is used to retrieve information about installed OS languages from Windows. // Its methods returns empty when run on non-Windows platforms. diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneFalse.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneFalse.cs new file mode 100644 index 0000000000000..b5d5fb948445d --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneFalse.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Threading; +using System.Runtime.CompilerServices; + +/// +/// Ensures setting InvariantTimezone = false still works in a trimmed app. +/// +class Program +{ + static int Main(string[] args) + { + if(GetInvariant(null)) + { + return -99; + } + + TimeZoneInfo utc = TimeZoneInfo.FindSystemTimeZoneById("UTC"); + if(utc.BaseUtcOffset != TimeSpan.Zero) + { + return -1; + } + + try + { + TimeZoneInfo tst = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo"); + if(utc == tst) + { + return -2; + } + if(tst.BaseUtcOffset == TimeSpan.Zero) + { + return -3; + } + } + // some AzDO images don't have tzdata installed + catch (TimeZoneNotFoundException tznfe) + { + if(tznfe.InnerException == null || tznfe.InnerException.GetType() != typeof(System.IO.DirectoryNotFoundException)) + { + return -4; + } + } + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_Invariant")] + static extern bool GetInvariant(TimeZoneInfo t); + + return 100; + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneTrue.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneTrue.cs new file mode 100644 index 0000000000000..8490673b71f55 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/InvariantTimezoneTrue.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; + +/// +/// Ensures setting InvariantTimezone = true still works in a trimmed app. +/// +class Program +{ + static int Main(string[] args) + { + if(!GetInvariant(null)) + { + return -99; + } + + TimeZoneInfo utc = TimeZoneInfo.FindSystemTimeZoneById("UTC"); + if(utc != TimeZoneInfo.Utc) + { + return -1; + } + if(utc.BaseUtcOffset != TimeSpan.Zero) + { + return -2; + } + if(TimeZoneInfo.Local == TimeZoneInfo.Utc) + { + return -3; + } + var tzs = TimeZoneInfo.GetSystemTimeZones(); + if(tzs.Count != 1) + { + Console.WriteLine($"tzs.Count {tzs.Count}"); + return -4; + } + if(tzs[0] != TimeZoneInfo.Utc) + { + Console.WriteLine($"GetSystemTimeZones()[0] {tzs[0]}"); + return -5; + } + try + { + TimeZoneInfo tst = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo"); + return -6; + } + catch (TimeZoneNotFoundException) + { + // expected + } + return 100; + + [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_Invariant")] + static extern bool GetInvariant(TimeZoneInfo t); + } +} diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/System.Runtime.TrimmingTests.proj b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/System.Runtime.TrimmingTests.proj index 7976b3a89c221..73b334d839ecf 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/System.Runtime.TrimmingTests.proj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/TrimmingTests/System.Runtime.TrimmingTests.proj @@ -37,6 +37,14 @@ HybridGlobalization InvariantGlobalization;PredefinedCulturesOnly + + InvariantTimezone + System.TimeZoneInfo.Invariant + + + InvariantTimezone + System.TimeZoneInfo.Invariant +