/// <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(TimeZoneInfo bclZone) { Preconditions.CheckNotNull(bclZone, nameof(bclZone)); Offset standardOffset = bclZone.BaseUtcOffset.ToOffset(); var rules = bclZone.GetAdjustmentRules(); if (!bclZone.SupportsDaylightSavingTime || rules.Length == 0) { var fixedInterval = new ZoneInterval(bclZone.StandardName, Instant.BeforeMinValue, Instant.AfterMaxValue, standardOffset, Offset.Zero); return(new BclDateTimeZone(bclZone, new SingleZoneIntervalMap(fixedInterval))); } BclAdjustmentRule[] convertedRules; if (AreWindowsStyleRules(rules)) { convertedRules = Array.ConvertAll(rules, rule => BclAdjustmentRule.FromWindowsAdjustmentRule(bclZone, rule)); } else { convertedRules = Array.ConvertAll(rules, rule => BclAdjustmentRule.FromUnixAdjustmentRule(bclZone, rule)); FixUnixTransitions(convertedRules); } IZoneIntervalMap uncachedMap = BuildMap(convertedRules, standardOffset, bclZone.StandardName); IZoneIntervalMap cachedMap = CachingZoneIntervalMap.CacheMap(uncachedMap); return(new BclDateTimeZone(bclZone, cachedMap)); }
/// <summary> /// Gets the zone offset period for the given instant. /// </summary> /// <param name="instant">The Instant to find.</param> /// <returns>The ZoneInterval including the given instant.</returns> public override ZoneInterval GetZoneInterval(Instant instant) { if (tailZone != null && instant >= tailZoneStart) { // Clamp the tail zone interval to start at the end of our final period, if necessary, so that the // join is seamless. ZoneInterval intervalFromTailZone = tailZone.GetZoneInterval(instant); return(intervalFromTailZone.RawStart < tailZoneStart ? firstTailZoneInterval : intervalFromTailZone); } int lower = 0; // Inclusive int upper = periods.Length; // Exclusive while (lower < upper) { int current = (lower + upper) / 2; var candidate = periods[current]; if (candidate.RawStart > instant) { upper = current; } // Safe to use RawEnd, as it's just for the comparison. else if (candidate.RawEnd <= instant) { lower = current + 1; } else { return(candidate); } } // Note: this would indicate a bug. The time zone is meant to cover the whole of time. throw new InvalidOperationException($"Instant {instant} did not exist in time zone {Id}"); }
private IEnumerable <ZoneInterval> CoalesceIntervals(IEnumerable <ZoneInterval> zoneIntervals) { ZoneInterval current = null; foreach (var zoneInterval in zoneIntervals) { if (current == null) { current = zoneInterval; continue; } if (zoneIntervalComparer.EqualExceptStartAndEnd(current, zoneInterval)) { current = current.WithEnd(zoneInterval.RawEnd); } else { yield return(current); current = zoneInterval; } } // current will only be null if start == end... if (current != null) { yield return(current); } }
/// <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 ReadLegacy(LegacyDateTimeZoneReader reader, string id) { string[] stringPool = new string[reader.ReadCount()]; for (int i = 0; i < stringPool.Length; i++) { stringPool[i] = reader.ReadString(); } int size = reader.ReadCount(); var periods = new ZoneInterval[size]; var start = reader.ReadZoneIntervalTransition(null); for (int i = 0; i < size; i++) { int nameIndex = stringPool.Length < 256 ? reader.ReadByte() : reader.ReadInt32(); var name = stringPool[nameIndex]; 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.ReadTimeZone(id + "-tail"); return(new PrecalculatedDateTimeZone(id, periods, tailZone)); }
/// <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(TimeZoneInfo bclZone) { Preconditions.CheckNotNull(bclZone, nameof(bclZone)); Offset standardOffset = bclZone.BaseUtcOffset.ToOffset(); var rules = bclZone.GetAdjustmentRules(); if (!bclZone.SupportsDaylightSavingTime || rules.Length == 0) { var fixedInterval = new ZoneInterval(bclZone.StandardName, Instant.BeforeMinValue, Instant.AfterMaxValue, standardOffset, Offset.Zero); return(new BclDateTimeZone(bclZone, standardOffset, standardOffset, new SingleZoneIntervalMap(fixedInterval))); } int windowsRules = rules.Count(IsWindowsRule); var ruleConverter = AreWindowsStyleRules(rules) ? rule => BclAdjustmentRule.FromWindowsAdjustmentRule(bclZone, rule) : (Converter <TimeZoneInfo.AdjustmentRule, BclAdjustmentRule>)(rule => BclAdjustmentRule.FromUnixAdjustmentRule(bclZone, rule)); BclAdjustmentRule[] convertedRules = Array.ConvertAll(rules, ruleConverter); Offset minRuleOffset = convertedRules.Aggregate(Offset.MaxValue, (min, rule) => Offset.Min(min, rule.Savings + rule.StandardOffset)); Offset maxRuleOffset = convertedRules.Aggregate(Offset.MinValue, (min, rule) => Offset.Max(min, rule.Savings + rule.StandardOffset)); IZoneIntervalMap uncachedMap = BuildMap(convertedRules, standardOffset, bclZone.StandardName); IZoneIntervalMap cachedMap = CachingZoneIntervalMap.CacheMap(uncachedMap); return(new BclDateTimeZone(bclZone, Offset.Min(standardOffset, minRuleOffset), Offset.Max(standardOffset, maxRuleOffset), cachedMap)); }
internal ZoneLocalMapping(DateTimeZone zone, LocalDateTime localDateTime, ZoneInterval earlyInterval, ZoneInterval lateInterval, int count) { this.zone = Preconditions.CheckNotNull(zone, "zone"); this.localDateTime = localDateTime; this.earlyInterval = Preconditions.CheckNotNull(earlyInterval, "earlyInterval"); this.lateInterval = Preconditions.CheckNotNull(lateInterval, "lateInterval"); Preconditions.CheckArgumentRange("count", count, 0, 2); this.count = count; }
internal ZoneLocalMapping([Trusted][NotNull] DateTimeZone zone, LocalDateTime localDateTime, [Trusted][NotNull] ZoneInterval earlyInterval, [Trusted][NotNull] ZoneInterval lateInterval, int count) { Preconditions.DebugCheckNotNull(zone, nameof(zone)); Preconditions.DebugCheckNotNull(earlyInterval, nameof(earlyInterval)); Preconditions.DebugCheckNotNull(lateInterval, nameof(lateInterval)); Preconditions.DebugCheckArgumentRange(nameof(count), count, 0, 2); this.Zone = zone; this.EarlyInterval = earlyInterval; this.LateInterval = lateInterval; this.LocalDateTime = localDateTime; this.Count = count; }
internal PrecalculatedDateTimeZone([NotNull] string id, [NotNull] ZoneInterval[] intervals, IZoneIntervalMapWithMinMax tailZone) : base(id, false, ComputeOffset(intervals, tailZone, Offset.Min), ComputeOffset(intervals, tailZone, Offset.Max)) { this.periods = intervals; this.tailZone = tailZone; this.tailZoneStart = intervals[intervals.Length - 1].RawEnd; // We want this to be AfterMaxValue for tail-less zones. if (tailZone != null) { // Cache a "clamped" zone interval for use at the start of the tail zone. firstTailZoneInterval = tailZone.GetZoneInterval(tailZoneStart).WithStart(tailZoneStart); } ValidatePeriods(intervals, tailZone); }
/// <summary> /// Initializes a new instance of the <see cref="PrecalculatedDateTimeZone"/> class. /// This is only visible to make testing simpler. /// </summary> /// <param name="id">The id.</param> /// <param name="periods">The periods.</param> /// <param name="tailZone">The tail zone.</param> internal PrecalculatedDateTimeZone(string id, ZoneInterval[] periods, DateTimeZone tailZone) : base(id, false, ComputeOffset(periods, p => p.WallOffset, tailZone, Offset.Min), ComputeOffset(periods, p => p.WallOffset, tailZone, Offset.Max)) { this.tailZone = tailZone; this.periods = periods; this.tailZone = tailZone; this.tailZoneStart = periods[periods.Length - 1].End; if (tailZone != null) { // Cache a "clamped" zone interval for use at the start of the tail zone. firstTailZoneInterval = tailZone.GetZoneInterval(tailZoneStart).WithStart(tailZoneStart); } ValidatePeriods(periods, tailZone); }
/// <summary> /// Creates a hash table node with all the information for this period. /// We start off by finding the interval for the start of the period, and /// then repeatedly check whether that interval ends after the end of the /// period - at which point we're done. If not, find the next interval, create /// a new node referring to that interval and the previous interval, and keep going. /// </summary> internal static HashCacheNode CreateNode(int period, IZoneIntervalMap map) { var periodStart = new Instant((long)period << PeriodShift); var periodEnd = new Instant((long)(period + 1) << PeriodShift); var interval = map.GetZoneInterval(periodStart); var node = new HashCacheNode(interval, period, null); // Keep going while the current interval ends before the period. while (interval.End < periodEnd) { interval = map.GetZoneInterval(interval.End); node = new HashCacheNode(interval, period, node); } return(node); }
/// <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)); }
/// <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> [NotNull] public static BclDateTimeZone FromTimeZoneInfo([NotNull] TimeZoneInfo bclZone) { Preconditions.CheckNotNull(bclZone, nameof(bclZone)); Offset standardOffset = bclZone.BaseUtcOffset.ToOffset(); var rules = bclZone.GetAdjustmentRules(); if (!bclZone.SupportsDaylightSavingTime || rules.Length == 0) { var fixedInterval = new ZoneInterval(bclZone.StandardName, Instant.BeforeMinValue, Instant.AfterMaxValue, standardOffset, Offset.Zero); return(new BclDateTimeZone(bclZone, standardOffset, standardOffset, new SingleZoneIntervalMap(fixedInterval))); } BclAdjustmentRule[] convertedRules = Array.ConvertAll(rules, rule => new BclAdjustmentRule(bclZone, rule)); Offset minRuleOffset = convertedRules.Aggregate(Offset.MaxValue, (min, rule) => Offset.Min(min, rule.Savings + rule.StandardOffset)); Offset maxRuleOffset = convertedRules.Aggregate(Offset.MinValue, (min, rule) => Offset.Max(min, rule.Savings + rule.StandardOffset)); IZoneIntervalMap uncachedMap = BuildMap(convertedRules, standardOffset, bclZone.StandardName); IZoneIntervalMap cachedMap = CachingZoneIntervalMap.CacheMap(uncachedMap, CachingZoneIntervalMap.CacheType.Hashtable); return(new BclDateTimeZone(bclZone, Offset.Min(standardOffset, minRuleOffset), Offset.Max(standardOffset, maxRuleOffset), cachedMap)); }
/// <summary> /// Gets the zone offset period for the given instant. /// </summary> /// <param name="instant">The Instant to find.</param> /// <returns>The ZoneInterval including the given instant.</returns> public override ZoneInterval GetZoneInterval(Instant instant) { if (tailZone != null && instant >= tailZoneStart) { // Clamp the tail zone interval to start at the end of our final period, if necessary, so that the // join is seamless. ZoneInterval intervalFromTailZone = tailZone.GetZoneInterval(instant); return(intervalFromTailZone.Start < tailZoneStart ? firstTailZoneInterval : intervalFromTailZone); } // Special case to avoid the later logic being problematic if (instant == Instant.MaxValue) { return(periods[periods.Length - 1]); } int lower = 0; // Inclusive int upper = periods.Length; // Exclusive while (lower < upper) { int current = (lower + upper) / 2; var candidate = periods[current]; if (candidate.Start > instant) { upper = current; } else if (candidate.End <= instant) { lower = current + 1; } else { return(candidate); } } // Note: this would indicate a bug. The time zone is meant to cover the whole of time. throw new InvalidOperationException(string.Format("Instant {0} did not exist in time zone {1}", instant, Id)); }
/// <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 static ZoneIntervalPair Ambiguous(ZoneInterval early, ZoneInterval late) { return(new ZoneIntervalPair(early, late, 2)); }
// Visible for tests internal BclAdjustmentRule(ZoneInterval zoneInterval) { StandardOffset = zoneInterval.StandardOffset; Savings = zoneInterval.Savings; PartialMap = PartialZoneIntervalMap.ForZoneInterval(zoneInterval); }
internal static BclAdjustmentRule ConvertUnixRuleToBclAdjustmentRule(TimeZoneInfo.AdjustmentRule rule, string standardName, string daylightName, TimeSpan zoneStandardOffset, TimeSpan ruleStandardOffset, bool forceDaylightSavings) { // On .NET Core on Unix, each "adjustment rule" is effectively just a zone interval. The transitions are only used // to give the time of day values to combine with rule.DateStart and rule.DateEnd. It's all a bit odd. // The *last* adjustment rule internally can work like a normal Windows standard/daylight rule, but that's only // exposed in .NET 6.0, and those rules are handled in FromUnixAdjustmentRule. // (Currently we don't have a way of handling a fixed-date rule to the end of time that really represents alternating // standard/daylight. Apparently that's not an issue.) // The start of each rule is indicated by the start date with the time-of-day of the transition, interpreted as being in the *zone* standard offset // (rather than the *rule* standard offset). Some rules effectively start in daylight time, but only when there are consecutive daylight time // rules. This is handled in FixOverlappingUnixRules. var bclLocalStart = rule.DateStart + rule.DaylightTransitionStart.TimeOfDay.TimeOfDay; var bclUtcStart = DateTime.SpecifyKind(bclLocalStart == DateTime.MinValue ? DateTime.MinValue : bclLocalStart - zoneStandardOffset, DateTimeKind.Utc); var bclSavings = rule.DaylightDelta; // The end of each rule is indicated by the start date with the time-of-day of the transition, interpreted as being in the *zone* standard offset // with the *rule* daylight delta. var bclLocalEnd = rule.DateEnd + rule.DaylightTransitionEnd.TimeOfDay.TimeOfDay; var bclUtcEnd = DateTime.SpecifyKind(rule.DateEnd == MaxDate ? DateTime.MaxValue : bclLocalEnd - (zoneStandardOffset + bclSavings), DateTimeKind.Utc); // For just a couple of time zones in .NET 6, there are adjustment rules which appear to be invalid // in the normal expectation of "starts in standard, ends in daylight". // Example in America/Creston: 1944-01-01 - 1944-01-01: Daylight delta: +01; DST starts January 01 at 00:00:00 and ends January 01 at 00:00:59.999 // Handle this by treating the rule as starting in daylight time. if (bclUtcStart >= bclUtcEnd) { bclUtcStart -= bclSavings; } // If the zone says that the start of the rule is in DST, but the rule has no daylight savings, // assume we actually want an hour of DST but one less hour of standard offset. // See Europe/Dublin in 1960 for example, in .NET 6: // 1960-04-10 - 1960-10-02: Daylight delta: +00; DST starts April 10 at 03:00:00 and ends October 02 at 02:59:59.999 (force daylight) if (forceDaylightSavings) { bclSavings = TimeSpan.FromHours(1); ruleStandardOffset -= bclSavings; } // Handle changes crossing the international date line, which used to be represented as savings of +/-23 // hours (but could conceivably be more). // Note: I can't currently reproduce this in .NET Core 3.1 or .NET 6. It may be a legacy Mono artifact; // it does no harm to preserve it, however. if (bclSavings.Hours < -14) { bclSavings += TimeSpan.FromDays(1); } else if (bclSavings.Hours > 14) { bclSavings -= TimeSpan.FromDays(1); } // Now all the values are sensible - and in particular, now the daylight savings are in a range that can be represented by // Offset - we can converted everything to Noda Time types. var nodaStart = bclUtcStart == DateTime.MinValue ? Instant.BeforeMinValue : bclUtcStart.ToInstant(); // The representation returned to us (not the internal representation) has an end point one second (before .NET 6) // or one millisecond (.NET 6 onwards) before the transition. We round up to a properly exclusive end instant. var endTimeCompensation = Duration.FromSeconds(1) - Duration.FromMilliseconds(bclLocalEnd.Millisecond); var nodaEnd = bclUtcEnd == DateTime.MaxValue ? Instant.AfterMaxValue : bclUtcEnd.ToInstant() + endTimeCompensation; var nodaStandard = ruleStandardOffset.ToOffset(); var nodaSavings = bclSavings.ToOffset(); var nodaWallOffset = nodaStandard + nodaSavings; var zoneInterval = new ZoneInterval(nodaSavings == Offset.Zero ? standardName : daylightName, nodaStart, nodaEnd, nodaWallOffset, nodaSavings); return(new BclAdjustmentRule(zoneInterval)); }
private ZonedDateTime BuildZonedDateTime(ZoneInterval interval) => new ZonedDateTime(LocalDateTime.WithOffset(interval.WallOffset), Zone);
/// <summary> /// Initializes a new instance of the <see cref="HashCacheNode"/> class. /// </summary> /// <param name="interval">The zone interval.</param> /// <param name="period"></param> /// <param name="previous">The previous <see cref="HashCacheNode"/> node.</param> private HashCacheNode(ZoneInterval interval, int period, HashCacheNode previous) { this.Period = period; this.Interval = interval; this.Previous = previous; }
internal BclZoneIntervalMap(List <AdjustmentInterval> adjustmentIntervals, ZoneInterval headInterval, ZoneInterval tailInterval) { this.adjustmentIntervals = adjustmentIntervals; this.headInterval = headInterval; this.tailInterval = tailInterval; }
internal FixedZoneIntervalMap(ZoneInterval interval) { this.interval = interval; }
internal static ZoneIntervalPair Unambiguous(ZoneInterval interval) { return(new ZoneIntervalPair(interval, null, 1)); }
/// <summary> /// Returns whether this zone interval has the same offsets and name as another. /// </summary> internal bool EqualIgnoreBounds([Trusted] ZoneInterval other) { Preconditions.DebugCheckNotNull(other, nameof(other)); return(other.WallOffset == WallOffset && other.Savings == Savings && other.Name == Name); }
/// <summary> /// Initializes a new instance of the <see cref="FixedDateTimeZone"/> class. /// </summary> /// <param name="id">The id.</param> /// <param name="offset">The offset.</param> public FixedDateTimeZone(string id, Offset offset) : base(id, true, offset, offset) { this.offset = offset; interval = new ZoneInterval(id, Instant.MinValue, Instant.MaxValue, offset, Offset.Zero); intervalPair = ZoneIntervalPair.Unambiguous(interval); }
/// <summary> /// Builds a PartialZoneIntervalMap wrapping the given zone interval, taking its start and end as the start and end of /// the portion of the time line handled by the partial map. /// </summary> internal static PartialZoneIntervalMap ForZoneInterval(ZoneInterval interval) => new PartialZoneIntervalMap(interval.RawStart, interval.RawEnd, new SingleZoneIntervalMap(interval));
internal AdjustmentInterval(Instant start, DaylightSavingsDateTimeZone adjustmentZone, ZoneInterval seam) { this.start = start; this.seam = seam; this.adjustmentZone = adjustmentZone; }
/// <summary> /// Initializes a new instance of the <see cref="FixedDateTimeZone"/> class. /// </summary> /// <remarks>The name (for the <see cref="ZoneInterval"/>) is deemed to be the same as the ID.</remarks> /// <param name="id">The id.</param> /// <param name="offset">The offset.</param> /// <param name="name">The name to use in the sole <see cref="ZoneInterval"/> in this zone.</param> internal FixedDateTimeZone([NotNull] string id, Offset offset, [NotNull] string name) : base(id, true, offset, offset) { interval = new ZoneInterval(name, Instant.BeforeMinValue, Instant.AfterMaxValue, offset, Offset.Zero); }
private ZonedDateTime BuildZonedDateTime(ZoneInterval interval) { return(new ZonedDateTime(localDateTime, interval.WallOffset, zone)); }
private ZoneIntervalPair(ZoneInterval early, ZoneInterval late, int matchingIntervals) { this.earlyInterval = early; this.lateInterval = late; this.matchingIntervals = matchingIntervals; }
/// <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)); }