diff options
author | Eric Erhardt <eric.erhardt@microsoft.com> | 2015-08-10 16:55:26 -0500 |
---|---|---|
committer | Eric Erhardt <eric.erhardt@microsoft.com> | 2015-08-10 16:55:26 -0500 |
commit | 0930fefcb2adfa5745964f30237494ea32a87c69 (patch) | |
tree | ce72ea8a989dc08956788e0497e3b3d71ad6e437 /src | |
parent | c8265f00f53adef379531f6fc276a2038ff0cce1 (diff) | |
parent | 776381d2d7d7760b8f4d78174d7b68914824ca88 (diff) | |
download | coreclr-0930fefcb2adfa5745964f30237494ea32a87c69.tar.gz coreclr-0930fefcb2adfa5745964f30237494ea32a87c69.tar.bz2 coreclr-0930fefcb2adfa5745964f30237494ea32a87c69.zip |
Merge pull request #1352 from eerhardt/master
TimeZoneInfo parse v2 tzfile and create AdjustmentRules
Diffstat (limited to 'src')
-rw-r--r-- | src/mscorlib/src/System/TimeZoneInfo.cs | 1191 | ||||
-rw-r--r-- | src/mscorlib/src/mscorlib.txt | 5 |
2 files changed, 798 insertions, 398 deletions
diff --git a/src/mscorlib/src/System/TimeZoneInfo.cs b/src/mscorlib/src/System/TimeZoneInfo.cs index ef43bb602e..4694ce0072 100644 --- a/src/mscorlib/src/System/TimeZoneInfo.cs +++ b/src/mscorlib/src/System/TimeZoneInfo.cs @@ -90,12 +90,6 @@ namespace System { #endif // FEATURE_WIN32_REGISTRY #if PLATFORM_UNIX - // use for generating multi-year DST periods - private static readonly TransitionTime c_transition5_15 = TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1), 05, 15); - private static readonly TransitionTime c_transition7_15 = TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1), 07, 15); - private static readonly TransitionTime c_transition10_15 = TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1), 10, 15); - private static readonly TransitionTime c_transition12_15 = TransitionTime.CreateFixedDateRule(new DateTime(1, 1, 1), 12, 15); - private const string c_defaultTimeZoneDirectory = "/usr/share/zoneinfo/"; private const string c_timeZoneEnvironmentVariable = "TZ"; private const string c_timeZoneDirectoryEnvironmentVariable = "TZDIR"; @@ -354,7 +348,7 @@ namespace System { DateTime adjustedTime = (TimeZoneInfo.ConvertTime(dateTimeOffset, this)).DateTime; Boolean isAmbiguous = false; - AdjustmentRule rule = GetAdjustmentRuleForTime(adjustedTime); + AdjustmentRule rule = GetAdjustmentRuleForAmbiguousOffsets(adjustedTime); if (rule != null && rule.HasDaylightSaving) { DaylightTime daylightTime = GetDaylightTime(adjustedTime.Year, rule); isAmbiguous = GetIsAmbiguousTime(adjustedTime, rule, daylightTime); @@ -402,7 +396,7 @@ namespace System { } Boolean isAmbiguous = false; - AdjustmentRule rule = GetAdjustmentRuleForTime(adjustedTime); + AdjustmentRule rule = GetAdjustmentRuleForAmbiguousOffsets(adjustedTime); if (rule != null && rule.HasDaylightSaving) { DaylightTime daylightTime = GetDaylightTime(adjustedTime.Year, rule); isAmbiguous = GetIsAmbiguousTime(adjustedTime, rule, daylightTime); @@ -428,6 +422,40 @@ namespace System { return timeSpans; } + // note the time is already adjusted + private AdjustmentRule GetAdjustmentRuleForAmbiguousOffsets(DateTime adjustedTime) + { + AdjustmentRule rule = GetAdjustmentRuleForTime(adjustedTime); + if (rule != null && rule.NoDaylightTransitions && !rule.HasDaylightSaving) + { + // When using NoDaylightTransitions rules, each rule is only for one offset. + // When looking for the Daylight savings rules, and we found the non-DST rule, + // then we get the rule right before this rule. + return GetPreviousAdjustmentRule(rule); + } + + return rule; + } + + /// <summary> + /// Gets the AdjustmentRule that is immediately preceeding the specified rule. + /// If the specified rule is the first AdjustmentRule, or it isn't in m_adjustmentRules, + /// then the specified rule is returned. + /// </summary> + private AdjustmentRule GetPreviousAdjustmentRule(AdjustmentRule rule) + { + AdjustmentRule result = rule; + for (int i = 1; i < m_adjustmentRules.Length; i++) + { + if (rule.Equals(m_adjustmentRules[i])) + { + result = m_adjustmentRules[i - 1]; + break; + } + } + return result; + } + // // GetUtcOffset - // @@ -791,7 +819,7 @@ namespace System { sourceOffset = sourceOffset + sourceRule.BaseUtcOffsetDelta; if (sourceRule.HasDaylightSaving) { Boolean sourceIsDaylightSavings = false; - DaylightTime sourceDaylightTime = GetDaylightTime(dateTime.Year, sourceRule); + DaylightTime sourceDaylightTime = sourceTimeZone.GetDaylightTime(dateTime.Year, sourceRule); // 'dateTime' might be in an invalid time range since it is in an AdjustmentRule // period that supports DST @@ -1129,9 +1157,10 @@ namespace System { String zoneAbbreviations; Boolean[] StandardTime; Boolean[] GmtTime; + string futureTransitionsPosixFormat; // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. - TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime); + TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); m_id = id; m_displayName = c_localId; @@ -1173,7 +1202,7 @@ namespace System { if (!dstDisabled) { // only create the adjustment rule if DST is enabled - TZif_GenerateAdjustmentRules(out m_adjustmentRules, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime); + TZif_GenerateAdjustmentRules(out m_adjustmentRules, m_baseUtcOffset, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); } ValidateTimeZoneInfo(m_id, m_baseUtcOffset, m_adjustmentRules, out m_supportsDaylightSavingTime); @@ -1341,28 +1370,140 @@ namespace System { // ----- SECTION: internal instance utility methods ----------------* - // assumes dateTime is in the current time zone's time - private AdjustmentRule GetAdjustmentRuleForTime(DateTime dateTime) { + private AdjustmentRule GetAdjustmentRuleForTime(DateTime dateTime, bool dateTimeisUtc = false) { if (m_adjustmentRules == null || m_adjustmentRules.Length == 0) { return null; } - // Only check the whole-date portion of the dateTime - + // Only check the whole-date portion of the dateTime for DateTimeKind.Unspecified rules - // This is because the AdjustmentRule DateStart & DateEnd are stored as // Date-only values {4/2/2006 - 10/28/2006} but actually represent the // time span {4/2/2006@00:00:00.00000 - 10/28/2006@23:59:59.99999} - DateTime date = dateTime.Date; + DateTime date; + if (dateTimeisUtc) + { + date = (dateTime + BaseUtcOffset).Date; + } + else + { + date = dateTime.Date; + } for (int i = 0; i < m_adjustmentRules.Length; i++) { - if (m_adjustmentRules[i].DateStart <= date && m_adjustmentRules[i].DateEnd >= date) { - return m_adjustmentRules[i]; + AdjustmentRule rule = m_adjustmentRules[i]; + AdjustmentRule previousRule = i > 0 ? m_adjustmentRules[i - 1] : rule; + if (IsAdjustmentRuleValid(rule, previousRule, dateTime, date, dateTimeisUtc)) { + return rule; } } return null; } + /// <summary> + /// Determines if 'rule' is the correct AdjustmentRule for the given dateTime. + /// </summary> + private bool IsAdjustmentRuleValid(AdjustmentRule rule, AdjustmentRule previousRule, + DateTime dateTime, DateTime dateOnly, bool dateTimeisUtc) + { + bool isAfterStart; + if (rule.DateStart.Kind == DateTimeKind.Utc) + { + DateTime dateTimeToCompare; + if (dateTimeisUtc) + { + dateTimeToCompare = dateTime; + } + else + { + dateTimeToCompare = ConvertToUtc(dateTime, + // use the previous rule to compute the dateTimeToCompare, since the time daylight savings "switches" + // is based on the previous rule's offset + previousRule.DaylightDelta, previousRule.BaseUtcOffsetDelta); + } + + isAfterStart = dateTimeToCompare >= rule.DateStart; + } + else + { + // if the rule's DateStart is Unspecified, then use the whole-date portion + isAfterStart = dateTime >= rule.DateStart; + } + + if (!isAfterStart) + { + return false; + } + + bool isBeforeEnd; + if (rule.DateEnd.Kind == DateTimeKind.Utc) + { + DateTime dateTimeToCompare; + if (dateTimeisUtc) + { + dateTimeToCompare = dateTime; + } + else + { + dateTimeToCompare = ConvertToUtc(dateTime, rule.DaylightDelta, rule.BaseUtcOffsetDelta); + } + + isBeforeEnd = dateTimeToCompare <= rule.DateEnd; + } + else + { + // if the rule's DateEnd is Unspecified, then use the whole-date portion + isBeforeEnd = dateTime <= rule.DateEnd; + } + + return isBeforeEnd; + } + + /// <summary> + /// Converts the dateTime to UTC using the specified deltas. + /// </summary> + private DateTime ConvertToUtc(DateTime dateTime, TimeSpan daylightDelta, TimeSpan baseUtcOffsetDelta) + { + return ConvertToFromUtc(dateTime, daylightDelta, baseUtcOffsetDelta, convertToUtc: true); + } + + /// <summary> + /// Converts the dateTime from UTC using the specified deltas. + /// </summary> + private DateTime ConvertFromUtc(DateTime dateTime, TimeSpan daylightDelta, TimeSpan baseUtcOffsetDelta) + { + return ConvertToFromUtc(dateTime, daylightDelta, baseUtcOffsetDelta, convertToUtc: false); + } + + /// <summary> + /// Converts the dateTime to or from UTC using the specified deltas. + /// </summary> + private DateTime ConvertToFromUtc(DateTime dateTime, TimeSpan daylightDelta, TimeSpan baseUtcOffsetDelta, bool convertToUtc) + { + TimeSpan offset = BaseUtcOffset + daylightDelta + baseUtcOffsetDelta; + if (convertToUtc) + { + offset = offset.Negate(); + } + + long ticks = dateTime.Ticks + offset.Ticks; + + DateTime result; + if (ticks > DateTime.MaxValue.Ticks) + { + result = DateTime.MaxValue; + } + else if (ticks < DateTime.MinValue.Ticks) + { + result = DateTime.MinValue; + } + else + { + result = new DateTime(ticks); + } + return result; + } // ----- SECTION: internal static utility methods ----------------* @@ -1450,7 +1591,8 @@ namespace System { TimeSpan.Zero, // no daylight saving transition TransitionTime.CreateFixedDateRule(DateTime.MinValue, 1, 1), TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(1), 1, 1), - new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0)); // Bias delta is all what we need from this rule + new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0), // Bias delta is all what we need from this rule + noDaylightTransitions: false); } // @@ -1477,7 +1619,8 @@ namespace System { new TimeSpan(0, -timeZoneInformation.DaylightBias, 0), (TransitionTime)daylightTransitionStart, (TransitionTime)daylightTransitionEnd, - new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0)); + new TimeSpan(0, defaultBaseUtcOffset - timeZoneInformation.Bias, 0), + noDaylightTransitions: false); return rule; } @@ -1530,10 +1673,27 @@ namespace System { // // Helper function that returns a DaylightTime from a year and AdjustmentRule // - static private DaylightTime GetDaylightTime(Int32 year, AdjustmentRule rule) { + private DaylightTime GetDaylightTime(Int32 year, AdjustmentRule rule) { TimeSpan delta = rule.DaylightDelta; - DateTime startTime = TransitionTimeToDateTime(year, rule.DaylightTransitionStart); - DateTime endTime = TransitionTimeToDateTime(year, rule.DaylightTransitionEnd); + DateTime startTime; + DateTime endTime; + if (rule.NoDaylightTransitions) + { + // NoDaylightTransitions rules don't use DaylightTransition Start and End, instead + // the DateStart and DateEnd are UTC times that represent when daylight savings time changes. + // Convert the UTC times into adjusted time zone times. + + // use the previous rule to calculate the startTime, since the DST change happens w.r.t. the previous rule + AdjustmentRule previousRule = GetPreviousAdjustmentRule(rule); + startTime = ConvertFromUtc(rule.DateStart, previousRule.DaylightDelta, previousRule.BaseUtcOffsetDelta); + + endTime = ConvertFromUtc(rule.DateEnd, rule.DaylightDelta, rule.BaseUtcOffsetDelta); + } + else + { + startTime = TransitionTimeToDateTime(year, rule.DaylightTransitionStart); + endTime = TransitionTimeToDateTime(year, rule.DaylightTransitionEnd); + } return new DaylightTime(startTime, endTime, delta); } @@ -1596,6 +1756,32 @@ namespace System { return isDst; } + /// <summary> + /// Gets the offset that should be used to calculate DST start times from a UTC time. + /// </summary> + private TimeSpan GetDaylightSavingsStartOffsetFromUtc(TimeSpan baseUtcOffset, AdjustmentRule rule) + { + if (rule.NoDaylightTransitions) + { + // use the previous rule to calculate the startTime, since the DST change happens w.r.t. the previous rule + AdjustmentRule previousRule = GetPreviousAdjustmentRule(rule); + return baseUtcOffset + previousRule.BaseUtcOffsetDelta + previousRule.DaylightDelta; + } + else + { + return baseUtcOffset + rule.BaseUtcOffsetDelta; /* FUTURE: + rule.StandardDelta; */ + } + } + + /// <summary> + /// Gets the offset that should be used to calculate DST end times from a UTC time. + /// </summary> + private TimeSpan GetDaylightSavingsEndOffsetFromUtc(TimeSpan baseUtcOffset, AdjustmentRule rule) + { + // NOTE: even NoDaylightTransitions rules use this logic since DST ends w.r.t. the current rule + + return baseUtcOffset + rule.BaseUtcOffsetDelta + rule.DaylightDelta; /* FUTURE: + rule.StandardDelta; */ + } // // GetIsDaylightSavingsFromUtc - @@ -1610,9 +1796,9 @@ namespace System { return false; } + // Get the daylight changes for the year of the specified time. - TimeSpan offset = utc + rule.BaseUtcOffsetDelta; /* FUTURE: + rule.StandardDelta; */ - DaylightTime daylightTime = GetDaylightTime(Year, rule); + DaylightTime daylightTime = zone.GetDaylightTime(Year, rule); // The start and end times represent the range of universal times that are in DST for that year. // Within that there is an ambiguous hour, usually right at the end, but at the beginning in @@ -1626,20 +1812,22 @@ namespace System { // Note we handle the similar case when rule year start with daylight saving and previous year end with daylight saving. bool ignoreYearAdjustment = false; + TimeSpan dstStartOffset = zone.GetDaylightSavingsStartOffsetFromUtc(utc, rule); DateTime startTime; if (rule.IsStartDateMarkerForBeginningOfYear() && daylightTime.Start.Year > DateTime.MinValue.Year) { AdjustmentRule previousYearRule = zone.GetAdjustmentRuleForTime(new DateTime(daylightTime.Start.Year - 1, 12, 31)); if (previousYearRule != null && previousYearRule.IsEndDateMarkerForEndOfYear()) { - DaylightTime previousDaylightTime = GetDaylightTime(daylightTime.Start.Year - 1, previousYearRule); + DaylightTime previousDaylightTime = zone.GetDaylightTime(daylightTime.Start.Year - 1, previousYearRule); startTime = previousDaylightTime.Start - utc - previousYearRule.BaseUtcOffsetDelta; ignoreYearAdjustment = true; } else { - startTime = new DateTime(daylightTime.Start.Year, 1, 1, 0, 0, 0) - offset; + startTime = new DateTime(daylightTime.Start.Year, 1, 1, 0, 0, 0) - dstStartOffset; } } else { - startTime = daylightTime.Start - offset; + startTime = daylightTime.Start - dstStartOffset; } + TimeSpan dstEndOffset = zone.GetDaylightSavingsEndOffsetFromUtc(utc, rule); DateTime endTime; if (rule.IsEndDateMarkerForEndOfYear() && daylightTime.End.Year < DateTime.MaxValue.Year) { AdjustmentRule nextYearRule = zone.GetAdjustmentRuleForTime(new DateTime(daylightTime.End.Year + 1, 1, 1)); @@ -1647,15 +1835,15 @@ namespace System { if (nextYearRule.IsEndDateMarkerForEndOfYear()) {// next year end with daylight saving on too endTime = new DateTime(daylightTime.End.Year + 1, 12, 31) - utc - nextYearRule.BaseUtcOffsetDelta - nextYearRule.DaylightDelta; } else { - DaylightTime nextdaylightTime = GetDaylightTime(daylightTime.End.Year + 1, nextYearRule); + DaylightTime nextdaylightTime = zone.GetDaylightTime(daylightTime.End.Year + 1, nextYearRule); endTime = nextdaylightTime.End - utc - nextYearRule.BaseUtcOffsetDelta - nextYearRule.DaylightDelta; } ignoreYearAdjustment = true; } else { - endTime = new DateTime(daylightTime.End.Year + 1, 1, 1, 0, 0, 0).AddTicks(-1) - offset - rule.DaylightDelta; ; + endTime = new DateTime(daylightTime.End.Year + 1, 1, 1, 0, 0, 0).AddTicks(-1) - dstEndOffset; } } else { - endTime = daylightTime.End - offset - rule.DaylightDelta; + endTime = daylightTime.End - dstEndOffset; } DateTime ambiguousStart; @@ -2317,7 +2505,7 @@ namespace System { if (rule != null) { baseOffset = baseOffset + rule.BaseUtcOffsetDelta; if (rule.HasDaylightSaving) { - DaylightTime daylightTime = GetDaylightTime(time.Year, rule); + DaylightTime daylightTime = zone.GetDaylightTime(time.Year, rule); Boolean isDaylightSavings = GetIsDaylightSavings(time, rule, daylightTime, flags); baseOffset += (isDaylightSavings ? rule.DaylightDelta : TimeSpan.Zero /* FUTURE: rule.StandardDelta */); } @@ -2385,15 +2573,14 @@ namespace System { year = 1; } else { - DateTime targetTime = time + baseOffset; + rule = zone.GetAdjustmentRuleForTime(time, dateTimeisUtc: true); // As we get the associated rule using the adjusted targetTime, we should use the adjusted year (targetTime.Year) too as after adding the baseOffset, // sometimes the year value can change if the input datetime was very close to the beginning or the end of the year. Examples of such cases: // “Libya Standard Time” when used with the date 2011-12-31T23:59:59.9999999Z // "W. Australia Standard Time" used with date 2005-12-31T23:59:00.0000000Z + DateTime targetTime = time + baseOffset; year = targetTime.Year; - - rule = zone.GetAdjustmentRuleForTime(targetTime); } if (rule != null) { @@ -3353,371 +3540,493 @@ namespace System { // BSD July 18, 2003 BSD // // + static private void TZif_GenerateAdjustmentRules(out AdjustmentRule[] rules, TimeSpan baseUtcOffset, DateTime[] dts, Byte[] typeOfLocalTime, + TZifType[] transitionType, Boolean[] StandardTime, Boolean[] GmtTime, string futureTransitionsPosixFormat) + { + rules = null; - // - // TZif_CalculateTransitionTime - - // - // Example inputs: - // ----------------- - // utc = 1918-03-31T10:00:00.0000000Z - // transitionType = {-08:00:00 DST=False, Index 4} - // standardTime = False - // gmtTime = False - // - static private TransitionTime TZif_CalculateTransitionTime(DateTime utc, TimeSpan offset, - TZifType transitionType, Boolean standardTime, - Boolean gmtTime, out DateTime ruleDate) { + if (dts.Length > 0) + { + int index = 0; + List<AdjustmentRule> rulesList = new List<AdjustmentRule>(); - // convert from UTC to local clock time - Int64 ticks = utc.Ticks + offset.Ticks; - if (ticks > DateTime.MaxValue.Ticks) { - utc = DateTime.MaxValue; - } - else if (ticks < DateTime.MinValue.Ticks) { - utc = DateTime.MinValue; + while (index <= dts.Length) + { + TZif_GenerateAdjustmentRule(ref index, baseUtcOffset, rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime, futureTransitionsPosixFormat); + } + + rules = rulesList.ToArray(); + if (rules != null && rules.Length == 0) + { + rules = null; + } } - else { - utc = new DateTime(ticks); + } + + static private void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZoneBaseUtcOffset, List<AdjustmentRule> rulesList, DateTime[] dts, + Byte[] typeOfLocalTime, TZifType[] transitionTypes, Boolean[] StandardTime, Boolean[] GmtTime, string futureTransitionsPosixFormat) + { + // To generate AdjustmentRules, use the following approach: + // 1. The first AdjustmentRule goes from DateTime.Min to the first transition and uses the first standard transitionType + // (or the first transitionType if none of them are standard) + // 2. Create an AdjustmentRule for each transition, i.e. from dts[index - 1] to dts[index]. + // This rule uses the transitionType[index - 1] and the whole AdjustmentRule only describes a single offset - either + // all daylight savings, or all stanard time. + // 3. After all the transitions are filled out, the last AdjustmentRule is created from either: + // a. a POSIX-style timezone description ("futureTransitionsPosixFormat"), if there is one or + // b. continue the last transition offset until DateTime.Max + + if (index == 0) + { + TZifType transitionType = TZif_GetEarlyDateTransitionType(transitionTypes); + DateTime endTransitionDate = dts[index]; + + TimeSpan transitionOffset = TZif_CalculateTranistionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + DateTime.MinValue, + endTransitionDate.AddTicks(-1), + daylightDelta, + default(TransitionTime), + default(TransitionTime), + baseUtcDelta, + noDaylightTransitions: true); + rulesList.Add(r); } + else if (index < dts.Length) + { + DateTime startTransitionDate = dts[index - 1]; + TZifType startTransitionType = transitionTypes[typeOfLocalTime[index - 1]]; - DateTime timeOfDay = new DateTime(1, 1, 1, utc.Hour, utc.Minute, utc.Second, utc.Millisecond); - int month = utc.Month; - int day = utc.Day; + DateTime endTransitionDate = dts[index]; - ruleDate = new DateTime(utc.Year, month, day); - // FUTURE: take standardTime/gmtTime into account - return TransitionTime.CreateFixedDateRule(timeOfDay, month, day); - } + TimeSpan transitionOffset = TZif_CalculateTranistionOffsetFromBase(startTransitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = startTransitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = startTransitionType.IsDst ? TimeSpan.Zero : transitionOffset; - static private void TZif_GenerateAdjustmentRules(out AdjustmentRule[] rules, DateTime[] dts, Byte[] typeOfLocalTime, - TZifType[] transitionType, Boolean[] StandardTime, Boolean[] GmtTime) { - rules = null; + TransitionTime dstStart; + if (startTransitionType.IsDst) + { + // 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. + dstStart = TransitionTime.CreateFixedDateRule(DateTime.MinValue.AddMilliseconds(2), 1, 1); + } + else + { + dstStart = default(TransitionTime); + } - int index = 0; - List<AdjustmentRule> rulesList = new List<AdjustmentRule>(1); - bool succeeded = true; - - while (succeeded && index < dts.Length) { - succeeded = TZif_GenerateAdjustmentRule(ref index, ref rulesList, dts, typeOfLocalTime, transitionType, StandardTime, GmtTime); + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + endTransitionDate.AddTicks(-1), + daylightDelta, + dstStart, + default(TransitionTime), + baseUtcDelta, + noDaylightTransitions: true); + rulesList.Add(r); } + else + { + // create the AdjustmentRule that will be used for all DateTimes after the last transition - rules = rulesList.ToArray(); - if (rules != null && rules.Length == 0) { - rules = null; + // NOTE: index == dts.Length + DateTime startTransitionDate = dts[index - 1]; + + if (!string.IsNullOrEmpty(futureTransitionsPosixFormat)) + { + AdjustmentRule r = TZif_CreateAdjustmentRuleForPosixFormat(futureTransitionsPosixFormat, startTransitionDate, timeZoneBaseUtcOffset); + if (r != null) + { + rulesList.Add(r); + } + } + else + { + // just use the last transition as the rule which will be used until the end of time + + TZifType transitionType = transitionTypes[typeOfLocalTime[index - 1]]; + TimeSpan transitionOffset = TZif_CalculateTranistionOffsetFromBase(transitionType.UtcOffset, timeZoneBaseUtcOffset); + TimeSpan daylightDelta = transitionType.IsDst ? transitionOffset : TimeSpan.Zero; + TimeSpan baseUtcDelta = transitionType.IsDst ? TimeSpan.Zero : transitionOffset; + + AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightDelta, + default(TransitionTime), + default(TransitionTime), + baseUtcDelta, + noDaylightTransitions: true); + rulesList.Add(r); + } } - } + index++; + } - static private bool TZif_GenerateAdjustmentRule(ref int startIndex, ref List<AdjustmentRule> rulesList, DateTime[] dts, Byte[] typeOfLocalTime, - TZifType[] transitionType, Boolean[] StandardTime, Boolean[] GmtTime) { + private static TimeSpan TZif_CalculateTranistionOffsetFromBase(TimeSpan transitionOffset, TimeSpan timeZoneBaseUtcOffset) + { + TimeSpan result = transitionOffset - timeZoneBaseUtcOffset; - int index = startIndex; - bool Dst = false; - int DstStartIndex = -1; - int DstEndIndex = -1; - DateTime startDate = DateTime.MinValue.Date; - DateTime endDate = DateTime.MaxValue.Date; + // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns + // with DateTimeOffset, SQL Server, and the W3C XML Specification + if (result.Ticks % TimeSpan.TicksPerMinute != 0) + { + result = new TimeSpan(result.Hours, result.Minutes, 0); + } + return result; + } - // find the next DST transition start time index - while (!Dst && index < typeOfLocalTime.Length) { - int typeIndex = typeOfLocalTime[index]; - if (typeIndex < transitionType.Length && transitionType[typeIndex].IsDst) { - // found the next DST transition start time - Dst = true; - DstStartIndex = index; - } - else { - index++; + /// <summary> + /// Gets the first standard-time transition type, or simply the first transition type + /// if there are no standard transition types. + /// </summary>> + /// <remarks> + /// from 'man tzfile': + /// localtime(3) uses the first standard-time ttinfo structure in the file + /// (or simply the first ttinfo structure in the absence of a standard-time + /// structure) if either tzh_timecnt is zero or the time argument is less + /// than the first transition time recorded in the file. + /// </remarks> + private static TZifType TZif_GetEarlyDateTransitionType(TZifType[] transitionTypes) + { + for (int i = 0; i < transitionTypes.Length; i++) + { + TZifType transitionType = transitionTypes[i]; + if (!transitionType.IsDst) + { + return transitionType; } } - // find the next DST transition end time index - while (Dst && index < typeOfLocalTime.Length) { - int typeIndex = typeOfLocalTime[index]; - if (typeIndex < transitionType.Length && !transitionType[typeIndex].IsDst) { - // found the next DST transition end time - Dst = false; - DstEndIndex = index; - } - else { - index++; - } + if (transitionTypes.Length > 0) + { + return transitionTypes[0]; } + throw new InvalidTimeZoneException(Environment.GetResourceString("InvalidTimeZone_NoTTInfoStructures")); + } - // - // construct the adjustment rule from the two indices - // - if (DstStartIndex >= 0) { - DateTime startTransitionDate = dts[DstStartIndex]; - DateTime endTransitionDate; + /// <summary> + /// Creates an AdjustmentRule given the POSIX TZ environment variable string. + /// </summary> + /// <remarks> + /// See http://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html for the format and semantics of this POSX string. + /// </remarks> + private static AdjustmentRule TZif_CreateAdjustmentRuleForPosixFormat(string posixFormat, DateTime startTransitionDate, TimeSpan timeZoneBaseUtcOffset) + { + string standardName; + string standardOffset; + string daylightSavingsName; + string daylightSavingsOffset; + string start; + string startTime; + string end; + string endTime; + + if (TZif_ParsePosixFormat(posixFormat, out standardName, out standardOffset, out daylightSavingsName, + out daylightSavingsOffset, out start, out startTime, out end, out endTime)) + { + // a valid posixFormat has at least standardName and standardOffset + TimeSpan? parsedBaseOffset = TZif_ParseOffsetString(standardOffset); + if (parsedBaseOffset.HasValue) + { + TimeSpan baseOffset = parsedBaseOffset.Value.Negate(); // offsets are backwards in POSIX notation + baseOffset = TZif_CalculateTranistionOffsetFromBase(baseOffset, timeZoneBaseUtcOffset); - if (DstEndIndex == -1) { - // we found a DST start but no DST end; in this case use the - // prior non-DST entry if it exists, else use the current entry for both start and end (e.g., zero daylightDelta) - if (DstStartIndex > 0) { - DstEndIndex = DstStartIndex - 1; + // having a daylightSavingsName means there is a DST rule + if (!string.IsNullOrEmpty(daylightSavingsName)) + { + TimeSpan? parsedDaylightSavings = TZif_ParseOffsetString(daylightSavingsOffset); + TimeSpan daylightSavingsTimeSpan; + if (!parsedDaylightSavings.HasValue) + { + // default DST to 1 hour if it isn't specified + daylightSavingsTimeSpan = new TimeSpan(1, 0, 0); + } + else + { + daylightSavingsTimeSpan = parsedDaylightSavings.Value.Negate(); // offsets are backwards in POSIX notation + daylightSavingsTimeSpan = TZif_CalculateTranistionOffsetFromBase(daylightSavingsTimeSpan, timeZoneBaseUtcOffset); + daylightSavingsTimeSpan = TZif_CalculateTranistionOffsetFromBase(daylightSavingsTimeSpan, baseOffset); + } + + TransitionTime dstStart = TZif_CreateTransitionTimeFromPosixRule(start, startTime); + TransitionTime dstEnd = TZif_CreateTransitionTimeFromPosixRule(end, endTime); + + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + daylightSavingsTimeSpan, + dstStart, + dstEnd, + baseOffset, + noDaylightTransitions: false); } - else { - DstEndIndex = DstStartIndex; + else + { + // if there is no daylightSavingsName, the whole AdjustmentRule should be with no transitions - just the baseOffset + return AdjustmentRule.CreateAdjustmentRule( + startTransitionDate, + DateTime.MaxValue, + TimeSpan.Zero, + default(TransitionTime), + default(TransitionTime), + baseOffset, + noDaylightTransitions: true); } - endTransitionDate = DateTime.MaxValue; - } - else { - endTransitionDate = dts[DstEndIndex]; } + } + + return null; + } - int dstStartTypeIndex = typeOfLocalTime[DstStartIndex]; - int dstEndTypeIndex = typeOfLocalTime[DstEndIndex]; + private static TimeSpan? TZif_ParseOffsetString(string offset) + { + TimeSpan? result = null; - TimeSpan daylightBias = transitionType[dstStartTypeIndex].UtcOffset - transitionType[dstEndTypeIndex].UtcOffset; - // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns - // with DateTimeOffset, SQL Server, and the W3C XML Specification - if (daylightBias.Ticks % TimeSpan.TicksPerMinute != 0) { - daylightBias = new TimeSpan(daylightBias.Hours, daylightBias.Minutes, 0); + if (!string.IsNullOrEmpty(offset)) + { + bool negative = offset[0] == '-'; + if (negative || offset[0] == '+') + { + offset = offset.Substring(1); } - // - // the normal case is less than 12 months between transition times. However places like America/Catamarca - // have DST from 1946-1963 straight without a gap. In that case we need to create a series of Adjustment - // Rules to fudge the multi-year DST period - // - if ((endTransitionDate - startTransitionDate).Ticks <= TimeSpan.TicksPerDay * 364) { - TransitionTime dstStart; - TransitionTime dstEnd; - TimeSpan startTransitionOffset = (DstStartIndex > 0 ? transitionType[typeOfLocalTime[DstStartIndex - 1]].UtcOffset : transitionType[dstEndTypeIndex].UtcOffset); - TimeSpan endTransitionOffset = (DstEndIndex > 0 ? transitionType[typeOfLocalTime[DstEndIndex - 1]].UtcOffset : transitionType[dstStartTypeIndex].UtcOffset); - + // Try parsing just hours first. + // Note, TimeSpan.TryParseExact "%h" can't be used here because some time zones using values + // like "26" or "144" and TimeSpan parsing would turn that into 26 or 144 *days* instead of hours. + int hours; + if (int.TryParse(offset, out hours)) + { + result = new TimeSpan(hours, 0, 0); + } + else + { + TimeSpan parsedTimeSpan; + if (TimeSpan.TryParseExact(offset, "g", CultureInfo.InvariantCulture, out parsedTimeSpan)) + { + result = parsedTimeSpan; + } + } - dstStart = TZif_CalculateTransitionTime(startTransitionDate, - startTransitionOffset, - transitionType[dstStartTypeIndex], - StandardTime[dstStartTypeIndex], - GmtTime[dstStartTypeIndex], - out startDate); + if (result.HasValue && negative) + { + result = result.Value.Negate(); + } + } - dstEnd = TZif_CalculateTransitionTime(endTransitionDate, - endTransitionOffset, - transitionType[dstEndTypeIndex], - StandardTime[dstEndTypeIndex], - GmtTime[dstEndTypeIndex], - out endDate); + return result; + } + private static TransitionTime TZif_CreateTransitionTimeFromPosixRule(string date, string time) + { + if (string.IsNullOrEmpty(date)) + { + return default(TransitionTime); + } + if (date[0] == 'M') + { + // Mm.w.d + // This specifies day d of week w of month m. The day d must be between 0(Sunday) and 6.The week w must be between 1 and 5; + // week 1 is the first week in which day d occurs, and week 5 specifies the last d day in the month. The month m should be between 1 and 12. + + int month; + int week; + DayOfWeek day; + if (!TZif_ParseMDateRule(date, out month, out week, out day)) + { + throw new InvalidTimeZoneException(Environment.GetResourceString("InvalidTimeZone_UnparseablePosixMDateString", date)); + } - // calculate the AdjustmentRule end date - if (DstStartIndex >= DstEndIndex) { - // we found a DST start but no DST end - endDate = DateTime.MaxValue.Date; + DateTime timeOfDay; + TimeSpan? timeOffset = TZif_ParseOffsetString(time); + if (timeOffset.HasValue) + { + // This logic isn't correct and can't be corrected until https://github.com/dotnet/corefx/issues/2618 is fixed. + // Some time zones use time values like, "26", "144", or "-2". + // This allows the week to sometimes be week 4 and sometimes week 5 in the month. + // For now, strip off any 'days' in the offset, and just get the time of day correct + timeOffset = new TimeSpan(timeOffset.Value.Hours, timeOffset.Value.Minutes, timeOffset.Value.Seconds); + if (timeOffset.Value < TimeSpan.Zero) + { + timeOfDay = new DateTime(1, 1, 2, 0, 0, 0); + } + else + { + timeOfDay = new DateTime(1, 1, 1, 0, 0, 0); } - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(startDate, endDate, daylightBias, dstStart, dstEnd); - rulesList.Add(r); + timeOfDay += timeOffset.Value; } - else { - // create the multi-year DST rule series: - // - // For example America/Catamarca: - // 1946-10-01T04:00:00.0000000Z {-03:00:00 DST=True} - // 1963-10-01T03:00:00.0000000Z {-04:00:00 DST=False} - // - // gets converted into a series of overlapping 5/7month adjustment rules: - // - // [AdjustmentRule #0] // start rule - // [1946/09/31 - 1947/06/15] // * starts 1 day prior to startTransitionDate - // [start: 10/01 @4:00 ] // * N months long, stopping at month 6 or 11 - // [end : 07/15 ] // notice how the _end_ is outside the range - // - // [AdjustmentRule #1] // middle-year all-DST rule - // [1947/06/16 - 1947/11/15] // * starts 1 day after last day in previous rule - // [start: 05/15 ] // * 5 months long, stopping at month 6 or 11 - // [end : 12/15 ] // notice how the _start and end_ are outside the range - // - // [AdjustmentRule #2] // middle-year all-DST rule - // [1947/11/16 - 1947/06/15] // * starts 1 day after last day in previous rule - // [start: 10/01 ] // * 7 months long, stopping at month 6 or 11 - // [end : 07/15 ] // notice how the _start and end_ are outside the range - // - // ......................... - // - // [AdjustmentRule #N] // end rule - // [1963/06/16 - 1946/10/02] // * starts 1 day after last day in previous rule - // [start: 05/15 ] // * N months long, stopping 1 day after endTransitionDate - // [end : 10/01 ] // notice how the _start_ is outside the range - // - - // create the first rule from N to either 06/15 or 11/15 - TZif_CreateFirstMultiYearRule(ref rulesList, daylightBias, startTransitionDate, DstStartIndex, dstStartTypeIndex, dstEndTypeIndex, - dts, transitionType, typeOfLocalTime, StandardTime, GmtTime); - - // create the filler rules - TZif_CreateMiddleMultiYearRules(ref rulesList, daylightBias, endTransitionDate); - - // create the last rule - TZif_CreateLastMultiYearRule(ref rulesList, daylightBias, endTransitionDate, DstStartIndex, dstStartTypeIndex, DstEndIndex, dstEndTypeIndex, - dts, transitionType, typeOfLocalTime, StandardTime, GmtTime); + else + { + // default to 2AM. + timeOfDay = new DateTime(1, 1, 1, 2, 0, 0); } - startIndex = index + 1; - return true; + return TransitionTime.CreateFloatingDateRule(timeOfDay, month, week, day); } + else + { + // Jn + // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years. - // setup the start values for the next call to TZif_GenerateAdjustmentRule(...) - startIndex = index + 1; - return false; // did not create a new AdjustmentRule - } - - static private void TZif_CreateFirstMultiYearRule(ref List<AdjustmentRule> rulesList, TimeSpan daylightBias, DateTime startTransitionDate, - int DstStartIndex, int dstStartTypeIndex, int dstEndTypeIndex, DateTime[] dts, TZifType[] transitionType, - Byte[] typeOfLocalTime, bool[] StandardTime, bool[] GmtTime) { - - // [AdjustmentRule #0] // start rule - // [1946/09/31 - 1947/06/15] // * starts 1 day prior to startTransitionDate - // [start: 10/01 @4:00 ] // * N months long, stopping at month 6 or 11 - // [end : 07/15 ] // notice how the _end_ is outside the range + // n + // This specifies the Julian day, with n between 0 and 365.February 29 is counted in leap years. - DateTime startDate; - DateTime endDate; - TransitionTime dstStart; - TransitionTime dstEnd; + // These two rules cannot be expressed with the current AdjustmentRules + // One of them *could* be supported if we relaxed the TransitionTime validation rules, and allowed + // "IsFixedDateRule = true, Month = 0, Day = n" to mean the nth day of the year, picking one of the rules above - TimeSpan startTransitionOffset = (DstStartIndex > 0 ? transitionType[typeOfLocalTime[DstStartIndex - 1]].UtcOffset : transitionType[dstEndTypeIndex].UtcOffset); + throw new InvalidTimeZoneException(Environment.GetResourceString("InvalidTimeZone_JulianDayNotSupported")); + } + } - dstStart = TZif_CalculateTransitionTime(startTransitionDate, - startTransitionOffset, - transitionType[dstStartTypeIndex], - StandardTime[dstStartTypeIndex], - GmtTime[dstStartTypeIndex], - out startDate); + /// <summary> + /// Parses a string like Mm.w.d into month, week and DayOfWeek values. + /// </summary> + /// <returns> + /// true if the parsing succeeded; otherwise, false. + /// </returns> + private static bool TZif_ParseMDateRule(string dateRule, out int month, out int week, out DayOfWeek dayOfWeek) + { + month = 0; + week = 0; + dayOfWeek = default(DayOfWeek); - // - // Choosing the endDate based on the startDate: - // - // startTransitionDate.Month -> end - // 1 4|5 8|9 12 - // [-> 06/15]|[-> 11/15]|[-> 06/15] - // - int startDateMonth = startDate.Month; - int startDateYear = startDate.Year; + if (dateRule[0] == 'M') + { + int firstDotIndex = dateRule.IndexOf('.'); + if (firstDotIndex > 0) + { + int secondDotIndex = dateRule.IndexOf('.', firstDotIndex + 1); + if (secondDotIndex > 0) + { + string monthString = dateRule.Substring(1, firstDotIndex - 1); + string weekString = dateRule.Substring(firstDotIndex + 1, secondDotIndex - firstDotIndex - 1); + string dayString = dateRule.Substring(secondDotIndex + 1); - if (startDateMonth <= 4) { - endDate = new DateTime(startDateYear, 06, 15); - dstEnd = c_transition7_15; - } else if (startDateMonth <= 8) { - endDate = new DateTime(startDateYear, 11, 15); - dstEnd = c_transition12_15; - } - else if (startDateYear < 9999) { - endDate = new DateTime(startDateYear+1, 06, 15); - dstEnd = c_transition7_15; - } - else { - endDate = DateTime.MaxValue; - dstEnd = c_transition7_15; + if (int.TryParse(monthString, out month)) + { + if (int.TryParse(weekString, out week)) + { + int day; + if (int.TryParse(dayString, out day)) + { + dayOfWeek = (DayOfWeek)day; + return true; + } + } + } + } + } } - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(startDate, endDate, daylightBias, dstStart, dstEnd); - rulesList.Add(r); - } + return false; + } + private static bool TZif_ParsePosixFormat( + string posixFormat, + out string standardName, + out string standardOffset, + out string daylightSavingsName, + out string daylightSavingsOffset, + out string start, + out string startTime, + out string end, + out string endTime) + { + standardName = null; + standardOffset = null; + daylightSavingsName = null; + daylightSavingsOffset = null; + start = null; + startTime = null; + end = null; + endTime = null; - static private void TZif_CreateLastMultiYearRule(ref List<AdjustmentRule> rulesList, TimeSpan daylightBias, DateTime endTransitionDate, - int DstStartIndex, int dstStartTypeIndex, int DstEndIndex, int dstEndTypeIndex, DateTime[] dts, TZifType[] transitionType, - Byte[] typeOfLocalTime, bool[] StandardTime, bool[] GmtTime) { + int index = 0; + standardName = TZif_ParsePosixName(posixFormat, ref index); + standardOffset = TZif_ParsePosixOffset(posixFormat, ref index); - // [AdjustmentRule #N] // end rule - // [1963/06/16 - 1946/10/02] // * starts 1 day after last day in previous rule - // [start: 05/15 ] // * N months long, stopping 1 day after endTransitionDate - // [end : 10/01 ] // notice how the _start_ is outside the range + daylightSavingsName = TZif_ParsePosixName(posixFormat, ref index); + if (!string.IsNullOrEmpty(daylightSavingsName)) + { + daylightSavingsOffset = TZif_ParsePosixOffset(posixFormat, ref index); - DateTime endDate; - TransitionTime dstEnd; + if (index < posixFormat.Length && posixFormat[index] == ',') + { + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out start, out startTime); - TimeSpan endTransitionOffset = (DstEndIndex > 0 ? transitionType[typeOfLocalTime[DstEndIndex - 1]].UtcOffset : transitionType[dstStartTypeIndex].UtcOffset); + if (index < posixFormat.Length && posixFormat[index] == ',') + { + index++; + TZif_ParsePosixDateTime(posixFormat, ref index, out end, out endTime); + } + } + } + return !string.IsNullOrEmpty(standardName) && !string.IsNullOrEmpty(standardOffset); + } - dstEnd = TZif_CalculateTransitionTime(endTransitionDate, - endTransitionOffset, - transitionType[dstEndTypeIndex], - StandardTime[dstEndTypeIndex], - GmtTime[dstEndTypeIndex], - out endDate); + private static string TZif_ParsePosixName(string posixFormat, ref int index) + { + return TZif_ParsePosixString(posixFormat, ref index, c => char.IsDigit(c) || c == '+' || c == '-' || c == ','); + } - if (DstStartIndex >= DstEndIndex) { - // we found a DST start but no DST end - endDate = DateTime.MaxValue.Date; - } + private static string TZif_ParsePosixOffset(string posixFormat, ref int index) + { + return TZif_ParsePosixString(posixFormat, ref index, c => !char.IsDigit(c) && c != '+' && c != '-' && c != ':'); + } - AdjustmentRule prevRule = rulesList[ rulesList.Count - 1]; // grab the last element of the MultiYearRule sequence - int y = prevRule.DateEnd.Year; - if (prevRule.DateEnd.Month <= 6) { - // create a rule from 06/16/YYYY to endDate - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(new DateTime(y, 06, 16), endDate, daylightBias, c_transition5_15, dstEnd); - rulesList.Add(r); - } - else { - // create a rule from 11/16/YYYY to endDate - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(new DateTime(y, 11, 16), endDate, daylightBias, c_transition10_15, dstEnd); - rulesList.Add(r); + private static void TZif_ParsePosixDateTime(string posixFormat, ref int index, out string date, out string time) + { + time = null; + + date = TZif_ParsePosixDate(posixFormat, ref index); + if (index < posixFormat.Length && posixFormat[index] == '/') + { + index++; + time = TZif_ParsePosixTime(posixFormat, ref index); } - } + } - - static private void TZif_CreateMiddleMultiYearRules(ref List<AdjustmentRule> rulesList, TimeSpan daylightBias, DateTime endTransitionDate) { - // - // [AdjustmentRule #1] // middle-year all-DST rule - // [1947/06/16 - 1947/11/15] // * starts 1 day after last day in previous rule - // [start: 05/15 ] // * 5 months long, stopping at month 6 or 11 - // [end : 12/15 ] // notice how the _start and end_ are outside the range - // - // [AdjustmentRule #2] // middle-year all-DST rule - // [1947/11/16 - 1947/06/15] // * starts 1 day after last day in previous rule - // [start: 10/01 ] // * 7 months long, stopping at month 6 or 11 - // [end : 07/15 ] // notice how the _start and end_ are outside the range - // - // ......................... + private static string TZif_ParsePosixDate(string posixFormat, ref int index) + { + return TZif_ParsePosixString(posixFormat, ref index, c => c == '/' || c == ','); + } - AdjustmentRule prevRule = rulesList[ rulesList.Count - 1]; // grab the first element of the MultiYearRule sequence - DateTime endDate; + private static string TZif_ParsePosixTime(string posixFormat, ref int index) + { + return TZif_ParsePosixString(posixFormat, ref index, c => c == ','); + } - // - // Choosing the last endDate based on the endTransitionDate - // - // endTransitionDate.Month -> end - // 1 4|5 8|9 12 - // [11/15 <-]|[11/15 <-]|[06/15 <-] - // - if (endTransitionDate.Month <= 8) { - // set the end date to 11/15/YYYY-1 - endDate = new DateTime(endTransitionDate.Year - 1, 11, 15); - } - else { - // set the end date to 06/15/YYYY - endDate = new DateTime(endTransitionDate.Year, 06, 15); - } - - while (prevRule.DateEnd < endDate) { - // the last endDate will be on either 06/15 or 11/15 - int y = prevRule.DateEnd.Year; - if (prevRule.DateEnd.Month <= 6) { - // create a rule from 06/16/YYYY to 11/15/YYYY - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(new DateTime(y, 06, 16), new DateTime(y, 11, 15), - daylightBias, c_transition5_15, c_transition12_15); - prevRule = r; - rulesList.Add(r); - } - else { - // create a rule from 11/16/YYYY to 06/15/YYYY+1 - AdjustmentRule r = AdjustmentRule.CreateAdjustmentRule(new DateTime(y, 11, 16), new DateTime(y+1, 06, 15), - daylightBias, c_transition10_15, c_transition7_15); - prevRule = r; - rulesList.Add(r); + private static string TZif_ParsePosixString(string posixFormat, ref int index, Func<char, bool> breakCondition) + { + int startIndex = index; + for (; index < posixFormat.Length; index++) + { + char current = posixFormat[index]; + if (breakCondition(current)) + { + break; } } - } + return posixFormat.Substring(startIndex, index - startIndex); + } // Returns the Substring from zoneAbbreviations starting at index and ending at '\0' // zoneAbbreviations is expected to be in the form: "PST\0PDT\0PWT\0\PPT" @@ -3731,21 +4040,6 @@ namespace System { } } - // verify the 'index' is referenced from the typeOfLocalTime byte array. - // - static private Boolean TZif_ValidTransitionType(int index, Byte[] typeOfLocalTime) { - Boolean result = false; - - if (typeOfLocalTime != null) { - for (int i = 0; !result && i < typeOfLocalTime.Length; i++) { - if (index == typeOfLocalTime[i]) { - result = true; - } - } - } - return result; - } - // Converts an array of bytes into an int - always using standard byte order (Big Endian) // per TZif file standard [System.Security.SecuritySafeCritical] // auto-generated @@ -3755,9 +4049,34 @@ namespace System { } } - static private void TZif_ParseRaw(Byte[] data, out TZifHead t, out DateTime[] dts, out Byte[] typeOfLocalTime, out TZifType[] transitionType, - out String zoneAbbreviations, out Boolean[] StandardTime, out Boolean[] GmtTime) { + // Converts an array of bytes into a long - always using standard byte order (Big Endian) + // per TZif file standard + [System.Security.SecuritySafeCritical] // auto-generated + static private unsafe long TZif_ToInt64(byte[] value, int startIndex) + { + fixed (byte* pbyte = &value[startIndex]) + { + int i1 = (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3)); + int i2 = (*(pbyte + 4) << 24) | (*(pbyte + 5) << 16) | (*(pbyte + 6) << 8) | (*(pbyte + 7)); + return (uint)i2 | ((long)i1 << 32); + } + } + + static private long TZif_ToUnixTime(byte[] value, int startIndex, TZVersion version) + { + if (version != TZVersion.V1) + { + return TZif_ToInt64(value, startIndex); + } + else + { + return TZif_ToInt32(value, startIndex); + } + } + static private void TZif_ParseRaw(Byte[] data, out TZifHead t, out DateTime[] dts, out Byte[] typeOfLocalTime, out TZifType[] transitionType, + out String zoneAbbreviations, out Boolean[] StandardTime, out Boolean[] GmtTime, out string futureTransitionsPosixFormat) + { // initialize the out parameters in case the TZifHead ctor throws dts = null; typeOfLocalTime = null; @@ -3765,11 +4084,25 @@ namespace System { zoneAbbreviations = String.Empty; StandardTime = null; GmtTime = null; + futureTransitionsPosixFormat = null; // read in the 44-byte TZ header containing the count/length fields // - t = new TZifHead(data, 0); - int index = TZifHead.Length; + int index = 0; + t = new TZifHead(data, index); + index += TZifHead.Length; + + int timeValuesLength = 4; // the first version uses 4-bytes to specify times + if (t.Version != TZVersion.V1) + { + // move index past the V1 information to read the V2 information + index += (int)((timeValuesLength * t.TimeCount) + t.TimeCount + (6 * t.TypeCount) + ((timeValuesLength + 4) * t.LeapCount) + t.IsStdCount + t.IsGmtCount + t.CharCount); + + // read the V2 header + t = new TZifHead(data, index); + index += TZifHead.Length; + timeValuesLength = 8; // the second version uses 8-bytes + } // initialize the containers for the rest of the TZ data dts = new DateTime[t.TimeCount]; @@ -3779,13 +4112,12 @@ namespace System { StandardTime = new Boolean[t.TypeCount]; GmtTime = new Boolean[t.TypeCount]; - - // read in the 4-byte UTC transition points and convert them to Windows + // read in the UTC transition points and convert them to Windows // for (int i = 0; i < t.TimeCount; i++) { - int unixTime = TZif_ToInt32(data, index); - dts[i] = TZif_UnixTimeToWindowsTime(unixTime); - index += 4; + long unixTime = TZif_ToUnixTime(data, index, t.Version); + dts[i] = DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; + index += timeValuesLength; } // read in the Type Indices; there is a 1:1 mapping of UTC transition points to Type Indices @@ -3816,7 +4148,7 @@ namespace System { // skip ahead of the Leap-Seconds Adjustment data. In a future release, consider adding // support for Leap-Seconds // - index += (int)(t.LeapCount * 8); // skip the leap second transition times + index += (int)(t.LeapCount * (timeValuesLength + 4)); // skip the leap second transition times // read in the Standard Time table. There should be a 1:1 mapping between Type-Index and Standard // Time table entries. @@ -3839,17 +4171,15 @@ namespace System { for (int i = 0; i < t.IsGmtCount && i < t.TypeCount && index < data.Length; i++) { GmtTime[i] = (data[index++] != 0); } - } - - // Windows NT time is specified as the number of 100 nanosecond intervals since January 1, 1601. - // UNIX time is specified as the number of seconds since January 1, 1970. There are 134,774 days - // (or 11,644,473,600 seconds) between these dates. - // - private static DateTime TZif_UnixTimeToWindowsTime(int unixTime) { - // Add 11,644,473,600 and multiply by 10,000,000. - Int64 ntTime = (((Int64)unixTime) + 11644473600) * 10000000; - return DateTime.FromFileTimeUtc(ntTime); + if (t.Version != TZVersion.V1) + { + // read the POSIX-style format, which should be wrapped in newlines with the last newline at the end of the file + if (data[index++] == '\n' && data[data.Length - 1] == '\n') + { + futureTransitionsPosixFormat = enc.GetString(data, index, data.Length - index - 1); + } + } } #endif // PLATFORM_UNIX @@ -3922,8 +4252,7 @@ namespace System { if (UtcOffsetOutOfRange(baseUtcOffset + current.DaylightDelta)) { throw new InvalidTimeZoneException(Environment.GetResourceString("ArgumentOutOfRange_UtcOffsetAndDaylightDelta")); - } - + } if (prev != null && current.DateStart <= prev.DateEnd) { // verify the rules are in chronological order and the DateStart/DateEnd do not overlap @@ -3958,7 +4287,7 @@ namespace System { private TransitionTime m_daylightTransitionStart; private TransitionTime m_daylightTransitionEnd; private TimeSpan m_baseUtcOffsetDelta; // delta from the default Utc offset (utcOffset = defaultUtcOffset + m_baseUtcOffsetDelta) - + private bool m_noDaylightTransitions; // ---- SECTION: public properties --------------* public DateTime DateStart { @@ -3999,11 +4328,25 @@ namespace System { } } + /// <summary> + /// Gets a value indicating that this AdjustmentRule fixes the time zone offset + /// from DateStart to DateEnd without any daylight transitions in between. + /// </summary> + internal bool NoDaylightTransitions { + get { + return this.m_noDaylightTransitions; + } + } + internal bool HasDaylightSaving { get { - return this.DaylightDelta != TimeSpan.Zero || - this.DaylightTransitionStart.TimeOfDay != DateTime.MinValue || - this.DaylightTransitionEnd.TimeOfDay != DateTime.MinValue.AddMilliseconds(1); + return this.DaylightDelta != TimeSpan.Zero + || + (this.DaylightTransitionStart != default(TransitionTime) + && this.DaylightTransitionStart.TimeOfDay != DateTime.MinValue) + || + (this.DaylightTransitionEnd != default(TransitionTime) + && this.DaylightTransitionEnd.TimeOfDay != DateTime.MinValue.AddMilliseconds(1)); } } @@ -4037,15 +4380,15 @@ namespace System { // -------- SECTION: factory methods -----------------* - static public AdjustmentRule CreateAdjustmentRule( + static internal AdjustmentRule CreateAdjustmentRule( DateTime dateStart, DateTime dateEnd, TimeSpan daylightDelta, TransitionTime daylightTransitionStart, - TransitionTime daylightTransitionEnd) { - + TransitionTime daylightTransitionEnd, + bool noDaylightTransitions) { ValidateAdjustmentRule(dateStart, dateEnd, daylightDelta, - daylightTransitionStart, daylightTransitionEnd); + daylightTransitionStart, daylightTransitionEnd, noDaylightTransitions); AdjustmentRule rule = new AdjustmentRule(); @@ -4055,22 +4398,37 @@ namespace System { rule.m_daylightTransitionStart = daylightTransitionStart; rule.m_daylightTransitionEnd = daylightTransitionEnd; rule.m_baseUtcOffsetDelta = TimeSpan.Zero; + rule.m_noDaylightTransitions = noDaylightTransitions; return rule; } + static public AdjustmentRule CreateAdjustmentRule( + DateTime dateStart, + DateTime dateEnd, + TimeSpan daylightDelta, + TransitionTime daylightTransitionStart, + TransitionTime daylightTransitionEnd) + { + return CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, + daylightTransitionStart, daylightTransitionEnd, noDaylightTransitions: false); + } + static internal AdjustmentRule CreateAdjustmentRule( DateTime dateStart, DateTime dateEnd, TimeSpan daylightDelta, TransitionTime daylightTransitionStart, TransitionTime daylightTransitionEnd, - TimeSpan baseUtcOffsetDelta) { - AdjustmentRule rule = CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, daylightTransitionStart, daylightTransitionEnd); + TimeSpan baseUtcOffsetDelta, + bool noDaylightTransitions) { + AdjustmentRule rule = CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, + daylightTransitionStart, daylightTransitionEnd, noDaylightTransitions); + rule.m_baseUtcOffsetDelta = baseUtcOffsetDelta; return rule; } - + // ----- SECTION: internal utility methods ----------------* // @@ -4078,7 +4436,8 @@ namespace System { // We have to special case this value and not adjust it when checking if any date is in the daylight saving period. // internal bool IsStartDateMarkerForBeginningOfYear() { - return DaylightTransitionStart.Month == 1 && DaylightTransitionStart.Day == 1 && DaylightTransitionStart.TimeOfDay.Hour == 0 && + return !NoDaylightTransitions && + DaylightTransitionStart.Month == 1 && DaylightTransitionStart.Day == 1 && DaylightTransitionStart.TimeOfDay.Hour == 0 && DaylightTransitionStart.TimeOfDay.Minute == 0 && DaylightTransitionStart.TimeOfDay.Second == 0 && m_dateStart.Year == m_dateEnd.Year; } @@ -4088,7 +4447,8 @@ namespace System { // We have to special case this value and not adjust it when checking if any date is in the daylight saving period. // internal bool IsEndDateMarkerForEndOfYear() { - return DaylightTransitionEnd.Month == 1 && DaylightTransitionEnd.Day == 1 && DaylightTransitionEnd.TimeOfDay.Hour == 0 && + return !NoDaylightTransitions && + DaylightTransitionEnd.Month == 1 && DaylightTransitionEnd.Day == 1 && DaylightTransitionEnd.TimeOfDay.Hour == 0 && DaylightTransitionEnd.TimeOfDay.Minute == 0 && DaylightTransitionEnd.TimeOfDay.Second == 0 && m_dateStart.Year == m_dateEnd.Year; } @@ -4104,18 +4464,19 @@ namespace System { DateTime dateEnd, TimeSpan daylightDelta, TransitionTime daylightTransitionStart, - TransitionTime daylightTransitionEnd) { + TransitionTime daylightTransitionEnd, + bool noDaylightTransitions) { - if (dateStart.Kind != DateTimeKind.Unspecified) { - throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeKindMustBeUnspecified"), "dateStart"); + if (dateStart.Kind != DateTimeKind.Unspecified && dateStart.Kind != DateTimeKind.Utc) { + throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeKindMustBeUnspecifiedOrUtc"), "dateStart"); } - if (dateEnd.Kind != DateTimeKind.Unspecified) { - throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeKindMustBeUnspecified"), "dateEnd"); + if (dateEnd.Kind != DateTimeKind.Unspecified && dateEnd.Kind != DateTimeKind.Utc) { + throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeKindMustBeUnspecifiedOrUtc"), "dateEnd"); } - if (daylightTransitionStart.Equals(daylightTransitionEnd)) { + if (daylightTransitionStart.Equals(daylightTransitionEnd) && !noDaylightTransitions) { throw new ArgumentException(Environment.GetResourceString("Argument_TransitionTimesAreIdentical"), "daylightTransitionEnd"); } @@ -4125,7 +4486,11 @@ namespace System { throw new ArgumentException(Environment.GetResourceString("Argument_OutOfOrderDateTimes"), "dateStart"); } - if (TimeZoneInfo.UtcOffsetOutOfRange(daylightDelta)) { + // This cannot use UtcOffsetOutOfRange to account for the scenario where Samoa moved across the International Date Line, + // which caused their current BaseUtcOffset to be +13. But on the other side of the line it was UTC-11 (+1 for daylight). + // So when trying to describe DaylightDeltas for those times, the DaylightDelta needs + // to be -23 (what it takes to go from UTC+13 to UTC-10) + if (daylightDelta.TotalHours < -23.0 || daylightDelta.TotalHours > 14.0) { throw new ArgumentOutOfRangeException("daylightDelta", daylightDelta, Environment.GetResourceString("ArgumentOutOfRange_UtcOffset")); } @@ -4135,12 +4500,12 @@ namespace System { "daylightDelta"); } - if (dateStart.TimeOfDay != TimeSpan.Zero) { + if (dateStart != DateTime.MinValue && dateStart.Kind == DateTimeKind.Unspecified && dateStart.TimeOfDay != TimeSpan.Zero) { throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeHasTimeOfDay"), "dateStart"); } - if (dateEnd.TimeOfDay != TimeSpan.Zero) { + if (dateEnd != DateTime.MaxValue && dateEnd.Kind == DateTimeKind.Unspecified && dateEnd.TimeOfDay != TimeSpan.Zero) { throw new ArgumentException(Environment.GetResourceString("Argument_DateTimeHasTimeOfDay"), "dateEnd"); } @@ -4158,7 +4523,7 @@ namespace System { try { ValidateAdjustmentRule(m_dateStart, m_dateEnd, m_daylightDelta, - m_daylightTransitionStart, m_daylightTransitionEnd); + m_daylightTransitionStart, m_daylightTransitionEnd, m_noDaylightTransitions); } catch (ArgumentException e) { throw new SerializationException(Environment.GetResourceString("Serialization_InvalidData"), e); @@ -4178,6 +4543,7 @@ namespace System { info.AddValue("DaylightTransitionStart", m_daylightTransitionStart); info.AddValue("DaylightTransitionEnd", m_daylightTransitionEnd); info.AddValue("BaseUtcOffsetDelta", m_baseUtcOffsetDelta); + info.AddValue("NoDaylightTransitions", m_noDaylightTransitions); } AdjustmentRule(SerializationInfo info, StreamingContext context) { @@ -4195,10 +4561,15 @@ namespace System { if (o != null) { m_baseUtcOffsetDelta = (TimeSpan) o; } + + o = info.GetValueNoThrow("NoDaylightTransitions", typeof(bool)); + if (o != null) { + m_noDaylightTransitions = (bool)o; + } } #endif } - + /*============================================================ ** @@ -4547,6 +4918,10 @@ namespace System { serializedText.Append(SerializeSubstitute(rule.BaseUtcOffsetDelta.TotalMinutes.ToString(CultureInfo.InvariantCulture))); serializedText.Append(sep); } + if (rule.NoDaylightTransitions) { // Serialize it only when NoDaylightTransitions is true to reduce the impact of adding rule.NoDaylightTransitions + serializedText.Append(SerializeSubstitute("1")); + serializedText.Append(sep); + } serializedText.Append(rhs); } } @@ -4935,6 +5310,8 @@ namespace System { TransitionTime daylightStart = GetNextTransitionTimeValue(false); TransitionTime daylightEnd = GetNextTransitionTimeValue(false); TimeSpan baseUtcOffsetDelta = TimeSpan.Zero; + Int32 noDaylightTransitions = 0; + // verify that the string is now at the right-hand-side marker ("]") ... if (m_state == State.EndOfLine || m_currentTokenStartIndex >= m_serializedText.Length) { @@ -4947,6 +5324,11 @@ namespace System { baseUtcOffsetDelta = GetNextTimeSpanValue(false); } + // Check if we have NoDaylightTransitions in the serialized string and then deserialize it + if ((m_serializedText[m_currentTokenStartIndex] >= '0' && m_serializedText[m_currentTokenStartIndex] <= '1')) { + noDaylightTransitions = GetNextInt32Value(false); + } + if (m_state == State.EndOfLine || m_currentTokenStartIndex >= m_serializedText.Length) { throw new SerializationException(Environment.GetResourceString("Serialization_InvalidData")); } @@ -4967,7 +5349,7 @@ namespace System { AdjustmentRule rule; try { - rule = AdjustmentRule.CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, daylightStart, daylightEnd, baseUtcOffsetDelta); + rule = AdjustmentRule.CreateAdjustmentRule(dateStart, dateEnd, daylightDelta, daylightStart, daylightEnd, baseUtcOffsetDelta, noDaylightTransitions > 0); } catch (ArgumentException e) { throw new SerializationException(Environment.GetResourceString("Serialization_InvalidData"), e); @@ -5157,13 +5539,19 @@ namespace System { throw new ArgumentException(Environment.GetResourceString("Argument_TimeZoneInfoBadTZif"), "data"); } + byte version = data[index + 04]; + Version = version == '2' ? TZVersion.V2 : + version == '3' ? TZVersion.V3 : + TZVersion.V1; // default/fallback to V1 to guard against future, unsupported version numbers + + // skip the 15 byte reserved field + // don't use the BitConverter class which parses data // based on the Endianess of the machine architecture. // this data is expected to always be in "standard byte order", // regardless of the machine it is being processed on. IsGmtCount = (uint)TZif_ToInt32(data, index + 20); - // skip the 16 byte reserved field IsStdCount = (uint)TZif_ToInt32(data, index + 24); LeapCount = (uint)TZif_ToInt32(data, index + 28); TimeCount = (uint)TZif_ToInt32(data, index + 32); @@ -5172,7 +5560,8 @@ namespace System { } public UInt32 Magic; // TZ_MAGIC "TZif" - // public Byte[16] Reserved; // reserved for future use + public TZVersion Version; // 1 byte for a \0 or 2 or 3 + // public Byte[15] Reserved; // reserved for future use public UInt32 IsGmtCount; // number of transition time flags public UInt32 IsStdCount; // number of transition time flags public UInt32 LeapCount; // number of leap seconds @@ -5180,6 +5569,14 @@ namespace System { public UInt32 TypeCount; // number of local time types public UInt32 CharCount; // number of abbreviated characters } + + private enum TZVersion : byte + { + V1 = 0, + V2, + V3, + // when adding more versions, ensure all the logic using TZVersion is still correct + } #endif // PLATFORM_UNIX } // TimezoneInfo diff --git a/src/mscorlib/src/mscorlib.txt b/src/mscorlib/src/mscorlib.txt index 6e3df77ccd..39a40d2b0f 100644 --- a/src/mscorlib/src/mscorlib.txt +++ b/src/mscorlib/src/mscorlib.txt @@ -2304,7 +2304,7 @@ Argument_DateTimeHasTimeOfDay = The supplied DateTime includes a TimeOfDay setti Argument_DateTimeIsInvalid = The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. Argument_DateTimeIsNotAmbiguous = The supplied DateTime is not in an ambiguous time range. Argument_DateTimeOffsetIsNotAmbiguous = The supplied DateTimeOffset is not in an ambiguous time range. -Argument_DateTimeKindMustBeUnspecified = The supplied DateTime must have the Kind property set to DateTimeKind.Unspecified. +Argument_DateTimeKindMustBeUnspecifiedOrUtc = The supplied DateTime must have the Kind property set to DateTimeKind.Unspecified or DateTimeKind.Utc. Argument_DateTimeHasTicks = The supplied DateTime must have the Year, Month, and Day properties set to 1. The time cannot be specified more precisely than whole milliseconds. Argument_InvalidId = The specified ID parameter '{0}' is not supported. Argument_InvalidSerializedString = The specified serialized string '{0}' is not supported. @@ -2329,6 +2329,9 @@ ArgumentOutOfRange_Week = The Week parameter must be in the range 1 through 5. InvalidTimeZone_InvalidRegistryData = The time zone ID '{0}' was found on the local computer, but the registry information was corrupt. InvalidTimeZone_InvalidFileData = The time zone ID '{0}' was found on the local computer, but the file at '{1}' was corrupt. InvalidTimeZone_InvalidWin32APIData = The Local time zone was found on the local computer, but the data was corrupt. +InvalidTimeZone_NoTTInfoStructures = There are no ttinfo structures in the tzfile. At least one ttinfo structure is required in order to construct a TimeZoneInfo object. +InvalidTimeZone_UnparseablePosixMDateString = '{0}' is not a valid POSIX-TZ-environment-variable MDate rule. A valid rule has the format 'Mm.w.d'. +InvalidTimeZone_JulianDayNotSupported = Julian dates in POSIX strings are unsupported. ; ; begin System.TimeZoneInfo SecurityException's ; |