Example #1
0
 private BclAdjustmentRule(Instant start, Instant end, Offset standardOffset, Offset savings, PartialZoneIntervalMap partialMap)
 {
     Start          = start;
     End            = end;
     StandardOffset = standardOffset;
     Savings        = savings;
     PartialMap     = partialMap;
 }
Example #2
0
            internal static BclAdjustmentRule FromUnixAdjustmentRule(TimeZoneInfo zone, TimeZoneInfo.AdjustmentRule rule)
            {
                // 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 currently that's
                // not exposed properly.
                var bclLocalStart = rule.DateStart + rule.DaylightTransitionStart.TimeOfDay.TimeOfDay;
                var bclLocalEnd   = rule.DateEnd + rule.DaylightTransitionEnd.TimeOfDay.TimeOfDay;
                var bclUtcStart   = DateTime.SpecifyKind(bclLocalStart == DateTime.MinValue ? DateTime.MinValue : bclLocalStart - zone.BaseUtcOffset, DateTimeKind.Utc);
                var bclWallOffset = zone.GetUtcOffset(bclUtcStart);
                var bclSavings    = rule.DaylightDelta;
                var bclUtcEnd     = DateTime.SpecifyKind(rule.DateEnd == MaxDate ? DateTime.MaxValue : bclLocalEnd - (zone.BaseUtcOffset + bclSavings), DateTimeKind.Utc);
                var isDst         = zone.IsDaylightSavingTime(bclUtcStart);

                // The BCL rule can't express "It's DST with a changed standard time" so we sometimes end
                // up with DST but no savings. Assume this means a savings of 1 hour. That's not a valid
                // assumption in all cases, but it's probably better than alternatives, given limited information.
                if (isDst && bclSavings == TimeSpan.Zero)
                {
                    bclSavings = TimeSpan.FromHours(1);
                }
                // Sometimes the rule says "This rule doesn't apply daylight savings" but still has a daylight
                // savings delta. Extremely bizarre: just override the savings to zero.
                if (!isDst && bclSavings != TimeSpan.Zero)
                {
                    bclSavings = TimeSpan.Zero;
                }

                // Handle changes crossing the international date line, which are represented as savings of +/-23
                // hours (but could conceivably be more).
                if (bclSavings.Hours < -14)
                {
                    bclSavings += TimeSpan.FromDays(1);
                }
                else if (bclSavings.Hours > 14)
                {
                    bclSavings -= TimeSpan.FromDays(1);
                }
                var bclStandard = bclWallOffset - bclSavings;

                // 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 the transition.
                var nodaEnd        = bclUtcEnd == DateTime.MaxValue ? Instant.AfterMaxValue : bclUtcEnd.ToInstant() + Duration.FromSeconds(1);
                var nodaWallOffset = bclWallOffset.ToOffset();
                var nodaStandard   = bclStandard.ToOffset();
                var nodaSavings    = bclSavings.ToOffset();

                var partialMap = PartialZoneIntervalMap.ForZoneInterval(isDst ? zone.StandardName : zone.DaylightName, nodaStart, nodaEnd, nodaWallOffset, nodaSavings);

                return(new BclAdjustmentRule(nodaStart, nodaEnd, nodaStandard, nodaSavings, partialMap));
            }
Example #3
0
            internal static BclAdjustmentRule FromWindowsAdjustmentRule(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;
                }
                var 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.
                var 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.
                var end     = rule.DateEnd == MaxDate ? Instant.AfterMaxValue : rule.DateEnd.ToLocalDateTime().PlusDays(1).WithOffset(zoneStandardOffset).ToInstant();
                var savings = rule.DaylightDelta.ToOffset();

                PartialZoneIntervalMap partialMap;

                // 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);
                    IZoneIntervalMap recurringMap = new StandardDaylightAlternatingMap(standardOffset, standardRecurrence, daylightRecurrence);
                    // Fake 1 hour savings if the adjustment rule claims to be 0 savings. See DaylightFakingZoneIntervalMap documentation below for more details.
                    if (savings == Offset.Zero)
                    {
                        recurringMap = new DaylightFakingZoneIntervalMap(recurringMap, zone.DaylightName);
                    }
                    partialMap = new PartialZoneIntervalMap(start, end, recurringMap);
                }
                return(new BclAdjustmentRule(start, end, standardOffset, savings, partialMap));
            }
