Skip to content

Commit 26dced2

Browse files
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
1 parent 21b58c9 commit 26dced2

File tree

3 files changed

+89
-22
lines changed

3 files changed

+89
-22
lines changed

mcs/class/corlib/System/TimeZoneInfo.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,33 @@ public bool IsAmbiguousTime (DateTime dateTime)
927927
AdjustmentRule rule = GetApplicableRule (dateTime);
928928
if (rule != null) {
929929
DateTime tpoint = TransitionPoint (rule.DaylightTransitionEnd, dateTime.Year);
930-
if (dateTime > tpoint - rule.DaylightDelta && dateTime <= tpoint)
930+
if (dateTime >= tpoint - rule.DaylightDelta && dateTime < tpoint)
931+
return true;
932+
}
933+
934+
return false;
935+
}
936+
937+
private bool IsAmbiguousLocalDstFromUtc (DateTime dateTime)
938+
{
939+
// This method determines if a dateTime in UTC falls into the Dst side
940+
// of the ambiguous local time (the local time that occurs twice).
941+
942+
if (dateTime.Kind == DateTimeKind.Local)
943+
return false;
944+
945+
if (this == TimeZoneInfo.Utc)
946+
return false;
947+
948+
AdjustmentRule rule = GetApplicableRule (dateTime);
949+
if (rule != null) {
950+
DateTime tpoint = TransitionPoint (rule.DaylightTransitionEnd, dateTime.Year);
951+
// tpoint is the local time in daylight savings time when daylight savings time will end, convert it to UTC
952+
DateTime tpointUtc;
953+
if (!TryAddTicks(tpoint, -(BaseUtcOffset.Ticks + rule.DaylightDelta.Ticks), out tpointUtc, DateTimeKind.Utc))
954+
return false;
955+
956+
if (dateTime >= tpointUtc - rule.DaylightDelta && dateTime < tpointUtc)
931957
return true;
932958
}
933959

@@ -946,7 +972,18 @@ private bool IsInDST (AdjustmentRule rule, DateTime dateTime)
946972
return true;
947973

948974
// We might be in the dateTime previous year's DST period
949-
return dateTime.Year > 1 && IsInDSTForYear (rule, dateTime, dateTime.Year - 1);
975+
if (dateTime.Year > 1 && IsInDSTForYear(rule, dateTime, dateTime.Year - 1))
976+
return true;
977+
978+
// If we are checking an ambiguous local time, that is the local time that occurs twice during a DST "fall back"
979+
// check if it was marked as being in the DST side of the ambiguous time when it was created
980+
// We need to re-check IsAmbiguousTime because the IsAmbiguousDaylightSavingTime flag is not cleared when using DateTime.Add/Subtract
981+
if (dateTime.Kind == DateTimeKind.Local && IsAmbiguousTime(dateTime))
982+
{
983+
return dateTime.IsAmbiguousDaylightSavingTime();
984+
}
985+
986+
return false;
950987
}
951988

952989
bool IsInDSTForYear (AdjustmentRule rule, DateTime dateTime, int year)
@@ -1281,6 +1318,15 @@ private bool TryGetTransitionOffset (DateTime dateTime, out TimeSpan offset, out
12811318
offset = baseUtcOffset;
12821319
isDst = false;
12831320
}
1321+
1322+
// If we are checking an ambiguous local time, that is the local time that occurs twice during a DST "fall back"
1323+
// check if it was marked as being in the DST side of the ambiguous time when it was created
1324+
// We need to re-check IsAmbiguousTime because the IsAmbiguousDaylightSavingTime flag is not cleared when using DateTime.Add/Subtract
1325+
if (!isDst && dateTime.Kind == DateTimeKind.Local && IsAmbiguousTime(dateTime) && dateTime.IsAmbiguousDaylightSavingTime())
1326+
{
1327+
offset += current.DaylightDelta;
1328+
isDst = true;
1329+
}
12841330

