diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AdjustmentRule.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AdjustmentRule.cs
index caac7f0a4b3cdf..fb148e31235f12 100644
--- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AdjustmentRule.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.AdjustmentRule.cs
@@ -76,6 +76,24 @@ private AdjustmentRule(
_noDaylightTransitions = noDaylightTransitions;
}
+ internal static AdjustmentRule CreateAdjustmentRule(
+ DateTime dateStart,
+ DateTime dateEnd,
+ TimeSpan daylightDelta,
+ TransitionTime daylightTransitionStart,
+ TransitionTime daylightTransitionEnd,
+ TimeSpan baseUtcOffsetDelta)
+ {
+ return new AdjustmentRule(
+ dateStart,
+ dateEnd,
+ daylightDelta,
+ daylightTransitionStart,
+ daylightTransitionEnd,
+ baseUtcOffsetDelta,
+ noDaylightTransitions: false);
+ }
+
public static AdjustmentRule CreateAdjustmentRule(
DateTime dateStart,
DateTime dateEnd,
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 fd2d2556527c6d..6e595737317d18 100644
--- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs
@@ -114,6 +114,17 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled)
ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime);
}
+ // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true.
+ // However, there are some cases in the past where DST = true, and the daylight savings offset
+ // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset
+ // is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving.
+ // To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic
+ // in HasDaylightSaving return true.
+ private static readonly TransitionTime s_daylightRuleMarker = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1);
+
+ // Truncate the date and the time to Milliseconds precision
+ private static DateTime GetTimeOnlyInMillisecondsPrecision(DateTime input) => new DateTime((input.TimeOfDay.Ticks / TimeSpan.TicksPerMillisecond) * TimeSpan.TicksPerMillisecond);
+
///
/// Returns a cloned array of AdjustmentRule objects
///
@@ -128,11 +139,20 @@ public AdjustmentRule[] GetAdjustmentRules()
// as the rules now is public, we should fill it properly so the caller doesn't have to know how we use it internally
// and can use it as it is used in Windows
- AdjustmentRule[] rules = new AdjustmentRule[_adjustmentRules.Length];
+ List rulesList = new List(_adjustmentRules.Length);
for (int i = 0; i < _adjustmentRules.Length; i++)
{
- AdjustmentRule? rule = _adjustmentRules[i];
+ AdjustmentRule rule = _adjustmentRules[i];
+
+ if (rule.NoDaylightTransitions &&
+ rule.DaylightTransitionStart != s_daylightRuleMarker &&
+ rule.DaylightDelta == TimeSpan.Zero && rule.BaseUtcOffsetDelta == TimeSpan.Zero)
+ {
+ // This rule has no time transition, ignore it.
+ continue;
+ }
+
DateTime start = rule.DateStart.Kind == DateTimeKind.Utc ?
// At the daylight start we didn't start the daylight saving yet then we convert to Local time
// by adding the _baseUtcOffset to the UTC time
@@ -144,13 +164,51 @@ public AdjustmentRule[] GetAdjustmentRules()
new DateTime(rule.DateEnd.Ticks + _baseUtcOffset.Ticks + rule.DaylightDelta.Ticks, DateTimeKind.Unspecified) :
rule.DateEnd;
- TransitionTime startTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, start.Hour, start.Minute, start.Second), start.Month, start.Day);
- TransitionTime endTransition = TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, end.Hour, end.Minute, end.Second), end.Month, end.Day);
+ if (start.Year == end.Year || !rule.NoDaylightTransitions)
+ {
+ // If the rule is covering only one year then the start and end transitions would occur in that year, we don't need to split the rule.
+ // Also, rule.NoDaylightTransitions be false in case the rule was created from a POSIX time zone string and having a DST transition. We can represent this in one rule too
+ TransitionTime startTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day) : rule.DaylightTransitionStart;
+ TransitionTime endTransition = rule.NoDaylightTransitions ? TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day) : rule.DaylightTransitionEnd;
+ rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta));
+ }
+ else
+ {
+ // For rules spanning more than one year. The time transition inside this rule would apply for the whole time spanning these years
+ // and not for partial time of every year.
+ // AdjustmentRule cannot express such rule using the DaylightTransitionStart and DaylightTransitionEnd because
+ // the DaylightTransitionStart and DaylightTransitionEnd express the transition for every year.
+ // We split the rule into more rules. The first rule will start from the start year of the original rule and ends at the end of the same year.
+ // The second splitted rule would cover the middle range of the original rule and ranging from the year start+1 to
+ // year end-1. The transition time in this rule would start from Jan 1st to end of December.
+ // The last splitted rule would start from the Jan 1st of the end year of the original rule and ends at the end transition time of the original rule.
+
+ // Add the first rule.
+ DateTime endForFirstRule = new DateTime(start.Year + 1, 1, 1).AddMilliseconds(-1); // At the end of the first year
+ TransitionTime startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(start), start.Month, start.Day);
+ TransitionTime endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endForFirstRule), endForFirstRule.Month, endForFirstRule.Day);
+ rulesList.Add(AdjustmentRule.CreateAdjustmentRule(start.Date, endForFirstRule.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta));
+
+ // Check if there is range of years between the start and the end years
+ if (end.Year - start.Year > 1)
+ {
+ // Add the middle rule.
+ DateTime middleYearStart = new DateTime(start.Year + 1, 1, 1);
+ DateTime middleYearEnd = new DateTime(end.Year, 1, 1).AddMilliseconds(-1);
+ startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearStart), middleYearStart.Month, middleYearStart.Day);
+ endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(middleYearEnd), middleYearEnd.Month, middleYearEnd.Day);
+ rulesList.Add(AdjustmentRule.CreateAdjustmentRule(middleYearStart.Date, middleYearEnd.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta));
+ }
- rules[i] = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(start.Date, end.Date, rule.DaylightDelta, startTransition, endTransition);
+ // Add the end rule.
+ DateTime endYearStart = new DateTime(end.Year, 1, 1); // At the beginning of the last year
+ startTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(endYearStart), endYearStart.Month, endYearStart.Day);
+ endTransition = TransitionTime.CreateFixedDateRule(GetTimeOnlyInMillisecondsPrecision(end), end.Month, end.Day);
+ rulesList.Add(AdjustmentRule.CreateAdjustmentRule(endYearStart.Date, end.Date, rule.DaylightDelta, startTransition, endTransition, rule.BaseUtcOffsetDelta));
+ }
}
- return rules;
+ return rulesList.ToArray();
}
private static void PopulateAllSystemTimeZones(CachedData cachedData)
@@ -957,7 +1015,7 @@ private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZone
// is going to be TimeSpan.Zero. But we still need to return 'true' from AdjustmentRule.HasDaylightSaving.
// To ensure we always return true from HasDaylightSaving, make a "special" dstStart that will make the logic
// in HasDaylightSaving return true.
- dstStart = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1);
+ dstStart = s_daylightRuleMarker;
}
else
{
@@ -1068,7 +1126,7 @@ private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTyp
/// Creates an AdjustmentRule given the POSIX TZ environment variable string.
///
///
- /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSX string.
+ /// See http://man7.org/linux/man-pages/man3/tzset.3.html for the format and semantics of this POSIX string.
///
private static AdjustmentRule? TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset)
{
diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
index ab96edddb8bc31..35861477d23883 100644
--- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
+++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
@@ -1819,6 +1819,69 @@ public static void IsDaylightSavingTime_CasablancaMultiYearDaylightSavings(strin
Assert.Equal(offset, s_casablancaTz.GetUtcOffset(dt));
}
+ [Fact]
+ [PlatformSpecific(~TestPlatforms.Windows)]
+ public static void TestSplittingRulesWhenReported()
+ {
+ // This test confirm we are splitting the rules which span multiple years on Linux
+ // we use "America/Los_Angeles" which has the rule covering 2/9/1942 to 8/14/1945
+ // with daylight transition by 01:00:00. This rule should be split into 3 rules:
+ // - rule 1 from 2/9/1942 to 12/31/1942
+ // - rule 2 from 1/1/1943 to 12/31/1944
+ // - rule 3 from 1/1/1945 to 8/14/1945
+ TimeZoneInfo.AdjustmentRule[] rules = TimeZoneInfo.FindSystemTimeZoneById(s_strPacific).GetAdjustmentRules();
+
+ bool ruleEncountered = false;
+ for (int i = 0; i < rules.Length; i++)
+ {
+ if (rules[i].DateStart == new DateTime(1942, 2, 9))
+ {
+ Assert.True(i + 2 <= rules.Length - 1);
+ TimeSpan daylightDelta = TimeSpan.FromHours(1);
+
+ // DateStart : 2/9/1942 12:00:00 AM (Unspecified)
+ // DateEnd : 12/31/1942 12:00:00 AM (Unspecified)
+ // DaylightDelta : 01:00:00
+ // DaylightTransitionStart : ToD:02:00:00 M:2, D:9, W:1, DoW:Sunday, FixedDate:True
+ // DaylightTransitionEnd : ToD:23:59:59.9990000 M:12, D:31, W:1, DoW:Sunday, FixedDate:True
+
+ Assert.Equal(new DateTime(1942, 12, 31), rules[i].DateEnd);
+ Assert.Equal(daylightDelta, rules[i].DaylightDelta);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 2, 0, 0), 2, 9), rules[i].DaylightTransitionStart);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 23, 59, 59, 999), 12, 31), rules[i].DaylightTransitionEnd);
+
+ // DateStart : 1/1/1943 12:00:00 AM (Unspecified)
+ // DateEnd : 12/31/1944 12:00:00 AM (Unspecified)
+ // DaylightDelta : 01:00:00
+ // DaylightTransitionStart : ToD:00:00:00 M:1, D:1, W:1, DoW:Sunday, FixedDate:True
+ // DaylightTransitionEnd : ToD:23:59:59.9990000 M:12, D:31, W:1, DoW:Sunday, FixedDate:True
+
+ Assert.Equal(new DateTime(1943, 1, 1), rules[i + 1].DateStart);
+ Assert.Equal(new DateTime(1944, 12, 31), rules[i + 1].DateEnd);
+ Assert.Equal(daylightDelta, rules[i + 1].DaylightDelta);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1), rules[i + 1].DaylightTransitionStart);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 23, 59, 59, 999), 12, 31), rules[i + 1].DaylightTransitionEnd);
+
+ // DateStart : 1/1/1945 12:00:00 AM (Unspecified)
+ // DateEnd : 8/14/1945 12:00:00 AM (Unspecified)
+ // DaylightDelta : 01:00:00
+ // DaylightTransitionStart : ToD:00:00:00 M:1, D:1, W:1, DoW:Sunday, FixedDate:True
+ // DaylightTransitionEnd : ToD:15:59:59.9990000 M:8, D:14, W:1, DoW:Sunday, FixedDate:True
+
+ Assert.Equal(new DateTime(1945, 1, 1), rules[i + 2].DateStart);
+ Assert.Equal(new DateTime(1945, 8, 14), rules[i + 2].DateEnd);
+ Assert.Equal(daylightDelta, rules[i + 2].DaylightDelta);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 0, 0, 0), 1, 1), rules[i + 2].DaylightTransitionStart);
+ Assert.Equal(TimeZoneInfo.TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1, 15, 59, 59, 999), 8, 14), rules[i + 2].DaylightTransitionEnd);
+
+ ruleEncountered = true;
+ break;
+ }
+ }
+
+ Assert.True(ruleEncountered, "The 1942 rule of America/Los_Angeles not found.");
+ }
+
[Theory]
[PlatformSpecific(TestPlatforms.AnyUnix)] // Linux will use local mean time for DateTimes before standard time came into effect.
// in 1996 Europe/Lisbon changed from standard time to DST without changing the UTC offset