From 9f0ce365c7812d8c1f6161397982eae82b8e8180 Mon Sep 17 00:00:00 2001 From: Scott Ferguson Date: Thu, 19 Nov 2020 09:13:40 -0500 Subject: [PATCH] Fix Incorrect UTC offset during DST transition (case 1288231) Corrects an issue where for the hour after the DST transition, the local UTC offset was listed. The UTC offset was the DST offset instead of the standard time offset. The runtime library captures this an ambiguous time. That is the local time that occurs twice - once in DST then once in standard time. If DST is an extra 1:00 a.m. offset and ends at 2:00 a.m., 1:00 a.m. to 1:59:59.9999.... occurs twice. First in DST then again in standard time. The classlibs had this incorrect - they did not consider 1:00 a.m. an ambiguous time, and considered 2:00 a.m. ambiguous. However it should be reversed. 1:00 a.m. occurs twice, but 2:00 a.m. only occurs once. The instance we would hit 2:00 a.m. DST, we instantaneous switch to 1:00 a.m. standard. The classlibs were also not recording enough information to record which side of DST a local time was. When converting a time from UTC, or using DateTime.Now an internal flag, IsAmbiguousDaylightSavingTime, should be set if the time is an ambiguous local time that is on the DST side of the transition. The classlibs were calling TimeZone.IsAmbigousTime which has a wider defintion for ambiguous time that the IsAmbiguousDaylightSavingTime should have. It returns true for local times on either side of DST. So a new method IsAmbiguousLocalDstFromUtc was added to check this case. The classlibs were also not checking the IsAmbiguousLocalDstFromUtc flag when getting the UTC offset for a local time. So a check was inserted in two locations to correct for that. Some tests has to be updated to reflect these new definitions of when DST starts and ends and which times are ambiguous. These also account for some test changes required by cherry-picked changes to TimeZoneInfo.cs where the corresponding test changes were not cherry-picked. Some of those changes where in PR's that updated to the CoreFx TimeZoneInfo class. All these changes have been verified against the behavior of the .Net Framework and they match. Fix case 1288231: Mono: Fix incorrect UTC offset during daylight savings time transitions --- mcs/class/corlib/System/TimeZoneInfo.cs | 54 +++++++++++++++++-- .../corlib/Test/System/TimeZoneInfoTest.cs | 25 ++++----- mcs/class/corlib/Test/System/TimeZoneTest.cs | 32 ++++++++--- 3 files changed, 89 insertions(+), 22 deletions(-) diff --git a/mcs/class/corlib/System/TimeZoneInfo.cs b/mcs/class/corlib/System/TimeZoneInfo.cs index 11eec8cfa95..3360421500a 100644 --- a/mcs/class/corlib/System/TimeZoneInfo.cs +++ b/mcs/class/corlib/System/TimeZoneInfo.cs @@ -927,7 +927,33 @@ namespace System AdjustmentRule rule = GetApplicableRule (dateTime); if (rule != null) { DateTime tpoint = TransitionPoint (rule.DaylightTransitionEnd, dateTime.Year); - if (dateTime > tpoint - rule.DaylightDelta && dateTime <= tpoint) + if (dateTime >= tpoint - rule.DaylightDelta && dateTime < tpoint) + return true; + } + + return false; + } + + private bool IsAmbiguousLocalDstFromUtc (DateTime dateTime) + { + // This method determines if a dateTime in UTC falls into the Dst side + // of the ambiguous local time (the local time that occurs twice). + + if (dateTime.Kind == DateTimeKind.Local) + return false; + + if (this == TimeZoneInfo.Utc) + return false; + + AdjustmentRule rule = GetApplicableRule (dateTime); + if (rule != null) { + DateTime tpoint = TransitionPoint (rule.DaylightTransitionEnd, dateTime.Year); + // tpoint is the local time in daylight savings time when daylight savings time will end, convert it to UTC + DateTime tpointUtc; + if (!TryAddTicks(tpoint, -(BaseUtcOffset.Ticks + rule.DaylightDelta.Ticks), out tpointUtc, DateTimeKind.Utc)) + return false; + + if (dateTime >= tpointUtc - rule.DaylightDelta && dateTime < tpointUtc) return true; } @@ -946,7 +972,18 @@ namespace System return true; // We might be in the dateTime previous year's DST period - return dateTime.Year > 1 && IsInDSTForYear (rule, dateTime, dateTime.Year - 1); + if (dateTime.Year > 1 && IsInDSTForYear(rule, dateTime, dateTime.Year - 1)) + return true; + + // If we are checking an ambiguous local time, that is the local time that occurs twice during a DST "fall back" + // check if it was marked as being in the DST side of the ambiguous time when it was created + // We need to re-check IsAmbiguousTime because the IsAmbiguousDaylightSavingTime flag is not cleared when using DateTime.Add/Subtract + if (dateTime.Kind == DateTimeKind.Local && IsAmbiguousTime(dateTime)) + { + return dateTime.IsAmbiguousDaylightSavingTime(); + } + + return false; } bool IsInDSTForYear (AdjustmentRule rule, DateTime dateTime, int year) @@ -1281,6 +1318,15 @@ namespace System offset = baseUtcOffset; isDst = false; } + + // If we are checking an ambiguous local time, that is the local time that occurs twice during a DST "fall back" + // check if it was marked as being in the DST side of the ambiguous time when it was created + // We need to re-check IsAmbiguousTime because the IsAmbiguousDaylightSavingTime flag is not cleared when using DateTime.Add/Subtract + if (!isDst && dateTime.Kind == DateTimeKind.Local && IsAmbiguousTime(dateTime) && dateTime.IsAmbiguousDaylightSavingTime()) + { + offset += current.DaylightDelta; + isDst = true; + } return true; } @@ -1578,7 +1624,7 @@ namespace System isAmbiguousLocalDst = false; TimeSpan baseOffset = zone.BaseUtcOffset; - if (zone.IsAmbiguousTime (time)) { + if (zone.IsAmbiguousLocalDstFromUtc (time)) { isAmbiguousLocalDst = true; // return baseOffset; } @@ -1608,4 +1654,4 @@ namespace System } #endif } -} \ No newline at end of file +} diff --git a/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs b/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs index c7425a88137..1982d7355a9 100644 --- a/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs +++ b/mcs/class/corlib/Test/System/TimeZoneInfoTest.cs @@ -733,7 +733,7 @@ namespace MonoTests.System DateTime afterDST = new DateTime (2007, 10, 28, 2, 0, 0, DateTimeKind.Unspecified); Assert.IsFalse (london.IsDaylightSavingTime (beforeDST), "Just before DST"); Assert.IsTrue (london.IsDaylightSavingTime (startDST), "the first seconds of DST"); - Assert.IsTrue (london.IsDaylightSavingTime (endDST), "The last seconds of DST"); + Assert.IsFalse (london.IsDaylightSavingTime (endDST), "The last seconds of DST"); Assert.IsFalse (london.IsDaylightSavingTime (afterDST), "Just after DST"); } @@ -826,12 +826,12 @@ namespace MonoTests.System Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); date = new DateTime (2014, 3, 30 , 3, 1, 0); - Assert.IsTrue (tzi.IsDaylightSavingTime (date)); + Assert.IsFalse (tzi.IsDaylightSavingTime (date)); Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date)); Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); date = new DateTime (2014, 3, 30 , 3, 59, 0); - Assert.IsTrue (tzi.IsDaylightSavingTime (date)); + Assert.IsFalse (tzi.IsDaylightSavingTime (date)); Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date)); Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); @@ -859,17 +859,17 @@ namespace MonoTests.System try { var date = new DateTime (2014, 3, 30 , 3, 0, 0); - Assert.IsTrue (tzi.IsDaylightSavingTime (date)); + Assert.IsFalse (tzi.IsDaylightSavingTime (date)); Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date)); Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); date = new DateTime (2014, 3, 30 , 3, 1, 0); - Assert.IsTrue (tzi.IsDaylightSavingTime (date)); + Assert.IsFalse (tzi.IsDaylightSavingTime (date)); Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date)); Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); date = new DateTime (2014, 3, 30 , 3, 59, 0); - Assert.IsTrue (tzi.IsDaylightSavingTime (date)); + Assert.IsFalse (tzi.IsDaylightSavingTime (date)); Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date)); Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date)))); @@ -1494,19 +1494,22 @@ namespace MonoTests.System [Test] public void AmbiguousDates () { - Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0))); + Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0))); Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 1))); - Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0))); + Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0))); Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 1))); } [Test] public void AmbiguousUTCDates () { - Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 0, DateTimeKind.Utc))); + Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 0, DateTimeKind.Utc))); Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 1, DateTimeKind.Utc))); Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 59, 59, DateTimeKind.Utc))); - Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0, DateTimeKind.Utc))); + Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0, DateTimeKind.Utc))); + Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 59, 59, DateTimeKind.Utc))); + Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0, DateTimeKind.Utc))); + Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 1, DateTimeKind.Utc))); } #if SLOW_TESTS @@ -1885,7 +1888,6 @@ namespace MonoTests.System d = dst1End.Add (-dstOffset); Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,0,0,-1)))); - Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d)); Assert.AreEqual(baseUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,1,0, 1)))); d = dst2Start.Add (dstOffset); @@ -1895,7 +1897,6 @@ namespace MonoTests.System d = dst2End.Add (-dstOffset); Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,0,0,-1)))); - Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d)); Assert.AreEqual(baseUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,1,0, 1)))); } diff --git a/mcs/class/corlib/Test/System/TimeZoneTest.cs b/mcs/class/corlib/Test/System/TimeZoneTest.cs index 5f73383f882..05340de3107 100644 --- a/mcs/class/corlib/Test/System/TimeZoneTest.cs +++ b/mcs/class/corlib/Test/System/TimeZoneTest.cs @@ -299,12 +299,12 @@ public class TimeZoneTest { Assert.IsTrue (!tzInfo.IsAmbiguousTime(st)); Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 2)); st = new DateTime(2016, 10, 30, 2, 0, 0, DateTimeKind.Local); - Assert.IsTrue (tzInfo.IsDaylightSavingTime(st)); - Assert.IsTrue (!tzInfo.IsAmbiguousTime(st)); - Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 1)); - st = new DateTime(2016, 10, 30, 3, 0, 0, DateTimeKind.Local); Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st)); Assert.IsTrue (tzInfo.IsAmbiguousTime(st)); + Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 2)); + st = new DateTime(2016, 10, 30, 3, 0, 0, DateTimeKind.Local); + Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st)); + Assert.IsTrue (!tzInfo.IsAmbiguousTime(st)); Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 3)); st = new DateTime(2016, 10, 30, 4, 0, 0, DateTimeKind.Local); Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st)); @@ -348,9 +348,29 @@ public class TimeZoneTest { var dstOffset = tz.GetUtcOffset(daylightChanges.Start.AddMinutes(61)); // Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end)); - Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add (daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(1))))); - Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ()))); + Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end.Add (daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(1))))); + Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ()))); Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(-1))))); + + // This test assumes that the DST end is a "fall back" where we go to an earlier local time + if (daylightChanges.Delta > TimeSpan.Zero) + { + // dst_end is the end time of the DST in DST time. + // It is technically an ambiguous time because the same local time occurs twice, + // once in DST and then again in standard time + // The ToUniversalTime() will assume standard time for ambiguous times, so we subtract + // the DST delta to the the UTC time corresponding to the end of DST. Then + // the ToLocalTime() will encode some extra info letting the framework know that we + // are dealing with the ambiguous local time that is in DST. + var dst_ambiguous = tz.ToUniversalTime(dst_end.Add(daylightChanges.Delta.Negate())).ToUniversalTime() + .Add(daylightChanges.Delta.Negate()).ToLocalTime(); + + Assert.AreEqual(dstOffset, tz.GetUtcOffset(dst_ambiguous)); + + // The IsAmbiguousDaylightSavingTime flag is not cleared by DateTime.Add + Assert.AreEqual(standardOffset, tz.GetUtcOffset(dst_ambiguous.Add(daylightChanges.Delta))); + Assert.AreEqual(dstOffset, tz.GetUtcOffset(dst_ambiguous.Add(daylightChanges.Delta).Subtract(daylightChanges.Delta))); + } } -- GitLab