12851331
return true;
12861332
}
@@ -1578,7 +1624,7 @@ static internal TimeSpan GetUtcOffsetFromUtc (DateTime time, TimeZoneInfo zone,
15781624
isAmbiguousLocalDst = false;
15791625
TimeSpan baseOffset = zone.BaseUtcOffset;
15801626

1581-
if (zone.IsAmbiguousTime (time)) {
1627+
if (zone.IsAmbiguousLocalDstFromUtc (time)) {
15821628
isAmbiguousLocalDst = true;
15831629
// return baseOffset;
15841630
}
@@ -1608,4 +1654,4 @@ public override string ToString ()
16081654
}
16091655
#endif
16101656
}
1611-
}
1657+
}

mcs/class/corlib/Test/System/TimeZoneInfoTest.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ public void DSTTransitions ()
733733
DateTime afterDST = new DateTime (2007, 10, 28, 2, 0, 0, DateTimeKind.Unspecified);
734734
Assert.IsFalse (london.IsDaylightSavingTime (beforeDST), "Just before DST");
735735
Assert.IsTrue (london.IsDaylightSavingTime (startDST), "the first seconds of DST");
736-
Assert.IsTrue (london.IsDaylightSavingTime (endDST), "The last seconds of DST");
736+
Assert.IsFalse (london.IsDaylightSavingTime (endDST), "The last seconds of DST");
737737
Assert.IsFalse (london.IsDaylightSavingTime (afterDST), "Just after DST");
738738
}
739739

@@ -826,12 +826,12 @@ public void TestAthensDST_InDSTDelta ()
826826
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
827827

828828
date = new DateTime (2014, 3, 30 , 3, 1, 0);
829-
Assert.IsTrue (tzi.IsDaylightSavingTime (date));
829+
Assert.IsFalse (tzi.IsDaylightSavingTime (date));
830830
Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date));
831831
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
832832

833833
date = new DateTime (2014, 3, 30 , 3, 59, 0);
834-
Assert.IsTrue (tzi.IsDaylightSavingTime (date));
834+
Assert.IsFalse (tzi.IsDaylightSavingTime (date));
835835
Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date));
836836
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
837837

@@ -859,17 +859,17 @@ public void TestAthensDST_InDSTDelta_NoTransitions ()
859859
try {
860860

861861
var date = new DateTime (2014, 3, 30 , 3, 0, 0);
862-
Assert.IsTrue (tzi.IsDaylightSavingTime (date));
862+
Assert.IsFalse (tzi.IsDaylightSavingTime (date));
863863
Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date));
864864
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
865865

866866
date = new DateTime (2014, 3, 30 , 3, 1, 0);
867-
Assert.IsTrue (tzi.IsDaylightSavingTime (date));
867+
Assert.IsFalse (tzi.IsDaylightSavingTime (date));
868868
Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date));
869869
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
870870

871871
date = new DateTime (2014, 3, 30 , 3, 59, 0);
872-
Assert.IsTrue (tzi.IsDaylightSavingTime (date));
872+
Assert.IsFalse (tzi.IsDaylightSavingTime (date));
873873
Assert.AreEqual (new TimeSpan (2, 0, 0), tzi.GetUtcOffset (date));
874874
Assert.IsTrue (tzi.IsDaylightSavingTime (new DateTimeOffset (date, tzi.GetUtcOffset (date))));
875875

@@ -1494,19 +1494,22 @@ public void CreateTimeZones ()
14941494
[Test]
14951495
public void AmbiguousDates ()
14961496
{
1497-
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0)));
1497+
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0)));
14981498
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 1)));
1499-
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0)));
1499+
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0)));
15001500
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 1)));
15011501
}
15021502

15031503
[Test]
15041504
public void AmbiguousUTCDates ()
15051505
{
1506-
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 0, DateTimeKind.Utc)));
1506+
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 0, DateTimeKind.Utc)));
15071507
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 0, 1, DateTimeKind.Utc)));
15081508
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 0, 59, 59, DateTimeKind.Utc)));
1509-
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0, DateTimeKind.Utc)));
1509+
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 0, 0, DateTimeKind.Utc)));
1510+
Assert.IsTrue (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 1, 59, 59, DateTimeKind.Utc)));
1511+
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 0, DateTimeKind.Utc)));
1512+
Assert.IsFalse (london.IsAmbiguousTime (new DateTime (2007, 10, 28, 2, 0, 1, DateTimeKind.Utc)));
15101513
}
15111514

