protected override bool EqualsImpl(DateTimeZone other)
        {
            DaylightSavingsDateTimeZone otherZone = (DaylightSavingsDateTimeZone)other;

            return(Id == otherZone.Id &&
                   standardOffset == otherZone.standardOffset &&
                   dstRecurrence.Equals(otherZone.dstRecurrence) &&
                   standardRecurrence.Equals(otherZone.standardRecurrence));
        }
        /// <summary>
        /// Reads a time zone from the specified reader.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <param name="id">The id.</param>
        /// <returns>The time zone.</returns>
        public static DateTimeZone Read(IDateTimeZoneReader reader, string id)
        {
            int size    = reader.ReadCount();
            var periods = new ZoneInterval[size];
            var start   = reader.ReadZoneIntervalTransition(null);

            for (int i = 0; i < size; i++)
            {
                var name      = reader.ReadString();
                var offset    = reader.ReadOffset();
                var savings   = reader.ReadOffset();
                var nextStart = reader.ReadZoneIntervalTransition(start);
                periods[i] = new ZoneInterval(name, start, nextStart, offset, savings);
                start      = nextStart;
            }
            var tailZone = reader.ReadByte() == 1 ? DaylightSavingsDateTimeZone.Read(reader, id + "-tail") : null;

            return(new PrecalculatedDateTimeZone(id, periods, tailZone));
        }
Ejemplo n.º 3
0
            internal BclAdjustmentRule(TimeZoneInfo zone, TimeZoneInfo.AdjustmentRule rule)
            {
                // With .NET 4.6, adjustment rules can have their own standard offsets, allowing
                // a much more reasonable set of time zone data. Unfortunately, this isn't directly
                // exposed, but we can detect it by just finding the UTC offset for an arbitrary
                // time within the rule - the start, in this case - and then take account of the
                // possibility of that being in daylight saving time. Fortunately, we only need
                // to do this during the setup.
                var ruleStandardOffset = zone.GetUtcOffset(rule.DateStart);

                if (zone.IsDaylightSavingTime(rule.DateStart))
                {
                    ruleStandardOffset -= rule.DaylightDelta;
                }
                StandardOffset = ruleStandardOffset.ToOffset();

                // Although the rule may have its own standard offset, the start/end is still determined
                // using the zone's standard offset.
                var zoneStandardOffset = zone.BaseUtcOffset.ToOffset();

                // Note: this extends back from DateTime.MinValue to start of time, even though the BCL can represent
                // as far back as 1AD. This is in the *spirit* of a rule which goes back that far.
                Start = rule.DateStart == DateTime.MinValue ? Instant.BeforeMinValue : rule.DateStart.ToLocalDateTime().WithOffset(zoneStandardOffset).ToInstant();
                // The end instant (exclusive) is the end of the given date, so we need to add a day.
                End     = rule.DateEnd == MaxDate ? Instant.AfterMaxValue : rule.DateEnd.ToLocalDateTime().PlusDays(1).WithOffset(zoneStandardOffset).ToInstant();
                Savings = rule.DaylightDelta.ToOffset();

                // Some rules have DST start/end of "January 1st", to indicate that they're just in standard time. This is important
                // for rules which have a standard offset which is different to the standard offset of the zone itself.
                if (IsStandardOffsetOnlyRule(rule))
                {
                    PartialMap = PartialZoneIntervalMap.ForZoneInterval(zone.StandardName, Start, End, StandardOffset, Offset.Zero);
                }
                else
                {
                    var daylightRecurrence = new ZoneRecurrence(zone.DaylightName, Savings, ConvertTransition(rule.DaylightTransitionStart), int.MinValue, int.MaxValue);
                    var standardRecurrence = new ZoneRecurrence(zone.StandardName, Offset.Zero, ConvertTransition(rule.DaylightTransitionEnd), int.MinValue, int.MaxValue);
                    var recurringMap       = new DaylightSavingsDateTimeZone("ignored", StandardOffset, standardRecurrence, daylightRecurrence);
                    PartialMap = new PartialZoneIntervalMap(Start, End, recurringMap);
                }
            }
