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)); }
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); } }
/// <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)); }
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()); }
/// <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)); }
internal AdjustmentInterval(Instant start, DaylightSavingsDateTimeZone adjustmentZone, ZoneInterval seam) { this.start = start; this.seam = seam; this.adjustmentZone = adjustmentZone; }
/// <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); } } } }