15121515
#if SLOW_TESTS
@@ -1885,7 +1888,6 @@ public void GetUtcOffset_FromUnspecified ()
18851888

18861889
d = dst1End.Add (-dstOffset);
18871890
Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,0,0,-1))));
1888-
Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d));
18891891
Assert.AreEqual(baseUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,1,0, 1))));
18901892

18911893
d = dst2Start.Add (dstOffset);
@@ -1895,7 +1897,6 @@ public void GetUtcOffset_FromUnspecified ()
18951897

18961898
d = dst2End.Add (-dstOffset);
18971899
Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,0,0,-1))));
1898-
Assert.AreEqual(dstUtcOffset, cairo.GetUtcOffset (d));
18991900
Assert.AreEqual(baseUtcOffset, cairo.GetUtcOffset (d.Add (new TimeSpan(0,1,0, 1))));
19001901
}
19011902

mcs/class/corlib/Test/System/TimeZoneTest.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,12 @@ public void GetUTCNowAtDSTBoundaries ()
299299
Assert.IsTrue (!tzInfo.IsAmbiguousTime(st));
300300
Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 2));
301301
st = new DateTime(2016, 10, 30, 2, 0, 0, DateTimeKind.Local);
302-
Assert.IsTrue (tzInfo.IsDaylightSavingTime(st));
303-
Assert.IsTrue (!tzInfo.IsAmbiguousTime(st));
304-
Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 1));
305-
st = new DateTime(2016, 10, 30, 3, 0, 0, DateTimeKind.Local);
306302
Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st));
307303
Assert.IsTrue (tzInfo.IsAmbiguousTime(st));
304+
Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 2));
305+
st = new DateTime(2016, 10, 30, 3, 0, 0, DateTimeKind.Local);
306+
Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st));
307+
Assert.IsTrue (!tzInfo.IsAmbiguousTime(st));
308308
Assert.IsTrue ((TimeZoneInfo.ConvertTimeToUtc(st).Hour == 3));
309309
st = new DateTime(2016, 10, 30, 4, 0, 0, DateTimeKind.Local);
310310
Assert.IsTrue (!tzInfo.IsDaylightSavingTime(st));
@@ -348,9 +348,29 @@ public void GetUtcOffsetAtDSTBoundary ()
348348
var dstOffset = tz.GetUtcOffset(daylightChanges.Start.AddMinutes(61));
349349

350350
// Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end));
351-
Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add (daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(1)))));
352-
Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ())));
351+
Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end.Add (daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(1)))));
352+
Assert.AreEqual(standardOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ())));
353353
Assert.AreEqual(dstOffset, tz.GetUtcOffset (dst_end.Add(daylightChanges.Delta.Negate ().Add (TimeSpan.FromSeconds(-1)))));
354+
355+
// This test assumes that the DST end is a "fall back" where we go to an earlier local time
356+
if (daylightChanges.Delta > TimeSpan.Zero)
357+
{
358+
// dst_end is the end time of the DST in DST time.
359+
// It is technically an ambiguous time because the same local time occurs twice,
360+
// once in DST and then again in standard time
361+
// The ToUniversalTime() will assume standard time for ambiguous times, so we subtract
362+
// the DST delta to the the UTC time corresponding to the end of DST. Then
363+
// the ToLocalTime() will encode some extra info letting the framework know that we
364+
// are dealing with the ambiguous local time that is in DST.
365+
var dst_ambiguous = tz.ToUniversalTime(dst_end.Add(daylightChanges.Delta.Negate())).ToUniversalTime()
366+
.Add(daylightChanges.Delta.Negate()).ToLocalTime();
367+
368+
Assert.AreEqual(dstOffset, tz.GetUtcOffset(dst_ambiguous));
369+
370+
// The IsAmbiguousDaylightSavingTime flag is not cleared by DateTime.Add
371+
Assert.AreEqual(standardOffset, tz.GetUtcOffset(dst_ambiguous.Add(daylightChanges.Delta)));
372+
Assert.AreEqual(dstOffset, tz.GetUtcOffset(dst_ambiguous.Add(daylightChanges.Delta).Subtract(daylightChanges.Delta)));
373+
}
354374
}
355375

356376

0 commit comments

Comments
 (0)