Ejemplo n.º 4
0
        /// <summary>
        /// Reads a time zone from the specified reader.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <param name="id">The id.</param>
        /// <returns>The time zone.</returns>
        internal static DateTimeZone Read([Trusted][NotNull] IDateTimeZoneReader reader, [Trusted][NotNull] string id)
        {
            Preconditions.DebugCheckNotNull(reader, nameof(reader));
            Preconditions.DebugCheckNotNull(id, nameof(id));
            int size    = reader.ReadCount();
            var periods = new ZoneInterval[size];
            // It's not entirely clear why we don't just assume that the first zone interval always starts at Instant.BeforeMinValue
            // (given that we check that later) but we don't... and changing that now could cause compatibility issues.
            var start = reader.ReadZoneIntervalTransition(null);

            for (int i = 0; i < size; i++)
            {
                var name      = reader.ReadString();
                var offset    = reader.ReadOffset();
                var savings   = reader.ReadOffset();
                var nextStart = reader.ReadZoneIntervalTransition(start);
                periods[i] = new ZoneInterval(name, start, nextStart, offset, savings);
                start      = nextStart;
            }
            var tailZone = reader.ReadByte() == 1 ? DaylightSavingsDateTimeZone.Read(reader, id + "-tail") : null;

            return(new PrecalculatedDateTimeZone(id, periods, tailZone));
        }
Ejemplo n.º 5
0
            internal BclAdjustmentRule(TimeZoneInfo zone, TimeZoneInfo.AdjustmentRule rule)
            {
                // With .NET 4.6, adjustment rules can have their own standard offsets, allowing
                // a much more reasonable set of time zone data. Unfortunately, this isn't directly
                // exposed, but we can detect it by just finding the UTC offset for an arbitrary
                // time within the rule - the start, in this case - and then take account of the
                // possibility of that being in daylight saving time. Fortunately, we only need
                // to do this during the setup.
                var ruleStandardOffset = zone.GetUtcOffset(rule.DateStart);
                if (zone.IsDaylightSavingTime(rule.DateStart))
                {
                    ruleStandardOffset -= rule.DaylightDelta;
                }
                StandardOffset = ruleStandardOffset.ToOffset();

                // Although the rule may have its own standard offset, the start/end is still determined
                // using the zone's standard offset.
                var zoneStandardOffset = zone.BaseUtcOffset.ToOffset();

                // Note: this extends back from DateTime.MinValue to start of time, even though the BCL can represent
                // as far back as 1AD. This is in the *spirit* of a rule which goes back that far.
                Start = rule.DateStart == DateTime.MinValue ? Instant.BeforeMinValue : rule.DateStart.ToLocalDateTime().WithOffset(zoneStandardOffset).ToInstant();
                // The end instant (exclusive) is the end of the given date, so we need to add a day.
                End = rule.DateEnd == MaxDate ? Instant.AfterMaxValue : rule.DateEnd.ToLocalDateTime().PlusDays(1).WithOffset(zoneStandardOffset).ToInstant();
                Savings = rule.DaylightDelta.ToOffset();

                // Some rules have DST start/end of "January 1st", to indicate that they're just in standard time. This is important
                // for rules which have a standard offset which is different to the standard offset of the zone itself.
                if (IsStandardOffsetOnlyRule(rule))
                {
                    PartialMap = PartialZoneIntervalMap.ForZoneInterval(zone.StandardName, Start, End, StandardOffset, Offset.Zero);
                }
                else
                {
                    var daylightRecurrence = new ZoneRecurrence(zone.DaylightName, Savings, ConvertTransition(rule.DaylightTransitionStart), int.MinValue, int.MaxValue);
                    var standardRecurrence = new ZoneRecurrence(zone.StandardName, Offset.Zero, ConvertTransition(rule.DaylightTransitionEnd), int.MinValue, int.MaxValue);
                    var recurringMap = new DaylightSavingsDateTimeZone("ignored", StandardOffset, standardRecurrence, daylightRecurrence);
                    PartialMap = new PartialZoneIntervalMap(Start, End, recurringMap);
                }
            }
        public void Extremes()
        {
            ZoneRecurrence winter = new ZoneRecurrence("Winter", Offset.Zero,
                new ZoneYearOffset(TransitionMode.Wall, 10, 5, 0, false, new LocalTime(2, 0)), int.MinValue, int.MaxValue);

            ZoneRecurrence summer = new ZoneRecurrence("Summer", Offset.FromHours(1),
                new ZoneYearOffset(TransitionMode.Wall, 3, 10, 0, false, new LocalTime(1, 0)), int.MinValue, int.MaxValue);

            var zone = new DaylightSavingsDateTimeZone("infinite", Offset.Zero, winter, summer);

            var firstSpring = Instant.FromUtc(-9998, 3, 10, 1, 0);
            var firstAutumn = Instant.FromUtc(-9998, 10, 5, 1, 0); // 1am UTC = 2am wall

            var lastSpring = Instant.FromUtc(9999, 3, 10, 1, 0);
            var lastAutumn = Instant.FromUtc(9999, 10, 5, 1, 0); // 1am UTC = 2am wall

            var dstOffset = Offset.FromHours(1);

            // Check both year -9998 and 9999, both the infinite interval and the next one in
            var firstWinter = new ZoneInterval("Winter", Instant.BeforeMinValue, firstSpring, Offset.Zero, Offset.Zero);
            var firstSummer = new ZoneInterval("Summer", firstSpring, firstAutumn, dstOffset, dstOffset);
            var lastSummer = new ZoneInterval("Summer", lastSpring, lastAutumn, dstOffset, dstOffset);
            var lastWinter = new ZoneInterval("Winter", lastAutumn, Instant.AfterMaxValue, Offset.Zero, Offset.Zero);

            Assert.AreEqual(firstWinter, zone.GetZoneInterval(Instant.MinValue));
            Assert.AreEqual(firstWinter, zone.GetZoneInterval(Instant.FromUtc(-9998, 2, 1, 0, 0)));
            Assert.AreEqual(firstSummer, zone.GetZoneInterval(firstSpring));
            Assert.AreEqual(firstSummer, zone.GetZoneInterval(Instant.FromUtc(-9998, 5, 1, 0, 0)));

            Assert.AreEqual(lastSummer, zone.GetZoneInterval(lastSpring));
            Assert.AreEqual(lastSummer, zone.GetZoneInterval(Instant.FromUtc(9999, 5, 1, 0, 0)));
            Assert.AreEqual(lastWinter, zone.GetZoneInterval(lastAutumn));
            Assert.AreEqual(lastWinter, zone.GetZoneInterval(Instant.FromUtc(9999, 11, 1, 0, 0)));
            Assert.AreEqual(lastWinter, zone.GetZoneInterval(Instant.MaxValue));

            // And just for kicks, let's check we can get them all with GetZoneIntervals.
            IEnumerable<ZoneInterval> intervals = zone.GetZoneIntervals(new Interval(null, null)).ToList();
            Assert.AreEqual(firstWinter, intervals.First());
            Assert.AreEqual(firstSummer, intervals.Skip(1).First());
            Assert.AreEqual(lastSummer, intervals.Reverse().Skip(1).First());
            Assert.AreEqual(lastWinter, intervals.Last());
        }
