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"); }
/// <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; }
/// <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); } } } }