Example #4
0
        private static IZoneIntervalMap BuildMap(BclAdjustmentRule[] rules, Offset standardOffset, string standardName)
        {
            Preconditions.CheckNotNull(standardName, nameof(standardName));

            // First work out a naive list of partial maps. These will give the right offset at every instant, but not necessarily
            // correct intervals - we may we need to stitch intervals together.
            List <PartialZoneIntervalMap> maps = new List <PartialZoneIntervalMap>();

            // Handle the start of time until the start of the first rule, if necessary.
            if (rules[0].Start.IsValid)
            {
                maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, Instant.BeforeMinValue, rules[0].Start, standardOffset, Offset.Zero));
            }
            for (int i = 0; i < rules.Length - 1; i++)
            {
                var beforeRule = rules[i];
                var afterRule  = rules[i + 1];
                maps.Add(beforeRule.PartialMap);
                // If there's a gap between this rule and the next one, fill it with a fixed interval.
                if (beforeRule.End < afterRule.Start)
                {
                    maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, beforeRule.End, afterRule.Start, standardOffset, Offset.Zero));
                }
            }

            var lastRule = rules[rules.Length - 1];

            maps.Add(lastRule.PartialMap);

            // Handle the end of the last rule until the end of time, if necessary.
            if (lastRule.End.IsValid)
            {
                maps.Add(PartialZoneIntervalMap.ForZoneInterval(standardName, lastRule.End, Instant.AfterMaxValue, standardOffset, Offset.Zero));
            }
            return(PartialZoneIntervalMap.ConvertToFullMap(maps));
        }
 internal CombinedPartialZoneIntervalMap(PartialZoneIntervalMap[] partialMaps)
 {
     this.partialMaps = partialMaps;
 }
            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>
        /// Converts a sequence of PartialZoneIntervalMaps covering the whole time line into an IZoneIntervalMap.
        /// The partial maps are expected to be in order, with the start of the first map being Instant.BeforeMinValue,
        /// the end of the last map being Instant.AfterMaxValue, and each adjacent pair of maps abutting (i.e. current.End == next.Start).
        /// Zone intervals belonging to abutting maps but which are equivalent in terms of offset and name
        /// are coalesced in the resulting map.
        /// </summary>
        internal static IZoneIntervalMap ConvertToFullMap(IEnumerable <PartialZoneIntervalMap> maps)
        {
            var coalescedMaps = new List <PartialZoneIntervalMap>();
            PartialZoneIntervalMap current = null;

            foreach (var next in maps)
            {
                if (current is null)
                {
                    current = next;
                    Preconditions.DebugCheckArgument(current.Start == Instant.BeforeMinValue, nameof(maps), "First partial map must start at the beginning of time");
                    continue;
                }
                Preconditions.DebugCheckArgument(current.End == next.Start, nameof(maps), "Maps must abut");

                if (next.Start == next.End)
                {
                    continue;
                }

                var lastIntervalOfCurrent = current.GetZoneInterval(current.End - Duration.Epsilon);
                var firstIntervalOfNext   = next.GetZoneInterval(next.Start);

                if (!lastIntervalOfCurrent.EqualIgnoreBounds(firstIntervalOfNext))
                {
                    // There's a genuine transition at the boundary of the partial maps. Add the current one, and move on
                    // to the next.
                    coalescedMaps.Add(current);
                    current = next;
                }
                else
                {
                    // The boundary belongs to a single zone interval crossing the two maps. Some coalescing to do.

                    // If both the current and the next map are single zone interval maps, we can just make the current one
                    // go on until the end of the next one instead.
                    if (current.IsSingleInterval && next.IsSingleInterval)
                    {
                        current = ForZoneInterval(lastIntervalOfCurrent.WithEnd(next.End));
                    }
                    else if (current.IsSingleInterval)
                    {
                        // The next map has at least one transition. Add a single new map for the portion of time from the
                        // start of current to the first transition in next, then continue on with the next map, starting at the first transition.
                        coalescedMaps.Add(ForZoneInterval(lastIntervalOfCurrent.WithEnd(firstIntervalOfNext.End)));
                        current = next.WithStart(firstIntervalOfNext.End);
                    }
                    else if (next.IsSingleInterval)
                    {
                        // The current map as at least one transition. Add a version of that, clamped to end at the final transition,
                        // then continue with a new map which takes in the last portion of the current and the whole of next.
                        coalescedMaps.Add(current.WithEnd(lastIntervalOfCurrent.Start));
                        current = ForZoneInterval(firstIntervalOfNext.WithStart(lastIntervalOfCurrent.Start));
                    }
                    else
                    {
                        // Transitions in both maps. Add the part of current before the last transition, and a single map containing
                        // the coalesced interval across the boundary, then continue with the next map, starting at the first transition.
                        coalescedMaps.Add(current.WithEnd(lastIntervalOfCurrent.Start));
                        coalescedMaps.Add(ForZoneInterval(lastIntervalOfCurrent.WithEnd(firstIntervalOfNext.End)));
                        current = next.WithStart(firstIntervalOfNext.End);
                    }
                }
            }
            Preconditions.DebugCheckArgument(current != null, nameof(maps), "Collection of maps must not be empty");
            Preconditions.DebugCheckArgument(current.End == Instant.AfterMaxValue, nameof(maps), "Collection of maps must end at the end of time");

            // We're left with a map extending to the end of time, which couldn't have been coalesced with its predecessors.
            coalescedMaps.Add(current);
            return(new CombinedPartialZoneIntervalMap(coalescedMaps.ToArray()));
        }
Example #8
0
 private BclAdjustmentRule(Offset standardOffset, Offset savings, PartialZoneIntervalMap partialMap)
 {
     StandardOffset = standardOffset;
     Savings        = savings;
     PartialMap     = partialMap;
 }
Example #9
0
 // Visible for tests
 internal BclAdjustmentRule(ZoneInterval zoneInterval)
 {
     StandardOffset = zoneInterval.StandardOffset;
     Savings        = zoneInterval.Savings;
     PartialMap     = PartialZoneIntervalMap.ForZoneInterval(zoneInterval);
 }