Ejemplo n.º 7
0
        /// <summary>
        /// Creates a new <see cref="BclDateTimeZone" /> from a <see cref="TimeZoneInfo"/> from the Base Class Library.
        /// </summary>
        /// <param name="bclZone">The original time zone to take information from.</param>
        /// <returns>A <see cref="BclDateTimeZone"/> wrapping the given <c>TimeZoneInfo</c>.</returns>
        public static BclDateTimeZone FromTimeZoneInfo([NotNull] TimeZoneInfo bclZone)
        {
            Preconditions.CheckNotNull(bclZone, "bclZone");
            Offset standardOffset = Offset.FromTimeSpan(bclZone.BaseUtcOffset);
            Offset minSavings     = Offset.Zero; // Just in case we have negative savings!
            Offset maxSavings     = Offset.Zero;

            var rules = bclZone.GetAdjustmentRules();

            if (!bclZone.SupportsDaylightSavingTime || rules.Length == 0)
            {
                var fixedInterval = new ZoneInterval(bclZone.StandardName, Instant.MinValue, Instant.MaxValue, standardOffset, Offset.Zero);
                return(new BclDateTimeZone(bclZone, standardOffset, standardOffset, new FixedZoneIntervalMap(fixedInterval)));
            }
            var     adjustmentIntervals = new List <AdjustmentInterval>();
            var     headInterval        = ComputeHeadInterval(bclZone, rules[0]);
            Instant previousEnd         = headInterval != null ? headInterval.End : Instant.MinValue;

            // TODO(Post-V1): Tidy this up. All of this is horrible.
            for (int i = 0; i < rules.Length; i++)
            {
                var            rule = rules[i];
                ZoneRecurrence standard, daylight;
                GetRecurrences(bclZone, rule, out standard, out daylight);

                minSavings = Offset.Min(minSavings, daylight.Savings);
                maxSavings = Offset.Max(maxSavings, daylight.Savings);

                // Find the last valid transition by working back from the end of time. It's safe to unconditionally
                // take the value here, as there must *be* some recurrences.
                var        lastStandard    = standard.PreviousOrFail(LastValidTick, standardOffset, daylight.Savings);
                var        lastDaylight    = daylight.PreviousOrFail(LastValidTick, standardOffset, Offset.Zero);
                bool       standardIsLater = lastStandard.Instant > lastDaylight.Instant;
                Transition lastTransition  = standardIsLater ? lastStandard : lastDaylight;
                Offset     seamSavings     = lastTransition.NewOffset - standardOffset;
                string     seamName        = standardIsLater ? bclZone.StandardName : bclZone.DaylightName;

                Instant nextStart;

                Instant ruleEnd = GetRuleEnd(rule, lastTransition);

                // Handle the final rule, which may or may not to extend to the end of time. If it doesn't,
                // we transition to standard time at the end of the rule.
                if (i == rules.Length - 1)
                {
                    // If the final transition was to standard time, we can just treat the seam as going on forever.
                    nextStart = standardIsLater ? Instant.MaxValue : ruleEnd;
                    var seam           = new ZoneInterval(seamName, lastTransition.Instant, nextStart, lastTransition.NewOffset, seamSavings);
                    var adjustmentZone = new DaylightSavingsDateTimeZone("ignored", standardOffset, standard.ToInfinity(), daylight.ToInfinity());
                    adjustmentIntervals.Add(new AdjustmentInterval(previousEnd, adjustmentZone, seam));
                    previousEnd = nextStart;
                }
                else
                {
                    // Handle one rule going into another. This is the cause of much pain, as there are several options:
                    // 1) Going into a "normal" rule with two transitions per year.
                    // 2) Going into a "single transition" rule, signified by a transition "into" the current offset
                    //    right at the start of the year. This is treated as if the on-the-edge transition doesn't exist.
                    //
                    // Additionally, there's the possibility that the offset at the start of the next rule (i.e. the
                    // one before the first transition) isn't the same as the offset at the end of end of the current rule
                    // (i.e. lastTransition.NewOffset). This only occurs for Namibia time as far as we've seen, but in that
                    // case we create a seam which only goes until the end of this rule, then an extra zone interval for
                    // the time between the start of the rule and the first transition, then we're as normal, starting at
                    // the first transition. See bug 115 for a bit more information.
                    var            nextRule = rules[i + 1];
                    ZoneRecurrence nextStandard, nextDaylight;
                    GetRecurrences(bclZone, nextRule, out nextStandard, out nextDaylight);

                    // By using the seam's savings for *both* checks, we can detect "daylight to daylight" and
                    // "standard to standard" transitions as happening at the very start of the rule.
                    var firstStandard = nextStandard.NextOrFail(lastTransition.Instant, standardOffset, seamSavings);
                    var firstDaylight = nextDaylight.NextOrFail(lastTransition.Instant, standardOffset, seamSavings);

                    // Ignore any "right at the start of the rule"  transitions.
                    var  firstStandardInstant   = firstStandard.Instant == ruleEnd ? Instant.MaxValue : firstStandard.Instant;
                    var  firstDaylightInstant   = firstDaylight.Instant == ruleEnd ? Instant.MaxValue : firstDaylight.Instant;
                    bool firstStandardIsEarlier = firstStandardInstant < firstDaylightInstant;
                    var  firstTransition        = firstStandardIsEarlier ? firstStandard : firstDaylight;
                    nextStart = firstTransition.Instant;
                    var seamEnd = nextStart;

                    AdjustmentInterval startOfRuleExtraSeam = null;

                    Offset previousOffset = firstStandardIsEarlier ? firstDaylight.NewOffset : firstStandard.NewOffset;
                    if (previousOffset != lastTransition.NewOffset)
                    {
                        seamEnd = ruleEnd;
                        // Recalculate the *real* transition, as we're now going from a different wall offset...
                        var firstRule = firstStandardIsEarlier ? nextStandard : nextDaylight;
                        nextStart = firstRule.NextOrFail(ruleEnd, standardOffset, previousOffset - standardOffset).Instant;
                        var extraSeam = new ZoneInterval(firstStandardIsEarlier ? bclZone.DaylightName : bclZone.StandardName,
                                                         ruleEnd, nextStart, previousOffset, previousOffset - standardOffset);
                        // The extra adjustment interval is really just a single zone interval; we'll never need the DaylightSavingsDateTimeZone part.
                        startOfRuleExtraSeam = new AdjustmentInterval(extraSeam.Start, null, extraSeam);
                    }

                    var seam           = new ZoneInterval(seamName, lastTransition.Instant, seamEnd, lastTransition.NewOffset, seamSavings);
                    var adjustmentZone = new DaylightSavingsDateTimeZone("ignored", standardOffset, standard.ToInfinity(), daylight.ToInfinity());
                    adjustmentIntervals.Add(new AdjustmentInterval(previousEnd, adjustmentZone, seam));

                    if (startOfRuleExtraSeam != null)
                    {
                        adjustmentIntervals.Add(startOfRuleExtraSeam);
                    }
                    previousEnd = nextStart;
                }
            }
            ZoneInterval tailInterval = previousEnd == Instant.MaxValue ? null
                : new ZoneInterval(bclZone.StandardName, previousEnd, Instant.MaxValue, standardOffset, Offset.Zero);

            IZoneIntervalMap uncachedMap = new BclZoneIntervalMap(adjustmentIntervals, headInterval, tailInterval);
            IZoneIntervalMap cachedMap   = CachingZoneIntervalMap.CacheMap(uncachedMap, CachingZoneIntervalMap.CacheType.Hashtable);

            return(new BclDateTimeZone(bclZone, standardOffset + minSavings, standardOffset + maxSavings, cachedMap));
        }
