Skip to content

Commit 512ad80

Browse files
Merge pull request #1362 from Unity-Technologies/fix-dst-transition-bug
Fix Incorrect UTC offset during DST transition (case 1288231)
2 parents ad6f94d + 26dced2 commit 512ad80

File tree

3 files changed

+468
-44
lines changed

3 files changed

+468
-44
lines changed

mcs/class/corlib/System/TimeZoneInfo.cs

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ public TimeSpan GetUtcOffset (DateTimeOffset dateTimeOffset)
805805
return GetUtcOffset (dateTimeOffset.UtcDateTime, out isDST);
806806
}
807807

808-
private TimeSpan GetUtcOffset (DateTime dateTime, out bool isDST)
808+
private TimeSpan GetUtcOffset (DateTime dateTime, out bool isDST, bool forOffset = false)
809809
{
810810
isDST = false;
811811

@@ -817,7 +817,7 @@ private TimeSpan GetUtcOffset (DateTime dateTime, out bool isDST)
817817
tz = TimeZoneInfo.Local;
818818

819819
bool isTzDst;
820-
var tzOffset = GetUtcOffsetHelper (dateTime, tz, out isTzDst);
820+
var tzOffset = GetUtcOffsetHelper (dateTime, tz, out isTzDst, forOffset);
821821

822822
if (tz == this) {
823823
isDST = isTzDst;
@@ -828,11 +828,11 @@ private TimeSpan GetUtcOffset (DateTime dateTime, out bool isDST)
828828
if (!TryAddTicks (dateTime, -tzOffset.Ticks, out utcDateTime, DateTimeKind.Utc))
829829
return BaseUtcOffset;
830830

831-
return GetUtcOffsetHelper (utcDateTime, this, out isDST);
831+
return GetUtcOffsetHelper (utcDateTime, this, out isDST, forOffset);
832832
}
833833

834834
// This is an helper method used by the method above, do not use this on its own.
835-
private static TimeSpan GetUtcOffsetHelper (DateTime dateTime, TimeZoneInfo tz, out bool isDST)
835+
private static TimeSpan GetUtcOffsetHelper (DateTime dateTime, TimeZoneInfo tz, out bool isDST, bool forOffset = false)
836836
{
837837
if (dateTime.Kind == DateTimeKind.Local && tz != TimeZoneInfo.Local)
838838
throw new Exception ();
@@ -843,7 +843,7 @@ private static TimeSpan GetUtcOffsetHelper (DateTime dateTime, TimeZoneInfo tz,
843843
return TimeSpan.Zero;
844844

845845
TimeSpan offset;
846-
if (tz.TryGetTransitionOffset(dateTime, out offset, out isDST))
846+
if (tz.TryGetTransitionOffset(dateTime, out offset, out isDST, forOffset))
847847
return offset;
848848

849849
if (dateTime.Kind == DateTimeKind.Utc) {
@@ -870,10 +870,12 @@ private static TimeSpan GetUtcOffsetHelper (DateTime dateTime, TimeZoneInfo tz,
870870

871871
if (tzRule != null && tz.IsInDST (tzRule, dateTime)) {
872872
// Replicate what .NET does when given a time which falls into the hour which is lost when
873-
// DST starts. isDST should always be true but the offset should be BaseUtcOffset without the
873+
// DST starts. isDST should be false and the offset should be BaseUtcOffset without the
874874
// DST delta while in that hour.
875-
isDST = true;
875+
if (forOffset)
876+
isDST = true;
876877
if (tz.IsInDST (tzRule, dstUtcDateTime)) {
878+
isDST = true;
877879
return tz.BaseUtcOffset + tzRule.DaylightDelta;
878880
} else {
879881
return tz.BaseUtcOffset;
@@ -925,7 +927,33 @@ public bool IsAmbiguousTime (DateTime dateTime)
925927
AdjustmentRule rule = GetApplicableRule (dateTime);
926928
if (rule != null) {
927929
DateTime tpoint = TransitionPoint (rule.DaylightTransitionEnd, dateTime.Year);
928-
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)
929957
return true;
930958
}
931959

@@ -944,7 +972,18 @@ private bool IsInDST (AdjustmentRule rule, DateTime dateTime)
944972
return true;
945973

946974
// We might be in the dateTime previous year's DST period
947-
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;
948987
}
949988

950989
bool IsInDSTForYear (AdjustmentRule rule, DateTime dateTime, int year)
@@ -953,8 +992,9 @@ bool IsInDSTForYear (AdjustmentRule rule, DateTime dateTime, int year)
953992
DateTime DST_end = TransitionPoint (rule.DaylightTransitionEnd, year + ((rule.DaylightTransitionStart.Month < rule.DaylightTransitionEnd.Month) ? 0 : 1));
954993
if (dateTime.Kind == DateTimeKind.Utc) {
955994
DST_start -= BaseUtcOffset;
956-
DST_end -= (BaseUtcOffset + rule.DaylightDelta);
995+
DST_end -= BaseUtcOffset;
957996
}
997+
DST_end -= rule.DaylightDelta;
958998
return (dateTime >= DST_start && dateTime < DST_end);
959999
}
9601000

@@ -982,7 +1022,21 @@ internal bool IsDaylightSavingTime (DateTime dateTime, TimeZoneInfoOptions flags
9821022

9831023
public bool IsDaylightSavingTime (DateTimeOffset dateTimeOffset)
9841024
{
985-
return IsDaylightSavingTime (dateTimeOffset.DateTime);
1025+
var dateTime = dateTimeOffset.DateTime;
1026+
1027+
if (dateTime.Kind == DateTimeKind.Local && IsInvalidTime (dateTime))
1028+
throw new ArgumentException ("dateTime is invalid and Kind is Local");
1029+
1030+
if (this == TimeZoneInfo.Utc)
1031+
return false;
1032+
1033+
if (!SupportsDaylightSavingTime)
1034+
return false;
1035+
1036+
bool isDst;
1037+
GetUtcOffset (dateTime, out isDst, true);
1038+
1039+
return isDst;
9861040
}
9871041

9881042
internal DaylightTime GetDaylightChanges (int year)
@@ -1219,7 +1273,7 @@ private AdjustmentRule GetApplicableRule (DateTime dateTime)
12191273
return null;
12201274
}
12211275

1222-
private bool TryGetTransitionOffset (DateTime dateTime, out TimeSpan offset,out bool isDst)
1276+
private bool TryGetTransitionOffset (DateTime dateTime, out TimeSpan offset, out bool isDst, bool forOffset = false)
12231277
{
12241278
offset = BaseUtcOffset;
12251279
isDst = false;
@@ -1235,18 +1289,45 @@ private bool TryGetTransitionOffset (DateTime dateTime, out TimeSpan offset,out
12351289
return false;
12361290
}
12371291

1292+
var isUtc = false;
12381293
if (dateTime.Kind != DateTimeKind.Utc) {
12391294
if (!TryAddTicks (date, -BaseUtcOffset.Ticks, out date, DateTimeKind.Utc))
12401295
return false;
1241-
}
1296+
} else
1297+
isUtc = true;
1298+
12421299

1243-
AdjustmentRule current = GetApplicableRule(date);
1300+
AdjustmentRule current = GetApplicableRule (date);
12441301
if (current != null) {
1245-
DateTime tStart = TransitionPoint(current.DaylightTransitionStart, date.Year);
1246-
DateTime tEnd = TransitionPoint(current.DaylightTransitionEnd, date.Year);
1302+
DateTime tStart = TransitionPoint (current.DaylightTransitionStart, date.Year);
1303+
DateTime tEnd = TransitionPoint (current.DaylightTransitionEnd, date.Year);
1304+
TryAddTicks (tStart, -BaseUtcOffset.Ticks, out tStart, DateTimeKind.Utc);
1305+
TryAddTicks (tEnd, -BaseUtcOffset.Ticks, out tEnd, DateTimeKind.Utc);
12471306
if ((date >= tStart) && (date <= tEnd)) {
1248-
offset = baseUtcOffset + current.DaylightDelta;
1249-
isDst = true;
1307+
if (forOffset)
1308+
isDst = true;
1309+
offset = baseUtcOffset;
1310+
if (isUtc || (date >= new DateTime (tStart.Ticks + current.DaylightDelta.Ticks, DateTimeKind.Utc)))
1311+
{
1312+
offset += current.DaylightDelta;
1313+
isDst = true;
1314+
}
1315+
1316+
if (date >= new DateTime (tEnd.Ticks - current.DaylightDelta.Ticks, DateTimeKind.Utc))
1317+
{
1318+
offset = baseUtcOffset;
1319+
isDst = false;
1320+
}
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+
}
1330+
12501331
return true;
12511332
}
12521333
}
@@ -1255,8 +1336,11 @@ private bool TryGetTransitionOffset (DateTime dateTime, out TimeSpan offset,out
12551336

12561337
private static DateTime TransitionPoint (TransitionTime transition, int year)
12571338
{
1258-
if (transition.IsFixedDateRule)
1259-
return new DateTime (year, transition.Month, transition.Day) + transition.TimeOfDay.TimeOfDay;
1339+
if (transition.IsFixedDateRule) {
1340+
var daysInMonth = DateTime.DaysInMonth (year, transition.Month);
1341+
var transitionDay = transition.Day <= daysInMonth ? transition.Day : daysInMonth;
1342+
return new DateTime (year, transition.Month, transitionDay) + transition.TimeOfDay.TimeOfDay;
1343+
}
12601344

12611345
DayOfWeek first = (new DateTime (year, transition.Month, 1)).DayOfWeek;
12621346
int day = 1 + (transition.Week - 1) * 7 + (transition.DayOfWeek - first + 7) % 7;
@@ -1540,7 +1624,7 @@ static internal TimeSpan GetUtcOffsetFromUtc (DateTime time, TimeZoneInfo zone,
15401624
isAmbiguousLocalDst = false;
15411625
TimeSpan baseOffset = zone.BaseUtcOffset;
15421626

1543-
if (zone.IsAmbiguousTime (time)) {
1627+
if (zone.IsAmbiguousLocalDstFromUtc (time)) {
15441628
isAmbiguousLocalDst = true;
15451629
// return baseOffset;
15461630
}
@@ -1570,4 +1654,4 @@ public override string ToString ()
15701654
}
15711655
#endif
15721656
}
1573-
}
1657+
}

0 commit comments

Comments
 (0)