public void Construct_Normal()
 {
     const string name = "abc";
     var actual = new ZoneTransition(NodaConstants.UnixEpoch, name, Offset.Zero, Offset.Zero);
     Assert.AreEqual(NodaConstants.UnixEpoch, actual.Instant, "Instant");
     Assert.AreEqual(name, actual.Name, "GetName");
     Assert.AreEqual(Offset.Zero, actual.WallOffset, "WallOffset");
     Assert.AreEqual(Offset.Zero, actual.StandardOffset, "StandardOffset");
 }
Example #2
0
 /// <summary>
 /// Determines whether is a transition from the given transition.
 /// </summary>
 /// <remarks>
 /// To be a transition from another the instant at which the transition occurs must be
 /// greater than the given transition's and at least one aspect out of (name, standard
 /// offset, wall offset) must differ. If this is not true then this transition is considered
 /// to be redundant and should not be used. Note that there are a few transitions which
 /// keep the same wall offset and name, but differ in how that wall offset is divided into
 /// daylight saving and standard components. One notable example of this is October 27th 1968, when
 /// the UK went from "British Summer Time" (BST, standard=0, daylight=1) to "British Standard Time"
 /// (BST, standard=1, daylight=0).
 /// </remarks>
 /// <param name="other">The <see cref="ZoneTransition"/> to compare to.</param>
 /// <returns>
 /// <c>true</c> if this is a transition from the given transition; otherwise, <c>false</c>.
 /// </returns>
 internal bool IsTransitionFrom(ZoneTransition other)
 {
     if (other == null)
     {
         return true;
     }
     bool later = Instant > other.Instant;
     bool different = Name != other.Name || StandardOffset != other.StandardOffset || Savings != other.Savings;
     return later && different;
 }
        /// <summary>
        /// Adds the given transition to the transition list if it represents a new transition.
        /// </summary>
        /// <param name="transitions">The list of <see cref="ZoneTransition"/> to add to.</param>
        /// <param name="transition">The transition to add.</param>
        /// <returns><c>true</c> if the transition was added.</returns>
        private static bool AddTransition(IList<ZoneTransition> transitions, ZoneTransition transition)
        {
            int transitionCount = transitions.Count;
            if (transitionCount == 0)
            {
                transitions.Add(transition);
                return true;
            }

            ZoneTransition lastTransition = transitions[transitionCount - 1];
            if (!transition.IsTransitionFrom(lastTransition))
            {
                return false;
            }

            // A transition after the "beginning of time" one will always be valid.
            if (lastTransition.Instant == Instant.BeforeMinValue)
            {
                transitions.Add(transition);
                return true;
            }

            Offset lastOffset = transitions.Count < 2 ? Offset.Zero : transitions[transitions.Count - 2].WallOffset;
            Offset newOffset = lastTransition.WallOffset;
            // If the local time just before the new transition is the same as the local time just
            // before the previous one, just replace the last transition with new one.
            // TODO(Post-V1): It's not clear what this is doing... work it out and give an example
            LocalInstant lastLocalStart = lastTransition.Instant.Plus(lastOffset);
            LocalInstant newLocalStart = transition.Instant.Plus(newOffset);
            if (lastLocalStart == newLocalStart)
            {
                transitions.RemoveAt(transitionCount - 1);
                return AddTransition(transitions, transition);
            }
            transitions.Add(transition);
            return true;
        }