Ejemplo n.º 8
0
 internal AdjustmentInterval(Instant start, DaylightSavingsDateTimeZone adjustmentZone, ZoneInterval seam)
 {
     this.start          = start;
     this.seam           = seam;
     this.adjustmentZone = adjustmentZone;
 }
Ejemplo n.º 9
0
        /// <summary>
        /// Adds the intervals from the given rule set to the end of the zone
        /// being built. The rule is deemed to take effect from the end of the previous
        /// zone interval, or the start of time if this is the first rule set (which must
        /// be a fixed one). Intervals are added until the rule set expires, or
        /// until we determine that the rule set continues to the end of time,
        /// possibly with a tail zone - a pair of standard/daylight rules which repeat
        /// forever.
        /// </summary>
        private void AddIntervals(ZoneRuleSet ruleSet)
        {
            // We use the last zone interval computed so far (if there is one) to work out where to start.
            var lastZoneInterval = zoneIntervals.LastOrDefault();
            var start = lastZoneInterval?.End ?? Instant.BeforeMinValue;

            // Simple case: a zone line with fixed savings (or - for 0)
            // instead of a rule name. Just a single interval.
            if (ruleSet.IsFixed)
            {
                zoneIntervals.Add(ruleSet.CreateFixedInterval(start));
                return;
            }

            // Work on a copy of the rule set. We eliminate rules from it as they expire,
            // so that we can tell when we're down to an infinite pair which can be represented
            // as a tail zone.
            var activeRules = new List<ZoneRecurrence>(ruleSet.Rules);

            // Surprisingly tricky bit to work out: how to handle the transition from
            // one rule set to another. We know the instant at which the new rule set
            // come in, but not what offsets/name to use from that point onwards: which
            // of the new rules is in force. We find out which rule would have taken
            // effect most recently before or on the transition instant - but using
            // the offsets from the final interval before the transition, instead
            // of the offsets which would have been in force if the new rule set were
            // actually extended backwards forever.
            //
            // It's possible that the most recent transition we find would actually
            // have started before that final interval anyway - but this appears to
            // match what zic produces.
            //
            // If we don't have a zone interval at all, we're starting at the start of
            // time, so there definitely aren't any preceding rules.
            var firstRule = lastZoneInterval == null ? null :
                activeRules
                    .Select(rule => new { rule, prev = rule.PreviousOrSame(start, lastZoneInterval.StandardOffset, lastZoneInterval.Savings) })
                    .Where(pair => pair.prev != null)
                    .OrderBy(pair => pair.prev.Value.Instant)
                    .Select(pair => pair.rule)
                    .LastOrDefault();

            // Every transition in this rule set will use the same standard offset.
            var standardOffset = ruleSet.StandardOffset;

            // previousTransition here is ongoing as we loop through the transitions. It's not like
            // lastZoneInterval, lastStandard and lastSavings, which refer to the last aspects of the
            // previous rule set. When we set it up, this is effectively the *first* transition leading
            // into the period in which the new rule set is 
            ZoneTransition previousTransition;
            if (firstRule != null)
            {
                previousTransition = new ZoneTransition(start, firstRule.Name, standardOffset, firstRule.Savings);
            }
            else
            {
                // None of the rules in the current set have *any* transitions in the past, apparently.
                // For an example of this, see Europe/Prague (in 2015e, anyway). A zone line with the
                // Czech rule takes effect in 1944, but all the rules are from 1945 onwards.
                // Use standard time until the first transition, regardless of the previous savings,
                // and take the name for this first interval from the first standard time rule.
                var name = activeRules.First(rule => rule.Savings == Offset.Zero).Name;
                previousTransition = new ZoneTransition(start, name, standardOffset, Offset.Zero);
            }

            // Main loop - we keep going round until we run out of rules or hit infinity, each of which
            // corresponds with a return statement in the loop.
            while (true)
            {
                ZoneTransition bestTransition = null;
                for (int i = 0; i < activeRules.Count; i++)
                {
                    var rule = activeRules[i];
                    var nextTransition = rule.Next(previousTransition.Instant, standardOffset, previousTransition.Savings);
                    // Once a rule is no longer active, remove it from the list. That way we can tell
                    // when we can create a tail zone.
                    if (nextTransition == null)
                    {
                        activeRules.RemoveAt(i);
                        i--;
                        continue;
                    }
                    var zoneTransition = new ZoneTransition(nextTransition.Value.Instant, rule.Name, standardOffset, rule.Savings);
                    if (!zoneTransition.IsTransitionFrom(previousTransition))
                    {
                        continue;
                    }
                    if (bestTransition == null || zoneTransition.Instant <= bestTransition.Instant)
                    {
                        bestTransition = zoneTransition;
                    }
                }
                Instant currentUpperBound = ruleSet.GetUpperLimit(previousTransition.Savings);
                if (bestTransition == null || bestTransition.Instant >= currentUpperBound)
                {
                    // No more transitions to find. (We may have run out of rules, or they may be beyond where this rule set expires.)
                    // Add a final interval leading up to the upper bound of the rule set, unless the previous transition took us up to
                    // this current bound anyway.
                    // (This is very rare, but can happen if changing rule brings the upper bound down to the time
                    // that the transition occurred. Example: 2008d, Europe/Sofia, April 1945.)
                    if (currentUpperBound > previousTransition.Instant)
                    {
                        zoneIntervals.Add(previousTransition.ToZoneInterval(currentUpperBound));
                    }
                    return;
                }

                // We have a non-final transition. so add an interval from the previous transition to
                // this one.
                zoneIntervals.Add(previousTransition.ToZoneInterval(bestTransition.Instant));
                previousTransition = bestTransition;

                // Tail zone handling.
                // The final rule set must extend to infinity. There are potentially three ways
                // this can happen:
                // - All rules expire, leaving us with the final real transition, and an upper
                //   bound of infinity. This is handled above.
                // - 1 rule is left, but it cannot create more than one transition in a row,
                //   so again we end up with no transitions to record, and we bail out with
                //   a final infinite interval.
                // - 2 rules are left which would alternate infinitely. This is represented
                //   using a DaylightSavingZone as the tail zone.
                // 
                // The code here caters for that last option, but needs to do it in stages.
                // When we first realize we will have a tail zone (an infinite rule set,
                // two rules left, both of which are themselves infinite) we can create the
                // tail zone, but we don't yet know that we're into its regular tick/tock.
                // It's possible that one rule only starts years after our current transition,
                // so we need to hit the first transition of that rule before we can create a
                // "seam" from the list of precomputed zone intervals to the calculated-on-demand 
                // part of history.
                // For an example of why this is necessary, see Asia/Amman in 2013e: in late 2011
                // we hit "two rules left" but the final rule only starts in 2013 - we don't want
                // to see a bogus transition into that rule in 2012.
                // We could potentially record fewer zone intervals by keeping track of which
                // rules have created at least one transition, but this approach is simpler.
                if (ruleSet.IsInfinite && activeRules.Count == 2)
                {
                    if (tailZone != null)
                    {
                        // Phase two: both rules must now be active, so we're done.
                        return;
                    }
                    ZoneRecurrence startRule = activeRules[0];
                    ZoneRecurrence endRule = activeRules[1];
                    if (startRule.IsInfinite && endRule.IsInfinite)
                    {
                        // Phase one: build the zone, so we can go round once again and then return.
                        tailZone = new DaylightSavingsDateTimeZone("ignored", standardOffset, startRule, endRule);
                    }
                }
            }
        }