From 21b58c9631d399efc96c6bba98b5f78bfb4af471 Mon Sep 17 00:00:00 2001 From: Maxim Lipnin Date: Tue, 24 Sep 2019 20:58:59 +0300 Subject: [PATCH] Fix time zone issue when jumping into DST (#16430) Addresses an issue with jumping into DST for some time zones when the incorrect date-time offset is returned for date-time in UTC (which comes from DateTime.Now). The fix is to just check if the incoming date-time is in UTC. Also added a set of tests for some time zones to verify jumping into DST in general. Fixes https://github.com/mono/mono/issues/16395 --- mcs/class/corlib/System/TimeZoneInfo.cs | 7 +- .../corlib/Test/System/TimeZoneInfoTest.cs | 256 ++++++++++++++++++ 2 files changed, 261 insertions(+), 2 deletions(-) diff --git a/mcs/class/corlib/System/TimeZoneInfo.cs b/mcs/class/corlib/System/TimeZoneInfo.cs index 2dd75591321..11eec8cfa95 100644 --- a/mcs/class/corlib/System/TimeZoneInfo.cs +++ b/mcs/class/corlib/System/TimeZoneInfo.cs @@ -1252,10 +1252,13 @@ namespace System return false; } + var isUtc = false; if (dateTime.Kind != DateTimeKind.Utc) { if (!TryAddTicks (date, -BaseUtcOffset.Ticks, out date, DateTimeKind.Utc)) return false; - } + } else + isUtc = true; + AdjustmentRule current = GetApplicableRule (date); if (current != null) { @@ -1267,7 +1270,7 @@ namespace System if (forOffset) isDst = true; offset = baseUtcOffset; - if (date >= new DateTime (tStart.Ticks + current.DaylightDelta.Ticks, DateTimeKind.Utc)) + if (isUtc || (date >= new DateTime (tStart.Ticks + current.DaylightDelta.Ticks, DateTimeKind.Utc))) { offset += current.DaylightDelta; isDst = true; diff --git a/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs b/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs index 5dfb17dab38..c7425a88137 100644 --- a/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs +++ b/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs @@ -53,6 +53,11 @@ namespace MonoTests.System return "New Zealand Standard Time"; case "Europe/Athens": return "GTB Standard Time"; + case "Europe/Chisinau": + return "E. Europe Standard Time"; + case "America/New_York": + return "Eastern Standard Time"; + case "America/Chicago": case "US/Eastern": return "Eastern Standard Time"; case "US/Central": @@ -63,18 +68,57 @@ namespace MonoTests.System case "Australia/Melbourne": return "AUS Eastern Standard Time"; case "Europe/Brussels": + case "Europe/Copenhagen": + case "Europe/Paris": + case "Europe/Madrid": return "Romance Standard Time"; case "Africa/Kinshasa": return "W. Central Africa Standard Time"; case "Europe/Rome": case "Europe/Vatican": + case "Europe/Vienna": + case "Europe/Berlin": + case "Europe/Luxembourg": + case "Europe/Malta": + case "Europe/Monaco": + case "Europe/Amsterdam": + case "Europe/Oslo": + case "Europe/San_Marino": return "W. Europe Standard Time"; case "Canada/Eastern": return "Eastern Standard Time"; case "Asia/Tehran": return "Iran Standard Time"; case "Europe/Guernsey": + case "Europe/Dublin": + case "Europe/Isle_of_Man": + case "Europe/Jersey": + case "Europe/Lisbon": + case "Europe/London": return "GMT Standard Time"; + case "America/Havana": + return "Cuba Standard Time"; + case "America/Anchorage": + return "Alaskan Standard Time"; + case "Atlantic/Azores": + return "Azores Standard Time"; + case "Asia/Jerusalem": + return "Israel Standard Time"; + case "Asia/Amman": + return "Jordan Standard Time"; + case "Europe/Tirane": + case "Europe/Warsaw": + return "Central European Standard Time"; + case "Europe/Sofia": + case "Europe/Tallinn": + case "Europe/Riga": + case "Europe/Vilnius": + case "Europe/Kiev": + return "FLE Standard Time"; + case "Europe/Prague": + case "Europe/Budapest": + case "Europe/Bratislava": + return "Central Europe Standard Time"; default: Assert.Fail ($"No mapping defined for zone id '{id}'"); return null; @@ -905,6 +949,218 @@ namespace MonoTests.System Assert.AreEqual (new TimeSpan (0, 0, 0), tzi.GetUtcOffset (date)); #endif } + + [Test] + public void Bug_16395 () + { + // Cuba, Havana (Cuba Standard Time): Jumps ahead at 12:00 AM on 3/8/2020 to 1:00 AM + CheckJumpingIntoDST ("America/Havana", + new DateTime (2020, 3, 8, 0, 0, 0), new DateTime (2020, 3, 8, 0, 30, 0), new DateTime (2020, 3, 8, 1, 0, 0), + new TimeSpan (-5, 0, 0), new TimeSpan (-4, 0, 0)); + + // US, Kansas City, MO (US Central Time): Jumps ahead at 2:00 AM on 3/8/2020 to 3:00 AM + CheckJumpingIntoDST ("America/Chicago", + new DateTime (2020, 3, 8, 2, 0, 0), new DateTime (2020, 3, 8, 2, 30, 0), new DateTime (2020, 3, 8, 3, 0, 0), + new TimeSpan (-6, 0, 0), new TimeSpan (-5, 0, 0)); + + // Anchorage, AK (Alaska Time): Jumps ahead at 2:00 AM on 3/8/2020 to 3:00 AM + CheckJumpingIntoDST ("America/Anchorage", + new DateTime (2020, 3, 8, 2, 0, 0), new DateTime (2020, 3, 8, 2, 30, 0), new DateTime (2020, 3, 8, 3, 0, 0), + new TimeSpan (-9, 0, 0), new TimeSpan (-8, 0, 0)); + + // Azores ST (Ponta Delgada, Portugal): Jumps ahead at 12:00 AM on 3/29/2020 to 1:00 AM + CheckJumpingIntoDST ("Atlantic/Azores", + new DateTime (2020, 3, 29, 0, 0, 0), new DateTime (2020, 3, 29, 0, 30, 0), new DateTime (2020, 3, 29, 1, 0, 0), + new TimeSpan (-1, 0, 0), new TimeSpan (0, 0, 0)); + + // Iran, Tehran (Iran ST): Jumps ahead at 12:00 AM on 3/21/2020 to 1:00 AM + CheckJumpingIntoDST ("Asia/Tehran", + new DateTime (2020, 3, 21, 0, 0, 0), new DateTime (2020, 3, 21, 0, 30, 0), new DateTime (2020, 3, 21, 1, 0, 0), + new TimeSpan (3, 30, 0), new TimeSpan (4, 30, 0)); + + // Israel, Jerusalem (Israel ST): Jumps ahead at 2:00 AM on 3/27/2020 to 3:00 AM + CheckJumpingIntoDST ("Asia/Jerusalem", + new DateTime (2020, 3, 27, 2, 0, 0), new DateTime (2020, 3, 27, 2, 30, 0), new DateTime (2020, 3, 27, 3, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Jordan, Amman (Eastern European ST): Jumps ahead at 12:00 AM on 3/27/2020 to 1:00 AM + CheckJumpingIntoDST ("Asia/Amman", + new DateTime (2020, 3, 27, 0, 0, 0), new DateTime (2020, 3, 27, 0, 30, 0), new DateTime (2020, 3, 27, 1, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Albania, Tirana (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Tirane", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Austria, Vienna (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Vienna", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Belgium, Brussels (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Brussels", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Bulgaria, Sofia (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Sofia", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Czechia, Prague (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Prague", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Denmark, Copenhagen (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Copenhagen", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Estonia, Tallinn (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Tallinn", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // France, Paris (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Paris", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Germany, Berlin (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Berlin", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Greece, Athens (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Athens", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Guernsey (UK) Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + CheckJumpingIntoDST ("Europe/Guernsey", + new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + + // Holy See, Vatican City (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Vatican", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Hungary, Budapest (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Budapest", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // // Ireland, Dublin (Greenwich Mean Time -> Irish Standard Time): Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + // CheckJumpingIntoDST ("Europe/Dublin", + // new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + // new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + + // UK, Douglas, Isle of Man (GMT+1:00): Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + CheckJumpingIntoDST ("Europe/Isle_of_Man", + new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + + // Italy, Rome (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Rome", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Jersey (UK): Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + CheckJumpingIntoDST ("Europe/Jersey", + new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + + // Latvia, Riga (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Riga", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Lithuania, Vilnius (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Vilnius", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Luxembourg, Luxembourg (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Luxembourg", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Malta, Valletta (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Malta", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Moldova, Chişinău (Eastern European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Chisinau", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // Monaco, Monaco (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Monaco", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Netherlands, Amsterdam (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Amsterdam", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Norway, Oslo (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Oslo", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Poland, Warsaw (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Warsaw", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Portugal, Lisbon (Western European ST): Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + CheckJumpingIntoDST ("Europe/Lisbon", + new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + + // San Marino, San Marino (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/San_Marino", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Slovakia, Bratislava (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Bratislava", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Spain, Madrid (Central European ST): Jumps ahead at 2:00 AM on 3/29/2020 to 3:00 AM + CheckJumpingIntoDST ("Europe/Madrid", + new DateTime (2020, 3, 29, 2, 0, 0), new DateTime (2020, 3, 29, 2, 30, 0), new DateTime (2020, 3, 29, 3, 0, 0), + new TimeSpan (1, 0, 0), new TimeSpan (2, 0, 0)); + + // Ukraine, Kiev (Eastern European ST): Jumps ahead at 3:00 AM on 3/29/2020 to 4:00 AM + CheckJumpingIntoDST ("Europe/Kiev", + new DateTime (2020, 3, 29, 3, 0, 0), new DateTime (2020, 3, 29, 3, 30, 0), new DateTime (2020, 3, 29, 4, 0, 0), + new TimeSpan (2, 0, 0), new TimeSpan (3, 0, 0)); + + // United Kingdom, London (British ST): Jumps ahead at 1:00 AM on 3/29/2020 to 2:00 AM + CheckJumpingIntoDST ("Europe/London", + new DateTime (2020, 3, 29, 1, 0, 0), new DateTime (2020, 3, 29, 1, 30, 0), new DateTime (2020, 3, 29, 2, 0, 0), + new TimeSpan (0, 0, 0), new TimeSpan (1, 0, 0)); + } + + void CheckJumpingIntoDST (string tzId, DateTime dstDeltaStart, DateTime inDstDelta, DateTime dstDeltaEnd, TimeSpan baseOffset, TimeSpan dstOffset) + { + var tzi = TimeZoneInfo.FindSystemTimeZoneById (MapTimeZoneId (tzId)); + Assert.IsFalse (tzi.IsDaylightSavingTime (dstDeltaStart), $"{tzId}: #1"); + Assert.AreEqual (baseOffset, tzi.GetUtcOffset (dstDeltaStart), $"{tzId}: #2"); + + Assert.IsFalse (tzi.IsDaylightSavingTime (inDstDelta), $"{tzId}: #3"); + Assert.AreEqual (baseOffset, tzi.GetUtcOffset (inDstDelta), $"{tzId}: #4"); + + Assert.IsTrue (tzi.IsDaylightSavingTime (dstDeltaEnd), $"{tzId}: #5"); + Assert.AreEqual (dstOffset, tzi.GetUtcOffset (dstDeltaEnd), $"{tzId}: #6"); + } } [TestFixture] -- GitLab