Example #4
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 is 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 is 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 is null || zoneTransition.Instant <= bestTransition.Instant)
                    {
                        bestTransition = zoneTransition;
                    }
                }
                Instant currentUpperBound = ruleSet.GetUpperLimit(previousTransition.Savings);
                if (bestTransition is 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 StandardDaylightAlternatingMap(standardOffset, startRule, endRule);
                    }
                }
            }
        }
 public void IsTransitionFrom_earlierInstantAndUnequalSavings_false()
 {
     var newValue = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.Zero);
     var oldValue = new ZoneTransition(NodaConstants.UnixEpoch + Duration.Epsilon, "abc", Offset.Zero, Offset.MaxValue);
     Assert.False(newValue.IsTransitionFrom(oldValue));
 }
 public void IsTransitionFrom_unequalName_false()
 {
     var newValue = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.Zero);
     var oldValue = new ZoneTransition(NodaConstants.UnixEpoch, "qwe", Offset.Zero, Offset.Zero);
     Assert.False(newValue.IsTransitionFrom(oldValue));
 }
 public void IsTransitionFrom_identity_false()
 {
     var value = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.Zero);
     Assert.False(value.IsTransitionFrom(value));
 }
 public void IsTransitionFrom_null_true()
 {
     var value = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.Zero);
     Assert.True(value.IsTransitionFrom(null));
 }
 public void IsTransitionFrom_laterInstantAndUnequalNameAndSavings_true()
 {
     var newValue = new ZoneTransition(NodaConstants.UnixEpoch + Duration.Epsilon, "abc", Offset.Zero, Offset.Zero);
     var oldValue = new ZoneTransition(NodaConstants.UnixEpoch, "qwe", Offset.Zero, Offset.MaxValue);
     Assert.True(newValue.IsTransitionFrom(oldValue));
 }
 public void IsTransitionFrom_laterInstantAndEqualButOppositeStandardAndSavings_true()
 {
     var newValue = new ZoneTransition(NodaConstants.UnixEpoch + Duration.Epsilon, "abc", Offset.FromHours(1), Offset.Zero);
     var oldValue = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.FromHours(1));
     Assert.True(newValue.IsTransitionFrom(oldValue));
 }
 public void IsTransitionFrom_laterInstant_false()
 {
     var newValue = new ZoneTransition(NodaConstants.UnixEpoch + Duration.Epsilon, "abc", Offset.Zero, Offset.Zero);
     var oldValue = new ZoneTransition(NodaConstants.UnixEpoch, "abc", Offset.Zero, Offset.Zero);
     Assert.False(newValue.IsTransitionFrom(oldValue));
 }
        /// <summary>
        /// Processes all the rules and builds a DateTimeZone.
        /// </summary>
        /// <param name="zoneId">Time zone ID to assign</param>
        public DateTimeZone ToDateTimeZone(String zoneId)
        {
            Preconditions.CheckNotNull(zoneId, "zoneId");

            var          transitions = new List <ZoneTransition>();
            DateTimeZone tailZone    = null;
            Instant      instant     = Instant.MinValue;

            int  ruleSetCount      = ruleSets.Count;
            bool tailZoneSeamValid = false;

            for (int i = 0; i < ruleSetCount; i++)
            {
                var            ruleSet            = ruleSets[i];
                var            transitionIterator = ruleSet.Iterator(instant);
                ZoneTransition nextTransition     = transitionIterator.First();
                if (nextTransition == null)
                {
                    continue;
                }
                AddTransition(transitions, nextTransition);

                while ((nextTransition = transitionIterator.Next()) != null)
                {
                    if (AddTransition(transitions, nextTransition))
                    {
                        if (tailZone != null)
                        {
                            // Got the extra transition before DaylightSavingsTimeZone.
                            // This final transition has a valid start point and offset, but
                            // we don't know where it ends - which is fine, as the tail zone will
                            // take over.
                            tailZoneSeamValid = true;
                            break;
                        }
                    }
                    if (tailZone == null && i == ruleSetCount - 1)
                    {
                        tailZone = transitionIterator.BuildTailZone(zoneId);
                        // If tailZone is not null, don't break out of main loop until at least one
                        // more transition is calculated. This ensures a correct 'seam' to the
                        // DaylightSavingsTimeZone.
                    }
                }

                instant = ruleSet.GetUpperLimit(transitionIterator.Savings);
            }

            // Simple case where we don't have a trailing daylight saving zone.
            if (tailZone == null)
            {
                switch (transitions.Count)
                {
                case 0:
                    return(new FixedDateTimeZone(zoneId, Offset.Zero));

                case 1:
                    return(new FixedDateTimeZone(zoneId, transitions[0].WallOffset));

                default:
                    var ret = CreatePrecalculatedDateTimeZone(zoneId, transitions, Instant.MaxValue, null);
                    return(ret.IsCachable() ? CachedDateTimeZone.ForZone(ret) : ret);
                }
            }

            // Sanity check
            if (!tailZoneSeamValid)
            {
                throw new InvalidOperationException("Invalid time zone data for id " + zoneId + "; no valid transition before tail zone");
            }

            // The final transition should not be used for a zone interval,
            // although it should have the same offset etc as the tail zone for its starting point.
            var lastTransition        = transitions[transitions.Count - 1];
            var firstTailZoneInterval = tailZone.GetZoneInterval(lastTransition.Instant);

            if (lastTransition.StandardOffset != firstTailZoneInterval.StandardOffset ||
                lastTransition.WallOffset != firstTailZoneInterval.WallOffset ||
                lastTransition.Savings != firstTailZoneInterval.Savings ||
                lastTransition.Name != firstTailZoneInterval.Name)
            {
                throw new InvalidOperationException(
                          string.Format("Invalid seam to tail zone in time zone {0}; final transition {1} different to first tail zone interval {2}",
                                        zoneId, lastTransition, firstTailZoneInterval));
            }

            transitions.RemoveAt(transitions.Count - 1);
            var zone = CreatePrecalculatedDateTimeZone(zoneId, transitions, lastTransition.Instant, tailZone);

            return(zone.IsCachable() ? CachedDateTimeZone.ForZone(zone) : zone);
        }
        /// <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 StandardDaylightAlternatingMap(standardOffset, startRule, endRule);
                    }
                }
            }
        }